Compare commits

...

83 Commits

Author SHA1 Message Date
tom5079
6ebc386474 Fixed app crashing when deleting cache/download 2020-08-04 12:14:14 +09:00
tom5079
3e657bdc09 Merge branch 'dev' 2020-08-03 21:54:28 +09:00
tom5079
0b0adb76a1 Merge branch 'master' into dev
# Conflicts:
#	app/build.gradle
#	app/release/app-release.apk
#	app/release/output-metadata.json
2020-08-03 21:54:14 +09:00
tom5079
17b3e010aa fuck git 2020-08-03 21:49:04 +09:00
tom5079
20003acd73 App built
resolves #98
2020-08-03 21:34:24 +09:00
tom5079
2ab7672092 Search Backtracking Added 2020-08-03 21:31:39 +09:00
tom5079
c317abe64b Open Gallery Info dialog instead of opening up the gallery when opening a random gallery 2020-08-03 21:19:08 +09:00
tom5079
bc33ce1ebc Fix Settings opening up too late if the download folder is too big 2020-08-03 21:14:47 +09:00
Pupil
684c5cf38b Merge pull request #105 from tom5079/dev
4.19-hotfix2
2020-08-03 02:10:17 +09:00
tom5079
c34e15f0a1 Fixed image corruption when not using cache 2020-08-03 02:06:37 +09:00
Pupil
bad004f892 Merge pull request #104 from tom5079/dev
Version 4.19-hotfix1
2020-08-02 17:48:32 +09:00
tom5079
828d3de020 Fixed app crashing between android version 5.0 and 5.1 2020-08-02 17:45:24 +09:00
tom5079
132b3b9be1 Unable to fetch thumbnails fixed 2020-07-29 10:53:50 +09:00
Pupil
388bc6fda5 Merge pull request #103 from tom5079/Pupil-99
resolves #99
2020-07-29 10:28:01 +09:00
tom5079
a93edc184d resolves #99 2020-07-23 22:42:38 +09:00
tom5079
08672d10ac oops 2020-07-23 20:48:24 +09:00
tom5079
b563dae3a8 App rebuilt 2020-07-23 20:37:38 +09:00
tom5079
917f9672dd bug fix - unable to run on older devices
Version 4.18.4
2020-07-23 20:32:45 +09:00
Pupil
9ddb19530b Merge pull request #101 from tom5079/dev
Version 4.18.3
2020-07-12 20:09:12 +09:00
tom5079
431e56a9f1 Version 4.18.3 2020-07-12 20:07:59 +09:00
Pupil
71093aac4c Update README.md 2020-07-12 19:53:28 +09:00
tom5079
47c9e8127e Remove logic for resetting low quality setting 2020-07-07 10:21:30 +09:00
tom5079
24b801b346 resolves #52 2020-07-05 19:45:55 +09:00
tom5079
70608c3ed9 resolves #52 2020-07-05 19:40:14 +09:00
tom5079
f185196e85 Version Update 2020-07-05 19:33:56 +09:00
tom5079
a8766a8bbe Drawer logo fixed 2020-06-23 20:37:20 +09:00
Pupil
27a8c93cfe Merge pull request #97 from tom5079/Pupil-95
Pupil 95
2020-06-23 18:49:57 +09:00
tom5079
a3cd29fda9 Merge remote-tracking branch 'origin/Pupil-95' into Pupil-95 2020-06-23 18:48:24 +09:00
tom5079
adda8ab640 Pupil-95 Search tag icon not visible in light theme 2020-06-23 18:48:13 +09:00
tom5079
1538ea5fc8 Merge remote-tracking branch 'origin/dev' into dev 2020-06-23 18:47:58 +09:00
tom5079
2367a97a54 App built 2020-06-23 18:47:48 +09:00
tom5079
090ec0e4af Auto-Retry
Bug fix
2020-06-23 18:47:48 +09:00
tom5079
de7f552e5c Pupil-95 Search tag icon not visible in light theme 2020-06-23 18:40:43 +09:00
tom5079
d763f5dca0 App built 2020-06-22 09:52:25 +09:00
tom5079
9f41116241 Auto-Retry
Bug fix
2020-06-22 09:48:04 +09:00
Pupil
57faada201 Merge pull request #93 from tom5079/dev
Version 4.18.1
2020-06-21 23:03:59 +09:00
tom5079
1edb95f0c5 Prevent OOM when cache is disabled 2020-06-21 23:03:10 +09:00
tom5079
9f363d8900 idk wtf i'm doin' 2020-06-21 22:43:47 +09:00
Pupil
0bf2f1b6e1 Merge pull request #92 from tom5079/dev
Version 4.18
2020-06-21 18:19:40 +09:00
tom5079
68c7a38390 Several fixes 2020-06-21 18:19:13 +09:00
tom5079
841c8a7a15 Download Retry Button Added 2020-06-21 18:12:46 +09:00
Pupil
6c9688183b Merge pull request #91 from tom5079/Pupil-90
Pupil 90
2020-06-21 17:40:54 +09:00
tom5079
ccd84c91f6 Retry 5 times when failed 2020-06-21 17:40:03 +09:00
tom5079
318d6f9b52 User ID added for crash analysis 2020-06-21 17:31:46 +09:00
tom5079
8f5d612ee0 Migrate to vector image 2020-06-21 16:55:56 +09:00
tom5079
56b2a05596 SearchView Dark Theme 2020-06-21 16:30:10 +09:00
tom5079
4db0022d6a Fixed nomedia creation menu 2020-06-21 15:32:55 +09:00
Pupil
67f37d3188 Merge pull request #87 from tom5079/Pupil-77
Pupil 77
2020-06-21 15:00:39 +09:00
tom5079
ed81cc7207 APCJSA enabling dialog added 2020-06-21 15:00:05 +09:00
tom5079
065845f1be Cache disable setting added 2020-06-21 14:45:57 +09:00
tom5079
902f705e89 Added Filter 2020-06-21 11:55:34 +09:00
tom5079
ec2e0ef773 SuppressLint 2020-06-21 11:42:50 +09:00
tom5079
d28c5741d0 SuppressLint 2020-06-21 11:42:38 +09:00
Pupil
e6e3f9e8f8 Merge pull request #86 from tom5079/Pupil-59
Pupil 59
2020-06-21 11:36:13 +09:00
tom5079
90e1dc59bd Disable fingerprint when all the locks are disabled 2020-06-21 11:31:37 +09:00
tom5079
0b1c9b097c Disable fingerprint when all the locks are disabled 2020-06-21 11:24:30 +09:00
tom5079
2b553d1116 Changed Title/Subtitle 2020-06-20 23:32:38 +09:00
tom5079
567eec8bc5 Added Fingerprint Lock 2020-06-20 23:23:07 +09:00
tom5079
293ca5b31d Added PIN Lock 2020-06-20 22:48:47 +09:00
Pupil
0d0f2bd827 Merge pull request #85 from tom5079/Pupil-84
Pupil-84 Title doesn't show up when using hiyobi
2020-06-20 15:28:37 +09:00
tom5079
5bc4610061 Pupil-84 Title doesn't show up when using hiyobi 2020-06-20 15:28:01 +09:00
Pupil
e6b7c107f2 Merge pull request #83 from tom5079/Pupil-76
Pupil-76
2020-06-20 14:32:26 +09:00
tom5079
51a9bf2570 Pupil-76 Add Page count 2020-06-20 14:30:20 +09:00
Pupil
8385f6f390 Merge pull request #82 from tom5079/Pupil-79
Pupil-79
2020-06-20 14:05:36 +09:00
tom5079
772e9daf57 App Link Updated 2020-06-20 14:04:46 +09:00
Pupil
8adc4405c5 Merge pull request #81 from tom5079/Pupil-78
Pupil-78 Shorten the timout when hiyobi is down
2020-06-20 13:16:57 +09:00
tom5079
349da7aa81 Pupil-78 Shorten the timout when hiyobi is down 2020-06-20 13:14:09 +09:00
tom5079
01a01d481d Version up 2020-06-20 13:13:57 +09:00
tom5079
2f8445fb83 Dependency upgrade 2020-06-19 16:34:05 +09:00
tom5079
b04a5fc150 Dependency upgrade 2020-06-19 15:18:16 +09:00
Pupil
bbe29941df Merge pull request #75 from tom5079/dev
Version 4.17
2020-06-15 08:20:29 +09:00
tom5079
2720e445ea Changed low quality settting to true by default 2020-06-15 08:19:16 +09:00
tom5079
49ba579a59 Random Gallery Added
Changed tag search behavior
Loading time improved for hitomi.la
Bug fixed
2020-06-14 16:53:30 +09:00
Pupil
3198c6cbfd Merge pull request #74 from tom5079/dev
Version 4.15
2020-03-25 19:02:50 -07:00
pupil
b3feee6d9d Fast scroll Added
Not able to download after finished downloading to cache Fixed
Trying to move files from different threads causing exceptions Fixed
Import old galleries Added
2020-03-25 10:29:05 -07:00
Pupil
f0f53e6bce Fixed - Jump to page in Reader not working 2020-03-02 20:30:26 +09:00
Pupil
24486d13f2 Bug fix
Memory usage optimization
2020-02-29 13:23:37 +09:00
Pupil
20bc9461de Merge branch 'dev_rescued' 2020-02-29 09:43:36 +09:00
Pupil
c8e94cc295 Build tool update 2020-02-29 09:09:51 +09:00
Pupil
b2bfb0c237 Bug fix
Update downloader changed to DownloadManager
Fixed wierd download path
Fixed Crash on MainActivity
Fixed Crash when non-integer is inputted as Gallery ID

Version 4.13
2020-02-27 19:16:38 +09:00
Pupil
0a003da724 Bug fix 2020-02-26 09:51:16 +09:00
Pupil
b4f2a33016 Merge pull request #72 from tom5079/dev
Modified proguard rules to fix error occurs on Android 4
2020-02-25 22:02:48 +09:00
Pupil
ee7ede2885 Modified proguard rules to fix error occurs on Android 4 2020-02-25 21:50:36 +09:00
142 changed files with 2466 additions and 646 deletions

1
.idea/gradle.xml generated
View File

@@ -4,7 +4,6 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="delegatedBuild" value="false" />
<option name="testRunner" value="PLATFORM" /> <option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />

55
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="http://guardian.github.com/maven/repo-releases" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://guardian.github.com/maven/repo-releases" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://s3.amazonaws.com/fabric-artifacts-private/internal-snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="maven4" />
<option name="name" value="maven4" />
<option name="url" value="https://maven.fabric.io/public" />
</remote-repository>
</component>
</project>

View File

@@ -4,7 +4,6 @@
*Pupil, Hitomi.la viewer for Android* *Pupil, Hitomi.la viewer for Android*
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
I can speak English, Japanese and Korean. If you have any questions, head over to my discord server or DM me!
# Screenshot # Screenshot
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true) ![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true)

View File

@@ -7,7 +7,7 @@ apply plugin: 'kotlinx-serialization'
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) { if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
logger.lifecycle("Firebase Enabled") logger.lifecycle("Firebase Enabled")
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric' apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.google.firebase.firebase-perf' apply plugin: 'com.google.firebase.firebase-perf'
} else { } else {
logger.lifecycle("Firebase Disabled") logger.lifecycle("Firebase Disabled")
@@ -19,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 47 versionCode 57
versionName "4.11" versionName "4.20-hotfix1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -35,9 +35,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
release { release {
minifyEnabled true
shrinkResources true
buildConfigField('Boolean', 'CENSOR', 'false') buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
@@ -49,40 +46,44 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
buildToolsVersion = '29.0.2' buildToolsVersion = '29.0.3'
} }
dependencies { dependencies {
def markwonVersion = "3.0.1" def markwonVersion = '3.1.0'
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.biometric:biometric:1.0.1" implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3' implementation 'androidx.multidex:multidex:2.0.1'
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.2.0-alpha05' implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation 'com.google.firebase:firebase-core:17.2.2' implementation 'com.google.firebase:firebase-core:17.4.4'
implementation 'com.google.firebase:firebase-perf:19.0.5' implementation 'com.google.firebase:firebase-analytics:17.4.4'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.google.firebase:firebase-crashlytics:17.1.1'
implementation 'com.google.firebase:firebase-perf:19.0.8'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
implementation 'com.github.bumptech.glide:annotations:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0' kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false transitive = false
} }
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
implementation 'net.rdrei.android.dirchooser:library:3.2@aar' implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'

Binary file not shown.

View File

@@ -23,8 +23,16 @@
-dontobfuscate -dontobfuscate
-keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule -keep class * extends com.bumptech.glide.module.AppGlideModule {
<init>(...);
}
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { -keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES; **[] $VALUES;
public *; public *;
} }
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
*** rewind();
}
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl

View File

@@ -0,0 +1,20 @@
{
"version": 1,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "xyz.quaver.pupil",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 57,
"versionName": "4.20-hotfix1",
"enabled": true,
"outputFile": "app-release.apk"
}
]
}

View File

@@ -1 +0,0 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":47,"versionName":"4.11","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}]

View File

@@ -21,13 +21,13 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
@@ -35,6 +35,8 @@ import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import java.io.InputStreamReader
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -57,11 +59,20 @@ class ExampleInstrumentedTest {
val activityTestRule = ActivityTestRule(LockActivity::class.java) val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
ContextCompat.getExternalFilesDirs(appContext, null).forEachIndexed { index, file -> Runtime.getRuntime().exec("du -hs " + getDownloadDirectory(appContext)).let {
Log.i("PUPILD", "$index: ${file?.absolutePath}") 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 @Test
fun test_doSearch() { fun test_doSearch() {
val reader = getReader( 1426382) val reader = getReader( 1426382)
@@ -91,7 +102,7 @@ class ExampleInstrumentedTest {
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) { while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null") Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) if (worker.progress[galleryID]?.all { it.isInfinite() } == true)
break break
} }

View File

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application <application
@@ -20,7 +21,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:replace="android:theme" tools:replace="android:theme"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true"
tools:ignore="UnusedAttribute">
<provider <provider
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -34,6 +36,12 @@
</provider> </provider>
<receiver android:name=".BroadcastReciever" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</receiver>
<activity android:name=".ui.LockActivity" /> <activity android:name=".ui.LockActivity" />
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"
@@ -45,6 +53,61 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/galleries"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/manga"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/doujinshi"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/cg"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/reader"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="hitomi.la" android:host="hitomi.la"
android:pathPrefix="/galleries" android:pathPrefix="/galleries"
@@ -56,6 +119,61 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/manga"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/doujinshi"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/cg"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/reader"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hiyobi.me"
android:scheme="http"
android:pathPrefix="/reader" />
</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 <data
android:host="hiyobi.me" android:host="hiyobi.me"
android:pathPrefix="/reader" android:pathPrefix="/reader"
@@ -70,17 +188,6 @@
<data <data
android:host="e-hentai.org" android:host="e-hentai.org"
android:pathPrefix="/g" android:pathPrefix="/g"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/galleries"
android:scheme="http" /> android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
@@ -89,21 +196,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hiyobi.me"
android:scheme="http"
android:pathPrefix="/reader" />
</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 <data
android:host="e-hentai.org" android:host="e-hentai.org"
android:pathPrefix="/g" android:pathPrefix="/g"
android:scheme="http" /> android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity

View File

@@ -0,0 +1,35 @@
/*
* 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 com.arlib.floatingsearchview
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
class FloatingSearchViewDayNight @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null)
: FloatingSearchView(context, attrs) {
// hack to remove color attributes which should not be reused
override fun onSaveInstanceState(): Parcelable? {
super.onSaveInstanceState()
return null
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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
import android.app.DownloadManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
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 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"
}
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
when (intent?.action) {
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
// Validate download
val preference = PreferenceManager.getDefaultSharedPreferences(context)
val downloadID = preference.getLong("update_download_id", -1)
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadID)
return
// Get target uri
val query = DownloadManager.Query()
.setFilterById(downloadID)
val uri = downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
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
}
}
}
// Build Notification
val notificationManager = NotificationManagerCompat.from(context)
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}, 0)
val notification = NotificationCompat.Builder(context, "update")
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(context.getText(R.string.update_download_completed))
.setContentText(context.getText(R.string.update_download_completed_description))
.setContentIntent(pendingIntent)
.build()
notificationManager.notify(NOTIFICATION_ID_UPDATE, notification)
}
ACTION_CANCEL_IMPORT -> {
cancelImport = true
}
}
}
}

View File

@@ -31,10 +31,12 @@ import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import xyz.quaver.proxy import xyz.quaver.proxy
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getProxy import xyz.quaver.pupil.util.getProxy
import java.io.File import java.io.File
import java.util.*
class Pupil : MultiDexApplication() { class Pupil : MultiDexApplication() {
@@ -48,10 +50,23 @@ class Pupil : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val userID =
if (preference.getString("user_id", "").isNullOrEmpty()) {
UUID.randomUUID().toString().also {
preference.edit().putString("user_id", it).apply()
}
} else
preference.getString("user_id", "") ?: ""
FirebaseCrashlytics.getInstance().setUserId(userID)
proxy = getProxy(this) proxy = getProxy(this)
try { try {
preference.getString("dl_location", null) preference.getString("dl_location", null).also {
if (!File(it!!).canWrite())
throw Exception()
}
} catch (e: Exception) { } catch (e: Exception) {
preference.edit().remove("dl_location").apply() preference.edit().remove("dl_location").apply()
} }
@@ -72,13 +87,27 @@ class Pupil : MultiDexApplication() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_download_description) description = getString(R.string.channel_download_description)
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET lockscreenVisibility = Notification.VISIBILITY_SECRET
} })
manager.createNotificationChannel(channel)
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
description = getString(R.string.channel_update_description)
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
description = getString(R.string.channel_update_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
} }
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)

View File

@@ -0,0 +1,42 @@
/*
* 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
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
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
class PupilGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.append(
GlideUrl::class.java,
InputStream::class.java,
OkHttpUrlLoader.Factory(DownloadWorker.getInstance(context).client)
)
}
}

View File

@@ -28,11 +28,12 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
@@ -42,7 +43,9 @@ import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -54,7 +57,7 @@ import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface { class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType { enum class ViewType {
NEXT, NEXT,
@@ -62,7 +65,6 @@ class GalleryBlockAdapter(private val context: Context, private val galleries: L
PREV PREV
} }
private val glide = Glide.with(context)
private lateinit var favorites: Histories private lateinit var favorites: Histories
val timer = Timer() val timer = Timer()
@@ -76,7 +78,7 @@ class GalleryBlockAdapter(private val context: Context, private val galleries: L
val reader = Cache(context).getReaderOrNull(galleryID) val reader = Cache(context).getReaderOrNull(galleryID)
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (reader == null) { if (reader == null || PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false)) {
view.galleryblock_progressbar.visibility = View.GONE view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE view.galleryblock_progress_complete.visibility = View.GONE
return@launch return@launch
@@ -219,12 +221,12 @@ class GalleryBlockAdapter(private val context: Context, private val galleries: L
"male" -> { "male" -> {
setChipBackgroundColorResource(R.color.material_blue_700) setChipBackgroundColorResource(R.color.material_blue_700)
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white) ContextCompat.getDrawable(context, R.drawable.gender_male)
} }
"female" -> { "female" -> {
setChipBackgroundColorResource(R.color.material_pink_600) setChipBackgroundColorResource(R.color.material_pink_600)
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white) ContextCompat.getDrawable(context, R.drawable.gender_female)
} }
else -> null else -> null
} }
@@ -238,6 +240,15 @@ class GalleryBlockAdapter(private val context: Context, private val galleries: L
} }
galleryblock_id.text = galleryBlock.id.toString() galleryblock_id.text = galleryBlock.id.toString()
galleryblock_pagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching {
getReader(galleryBlock.id).galleryInfo.files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
galleryblock_pagecount.text = context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
if (!::favorites.isInitialized) if (!::favorites.isInitialized)
favorites = (context.applicationContext as Pupil).favorites favorites = (context.applicationContext as Pupil).favorites
@@ -353,7 +364,7 @@ class GalleryBlockAdapter(private val context: Context, private val galleries: L
mItemManger.closeAllExcept(layout) mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = holder.view.galleryblock_download.text =
if (Cache(context).isDownloading(gallery.id)) if (Cache(holder.view.context).isDownloading(gallery.id))
holder.view.context.getString(android.R.string.cancel) holder.view.context.getString(android.R.string.cancel)
else else
holder.view.context.getString(R.string.main_download) holder.view.context.getString(R.string.main_download)

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
@@ -60,6 +61,7 @@ class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewH
var onStartDrag : ((ViewHolder) -> Unit)? = null var onStartDrag : ((ViewHolder) -> Unit)? = null
var onItemMoved : ((List<String>) -> (Unit))? = null var onItemMoved : ((List<String>) -> (Unit))? = null
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder.view) { with(holder.view) {
mirror_name.text = mirrors[list.elementAt(position)] mirror_name.text = mirrors[list.elementAt(position)]

View File

@@ -18,61 +18,38 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.Context import android.app.Activity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager
import com.bumptech.glide.ListPreloader import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader import com.bumptech.glide.load.model.LazyHeaders
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.* import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.Code
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig 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.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ReaderAdapter(private val context: Context, class ReaderAdapter(private val glide: RequestManager,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { private val galleryID: Int,
private val activity: Activity) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
val glide = Glide.with(context)
//region Glide.RecyclerView
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.getOrNull(position)?.let {
arrayOf(it.width, it.height).toIntArray()
}
}
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
override fun getPreloadItems(position: Int): MutableList<File> {
return listOf(Cache(context).getImages(galleryID)?.getOrNull(position)).filterNotNullTo(mutableListOf())
}
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
return glide
.load(item)
.fitCenter()
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
}
}
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
//endregion
var reader: Reader? = null var reader: Reader? = null
val timer = Timer() val timer = Timer()
@@ -83,6 +60,8 @@ class ReaderAdapter(private val context: Context,
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
var downloadWorker: DownloadWorker? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate( return LayoutInflater.from(parent.context).inflate(
R.layout.item_reader, parent, false R.layout.item_reader, parent, false
@@ -94,6 +73,9 @@ class ReaderAdapter(private val context: Context,
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout holder.view as ConstraintLayout
if (downloadWorker == null)
downloadWorker = DownloadWorker.getInstance(holder.view.context)
if (isFullScreen) { if (isFullScreen) {
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
@@ -115,35 +97,57 @@ class ReaderAdapter(private val context: Context,
holder.view.reader_index.text = (position+1).toString() holder.view.reader_index.text = (position+1).toString()
val images = Cache(context).getImage(galleryID, position) val preferences = PreferenceManager.getDefaultSharedPreferences(holder.view.context)
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position) if (preferences.getBoolean("cache_disable", false)) {
val lowQuality = preferences.getBoolean("low_quality", false)
if (progress?.isInfinite() == true && images != null) {
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
val url = when (reader!!.code) {
Code.HITOMI ->
GlideUrl(
imageUrlFromImage(
galleryID,
reader!!.galleryInfo.files[position],
!lowQuality
)
, 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())
else -> null
}
holder.view.image.post { holder.view.image.post {
glide glide
.load(images) .load(url!!)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.fitCenter() .fitCenter()
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.into(holder.view.image) .into(holder.view.image)
} }
} else { } else {
holder.view.reader_item_progressbar.visibility = View.VISIBLE val image = Cache(holder.view.context).getImage(galleryID, position)
val progress = downloadWorker!!.progress[galleryID]?.get(position)
glide.clear(holder.view.image) if (progress?.isInfinite() == true && image != null) {
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
if (progress?.isNaN() == true) { holder.view.image.post {
if (Fabric.isInitialized()) glide
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position)) .load(image)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.fitCenter()
.error(R.drawable.image_broken_variant)
.into(holder.view.image)
}
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
return
} else { } else {
holder.view.reader_item_progressbar.visibility = View.VISIBLE
glide.clear(holder.view.image)
holder.view.reader_item_progressbar.progress = holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true) if (progress?.isInfinite() == true)
100 100
@@ -151,11 +155,11 @@ class ReaderAdapter(private val context: Context,
progress?.roundToInt() ?: 0 progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null) holder.view.image.setImageDrawable(null)
}
timer.schedule(1000) { timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
notifyItemChanged(position) notifyItemChanged(position)
}
} }
} }
} }

View File

@@ -22,9 +22,10 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() { class ThumbnailAdapter(private val glide: RequestManager, var thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view) class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
@@ -35,6 +36,7 @@ class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
glide glide
.load(thumbnails[position]) .load(thumbnails[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
override(5, 8) override(5, 8)

View File

@@ -0,0 +1,50 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager
import kotlin.math.min
class ThumbnailPageAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailPageAdapter.ViewHolder>() {
class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(RecyclerView(parent.context).apply {
layoutManager = GridLayoutManager(parent.context, 3)
adapter = ThumbnailAdapter(glide, listOf())
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
})
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
(holder.view.adapter as ThumbnailAdapter).apply {
thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size))
notifyDataSetChanged()
holder.view.layoutManager?.scrollToPosition(itemCount-1)
}
}
override fun getItemCount() = if (thumbnails.isEmpty()) 0 else thumbnails.size/9 + if (thumbnails.size%9 != 0) 1 else 0
}

View File

@@ -21,23 +21,157 @@ package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.appcompat.app.AppCompatActivity 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.andrognito.patternlockview.PatternLockView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_lock.* import kotlinx.android.synthetic.main.activity_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pin_lock.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.PINLockFragment
import xyz.quaver.pupil.ui.fragment.PatternLockFragment import xyz.quaver.pupil.ui.fragment.PatternLockFragment
import xyz.quaver.pupil.util.Lock import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.LockManager
class LockActivity : AppCompatActivity() { class LockActivity : AppCompatActivity() {
private lateinit var lockManager: LockManager
private var mode: String? = null
private val patternLockFragment = PatternLockFragment().apply {
var lastPass = ""
onPatternDrawn = {
when(mode) {
null -> {
val result = lockManager.check(it)
if (result == true) {
setResult(Activity.RESULT_OK)
finish()
} else
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
}
"add_lock" -> {
if (lastPass.isEmpty()) {
lastPass = it
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
} else {
if (lastPass == it) {
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
finish()
} else {
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
lastPass = ""
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
}
}
}
}
}
}
private val pinLockFragment = PINLockFragment().apply {
var lastPass = ""
onPINEntered = {
when(mode) {
null -> {
val result = lockManager.check(it)
if (result == true) {
setResult(Activity.RESULT_OK)
finish()
} else {
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) {
pin_lock_view.resetPinLockView()
pin_lock_view.isEnabled = true
}
override fun onAnimationStart(animation: Animation?) {
pin_lock_view.isEnabled = false
}
override fun onAnimationRepeat(animation: Animation?) {
// Do Nothing
}
})
})
}
}
"add_lock" -> {
if (lastPass.isEmpty()) {
lastPass = it
pin_lock_view.resetPinLockView()
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
} else {
if (lastPass == it) {
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
finish()
} else {
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) {
pin_lock_view.resetPinLockView()
pin_lock_view.isEnabled = true
}
override fun onAnimationStart(animation: Animation?) {
pin_lock_view.isEnabled = false
}
override fun onAnimationRepeat(animation: Animation?) {
// Do Nothing
}
})
})
lastPass = ""
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
}
}
}
}
}
}
private fun showBiometricPrompt() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(getText(R.string.settings_lock_fingerprint_prompt))
.setSubtitle(getText(R.string.settings_lock_fingerprint_prompt_subtitle))
.setNegativeButtonText(getText(android.R.string.cancel))
.setConfirmationRequired(false)
.build()
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
setResult(RESULT_OK)
finish()
return
}
})
// Displays the "log in" prompt.
biometricPrompt.authenticate(promptInfo)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock) setContentView(R.layout.activity_lock)
val lockManager = try { lockManager = try {
LockManager(this) LockManager(this)
} catch (e: Exception) { } catch (e: Exception) {
AlertDialog.Builder(this).apply { AlertDialog.Builder(this).apply {
@@ -50,12 +184,7 @@ class LockActivity : AppCompatActivity() {
return return
} }
val mode = intent.getStringExtra("mode") mode = intent.getStringExtra("mode")
lock_pattern.isEnabled = false
lock_pin.isEnabled = false
lock_fingerprint.isEnabled = false
lock_password.isEnabled = false
when(mode) { when(mode) {
null -> { null -> {
@@ -64,52 +193,75 @@ class LockActivity : AppCompatActivity() {
finish() finish()
return return
} }
if (
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lock_fingerprint", false)
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
) {
lock_fingerprint.apply {
isEnabled = true
setOnClickListener {
showBiometricPrompt()
}
}
showBiometricPrompt()
}
lock_pattern.apply {
isEnabled = lockManager.contains(Lock.Type.PATTERN)
setOnClickListener {
supportFragmentManager.beginTransaction().replace(
R.id.lock_content, patternLockFragment
).commit()
}
}
lock_pin.apply {
isEnabled = lockManager.contains(Lock.Type.PIN)
setOnClickListener {
supportFragmentManager.beginTransaction().replace(
R.id.lock_content, pinLockFragment
).commit()
}
}
lock_password.isEnabled = false
when (lockManager.locks!!.first().type) {
Lock.Type.PIN -> {
supportFragmentManager.beginTransaction().add(
R.id.lock_content, pinLockFragment
).commit()
}
Lock.Type.PATTERN -> {
supportFragmentManager.beginTransaction().add(
R.id.lock_content, patternLockFragment
).commit()
}
else -> return
}
} }
"add_lock" -> { "add_lock" -> {
lock_pattern.isEnabled = false
lock_pin.isEnabled = false
lock_fingerprint.isEnabled = false
lock_password.isEnabled = false
when(intent.getStringExtra("type")!!) { when(intent.getStringExtra("type")!!) {
"pattern" -> { "pattern" -> {
lock_pattern.isEnabled = true
supportFragmentManager.beginTransaction().add(
R.id.lock_content, patternLockFragment
).commit()
}
"pin" -> {
lock_pin.isEnabled = true
supportFragmentManager.beginTransaction().add(
R.id.lock_content, pinLockFragment
).commit()
} }
} }
} }
} }
supportFragmentManager.beginTransaction().add(
R.id.lock_content,
PatternLockFragment().apply {
var lastPass = ""
onPatternDrawn = {
when(mode) {
null -> {
val result = lockManager.check(it)
if (result == true) {
setResult(Activity.RESULT_OK)
finish()
} else
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
}
"add_lock" -> {
if (lastPass.isEmpty()) {
lastPass = it
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
} else {
if (lastPass == it) {
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
finish()
} else {
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
lastPass = ""
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
}
}
}
}
}
}
).commit()
} }
} }

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
@@ -25,15 +26,13 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.* import android.text.*
import android.text.style.AlignmentSpan import android.text.style.AlignmentSpan
import android.util.TypedValue
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
@@ -43,15 +42,16 @@ import androidx.core.view.GravityCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import com.arlib.floatingsearchview.util.view.SearchInputView
import com.crashlytics.android.Crashlytics import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import io.fabric.sdk.android.Fabric import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.list import kotlinx.serialization.builtins.list
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi import xyz.quaver.hitomi.getGalleryIDsFromNozomi
@@ -98,6 +98,7 @@ class MainActivity : AppCompatActivity() {
setText(query, TextView.BufferType.EDITABLE) setText(query, TextView.BufferType.EDITABLE)
} }
} }
private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST private var sortMode = SortMode.NEWEST
@@ -159,11 +160,12 @@ class MainActivity : AppCompatActivity() {
initView() initView()
} }
@OptIn(ExperimentalStdlibApi::class)
override fun onBackPressed() { override fun onBackPressed() {
when { when {
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START) main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
query.isNotEmpty() -> runOnUiThread { queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = "" query = queryStack.last()
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -195,7 +197,7 @@ class MainActivity : AppCompatActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")!!.toInt() val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) { return when(keyCode) {
@@ -278,6 +280,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.SEARCH mode = Mode.SEARCH
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -287,6 +290,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.HISTORY mode = Mode.HISTORY
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -296,6 +300,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.DOWNLOAD mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -305,6 +310,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.FAVORITE mode = Mode.FAVORITE
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -341,7 +347,7 @@ class MainActivity : AppCompatActivity() {
setImageResource(R.drawable.ic_jump) setImageResource(R.drawable.ic_jump)
setOnClickListener { setOnClickListener {
val preference = PreferenceManager.getDefaultSharedPreferences(context) val preference = PreferenceManager.getDefaultSharedPreferences(context)
val perPage = preference.getString("per_page", "25")!!.toInt() val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
val editText = EditText(context) val editText = EditText(context)
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
@@ -366,17 +372,53 @@ class MainActivity : AppCompatActivity() {
} }
} }
with(main_fab_random) {
setImageResource(R.drawable.shuffle_variant)
setOnClickListener {
runBlocking {
withTimeoutOrNull(100) {
galleryIDs?.await()
}
}.let {
if (it?.isEmpty() == false) {
val galleryID = it.random()
GalleryDialog(
this@MainActivity,
Glide.with(this@MainActivity),
galleryID
).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
dismiss()
}
}.show()
}
}
}
}
with(main_fab_id) { with(main_fab_id) {
setImageResource(R.drawable.numeric) setImageResource(R.drawable.numeric)
setOnClickListener { setOnClickListener {
val editText = EditText(context) val editText = EditText(context).apply {
inputType = InputType.TYPE_CLASS_NUMBER
}
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setView(editText) setView(editText)
setTitle(R.string.main_open_gallery_by_id) setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toInt() val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply { val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID) putExtra("galleryID", galleryID)
} }
@@ -395,9 +437,10 @@ class MainActivity : AppCompatActivity() {
loadBlocks() loadBlocks()
} }
@SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(main_recyclerview) { with(main_recyclerview) {
adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply { adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
query = it.toQuery() query = it.toQuery()
@@ -411,17 +454,18 @@ class MainActivity : AppCompatActivity() {
} }
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { position ->
val galleryID = galleries[position].id val galleryID = galleries[position].id
val worker = DownloadWorker.getInstance(context)
if (!completeFlag.get(galleryID, false)) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
val worker = DownloadWorker.getInstance(context) Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else {
if (Cache(context).isDownloading(galleryID)) //download in progress if (worker.progress.indexOfKey(galleryID) >= 0 && Cache(context).isDownloading(galleryID)) { //download in progress
Cache(context).setDownloading(galleryID, false)
worker.cancel(galleryID) worker.cancel(galleryID)
}
else { else {
Cache(context).setDownloading(galleryID, true) Cache(context).setDownloading(galleryID, true)
if (!worker.queue.contains(galleryID)) worker.queue.add(galleryID)
worker.queue.add(galleryID)
} }
} }
@@ -474,6 +518,7 @@ class MainActivity : AppCompatActivity() {
GalleryDialog( GalleryDialog(
this@MainActivity, this@MainActivity,
Glide.with(this@MainActivity),
galleryID galleryID
).apply { ).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
@@ -719,7 +764,7 @@ class MainActivity : AppCompatActivity() {
}) })
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
with(main_searchview as FloatingSearchView) { with(main_searchview as FloatingSearchViewDayNight) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json") val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
val serializer = Tag.serializer().list val serializer = Tag.serializer().list
@@ -728,6 +773,16 @@ class MainActivity : AppCompatActivity() {
favoritesFile.writeText(json.stringify(serializer, Tags(listOf()))) favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
} }
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
override fun onMenuOpened() {
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
}
override fun onMenuClosed() {
//Do Nothing
}
})
setOnMenuItemClickListener { setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS) R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
@@ -808,20 +863,23 @@ class MainActivity : AppCompatActivity() {
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}" val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
val color = TypedValue()
theme.resolveAttribute(R.attr.colorControlNormal, color, true)
leftIcon.setImageDrawable( leftIcon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
resources, resources,
when(item.n) { when(item.n) {
"female" -> R.drawable.ic_gender_female "female" -> R.drawable.gender_female
"male" -> R.drawable.ic_gender_male "male" -> R.drawable.gender_male
"language" -> R.drawable.ic_translate "language" -> R.drawable.translate
"group" -> R.drawable.ic_account_group "group" -> R.drawable.account_group
"character" -> R.drawable.ic_account_star "character" -> R.drawable.account_star
"series" -> R.drawable.ic_book_open "series" -> R.drawable.book_open
"artist" -> R.drawable.ic_brush "artist" -> R.drawable.brush
else -> R.drawable.ic_tag else -> R.drawable.tag
}, },
null) context.theme)
) )
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) { with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
@@ -938,6 +996,11 @@ class MainActivity : AppCompatActivity() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val defaultQuery = preference.getString("default_query", "")!! val defaultQuery = preference.getString("default_query", "")!!
if (query != queryStack.lastOrNull()) {
queryStack.remove(query)
queryStack.add(query)
}
galleryIDs = null galleryIDs = null
if (galleryIDs?.isActive == true) if (galleryIDs?.isActive == true)
@@ -1025,8 +1088,8 @@ class MainActivity : AppCompatActivity() {
} }
} catch (e: Exception) { } catch (e: Exception) {
if (Fabric.isInitialized() && e.message != "No result") if (e.message != "No result")
Crashlytics.logException(e) FirebaseCrashlytics.getInstance().recordException(e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
main_noresult.visibility = View.VISIBLE main_noresult.visibility = View.VISIBLE

View File

@@ -23,6 +23,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -32,12 +33,15 @@ import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.crashlytics.android.Crashlytics import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.fabric.sdk.android.Fabric import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.* import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.* import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -90,8 +94,7 @@ class ReaderActivity : AppCompatActivity() {
handleIntent(intent) handleIntent(intent)
if (Fabric.isInitialized()) FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
Crashlytics.setInt("GalleryID", galleryID)
if (galleryID == 0) { if (galleryID == 0) {
onBackPressed() onBackPressed()
@@ -99,7 +102,36 @@ class ReaderActivity : AppCompatActivity() {
} }
initView() initView()
initDownloader() if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("cache_disable", false)) {
reader_download_progressbar.visibility = View.GONE
CoroutineScope(Dispatchers.IO).launch {
val reader = Cache(this@ReaderActivity).getReader(galleryID)
launch(Dispatchers.Main) initDownloader@{
if (reader == null) {
Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
.show()
return@initDownloader
}
(reader_recyclerview.adapter as ReaderAdapter).apply {
this.reader = reader
notifyDataSetChanged()
}
title = reader.galleryInfo.title ?: ""
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
when (reader.code) {
Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
})
}
}
} else
initDownloader()
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@@ -112,14 +144,12 @@ class ReaderActivity : AppCompatActivity() {
val uri = intent.data val uri = intent.data
val lastPathSegment = uri?.lastPathSegment val lastPathSegment = uri?.lastPathSegment
if (uri != null && lastPathSegment != null) { if (uri != null && lastPathSegment != null) {
val nonNumber = Regex("[^-?0-9]+")
galleryID = when (uri.host) { galleryID = when (uri.host) {
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt() "hitomi.la" ->
"히요비.asia" -> lastPathSegment.toInt() Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt() "hiyobi.me" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt() "e-hentai.org" -> uri.pathSegments[1].toInt()
else -> return else -> 0
} }
} }
} else { } else {
@@ -157,10 +187,10 @@ class ReaderActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId) { when(item?.itemId) {
R.id.reader_menu_page_indicator -> { R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
with(view.dialog_number_picker) { with(view.dialog_number_picker) {
minValue=1 minValue=1
maxValue=reader_recyclerview?.adapter?.itemCount ?: 0 maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
value=currentPage value=currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
@@ -234,6 +264,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initDownloader() { private fun initDownloader() {
val worker = DownloadWorker.getInstance(this).apply { val worker = DownloadWorker.getInstance(this).apply {
cancel(galleryID)
queue.add(galleryID) queue.add(galleryID)
} }
@@ -250,7 +281,7 @@ class ReaderActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress = worker.progress[galleryID]?.count { !it.isFinite() } ?: 0 reader_download_progressbar.progress = worker.progress[galleryID]?.count { it.isInfinite() } ?: 0
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
@@ -275,7 +306,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished if (worker.progress[galleryID]?.all { it.isInfinite() } == true) { //Download finished
reader_download_progressbar.visibility = View.GONE reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false) animateDownloadFAB(false)
@@ -286,7 +317,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply { adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, this@ReaderActivity).apply {
onItemClickListener = { onItemClickListener = {
if (isScroll) { if (isScroll) {
isScroll = false isScroll = false
@@ -300,7 +331,6 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() { addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@@ -325,13 +355,27 @@ class ReaderActivity : AppCompatActivity() {
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
setOnClickListener { setOnClickListener {
if (Cache(context).isDownloading(galleryID)) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
Cache(context).setDownloading(galleryID, 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)
animateDownloadFAB(false) animateDownloadFAB(false)
} else { } else {
Cache(context).setDownloading(galleryID, true) Cache(context).setDownloading(galleryID, true)
animateDownloadFAB(true) animateDownloadFAB(true)
}
}
}
}
with(reader_fab_retry) {
setImageResource(R.drawable.refresh)
setOnClickListener {
DownloadWorker.getInstance(context).let {
it.cancel(galleryID)
it.queue.add(galleryID)
} }
} }
} }
@@ -385,7 +429,7 @@ class ReaderActivity : AppCompatActivity() {
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() { icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) { override fun onAnimationEnd(drawable: Drawable?) {
val worker = DownloadWorker.getInstance(context) val worker = DownloadWorker.getInstance(context)
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating if (worker.progress[galleryID]?.all { it.isInfinite() } == true) // If download is finished, stop animating
post { post {
setImageResource(R.drawable.ic_download) setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download_cancel) labelText = getString(R.string.reader_fab_download_cancel)

View File

@@ -30,12 +30,12 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.* import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.list import kotlinx.serialization.builtins.list
import kotlinx.serialization.serializer import kotlinx.serialization.builtins.serializer
import net.rdrei.android.dirchooser.DirectoryChooserActivity import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.LockFragment import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
import xyz.quaver.pupil.ui.fragment.SettingsFragment import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
@@ -84,7 +84,7 @@ class SettingsActivity : AppCompatActivity() {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.settings, LockFragment()) .replace(R.id.settings, LockSettingsFragment())
.addToBackStack("Lock") .addToBackStack("Lock")
.commitAllowingStateLoss() .commitAllowingStateLoss()
} }
@@ -156,6 +156,43 @@ class SettingsActivity : AppCompatActivity() {
.apply() .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) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }

View File

@@ -45,6 +45,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val excludeBL = "-male:yaoi" private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro") private val excludeGuro = listOf("-female:guro", "-male:guro")
private val excludeLoli = listOf("-female:loli", "-male:shota")
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@@ -68,6 +69,11 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
newTags.add(tag) newTags.add(tag)
} }
if (default_query_dialog_loli_checkbox.isChecked)
excludeLoli.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags) onPositiveButtonClickListener?.invoke(newTags)
} }
@@ -120,6 +126,14 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
} }
with(view.default_query_dialog_loli_checkbox) {
isChecked = excludeLoli.all { tags.contains(it) }
if (excludeLoli.all { tags.contains(it) })
excludeLoli.forEach {
tags.remove(it)
}
}
with(view.default_query_dialog_edittext) { with(view.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE) setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {

View File

@@ -26,14 +26,15 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.RequestManager
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.* import kotlinx.android.synthetic.main.dialog_gallery.*
import kotlinx.android.synthetic.main.gallery_details.view.* import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.* import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -46,14 +47,14 @@ import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailAdapter import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) { class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map { private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split -> it.split("|").let { split ->
@@ -61,8 +62,6 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
} }
}.toMap() }.toMap()
private val glide = Glide.with(context)
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -90,7 +89,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
try { try {
val gallery = getGallery(galleryID) val gallery = getGallery(galleryID)
launch(Dispatchers.Main) { gallery_cover.post {
gallery_progressbar.visibility = View.GONE gallery_progressbar.visibility = View.GONE
gallery_title.text = gallery.title gallery_title.text = gallery.title
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() } gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
@@ -112,7 +111,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
} }
} }
Glide.with(context) glide
.load(gallery.cover) .load(gallery.cover)
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
@@ -132,7 +131,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
private fun addDetails(gallery: Gallery) { private fun addDetails(gallery: Gallery) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_details) gallery_details.setText(R.string.gallery_details)
listOf( listOf(
@@ -171,12 +170,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
"male" -> { "male" -> {
setChipBackgroundColorResource(R.color.material_blue_700) setChipBackgroundColorResource(R.color.material_blue_700)
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white) ContextCompat.getDrawable(context, R.drawable.gender_male)
} }
"female" -> { "female" -> {
setChipBackgroundColorResource(R.color.material_pink_600) setChipBackgroundColorResource(R.color.material_pink_600)
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white) ContextCompat.getDrawable(context, R.drawable.gender_female)
} }
else -> null else -> null
} }
@@ -208,15 +207,21 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
private fun addThumbnails(gallery: Gallery) { private fun addThumbnails(gallery: Gallery) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_thumbnails) gallery_details.setText(R.string.gallery_thumbnails)
RecyclerView(context).apply { val pager = ViewPager2(context).apply {
layoutManager = GridLayoutManager(context, 3) adapter = ThumbnailPageAdapter(glide, gallery.thumbnails)
adapter = ThumbnailAdapter(glide, gallery.thumbnails)
}.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
} }
gallery_details_contents.addView(
pager,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
)
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
gallery_dotindicator.setViewPager2(pager)
}.let { }.let {
gallery_contents.addView(it) gallery_contents.addView(it)
} }
@@ -226,7 +231,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val galleries = ArrayList<GalleryBlock>() val galleries = ArrayList<GalleryBlock>()
val adapter = GalleryBlockAdapter(context, galleries).apply { val adapter = GalleryBlockAdapter(glide, galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler -> this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
@@ -247,7 +252,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
} }
} }
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_related) gallery_details.setText(R.string.gallery_related)
RecyclerView(context).apply { RecyclerView(context).apply {
@@ -264,6 +269,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
.setOnItemLongClickListener { _, position, _ -> .setOnItemLongClickListener { _, position, _ ->
GalleryDialog( GalleryDialog(
context, context,
glide,
galleries[position].id galleries[position].id
).apply { ).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->

View File

@@ -1,81 +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.fragment
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager
class LockFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("lock_pattern")?.summary =
if (lockManager.contains(Lock.Type.PATTERN))
getString(R.string.settings_lock_enabled)
else
""
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
with(findPreference<Preference>("lock_pattern")) {
this!!
if (LockManager(context!!).contains(Lock.Type.PATTERN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(context!!)
if (lockManager.contains(Lock.Type.PATTERN)) {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(context, LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pattern")
}
startActivity(intent)
}
true
}
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.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
class LockSettingsFragment :
PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(requireContext())
findPreference<Preference>("lock_pattern")?.summary =
if (lockManager.contains(Lock.Type.PATTERN))
getString(R.string.settings_lock_enabled)
else
""
findPreference<Preference>("lock_pin")?.summary =
if (lockManager.contains(Lock.Type.PIN))
getString(R.string.settings_lock_enabled)
else
""
if (lockManager.isEmpty()) {
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("lock_fingerprint", false).apply()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
with(findPreference<Preference>("lock_pattern")) {
this!!
if (LockManager(requireContext()).contains(Lock.Type.PATTERN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(requireContext())
if (lockManager.contains(Lock.Type.PATTERN)) {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(requireContext(), LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pattern")
}
startActivity(intent)
}
true
}
}
with(findPreference<Preference>("lock_pin")) {
this!!
if (LockManager(requireContext()).contains(Lock.Type.PIN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(requireContext())
if (lockManager.contains(Lock.Type.PIN)) {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PIN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(requireContext(), LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pin")
}
startActivity(intent)
}
true
}
}
with(findPreference<Preference>("lock_fingerprint")) {
this!!
setOnPreferenceChangeListener { _, newValue ->
this as SwitchPreferenceCompat
if (newValue == true && LockManager(requireContext()).isEmpty()) {
isChecked = false
Toast.makeText(requireContext(), R.string.settings_lock_fingerprint_without_lock, Toast.LENGTH_SHORT).show()
} else
isChecked = newValue as Boolean
false
}
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.andrognito.pinlockview.PinLockListener
import kotlinx.android.synthetic.main.fragment_pin_lock.view.*
import xyz.quaver.pupil.R
class PINLockFragment : Fragment(), PinLockListener {
var onPINEntered: ((String) -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_pin_lock, container, false).apply {
pin_lock_view.attachIndicatorDots(indicator_dots)
pin_lock_view.setPinLockListener(this@PINLockFragment)
}
}
override fun onComplete(pin: String?) {
onPINEntered?.invoke(pin!!)
}
override fun onEmpty() {
}
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
}
}

View File

@@ -18,17 +18,26 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.content.Intent import android.Manifest
import android.content.SharedPreferences import android.content.*
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar 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 xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
@@ -38,7 +47,9 @@ import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.ui.dialog.ProxyDialog import xyz.quaver.pupil.ui.dialog.ProxyDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.InputStreamReader
class SettingsFragment : class SettingsFragment :
@@ -47,16 +58,12 @@ class SettingsFragment :
Preference.OnPreferenceChangeListener, Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreate(savedInstanceState: Bundle?) { lateinit var sharedPreference: SharedPreferences
super.onCreate(savedInstanceState)
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val lockManager = LockManager(context!!) val lockManager = LockManager(requireContext())
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) { findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none) getString(R.string.settings_lock_none)
@@ -72,9 +79,13 @@ class SettingsFragment :
} }
private fun getDirSize(dir: File) : String { private fun getDirSize(dir: File) : String {
val size = dir.walk().map { it.length() }.sum() return getString(R.string.settings_storage_usage,
Runtime.getRuntime().exec("du -hs " + dir.absolutePath).let {
return getString(R.string.settings_clear_summary, byteToString(size)) BufferedReader(InputStreamReader(it.inputStream)).use { reader ->
reader.readLine()?.split('\t')?.firstOrNull() ?: "0"
}
}
)
} }
override fun onPreferenceClick(preference: Preference?): Boolean { override fun onPreferenceClick(preference: Preference?): Boolean {
@@ -86,39 +97,55 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true) checkUpdate(activity as SettingsActivity, true)
} }
"delete_cache" -> { "delete_cache" -> {
val dir = File(context.cacheDir, "imageCache") val dir = File(requireContext().cacheDir, "imageCache")
AlertDialog.Builder(context).apply { AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
setMessage(R.string.settings_clear_cache_alert_message) setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.yes) { _, _ -> setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists()) if (dir.exists())
dir.deleteRecursively() dir.deleteRecursively()
summary = getDirSize(dir) summary = getString(R.string.settings_storage_usage_loading)
CoroutineScope(Dispatchers.IO).launch {
getDirSize(dir).let {
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
}
} }
setNegativeButton(android.R.string.no) { _, _ -> } setNegativeButton(android.R.string.no) { _, _ -> }
}.show() }.show()
} }
"delete_downloads" -> { "delete_downloads" -> {
val dir = getDownloadDirectory(context) val dir = getDownloadDirectory(requireContext())
AlertDialog.Builder(context).apply { AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
setMessage(R.string.settings_clear_downloads_alert_message) setMessage(R.string.settings_clear_downloads_alert_message)
setPositiveButton(android.R.string.yes) { _, _ -> setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists()) if (dir.exists())
dir.deleteRecursively() dir.deleteRecursively()
summary = getDirSize(dir) summary = getString(R.string.settings_storage_usage_loading)
CoroutineScope(Dispatchers.IO).launch {
getDirSize(dir).let {
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
}
} }
setNegativeButton(android.R.string.no) { _, _ -> } setNegativeButton(android.R.string.no) { _, _ -> }
}.show() }.show()
} }
"clear_history" -> { "clear_history" -> {
val histories = (context.applicationContext as Pupil).histories val histories = (requireContext().applicationContext as Pupil).histories
AlertDialog.Builder(context).apply { AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
setMessage(R.string.settings_clear_history_alert_message) setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.yes) { _, _ -> setPositiveButton(android.R.string.yes) { _, _ ->
@@ -129,10 +156,10 @@ class SettingsFragment :
}.show() }.show()
} }
"dl_location" -> { "dl_location" -> {
DownloadLocationDialog(activity!!).show() DownloadLocationDialog(requireActivity()).show()
} }
"default_query" -> { "default_query" -> {
DefaultQueryDialog(context).apply { DefaultQueryDialog(requireContext()).apply {
onPositiveButtonClickListener = { newTags -> onPositiveButtonClickListener = { newTags ->
sharedPreferences.edit().putString("default_query", newTags.toString()).apply() sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = newTags.toString() summary = newTags.toString()
@@ -140,20 +167,23 @@ class SettingsFragment :
}.show() }.show()
} }
"app_lock" -> { "app_lock" -> {
val intent = Intent(context, LockActivity::class.java) val intent = Intent(requireContext(), LockActivity::class.java)
activity?.startActivityForResult(intent, REQUEST_LOCK) activity?.startActivityForResult(intent, REQUEST_LOCK)
} }
"mirrors" -> { "mirrors" -> {
MirrorDialog(context) MirrorDialog(requireContext())
.show() .show()
} }
"proxy" -> { "proxy" -> {
ProxyDialog(context) ProxyDialog(requireContext())
.show() .show()
} }
"nomedia" -> {
File(getDownloadDirectory(context), ".nomedia").createNewFile()
}
"backup" -> { "backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(ContextCompat.getDataDir(requireContext()), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"), File(getDownloadDirectory(requireContext()), "favorites.json"),
true true
) )
@@ -168,6 +198,37 @@ class SettingsFragment :
activity?.startActivityForResult(intent, REQUEST_RESTORE) 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", ""))
)
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show()
}
else -> return false else -> return false
} }
} }
@@ -201,10 +262,10 @@ class SettingsFragment :
when (key) { when (key) {
"proxy" -> { "proxy" -> {
summary = getProxyInfo(context).type.name summary = getProxyInfo(requireContext()).type.name
} }
"dl_location" -> { "dl_location" -> {
summary = getDownloadDirectory(context!!).canonicalPath summary = getDownloadDirectory(requireContext()).canonicalPath
} }
} }
} }
@@ -213,6 +274,9 @@ class SettingsFragment :
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
sharedPreference = PreferenceManager.getDefaultSharedPreferences(requireContext())
sharedPreference.registerOnSharedPreferenceChangeListener(this)
initPreferences() initPreferences()
} }
@@ -229,42 +293,56 @@ class SettingsFragment :
when (key) { when (key) {
"app_version" -> { "app_version" -> {
val manager = context.packageManager val manager = requireContext().packageManager
val info = manager.getPackageInfo(context.packageName, 0) val info = manager.getPackageInfo(requireContext().packageName, 0)
summary = context.getString(R.string.settings_app_version_description, info.versionName) summary = requireContext().getString(R.string.settings_app_version_description, info.versionName)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"delete_cache" -> { "delete_cache" -> {
val dir = File(context.cacheDir, "imageCache") val dir = File(requireContext().cacheDir, "imageCache")
summary = getDirSize(dir)
CoroutineScope(Dispatchers.IO).launch {
summary = getString(R.string.settings_storage_usage_loading)
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"delete_downloads" -> { "delete_downloads" -> {
val dir = getDownloadDirectory(context) val dir = getDownloadDirectory(requireContext())
summary = getDirSize(dir)
CoroutineScope(Dispatchers.IO).launch {
summary = getString(R.string.settings_storage_usage_loading)
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"clear_history" -> { "clear_history" -> {
val histories = (activity!!.application as Pupil).histories val histories = (requireActivity().application as Pupil).histories
summary = getString(R.string.settings_clear_history_summary, histories.size) summary = getString(R.string.settings_clear_history_summary, histories.size)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"dl_location" -> { "dl_location" -> {
summary = getDownloadDirectory(context).canonicalPath summary = getDownloadDirectory(requireContext()).canonicalPath
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"default_query" -> { "default_query" -> {
summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: "" summary = sharedPreference.getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"app_lock" -> { "app_lock" -> {
val lockManager = LockManager(context) val lockManager = LockManager(requireContext())
summary = summary =
if (lockManager.locks.isNullOrEmpty()) { if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none) getString(R.string.settings_lock_none)
@@ -284,19 +362,29 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"proxy" -> { "proxy" -> {
summary = getProxyInfo(context).type.name summary = getProxyInfo(requireContext()).type.name
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"dark_mode" -> { "dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }
"nomedia" -> {
onPreferenceClickListener = this@SettingsFragment
}
"backup" -> { "backup" -> {
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"restore" -> { "restore" -> {
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"old_import_galleries" -> {
onPreferenceClickListener = this@SettingsFragment
}
"user_id" -> {
summary = sharedPreference.getString("user_id", "")
onPreferenceClickListener = this@SettingsFragment
}
} }
} }

View File

@@ -20,11 +20,20 @@ package xyz.quaver.pupil.util
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration 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_LOCK = 38238
const val REQUEST_RESTORE = 16546 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 = 3874
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425 const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900 const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
const val NOTIFICATION_ID_UPDATE = 2345
val json = Json(JsonConfiguration.Stable) val json = Json(JsonConfiguration.Stable)

View File

@@ -21,12 +21,10 @@ package xyz.quaver.pupil.util.download
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.io.InputStream
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
@@ -35,14 +33,22 @@ import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf import xyz.quaver.pupil.util.isParentOf
import xyz.quaver.pupil.util.json import xyz.quaver.pupil.util.json
import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream
import java.net.URL import java.net.URL
import java.util.*
import java.util.concurrent.locks.Lock import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
class Cache(context: Context) : ContextWrapper(context) { class Cache(context: Context) : ContextWrapper(context) {
companion object {
private val moving = mutableListOf<Int>()
private val readers = SparseArray<Reader?>()
}
private val locks = SparseArray<Lock>() private val locks = SparseArray<Lock>()
private fun lock(galleryID: Int) { private fun lock(galleryID: Int) {
synchronized(locks) { synchronized(locks) {
@@ -82,6 +88,9 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
fun setCachedMetadata(galleryID: Int, metadata: Metadata) { fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
if (preference.getBoolean("cache_disable", false))
return
val file = File(getCachedGallery(galleryID), ".metadata").also { val file = File(getCachedGallery(galleryID), ".metadata").also {
if (!it.exists()) if (!it.exists())
it.createNewFile() it.createNewFile()
@@ -93,6 +102,7 @@ class Cache(context: Context) : ContextWrapper(context) {
suspend fun getThumbnail(galleryID: Int): String? { suspend fun getThumbnail(galleryID: Int): String? {
val metadata = Cache(this).getCachedMetadata(galleryID) val metadata = Cache(this).getCachedMetadata(galleryID)
@Suppress("BlockingMethodInNonBlockingContext")
val thumbnail = if (metadata?.thumbnail == null) val thumbnail = if (metadata?.thumbnail == null)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails val thumbnails = getGalleryBlock(galleryID)?.thumbnails
@@ -153,7 +163,7 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
fun getReaderOrNull(galleryID: Int): Reader? { fun getReaderOrNull(galleryID: Int): Reader? {
return getCachedMetadata(galleryID)?.reader return readers[galleryID] ?: getCachedMetadata(galleryID)?.reader
} }
suspend fun getReader(galleryID: Int): Reader? { suspend fun getReader(galleryID: Int): Reader? {
@@ -174,15 +184,21 @@ class Cache(context: Context) : ContextWrapper(context) {
it it
} }
val reader = if (metadata?.reader == null) { val reader =
CoroutineScope(Dispatchers.IO).async { if (readers[galleryID] != null)
return readers[galleryID]
else if (metadata?.reader == null) {
var retval: Reader? = null var retval: Reader? = null
for (source in sources) { for (source in sources) {
retval = try { retval = try {
source.value.invoke() withContext(Dispatchers.IO) {
withTimeoutOrNull(1000) {
source.value.invoke()
}
}
} catch (e: Exception) { } catch (e: Exception) {
Crashlytics.logException(e) FirebaseCrashlytics.getInstance().recordException(e)
null null
} }
@@ -191,9 +207,10 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
retval retval
}.await() ?: return null } else
} else metadata.reader
metadata.reader
readers.put(galleryID, reader)
setCachedMetadata( setCachedMetadata(
galleryID, galleryID,
@@ -235,37 +252,54 @@ class Cache(context: Context) : ContextWrapper(context) {
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) { fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
if (preference.getBoolean("cache_disable", false))
return
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also { val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
if (!it.exists()) if (!it.exists())
it.createNewFile() it.createNewFile()
} }
data.use { try {
it.copyTo(FileOutputStream(cache)) BufferedInputStream(data).use { inputStream ->
FileOutputStream(cache).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
} catch (e: Exception) {
cache.delete()
} }
} }
fun moveToDownload(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch { fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID).also { if (preference.getBoolean("cache_disable", false))
if (!it.exists()) return
if (moving.contains(galleryID))
return
CoroutineScope(Dispatchers.IO).launch {
val cache = getCachedGallery(galleryID).also {
if (!it.exists())
return@launch
}
val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
if (download.isParentOf(cache))
return@launch return@launch
FirebaseCrashlytics.getInstance().log("MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
cache.copyRecursively(download, true) { file, err ->
FirebaseCrashlytics.getInstance().log("MOVING ERROR ${file.canonicalPath} ${err.message}")
OnErrorAction.SKIP
}
FirebaseCrashlytics.getInstance().log("MOVED ${cache.canonicalPath}")
FirebaseCrashlytics.getInstance().log("DELETING ${cache.canonicalPath}")
cache.deleteRecursively()
FirebaseCrashlytics.getInstance().log("DELETED ${cache.canonicalPath}")
} }
val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
if (download.isParentOf(cache))
return@launch
Log.i("PUPILD", "MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
cache.copyRecursively(download, true) { file, err ->
Log.i("PUPILD", "MOVING ERROR ${file.canonicalPath} ${err.message}")
OnErrorAction.SKIP
}
Log.i("PUPILD", "MOVED ${cache.canonicalPath}")
Log.i("PUPILD", "DELETING ${cache.canonicalPath}")
cache.deleteRecursively()
Log.i("PUPILD", "DELETED ${cache.canonicalPath}")
} }
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true

View File

@@ -29,8 +29,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.* import okhttp3.*
import okio.* import okio.*
@@ -46,11 +45,10 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@UseExperimental(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) { class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -78,7 +76,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private var bufferedSource : BufferedSource? = null private var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength() override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType() ?: null override fun contentType() = responseBody.contentType()
override fun source(): BufferedSource { override fun source(): BufferedSource {
if (bufferedSource == null) if (bufferedSource == null)
@@ -130,40 +128,38 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
* SECONDARY VALUE * SECONDARY VALUE
* 0 <= value < 100 -> Download in progress * 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed * Float.POSITIVE_INFINITY -> Download completed
* Float.NaN -> Exception
*/ */
val progress = SparseArray<MutableList<Float>?>() val progress = SparseArray<MutableList<Float>?>()
/* val notification = SparseArray<NotificationCompat.Builder?>()
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress / Loading
* null -> Gallery doesn't exist
* SECONDARY VALUE
* Throwable -> Exception
* null -> Download in progress / Loading
*/
val exception = SparseArray<MutableList<Throwable?>?>()
val notification = SparseArray<NotificationCompat.Builder>()
private val loop = loop() private val loop = loop()
private val worker = SparseArray<Job?>() private val worker = SparseArray<Job?>()
val interceptor = Interceptor { chain -> val interceptor = Interceptor { chain ->
val request = chain.request() val request = chain.request()
val response = chain.proceed(request) var response = chain.proceed(request)
var retry = 5
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder() response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener)) .body(response.body()?.let {
.build() ProgressResponseBody(request.tag(), it, progressListener)
}).build()
} }
val client = val client =
OkHttpClient.Builder() OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(0, TimeUnit.SECONDS) .connectTimeout(0, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.readTimeout(0, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS)
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4))) .dispatcher(Dispatcher().apply {
maxRequests = 4
maxRequestsPerHost = 4
})
.proxy(proxy) .proxy(proxy)
.build() .build()
@@ -178,10 +174,13 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
worker[galleryID]?.cancel() worker[galleryID]?.cancel()
} }
client.dispatcher().cancelAll() client.dispatcher().queuedCalls().filter {
it.request().tag() is Pair<*, *>
}.forEach {
it.cancel()
}
progress.clear() progress.clear()
exception.clear()
notification.clear() notification.clear()
notificationManager.cancelAll() notificationManager.cancelAll()
} }
@@ -197,15 +196,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
progress.remove(galleryID) progress.remove(galleryID)
exception.remove(galleryID)
notification.remove(galleryID) notification.remove(galleryID)
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
} }
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true fun isCompleted(galleryID: Int) = progress[galleryID]?.all { it.isInfinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) { private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
val lowQuality = preferences.getBoolean("low_quality", false) val lowQuality = preferences.getBoolean("low_quality", false)
@@ -217,7 +212,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
imageUrlFromImage( imageUrlFromImage(
galleryID, galleryID,
reader.galleryInfo.files[index], reader.galleryInfo.files[index],
lowQuality !lowQuality
) )
) )
addHeader("Referer", getReferer(galleryID)) addHeader("Referer", getReferer(galleryID))
@@ -243,7 +238,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
//gallery doesn't exist //gallery doesn't exist
if (reader == null) { if (reader == null) {
progress.put(galleryID, null) progress.put(galleryID, null)
exception.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
return@launch return@launch
@@ -257,12 +251,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
else else
0F 0F
}.toMutableList()) }.toMutableList())
exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
if (notification[galleryID] == null) if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
notification[galleryID].setContentTitle(reader.galleryInfo.title) notification[galleryID]?.setContentTitle(reader.galleryInfo.title)
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
@@ -279,35 +272,26 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
for (i in reader.galleryInfo.files.indices) { for (i in reader.galleryInfo.files.indices) {
val callback = object : Callback { val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
if (e.message?.contains("cancel", true) != false)
return
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})") Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
if (Fabric.isInitialized() && e.message != "Canceled") FirebaseCrashlytics.getInstance().apply {
Crashlytics.logException(e) log("FAIL ${call.request().tag()} (${e.message})")
setCustomKey("POS", "FAIL")
progress[galleryID]?.set(i, Float.NaN) recordException(e)
exception[galleryID]?.set(i, e)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
}
} }
cancel(galleryID)
queue.add(galleryID)
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
Log.i("PUPILD", "OK ${call.request().tag()}")
val ext = call.request().url().encodedPath().split('.').last() val ext = call.request().url().encodedPath().split('.').last()
try { try {
response.body().use { response.body().use {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream()) Cache(this@DownloadWorker).putImage(galleryID, i, ext, it!!.byteStream())
} }
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY) progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
@@ -323,47 +307,29 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
} }
} }
Log.i("PUPILD", "SUCCESS ${call.request().tag()}")
} catch (e: Exception) { } catch (e: Exception) {
FirebaseCrashlytics.getInstance().apply {
progress[galleryID]?.set(i, Float.NaN) log("FAIL ON OK ${call.request().tag()} (${e.message})")
exception[galleryID]?.set(i, e) setCustomKey("POS", "FAIL ON OK")
recordException(e)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
}
} }
File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete() File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})") cancel(galleryID)
queue.add(galleryID)
} }
} }
} }
if (progress[galleryID]?.get(i)?.isFinite() == true) { if (progress[galleryID]?.get(i)?.isFinite() == true)
queueDownload(galleryID, reader, i, callback) queueDownload(galleryID, reader, i, callback)
Log.i("PUPILD", "$galleryID QUEUED $i")
} else {
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
}
} }
} }
private fun notify(galleryID: Int) { private fun notify(galleryID: Int) {
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0 val progress = progress[galleryID]?.count { it.isInfinite() } ?: 0
Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
notification[galleryID] notification[galleryID]
@@ -379,7 +345,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
?.setContentText("$progress/$max") ?.setContentText("$progress/$max")
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null) if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
notificationManager.notify(galleryID, notification[galleryID].build()) notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
else else
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
} }
@@ -411,15 +377,13 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val galleryID = queue.peek() ?: continue val galleryID = queue.peek() ?: continue
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading! if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue cancel(galleryID)
if (notification[galleryID] == null) if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID)) if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build()) notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
Log.i("PUPILD", "QUEUED $galleryID")
worker.put(galleryID, download(galleryID)) worker.put(galleryID, download(galleryID))
queue.poll() queue.poll()

View File

@@ -18,13 +18,14 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.list import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer import kotlinx.serialization.builtins.list
import kotlinx.serialization.builtins.serializer
import java.io.File import java.io.File
class Histories(private val file: File) : ArrayList<Int>() { class Histories(private val file: File) : ArrayList<Int>() {
val serializer = Int.serializer().list val serializer: KSerializer<List<Int>> = Int.serializer().list
init { init {
if (!file.exists()) if (!file.exists())
@@ -40,7 +41,7 @@ class Histories(private val file: File) : ArrayList<Int>() {
fun load() : Histories { fun load() : Histories {
return apply { return apply {
super.clear() super.clear()
addAll( super.addAll(
json.parse( json.parse(
serializer, serializer,
file.bufferedReader().use { it.readText() } file.bufferedReader().use { it.readText() }
@@ -66,6 +67,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
return true 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 { override fun remove(element: Int): Boolean {
load() load()
val retval = super.remove(element) val retval = super.remove(element)

View File

@@ -22,9 +22,7 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.builtins.list
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import java.io.File import java.io.File
import java.security.MessageDigest import java.security.MessageDigest

View File

@@ -22,7 +22,7 @@ import android.annotation.SuppressLint
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@UseExperimental(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
fun String.wordCapitalize() : String { fun String.wordCapitalize() : String {
val result = ArrayList<String>() val result = ArrayList<String>()

View File

@@ -42,7 +42,7 @@ data class ProxyInfo(
} }
fun authenticator() = Authenticator { _, response -> fun authenticator() = Authenticator { _, response ->
val credential = Credentials.basic(username, password) val credential = Credentials.basic(username ?: "", password ?: "")
response.request().newBuilder() response.request().newBuilder()
.header("Proxy-Authorization", credential) .header("Proxy-Authorization", credential)

View File

@@ -18,30 +18,39 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.webkit.MimeTypeMap import android.net.Uri
import android.util.Base64
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.content import kotlinx.serialization.json.content
import okhttp3.*
import ru.noties.markwon.Markwon 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.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.Metadata
import java.io.File import java.io.File
import java.io.IOException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
fun getReleases(url: String) : JsonArray { fun getReleases(url: String) : JsonArray {
return try { return try {
@@ -81,7 +90,7 @@ fun getApkUrl(releases: JsonObject) : String? {
} }
const val UPDATE_NOTIFICATION_ID = 384823 const val UPDATE_NOTIFICATION_ID = 384823
fun checkUpdate(context: AppCompatActivity, force: Boolean = false) { fun checkUpdate(context: Context, force: Boolean = false) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0) val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
@@ -143,56 +152,27 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setMessage(Markwon.create(context).toMarkdown(msg)) setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ -> setPositiveButton(android.R.string.yes) { _, _ ->
val notificationManager = NotificationManagerCompat.from(context) val preference = PreferenceManager.getDefaultSharedPreferences(context)
val builder = NotificationCompat.Builder(context, "download").apply {
setContentTitle(context.getString(R.string.update_notification_description)) val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
setSmallIcon(android.R.drawable.stat_sys_download)
priority = NotificationCompat.PRIORITY_LOW //Cancel any download queued before
setOngoing(true)
val id = preference.getLong("update_download_id", -1)
if (id != -1L)
downloadManager.remove(id)
val target = File(context.getExternalFilesDir(null), "Pupil.apk").also {
it.delete()
} }
CoroutineScope(Dispatchers.IO).launch io@{ val request = DownloadManager.Request(Uri.parse(url))
val target = File(getDownloadDirectory(context), "Pupil.apk") .setTitle(context.getText(R.string.update_notification_description))
.setDestinationUri(Uri.fromFile(target))
try { downloadManager.enqueue(request).also {
URL(url).download(target) { progress, fileSize -> preference.edit().putLong("update_download_id", it).apply()
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
} catch (e: Exception) {
builder.apply {
setContentText(context.getString(R.string.update_failed))
setMessage(context.getString(R.string.update_failed_message))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOngoing(false)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
return@io
}
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}
builder.apply {
setContentIntent(PendingIntent.getActivity(context, 0, install, 0))
setProgress(0, 0, false)
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentTitle(context.getString(R.string.update_download_completed))
setContentText(context.getString(R.string.update_download_completed_description))
setOngoing(false)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
if (context.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
context.startActivity(install)
else
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
} }
} }
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ -> setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
@@ -207,4 +187,149 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
dialog.show() dialog.show()
} }
} }
}
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)
.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)
}
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 {
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
}
} ?: return@thumbnail null, Base64.DEFAULT)
}
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
} }

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromXDelta="0"
android:interpolator="@anim/shake_cycle"
android:toXDelta="10" />

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:cycles="3" />

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="@android:color/darker_gray"/>
<item android:color="@color/colorPrimary"/>
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,8 @@
<!-- 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" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- 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" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- 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" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- 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" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- 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" />
</vector>

View File

@@ -1,8 +1,10 @@
<!-- drawable/fingerprint.xml --> <!-- drawable/fingerprint.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp" android:height="24dp"
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#000" 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" /> <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" />
</vector> </vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/gender_female.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?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" />
</vector>

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