Compare commits

..

50 Commits
4.9 ... 4.18

Author SHA1 Message Date
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
Pupil
6abc404eb7 Merge pull request #71 from tom5079/dev
Version 4.11
2020-02-25 20:35:41 +09:00
Pupil
61afe01e36 Fixed image loading from hiyobi.me 2020-02-25 20:34:31 +09:00
Pupil
c3e60f9988 Typo fixed 2020-02-25 19:19:27 +09:00
Pupil
593197cd7e Bug fix
Thin mode added
Cancel all downloads added
2020-02-25 19:17:23 +09:00
Pupil
ee1592b478 Bug fix 2020-02-25 10:38:11 +09:00
134 changed files with 2052 additions and 616 deletions

View File

@@ -1,9 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

2
.idea/gradle.xml generated
View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">

View File

@@ -7,7 +7,7 @@ apply plugin: 'kotlinx-serialization'
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
logger.lifecycle("Firebase Enabled")
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.google.firebase.firebase-perf'
} else {
logger.lifecycle("Firebase Disabled")
@@ -19,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 45
versionName "4.9"
versionCode 53
versionName "4.18"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -35,9 +35,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
@@ -49,42 +46,41 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion = '29.0.2'
buildToolsVersion = '29.0.3'
}
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-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
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.google.android.material:material:1.2.0-alpha05'
implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'com.google.firebase:firebase-core:17.4.3'
implementation 'com.google.firebase:firebase-analytics:17.4.3'
implementation 'com.google.firebase:firebase-crashlytics:17.1.0'
implementation 'com.google.firebase:firebase-perf:19.0.7'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false
}
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
implementation "ru.noties.markwon:core:${markwonVersion}"
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'

Binary file not shown.

View File

@@ -23,8 +23,13 @@
-dontobfuscate
-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$** {
**[] $VALUES;
public *;
}
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
*** rewind();
}

View File

@@ -1 +1,20 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":45,"versionName":"4.9","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
{
"version": 1,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "xyz.quaver.pupil",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 53,
"versionName": "53",
"enabled": true,
"outputFile": "app-release.apk"
}
]
}

View File

@@ -28,6 +28,7 @@ import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader
@@ -62,6 +63,13 @@ class ExampleInstrumentedTest {
}
}
@Test
fun test_nozomi() {
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
Log.i("PUPILD", nozomi.size.toString())
}
@Test
fun test_doSearch() {
val reader = getReader( 1426382)
@@ -105,9 +113,9 @@ class ExampleInstrumentedTest {
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
}
}

View File

@@ -8,6 +8,8 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:name=".Pupil"
@@ -19,7 +21,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:replace="android:theme"
android:requestLegacyExternalStorage="true">
android:requestLegacyExternalStorage="true"
tools:ignore="UnusedAttribute">
<provider
android:authorities="${applicationId}.provider"
@@ -33,6 +36,12 @@
</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.ReaderActivity"
@@ -44,6 +53,61 @@
<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" />
</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
android:host="hitomi.la"
android:pathPrefix="/galleries"
@@ -55,6 +119,61 @@
<category android:name="android.intent.category.DEFAULT" />
<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
android:host="hiyobi.me"
android:pathPrefix="/reader"
@@ -69,17 +188,6 @@
<data
android:host="e-hentai.org"
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" />
</intent-filter>
<intent-filter>
@@ -88,21 +196,10 @@
<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
android:host="e-hentai.org"
android:pathPrefix="/g"
android:scheme="http" />
android:scheme="https" />
</intent-filter>
</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.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import xyz.quaver.proxy
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getProxy
import java.io.File
import java.util.*
class Pupil : MultiDexApplication() {
@@ -48,14 +50,34 @@ class Pupil : MultiDexApplication() {
override fun onCreate() {
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)
try {
preference.getString("dl_location", null)
preference.getString("dl_location", null).also {
if (!File(it!!).canWrite())
throw Exception()
}
} catch (e: Exception) {
preference.edit().remove("dl_location").apply()
}
if (!preference.getBoolean("low_quality_reset", false)) {
preference.edit()
.putBoolean("low_quality", true)
.putBoolean("low_quality_reset", true)
.apply()
}
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
@@ -72,13 +94,27 @@ class Pupil : MultiDexApplication() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_download_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
}
manager.createNotificationChannel(channel)
})
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)

View File

@@ -28,11 +28,12 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
@@ -42,20 +43,21 @@ import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.wordCapitalize
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule
class GalleryBlockAdapter(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 {
NEXT,
@@ -63,20 +65,20 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
PREV
}
private val glide = Glide.with(context)
private lateinit var favorites: Histories
val timer = Timer()
var isThin = false
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var timerTask: TimerTask? = null
private fun updateProgress(context: Context, galleryID: Int) {
val cache = Cache(context).getCachedGallery(galleryID)
val reader = Cache(context).getReaderOrNull(galleryID)
CoroutineScope(Dispatchers.Main).launch {
if (reader == null) {
if (reader == null || PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false)) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
return@launch
@@ -84,9 +86,7 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
with(view.galleryblock_progressbar) {
progress = cache.listFiles()?.count { file ->
Regex("^[0-9]+.+\$").matches(file.name)
} ?: 0
progress = Cache(context).getImages(galleryID)?.size ?: 0
if (visibility == View.GONE) {
visibility = View.VISIBLE
@@ -126,6 +126,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
val artists = galleryBlock.artists
val series = galleryBlock.series
if (isThin)
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
R.dimen.galleryblock_thumbnail_thin
)
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
it.start()
})
@@ -138,16 +142,18 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
null
}
glide
.load(thumbnail)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(galleryblock_thumbnail)
galleryblock_thumbnail.post {
glide
.load(thumbnail)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(galleryblock_thumbnail)
}
}
//Check cache
@@ -215,12 +221,12 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
"male" -> {
setChipBackgroundColorResource(R.color.material_blue_700)
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" -> {
setChipBackgroundColorResource(R.color.material_pink_600)
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
}
@@ -234,6 +240,15 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
}
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)
favorites = (context.applicationContext as Pupil).favorites
@@ -264,6 +279,14 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
}
}
}
// Make some views invisible to make it thinner
if (isThin) {
galleryblock_language.visibility = View.GONE
galleryblock_type.visibility = View.GONE
galleryblock_tag_group.visibility = View.GONE
}
}
}
}
@@ -341,10 +364,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text =
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
holder.view.context.getString(R.string.main_download)
else
if (Cache(holder.view.context).isDownloading(gallery.id))
holder.view.context.getString(android.R.string.cancel)
else
holder.view.context.getString(R.string.main_download)
}
override fun onClose(layout: SwipeLayout?) {}

View File

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

View File

@@ -18,62 +18,30 @@
package xyz.quaver.pupil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class ReaderAdapter(private val context: Context,
class ReaderAdapter(private val glide: RequestManager,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
val glide = Glide.with(context)
//region Glide.RecyclerView
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.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
val timer = Timer()
@@ -83,6 +51,8 @@ class ReaderAdapter(private val context: Context,
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
var downloadWorker: DownloadWorker? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate(
R.layout.item_reader, parent, false
@@ -94,12 +64,18 @@ class ReaderAdapter(private val context: Context,
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout
if (downloadWorker == null)
downloadWorker = DownloadWorker.getInstance(holder.view.context)
if (isFullScreen) {
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
} else {
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
holder.view.container.layoutParams.height = 0
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
}
holder.view.image.setOnPhotoTapListener { _, _, _ ->
@@ -110,21 +86,19 @@ class ReaderAdapter(private val context: Context,
onItemClickListener?.invoke(position)
}
if (!isFullScreen)
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
holder.view.reader_index.text = (position+1).toString()
val images = Cache(context).getImage(galleryID, position)
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
val image = downloadWorker!!.results[galleryID]?.get(position)
val progress = downloadWorker!!.progress[galleryID]?.get(position)
if (progress?.isInfinite() == true && images != null) {
if (progress?.isInfinite() == true && image != null) {
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
holder.view.image.post {
glide
.load(images)
.load(image)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.fitCenter()
.error(R.drawable.image_broken_variant)
.into(holder.view.image)
@@ -136,13 +110,22 @@ class ReaderAdapter(private val context: Context,
glide.clear(holder.view.image)
if (progress?.isNaN() == true) {
if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
FirebaseCrashlytics.getInstance().recordException(
DownloadWorker.getInstance(holder.view.context).exception[galleryID]?.get(position)!!
)
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
Snackbar.make(holder.view.reader_layout, R.string.reader_error_retry, Snackbar.LENGTH_SHORT).apply {
setAction(android.R.string.no) { }
setAction(android.R.string.yes) {
downloadWorker!!.cancel(galleryID)
downloadWorker!!.queue.add(galleryID)
}
}.show()
return
} else {
holder.view.reader_item_progressbar.progress =

View File

@@ -21,23 +21,157 @@ package xyz.quaver.pupil.ui
import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.andrognito.patternlockview.PatternLockView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_lock.*
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.ui.fragment.PINLockFragment
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager
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?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock)
val lockManager = try {
lockManager = try {
LockManager(this)
} catch (e: Exception) {
AlertDialog.Builder(this).apply {
@@ -50,12 +184,7 @@ class LockActivity : AppCompatActivity() {
return
}
val mode = intent.getStringExtra("mode")
lock_pattern.isEnabled = false
lock_pin.isEnabled = false
lock_fingerprint.isEnabled = false
lock_password.isEnabled = false
mode = intent.getStringExtra("mode")
when(mode) {
null -> {
@@ -64,52 +193,75 @@ class LockActivity : AppCompatActivity() {
finish()
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" -> {
lock_pattern.isEnabled = false
lock_pin.isEnabled = false
lock_fingerprint.isEnabled = false
lock_password.isEnabled = false
when(intent.getStringExtra("type")!!) {
"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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Animatable
@@ -30,10 +31,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
@@ -43,15 +41,16 @@ import androidx.core.view.GravityCompat
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import com.crashlytics.android.Crashlytics
import com.bumptech.glide.Glide
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_content.*
import kotlinx.coroutines.*
import kotlinx.serialization.list
import kotlinx.serialization.builtins.list
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
@@ -145,6 +144,30 @@ class MainActivity : AppCompatActivity() {
preference.edit().putBoolean("https_block_alert", true).apply()
}
if (!preference.getBoolean("apcjsa_option", false)) {
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.apcjsa_option_title)
setMessage(R.string.apcjsa_option_message)
setPositiveButton(android.R.string.yes) { _, _ ->
val tags = Tags.parse(
preference.getString("default_query", "") ?: ""
)
tags.add("-female:loli")
tags.add("-male:shota")
preference.edit()
.putString("default_query", tags.toString())
.putBoolean("cache_disable", true)
.putBoolean("apcjsa_option", true)
.apply()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
preference.edit().putBoolean("apcjsa_option", true).apply()
}
}
with(application as Pupil) {
@@ -195,7 +218,7 @@ class MainActivity : AppCompatActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
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()
return when(keyCode) {
@@ -330,11 +353,18 @@ class MainActivity : AppCompatActivity() {
true
}
with(main_fab_cancel) {
setImageResource(R.drawable.cancel)
setOnClickListener {
DownloadWorker.getInstance(context).stop()
}
}
with(main_fab_jump) {
setImageResource(R.drawable.ic_jump)
setOnClickListener {
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)
AlertDialog.Builder(context).apply {
@@ -359,17 +389,42 @@ 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()
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
}
startActivity(intent)
histories.add(galleryID)
}
}
}
}
with(main_fab_id) {
setImageResource(R.drawable.numeric)
setOnClickListener {
val editText = EditText(context)
val editText = EditText(context).apply {
inputType = InputType.TYPE_CLASS_NUMBER
}
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_open_gallery_by_id)
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 {
putExtra("galleryID", galleryID)
}
@@ -388,9 +443,10 @@ class MainActivity : AppCompatActivity() {
loadBlocks()
}
@SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() {
with(main_recyclerview) {
adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply {
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
@@ -404,17 +460,16 @@ class MainActivity : AppCompatActivity() {
}
onDownloadClickedHandler = { position ->
val galleryID = galleries[position].id
if (!completeFlag.get(galleryID, false)) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
val worker = DownloadWorker.getInstance(context)
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else {
if (Cache(context).isDownloading(galleryID)) //download in progress
worker.cancel(galleryID)
else {
Cache(context).setDownloading(galleryID, true)
if (!worker.queue.contains(galleryID))
worker.queue.add(galleryID)
worker.queue.add(galleryID)
}
}
@@ -467,6 +522,7 @@ class MainActivity : AppCompatActivity() {
GalleryDialog(
this@MainActivity,
Glide.with(this@MainActivity),
galleryID
).apply {
onChipClickedHandler.add {
@@ -712,7 +768,7 @@ class MainActivity : AppCompatActivity() {
})
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 serializer = Tag.serializer().list
@@ -721,9 +777,28 @@ class MainActivity : AppCompatActivity() {
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 {
when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
it.isChecked = true
@@ -796,14 +871,14 @@ class MainActivity : AppCompatActivity() {
ResourcesCompat.getDrawable(
resources,
when(item.n) {
"female" -> R.drawable.ic_gender_female
"male" -> R.drawable.ic_gender_male
"language" -> R.drawable.ic_translate
"group" -> R.drawable.ic_account_group
"character" -> R.drawable.ic_account_star
"series" -> R.drawable.ic_book_open
"artist" -> R.drawable.ic_brush
else -> R.drawable.ic_tag
"female" -> R.drawable.gender_female
"male" -> R.drawable.gender_male
"language" -> R.drawable.translate
"group" -> R.drawable.account_group
"character" -> R.drawable.account_star
"series" -> R.drawable.book_open
"artist" -> R.drawable.brush
else -> R.drawable.tag
},
null)
)
@@ -1009,8 +1084,8 @@ class MainActivity : AppCompatActivity() {
}
} catch (e: Exception) {
if (Fabric.isInitialized() && e.message != "No result")
Crashlytics.logException(e)
if (e.message != "No result")
FirebaseCrashlytics.getInstance().recordException(e)
withContext(Dispatchers.Main) {
main_noresult.visibility = View.VISIBLE

View File

@@ -23,6 +23,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@@ -32,9 +33,9 @@ import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.crashlytics.android.Crashlytics
import com.bumptech.glide.Glide
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.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
@@ -90,8 +91,7 @@ class ReaderActivity : AppCompatActivity() {
handleIntent(intent)
if (Fabric.isInitialized())
Crashlytics.setInt("GalleryID", galleryID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
if (galleryID == 0) {
onBackPressed()
@@ -112,14 +112,12 @@ class ReaderActivity : AppCompatActivity() {
val uri = intent.data
val lastPathSegment = uri?.lastPathSegment
if (uri != null && lastPathSegment != null) {
val nonNumber = Regex("[^-?0-9]+")
galleryID = when (uri.host) {
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
"히요비.asia" -> lastPathSegment.toInt()
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
"hitomi.la" ->
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
"hiyobi.me" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt()
else -> return
else -> 0
}
}
} else {
@@ -157,10 +155,10 @@ class ReaderActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId) {
R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
with(view.dialog_number_picker) {
minValue=1
maxValue=reader_recyclerview?.adapter?.itemCount ?: 0
maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
value=currentPage
}
val dialog = AlertDialog.Builder(this).apply {
@@ -286,7 +284,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() {
with(reader_recyclerview) {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID).apply {
onItemClickListener = {
if (isScroll) {
isScroll = false
@@ -300,7 +298,6 @@ class ReaderActivity : AppCompatActivity() {
}
}
addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
@@ -325,13 +322,27 @@ class ReaderActivity : AppCompatActivity() {
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
setOnClickListener {
if (Cache(context).isDownloading(galleryID)) {
Cache(context).setDownloading(galleryID, false)
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else {
if (Cache(context).isDownloading(galleryID)) {
Cache(context).setDownloading(galleryID, false)
animateDownloadFAB(false)
} else {
Cache(context).setDownloading(galleryID, true)
animateDownloadFAB(true)
animateDownloadFAB(false)
} else {
Cache(context).setDownloading(galleryID, true)
animateDownloadFAB(true)
}
}
}
}
with(reader_fab_retry) {
setImageResource(R.drawable.refresh)
setOnClickListener {
DownloadWorker.getInstance(context).let {
it.cancel(galleryID)
it.queue.add(galleryID)
}
}
}
@@ -361,6 +372,8 @@ class ReaderActivity : AppCompatActivity() {
window.attributes = this
}
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
}
private fun scrollMode(isScroll: Boolean) {
@@ -386,7 +399,7 @@ class ReaderActivity : AppCompatActivity() {
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
labelText = getString(R.string.reader_fab_download_cancel)
}
else // Or continue animate
post {

View File

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

View File

@@ -45,6 +45,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro")
private val excludeLoli = listOf("-female:loli", "-male:shota")
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@@ -68,6 +69,11 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
newTags.add(tag)
}
if (default_query_dialog_loli_checkbox.isChecked)
excludeLoli.forEach { tag ->
newTags.add(tag)
}
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) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher {

View File

@@ -29,7 +29,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.*
@@ -53,7 +53,7 @@ import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
@@ -61,8 +61,6 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
}
}.toMap()
private val glide = Glide.with(context)
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -90,7 +88,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
try {
val gallery = getGallery(galleryID)
launch(Dispatchers.Main) {
gallery_cover.post {
gallery_progressbar.visibility = View.GONE
gallery_title.text = gallery.title
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
@@ -112,7 +110,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
}
}
Glide.with(context)
glide
.load(gallery.cover)
.apply {
if (BuildConfig.CENSOR)
@@ -171,12 +169,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
"male" -> {
setChipBackgroundColorResource(R.color.material_blue_700)
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" -> {
setChipBackgroundColorResource(R.color.material_pink_600)
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
}
@@ -226,7 +224,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
val inflater = LayoutInflater.from(context)
val galleries = ArrayList<GalleryBlock>()
val adapter = GalleryBlockAdapter(context, galleries).apply {
val adapter = GalleryBlockAdapter(glide, galleries).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag)
@@ -264,6 +262,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
.setOnItemLongClickListener { _, position, _ ->
GalleryDialog(
context,
glide,
galleries[position].id
).apply {
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,23 @@
package xyz.quaver.pupil.ui.fragment
import android.content.Intent
import android.content.SharedPreferences
import android.Manifest
import android.content.*
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
@@ -47,16 +53,12 @@ class SettingsFragment :
Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
}
lateinit var sharedPreference: SharedPreferences
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
val lockManager = LockManager(requireContext())
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
@@ -86,9 +88,9 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true)
}
"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)
setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
@@ -101,9 +103,9 @@ class SettingsFragment :
}.show()
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)
val dir = getDownloadDirectory(requireContext())
AlertDialog.Builder(context).apply {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_downloads_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
@@ -116,9 +118,9 @@ class SettingsFragment :
}.show()
}
"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)
setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
@@ -129,10 +131,10 @@ class SettingsFragment :
}.show()
}
"dl_location" -> {
DownloadLocationDialog(activity!!).show()
DownloadLocationDialog(requireActivity()).show()
}
"default_query" -> {
DefaultQueryDialog(context).apply {
DefaultQueryDialog(requireContext()).apply {
onPositiveButtonClickListener = { newTags ->
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = newTags.toString()
@@ -140,20 +142,23 @@ class SettingsFragment :
}.show()
}
"app_lock" -> {
val intent = Intent(context, LockActivity::class.java)
val intent = Intent(requireContext(), LockActivity::class.java)
activity?.startActivityForResult(intent, REQUEST_LOCK)
}
"mirrors" -> {
MirrorDialog(context)
MirrorDialog(requireContext())
.show()
}
"proxy" -> {
ProxyDialog(context)
ProxyDialog(requireContext())
.show()
}
"nomedia" -> {
File(getDownloadDirectory(context), ".nomedia").createNewFile()
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"),
File(ContextCompat.getDataDir(requireContext()), "favorites.json").copyTo(
File(getDownloadDirectory(requireContext()), "favorites.json"),
true
)
@@ -168,6 +173,37 @@ class SettingsFragment :
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
}
}
@@ -201,10 +237,10 @@ class SettingsFragment :
when (key) {
"proxy" -> {
summary = getProxyInfo(context).type.name
summary = getProxyInfo(requireContext()).type.name
}
"dl_location" -> {
summary = getDownloadDirectory(context!!).canonicalPath
summary = getDownloadDirectory(requireContext()).canonicalPath
}
}
}
@@ -213,6 +249,9 @@ class SettingsFragment :
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
sharedPreference = PreferenceManager.getDefaultSharedPreferences(requireContext())
sharedPreference.registerOnSharedPreferenceChangeListener(this)
initPreferences()
}
@@ -229,42 +268,42 @@ class SettingsFragment :
when (key) {
"app_version" -> {
val manager = context.packageManager
val info = manager.getPackageInfo(context.packageName, 0)
summary = context.getString(R.string.settings_app_version_description, info.versionName)
val manager = requireContext().packageManager
val info = manager.getPackageInfo(requireContext().packageName, 0)
summary = requireContext().getString(R.string.settings_app_version_description, info.versionName)
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
val dir = File(context.cacheDir, "imageCache")
val dir = File(requireContext().cacheDir, "imageCache")
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)
val dir = getDownloadDirectory(requireContext())
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"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)
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
summary = getDownloadDirectory(context).canonicalPath
summary = getDownloadDirectory(requireContext()).canonicalPath
onPreferenceClickListener = this@SettingsFragment
}
"default_query" -> {
summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: ""
summary = sharedPreference.getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment
}
"app_lock" -> {
val lockManager = LockManager(context)
val lockManager = LockManager(requireContext())
summary =
if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
@@ -284,19 +323,29 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"proxy" -> {
summary = getProxyInfo(context).type.name
summary = getProxyInfo(requireContext()).type.name
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment
}
"nomedia" -> {
onPreferenceClickListener = this@SettingsFragment
}
"backup" -> {
onPreferenceClickListener = this@SettingsFragment
}
"restore" -> {
onPreferenceClickListener = this@SettingsFragment
}
"old_import_galleries" -> {
onPreferenceClickListener = this@SettingsFragment
}
"user_id" -> {
summary = sharedPreference.getString("user_id", "")
onPreferenceClickListener = this@SettingsFragment
}
}
}

View File

@@ -20,11 +20,20 @@ package xyz.quaver.pupil.util
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import okhttp3.Dispatcher
import okhttp3.OkHttpClient
import xyz.quaver.proxy
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
const val REQUEST_LOCK = 38238
const val REQUEST_RESTORE = 16546
const val REQUEST_IMPORT_OLD_GALLERIES = 6458
const val REQUEST_IMPORT_OLD_GALLERIES_OLD = 5946
const val REQUEST_DOWNLOAD_FOLDER = 3874
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
const val NOTIFICATION_ID_UPDATE = 2345
val json = Json(JsonConfiguration.Stable)

View File

@@ -21,30 +21,33 @@ package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import android.util.Log
import android.util.SparseArray
import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.io.InputStream
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.proxy
import xyz.quaver.pupil.util.copyRecursively
import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf
import xyz.quaver.pupil.util.json
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.*
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
class Cache(context: Context) : ContextWrapper(context) {
companion object {
private val moving = mutableListOf<Int>()
private val readers = SparseArray<Reader?>()
}
private val locks = SparseArray<Lock>()
private fun lock(galleryID: Int) {
synchronized(locks) {
@@ -64,7 +67,7 @@ class Cache(context: Context) : ContextWrapper(context) {
// Search in this order
// Download -> Cache
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
if (!it.exists())
if (!it.exists() && !preference.getBoolean("cache_disable", false))
it.mkdirs()
}
@@ -84,6 +87,9 @@ class Cache(context: Context) : ContextWrapper(context) {
}
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
if (preference.getBoolean("cache_disable", false))
return
val file = File(getCachedGallery(galleryID), ".metadata").also {
if (!it.exists())
it.createNewFile()
@@ -95,6 +101,7 @@ class Cache(context: Context) : ContextWrapper(context) {
suspend fun getThumbnail(galleryID: Int): String? {
val metadata = Cache(this).getCachedMetadata(galleryID)
@Suppress("BlockingMethodInNonBlockingContext")
val thumbnail = if (metadata?.thumbnail == null)
withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
@@ -155,7 +162,7 @@ class Cache(context: Context) : ContextWrapper(context) {
}
fun getReaderOrNull(galleryID: Int): Reader? {
return getCachedMetadata(galleryID)?.reader
return readers[galleryID] ?: getCachedMetadata(galleryID)?.reader
}
suspend fun getReader(galleryID: Int): Reader? {
@@ -176,15 +183,21 @@ class Cache(context: Context) : ContextWrapper(context) {
it
}
val reader = if (metadata?.reader == null) {
CoroutineScope(Dispatchers.IO).async {
val reader =
if (readers[galleryID] != null)
return readers[galleryID]
else if (metadata?.reader == null) {
var retval: Reader? = null
for (source in sources) {
retval = try {
source.value.invoke()
withContext(Dispatchers.IO) {
withTimeoutOrNull(1000) {
source.value.invoke()
}
}
} catch (e: Exception) {
Crashlytics.logException(e)
FirebaseCrashlytics.getInstance().recordException(e)
null
}
@@ -193,9 +206,10 @@ class Cache(context: Context) : ContextWrapper(context) {
}
retval
}.await() ?: return null
} else
metadata.reader
} else
metadata.reader
readers.put(galleryID, reader)
setCachedMetadata(
galleryID,
@@ -236,26 +250,53 @@ 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: ByteArray) {
if (preference.getBoolean("cache_disable", false))
return
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
if (!it.exists())
it.createNewFile()
}
data.use {
it.copyTo(FileOutputStream(cache))
try {
FileOutputStream(cache).use {
it.write(data)
}
} catch (e: Exception) {
cache.delete()
}
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID).also {
if (!it.exists())
return
}
val download = File(getDownloadDirectory(this), galleryID.toString())
if (preference.getBoolean("cache_disable", false))
return
cache.copyRecursively(download, true) { _, _ -> OnErrorAction.SKIP }
cache.deleteRecursively()
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
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

View File

@@ -29,8 +29,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import okhttp3.*
import okio.*
@@ -46,11 +45,10 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
@UseExperimental(ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -78,7 +76,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType() ?: null
override fun contentType() = responseBody.contentType()
override fun source(): BufferedSource {
if (bufferedSource == null)
@@ -145,26 +143,37 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
* null -> Download in progress / Loading
*/
val exception = SparseArray<MutableList<Throwable?>?>()
val results = SparseArray<MutableList<ByteArray?>?>()
val notification = SparseArray<NotificationCompat.Builder>()
private val loop = loop()
private val worker = SparseArray<Job?>()
val clients = SparseArray<OkHttpClient>()
val interceptor = Interceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
var response = chain.proceed(request)
var retry = 5
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build()
.body(response.body()?.let {
ProgressResponseBody(request.tag(), it, progressListener)
}).build()
}
fun buildClient() =
val client =
OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(0, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.readTimeout(0, TimeUnit.SECONDS)
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.dispatcher(Dispatcher().apply {
maxRequests = 4
maxRequestsPerHost = 4
})
.proxy(proxy)
.build()
@@ -172,20 +181,22 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
queue.clear()
loop.cancel()
for (i in 0..worker.size()) {
for (i in 0 until worker.size()) {
val galleryID = worker.keyAt(i)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
worker[galleryID]?.cancel()
}
for (i in 0 until clients.size()) {
clients.valueAt(i).dispatcher().cancelAll()
client.dispatcher().queuedCalls().filter {
it.request().tag() is Pair<*, *>
}.forEach {
it.cancel()
}
clients.clear()
progress.clear()
exception.clear()
results.clear()
notification.clear()
notificationManager.cancelAll()
}
@@ -194,17 +205,20 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
queue.remove(galleryID)
worker[galleryID]?.cancel()
clients[galleryID]?.dispatcher()?.cancelAll()
clients.remove(galleryID)
client.dispatcher().queuedCalls().filter {
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
}.forEach {
it.cancel()
}
progress.remove(galleryID)
exception.remove(galleryID)
results.remove(galleryID)
notification.remove(galleryID)
notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0) {
if (progress.indexOfKey(galleryID) >= 0)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
}
}
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
@@ -219,7 +233,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
imageUrlFromImage(
galleryID,
reader.galleryInfo.files[index],
lowQuality
!lowQuality
)
)
addHeader("Referer", getReferer(galleryID))
@@ -236,10 +250,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
tag(galleryID to index)
}.build()
if (clients.get(galleryID) == null)
clients.put(galleryID, buildClient())
client.newCall(request).enqueue(callback)
clients[galleryID]?.newCall(request)?.enqueue(callback)
Log.i("PUPILD", "DOWNLOADING ($galleryID, $index) from ${request.url()}")
}
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
@@ -249,6 +262,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
if (reader == null) {
progress.put(galleryID, null)
exception.put(galleryID, null)
results.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
return@launch
@@ -263,6 +277,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
0F
}.toMutableList())
exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
results.put(galleryID, reader.galleryInfo.files.indices.map { index ->
cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index }?.readBytes()
}.toMutableList())
if (notification[galleryID] == null)
initNotification(galleryID)
@@ -285,8 +302,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
if (Fabric.isInitialized() && e.message != "Canceled")
Crashlytics.logException(e)
if (e.message?.contains("cancel", true) != true)
FirebaseCrashlytics.getInstance().recordException(e)
progress[galleryID]?.set(i, Float.NaN)
exception[galleryID]?.set(i, e)
@@ -294,8 +311,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
clients.remove(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
@@ -313,15 +329,20 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
try {
response.body().use {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
it!!
results[galleryID]?.set(i, it.source().readByteArray())
}
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
clients.remove(galleryID)
results[galleryID]?.get(i)?.also {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it)
}
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
@@ -340,8 +361,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
clients.remove(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
@@ -418,8 +438,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val galleryID = queue.peek() ?: continue
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
cancel(galleryID)
if (notification[galleryID] == null)
initNotification(galleryID)
@@ -427,10 +447,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build())
if (clients.size() >= preferences.getInt("max_download", 4))
continue
Log.i("PUPILD", "QUEUED $galleryID #${clients.size()+1}")
Log.i("PUPILD", "QUEUED $galleryID")
worker.put(galleryID, download(galleryID))
queue.poll()

View File

@@ -26,7 +26,6 @@ import android.os.storage.StorageManager
import android.provider.DocumentsContract
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.io.IOException
import java.io.File
import java.io.FileOutputStream
import java.lang.reflect.Array
@@ -214,64 +213,5 @@ fun Uri.toFile(context: Context): File? {
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
}
fun File.copyRecursively(
target: File,
overwrite: Boolean = false,
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean {
if (!exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist.")) !=
OnErrorAction.TERMINATE
}
try {
// We cannot break for loop from inside a lambda, so we have to use an exception here
for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw IOException("Walk failed") }) {
if (!src.exists()) {
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist.")) ==
OnErrorAction.TERMINATE)
return false
} else {
val relPath = src.toRelativeString(this)
val dstFile = File(target, relPath)
if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
val stillExists = if (!overwrite) true else {
if (dstFile.isDirectory)
!dstFile.deleteRecursively()
else
!dstFile.delete()
}
if (stillExists) {
if (onError(dstFile, FileAlreadyExistsException(file = src,
other = dstFile,
reason = "The destination file already exists.")) == OnErrorAction.TERMINATE)
return false
continue
}
}
if (src.isDirectory) {
dstFile.mkdirs()
} else {
val length = try {
src.copyTo(dstFile, overwrite).length()
} catch (e: IOException) {
if (onError(src, e) == OnErrorAction.TERMINATE)
return false
else
-1
}
if (length != src.length()) {
if (onError(src, IOException("Source file wasn't copied completely, length of destination file differs.")) == OnErrorAction.TERMINATE)
return false
}
}
}
}
return true
} catch (e: IOException) {
return false
}
}
fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath)

View File

@@ -18,13 +18,14 @@
package xyz.quaver.pupil.util
import kotlinx.serialization.list
import kotlinx.serialization.serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.list
import kotlinx.serialization.builtins.serializer
import java.io.File
class Histories(private val file: File) : ArrayList<Int>() {
val serializer = Int.serializer().list
val serializer: KSerializer<List<Int>> = Int.serializer().list
init {
if (!file.exists())

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,4 +1,4 @@
<!-- drawable/gender-male.xml -->
<!-- drawable/gender_male.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -4,5 +4,5 @@
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M14,12A2,2 0 0,1 16,10A2,2 0 0,1 18,12A2,2 0 0,1 16,14A2,2 0 0,1 14,12M8,12A2,2 0 0,1 10,10A2,2 0 0,1 12,12A2,2 0 0,1 10,14A2,2 0 0,1 8,12M2,12A2,2 0 0,1 4,10A2,2 0 0,1 6,12A2,2 0 0,1 4,14A2,2 0 0,1 2,12M22,5H20V19H22V5Z" />
<path android:fillColor="#fff" android:pathData="M14,12A2,2 0 0,1 16,10A2,2 0 0,1 18,12A2,2 0 0,1 16,14A2,2 0 0,1 14,12M8,12A2,2 0 0,1 10,10A2,2 0 0,1 12,12A2,2 0 0,1 10,14A2,2 0 0,1 8,12M2,12A2,2 0 0,1 4,10A2,2 0 0,1 6,12A2,2 0 0,1 4,14A2,2 0 0,1 2,12M22,5H20V19H22V5Z" />
</vector>

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