Compare commits

...

58 Commits

Author SHA1 Message Date
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
Pupil
dfe435c4f3 Merge pull request #70 from tom5079/dev
Version 4.9
2020-02-24 21:11:03 +09:00
Pupil
69e85f8b90 Bug fix 2020-02-24 21:10:10 +09:00
Pupil
c9bde3c487 Merge pull request #69 from tom5079/dev
Version 4.8
2020-02-24 20:48:55 +09:00
Pupil
65e9557d9f Bug fix 2020-02-24 20:02:44 +09:00
Pupil
4f249c07e7 Merge pull request #68 from tom5079/dev
Version 4.7
2020-02-24 12:49:56 +09:00
Pupil
5fd35b492c Bug fix 2020-02-24 12:49:19 +09:00
Pupil
9bddf95013 Image loading fixed 2020-02-23 21:18:19 +09:00
Pupil
03444f070f App built 2020-02-23 10:40:09 +09:00
Pupil
2f1a63eb64 Confilict resolved 2020-02-23 10:32:10 +09:00
Pupil
9d0898b26c Fixed image loading bug 2020-02-23 10:30:57 +09:00
Pupil
994aa99797 Fixed image loading bug 2020-02-23 10:28:29 +09:00
Pupil
8204a15276 Proxy applied to thumbnails 2020-02-22 20:30:42 +09:00
Pupil
4a8bff0b98 Merge pull request #67 from tom5079/dev
Version 4.6
2020-02-22 11:09:19 +09:00
Pupil
a4336cd954 Version 4.6 2020-02-22 11:08:30 +09:00
Pupil
4f0dbead79 Hiyobi file structure changed 2020-02-22 11:02:58 +09:00
Pupil
c0e7c87ca4 Fixed image loading error 2020-02-22 09:30:24 +09:00
Pupil
b967bf9a26 Merge branch 'issue-65' into dev 2020-02-21 20:44:03 +09:00
Pupil
764a265053 Image loading optimization 2020-02-21 20:11:43 +09:00
Pupil
68c2b2dbfa Update README.md
Added discord banner
2020-02-21 20:11:27 +09:00
Pupil
061f1263f4 App naming changed from beta to alpha 2020-02-17 20:33:12 +09:00
Pupil
2a27355479 App built 2020-02-17 20:31:36 +09:00
Pupil
ae2a8e8ada Fixed low quality settings not affected 2020-02-17 19:56:57 +09:00
Pupil
68dcc2333b App built 2020-02-17 19:09:45 +09:00
Pupil
66fb2e9a62 Fixed ArrayIndexOutOFBoundsException 2020-02-17 18:50:58 +09:00
Pupil
1dbfc64f37 Fixed not able to load from hiyobi 2020-02-17 16:46:51 +09:00
Pupil
98d1f88579 Fixed infinite loading when there's no result 2020-02-16 22:18:31 +09:00
Pupil
bb6fadc182 Fixed unending loading screen 2020-02-16 20:11:20 +09:00
Pupil
ac1ca71299 Proxy implemented 2020-02-16 19:59:51 +09:00
Pupil
0d93785581 Fixed proxy not applied 2020-02-16 18:23:50 +09:00
Pupil
69a9d63e1d Proxy added 2020-02-15 12:40:10 +09:00
Pupil
5dea35343b Fixed preference bug
Version fix
2020-02-15 01:59:42 +09:00
Pupil
5c768d2121 Firebase enabled 2020-02-15 00:25:59 +09:00
Pupil
4d5834821a Fixed wrong radio button selected when download folder is not selected 2020-02-14 20:48:33 +09:00
Pupil
ca077c4fee Apk built 2020-02-14 20:37:48 +09:00
Pupil
85d01f60f1 Changed galleryblock retrieve url 2020-02-14 20:31:10 +09:00
Pupil
066d73b217 Generated APK 2020-02-14 20:13:26 +09:00
Pupil
ba069d8f8e Image loading optimization 2020-02-14 20:10:04 +09:00
Pupil
275684c9ce now able to install Debug and release builds in one device
Fixed shrink serialization error
2020-02-14 17:02:53 +09:00
Pupil
49d87a08d2 Set download notifications non-dismissable 2020-02-13 20:29:45 +09:00
Pupil
04c500f3d8 Improved galleryBlock loading logic 2020-02-13 20:15:17 +09:00
Pupil
d05c1e4d08 Improved galleryBlock loading logic 2020-02-13 20:14:26 +09:00
Pupil
bb63959678 Allow download multiple galleries concurrently 2020-02-13 20:07:16 +09:00
Pupil
842148647f Changed to log fetchGallery exceptions 2020-02-13 19:42:25 +09:00
69 changed files with 7275 additions and 585 deletions

View File

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

2
.idea/gradle.xml generated
View File

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

View File

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

View File

@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
if (file("google-services.json").exists()) { if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
logger.lifecycle("Firebase Enabled") logger.lifecycle("Firebase Enabled")
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
@@ -19,19 +19,24 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 41 versionCode 51
versionName "4.5" versionName "4.15"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
buildTypes { buildTypes {
release { debug {
minifyEnabled false debuggable true
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
buildTypes.each { release {
it.buildConfigField('boolean', 'CENSOR', 'false') buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
kotlinOptions { kotlinOptions {
@@ -41,7 +46,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
buildToolsVersion = '29.0.2' buildToolsVersion = '29.0.3'
} }
dependencies { dependencies {
@@ -57,26 +62,25 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.1" implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3' implementation 'androidx.multidex:multidex:2.0.1'
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.2.0-alpha04' implementation 'com.google.android.material:material:1.2.0-alpha05'
implementation 'com.google.firebase:firebase-core:17.2.2' implementation 'com.google.firebase:firebase-core:17.2.3'
implementation 'com.google.firebase:firebase-perf:19.0.5' implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") { annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false transitive = false
} }
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'

View File

@@ -19,3 +19,17 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontobfuscate
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep 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 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":41,"versionName":"4.5","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":51,"versionName":"4.15","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}]

View File

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

View File

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

View File

@@ -6,8 +6,10 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -18,7 +20,9 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:replace="android:theme"> tools:replace="android:theme"
android:requestLegacyExternalStorage="true"
tools:ignore="UnusedAttribute">
<provider <provider
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -32,6 +36,12 @@
</provider> </provider>
<receiver android:name=".BroadcastReciever" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</receiver>
<activity android:name=".ui.LockActivity" /> <activity android:name=".ui.LockActivity" />
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"

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

@@ -30,7 +30,10 @@ import androidx.preference.PreferenceManager
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics
import xyz.quaver.proxy
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getProxy
import java.io.File import java.io.File
class Pupil : MultiDexApplication() { class Pupil : MultiDexApplication() {
@@ -45,8 +48,13 @@ class Pupil : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
proxy = getProxy(this)
try { try {
PreferenceManager.getDefaultSharedPreferences(this).getInt("dl_location", 0) preference.getString("dl_location", null).also {
if (!File(it!!).canWrite())
throw Exception()
}
} catch (e: Exception) { } catch (e: Exception) {
preference.edit().remove("dl_location").apply() preference.edit().remove("dl_location").apply()
} }
@@ -54,10 +62,8 @@ class Pupil : MultiDexApplication() {
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json")) histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json")) favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
val file = preference.getString("dl_location", null) if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
if (file?.startsWith("content") == true)
preference.edit().remove("dl_location").apply()
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)
@@ -69,13 +75,27 @@ class Pupil : MultiDexApplication() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_download_description) description = getString(R.string.channel_download_description)
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET lockscreenVisibility = Notification.VISIBILITY_SECRET
} })
manager.createNotificationChannel(channel)
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
description = getString(R.string.channel_update_description)
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
description = getString(R.string.channel_update_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
} }
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)

View File

@@ -32,7 +32,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
@@ -49,13 +49,12 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface { class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType { enum class ViewType {
NEXT, NEXT,
@@ -63,34 +62,32 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
PREV PREV
} }
private val glide = Glide.with(context)
private lateinit var favorites: Histories private lateinit var favorites: Histories
val timer = Timer() val timer = Timer()
var isThin = false
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) { inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var timerTask: TimerTask? = null var timerTask: TimerTask? = null
private fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.IO).launch { private fun updateProgress(context: Context, galleryID: Int) {
val cache = Cache(context).getCachedGallery(galleryID)
val reader = Cache(context).getReaderOrNull(galleryID) val reader = Cache(context).getReaderOrNull(galleryID)
launch(Dispatchers.Main) main@{ CoroutineScope(Dispatchers.Main).launch {
if (reader == null) { if (reader == null) {
view.galleryblock_progressbar.visibility = View.GONE view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE view.galleryblock_progress_complete.visibility = View.GONE
return@main return@launch
} }
with(view.galleryblock_progressbar) { with(view.galleryblock_progressbar) {
progress = cache.listFiles()?.count { file -> progress = Cache(context).getImages(galleryID)?.size ?: 0
Regex("^[0-9]+.+\$").matches(file.name)
} ?: 0
if (visibility == View.GONE) { if (visibility == View.GONE) {
visibility = View.VISIBLE visibility = View.VISIBLE
max = reader.galleryInfo.size max = reader.galleryInfo.files.size
} }
if (progress == max) { if (progress == max) {
@@ -126,6 +123,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
val artists = galleryBlock.artists val artists = galleryBlock.artists
val series = galleryBlock.series val series = galleryBlock.series
if (isThin)
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
R.dimen.galleryblock_thumbnail_thin
)
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also { galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
it.start() it.start()
}) })
@@ -138,6 +139,7 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
null null
} }
galleryblock_thumbnail.post {
glide glide
.load(thumbnail) .load(thumbnail)
.skipMemoryCache(true) .skipMemoryCache(true)
@@ -149,6 +151,7 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
} }
.into(galleryblock_thumbnail) .into(galleryblock_thumbnail)
} }
}
//Check cache //Check cache
val cache = Cache(context).getCachedGallery(galleryBlock.id) val cache = Cache(context).getCachedGallery(galleryBlock.id)
@@ -160,7 +163,7 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
} ?: 0 } ?: 0
with(galleryblock_progressbar) { with(galleryblock_progressbar) {
max = reader.galleryInfo.size max = reader.galleryInfo.files.size
progress = count progress = count
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -264,6 +267,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 +352,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
mItemManger.closeAllExcept(layout) mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = holder.view.galleryblock_download.text =
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0) if (Cache(holder.view.context).isDownloading(gallery.id))
holder.view.context.getString(R.string.main_download)
else
holder.view.context.getString(android.R.string.cancel) holder.view.context.getString(android.R.string.cancel)
else
holder.view.context.getString(R.string.main_download)
} }
override fun onClose(layout: SwipeLayout?) {} override fun onClose(layout: SwipeLayout?) {}

View File

@@ -18,16 +18,12 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
@@ -36,71 +32,23 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ReaderAdapter(private val context: Context, class ReaderAdapter(private val glide: RequestManager,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
//region Glide.RecyclerView
inner class SizeProvider : ListPreloader.PreloadSizeProvider<File> {
override fun getPreloadSize(item: File, adapterPosition: Int, itemPosition: Int): IntArray? {
return Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(itemPosition)?.let {
arrayOf(it.width, it.height).toIntArray()
}
}
}
inner class ModelProvider : ListPreloader.PreloadModelProvider<File> {
override fun getPreloadItems(position: Int): MutableList<File> {
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
}
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
return glide
.load(item)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
}
}
//endregion
var reader: Reader? = null var reader: Reader? = null
val glide = Glide.with(context)
val timer = Timer() val timer = Timer()
val sizeProvider = SizeProvider()
val modelProvider = ModelProvider()
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
var isFullScreen = false var isFullScreen = false
var onItemClickListener : ((Int) -> (Unit))? = null var onItemClickListener : ((Int) -> (Unit))? = null
init {
CoroutineScope(Dispatchers.IO).launch {
reader = Cache(context).getReader(galleryID)
launch(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -114,10 +62,16 @@ class ReaderAdapter(private val context: Context,
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout holder.view as ConstraintLayout
if (isFullScreen) if (isFullScreen) {
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
else holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
} else {
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
holder.view.container.layoutParams.height = 0
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
}
holder.view.image.setOnPhotoTapListener { _, _, _ -> holder.view.image.setOnPhotoTapListener { _, _, _ ->
onItemClickListener?.invoke(position) onItemClickListener?.invoke(position)
@@ -127,37 +81,38 @@ class ReaderAdapter(private val context: Context,
onItemClickListener?.invoke(position) onItemClickListener?.invoke(position)
} }
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
holder.view.reader_index.text = (position+1).toString() holder.view.reader_index.text = (position+1).toString()
CoroutineScope(Dispatchers.IO).launch { val images = Cache(holder.view.context).getImage(galleryID, position)
val images = Cache(context).getImages(galleryID) val progress = DownloadWorker.getInstance(holder.view.context).progress[galleryID]?.get(position)
launch(Dispatchers.Main) { if (progress?.isInfinite() == true && images != null) {
if (images?.get(position) != null) { holder.view.reader_item_progressbar.visibility = View.INVISIBLE
holder.view.image.post {
glide glide
.load(images[position]) .load(images)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true) .skipMemoryCache(true)
.fitCenter()
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image) .into(holder.view.image)
}
} else { } else {
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position) holder.view.reader_item_progressbar.visibility = View.VISIBLE
glide.clear(holder.view.image)
if (progress?.isNaN() == true) { if (progress?.isNaN() == true) {
if (Fabric.isInitialized()) if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position)) Crashlytics.logException(DownloadWorker.getInstance(holder.view.context).exception[galleryID]?.get(position))
glide glide
.load(R.drawable.image_broken_variant) .load(R.drawable.image_broken_variant)
.into(holder.view.image) .into(holder.view.image)
return
} else { } else {
holder.view.reader_item_progressbar.progress = holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true) if (progress?.isInfinite() == true)
@@ -175,9 +130,7 @@ class ReaderAdapter(private val context: Context,
} }
} }
} }
}
}
override fun getItemCount() = reader?.galleryInfo?.size ?: 0 override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
} }

View File

@@ -45,15 +45,14 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import com.arlib.floatingsearchview.util.view.SearchInputView
import com.bumptech.glide.Glide
import com.crashlytics.android.Crashlytics
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list import kotlinx.serialization.list
import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi import xyz.quaver.hitomi.getGalleryIDsFromNozomi
@@ -179,7 +178,7 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel() (main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
} }
override fun onResume() { override fun onResume() {
@@ -197,7 +196,7 @@ class MainActivity : AppCompatActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")!!.toInt() val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) { return when(keyCode) {
@@ -332,11 +331,18 @@ class MainActivity : AppCompatActivity() {
true true
} }
with(main_fab_cancel) {
setImageResource(R.drawable.cancel)
setOnClickListener {
DownloadWorker.getInstance(context).stop()
}
}
with(main_fab_jump) { with(main_fab_jump) {
setImageResource(R.drawable.ic_jump) setImageResource(R.drawable.ic_jump)
setOnClickListener { setOnClickListener {
val preference = PreferenceManager.getDefaultSharedPreferences(context) val preference = PreferenceManager.getDefaultSharedPreferences(context)
val perPage = preference.getString("per_page", "25")!!.toInt() val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
val editText = EditText(context) val editText = EditText(context)
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
@@ -364,14 +370,16 @@ class MainActivity : AppCompatActivity() {
with(main_fab_id) { with(main_fab_id) {
setImageResource(R.drawable.numeric) setImageResource(R.drawable.numeric)
setOnClickListener { setOnClickListener {
val editText = EditText(context) val editText = EditText(context).apply {
inputType = InputType.TYPE_CLASS_NUMBER
}
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setView(editText) setView(editText)
setTitle(R.string.main_open_gallery_by_id) setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toInt() val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply { val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID) putExtra("galleryID", galleryID)
} }
@@ -392,7 +400,7 @@ class MainActivity : AppCompatActivity() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(main_recyclerview) { with(main_recyclerview) {
adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply { adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
query = it.toQuery() query = it.toQuery()
@@ -406,19 +414,15 @@ class MainActivity : AppCompatActivity() {
} }
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { position ->
val galleryID = galleries[position].id val galleryID = galleries[position].id
if (!completeFlag.get(galleryID, false)) {
val worker = DownloadWorker.getInstance(context) val worker = DownloadWorker.getInstance(context)
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress if (Cache(context).isDownloading(galleryID)) //download in progress
worker.cancel(galleryID) worker.cancel(galleryID)
else { else {
Cache(context).setDownloading(galleryID, true) Cache(context).setDownloading(galleryID, true)
if (!worker.queue.contains(galleryID))
worker.queue.add(galleryID) worker.queue.add(galleryID)
} }
}
closeAllItems() closeAllItems()
} }
@@ -469,6 +473,7 @@ class MainActivity : AppCompatActivity() {
GalleryDialog( GalleryDialog(
this@MainActivity, this@MainActivity,
Glide.with(this@MainActivity),
galleryID galleryID
).apply { ).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
@@ -693,7 +698,6 @@ class MainActivity : AppCompatActivity() {
} }
private var suggestionJob : Job? = null private var suggestionJob : Job? = null
@UseExperimental(ImplicitReflectionSerializer::class)
private fun setupSearchBar() { private fun setupSearchBar() {
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text) val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
//Change upper case letters to lower case //Change upper case letters to lower case
@@ -717,17 +721,35 @@ class MainActivity : AppCompatActivity() {
with(main_searchview as FloatingSearchView) { with(main_searchview as FloatingSearchView) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json") val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
val json = Json(JsonConfiguration.Stable)
val serializer = Tag.serializer().list val serializer = Tag.serializer().list
if (!favoritesFile.exists()) { if (!favoritesFile.exists()) {
favoritesFile.createNewFile() favoritesFile.createNewFile()
favoritesFile.writeText(json.stringify(Tags(listOf()))) favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
} }
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
override fun onMenuOpened() {
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
}
override fun onMenuClosed() {
//Do Nothing
}
})
setOnMenuItemClickListener { setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS) R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> { R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST sortMode = SortMode.NEWEST
it.isChecked = true it.isChecked = true
@@ -840,7 +862,7 @@ class MainActivity : AppCompatActivity() {
favorites.add(tag) favorites.add(tag)
} }
favoritesFile.writeText(json.stringify(favorites)) favoritesFile.writeText(json.stringify(serializer, favorites))
} }
} }
@@ -939,58 +961,60 @@ class MainActivity : AppCompatActivity() {
when(sortMode) { when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all") else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply { }.also {
totalItems = size totalItems = it.size
} }
} }
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply { else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = size totalItems = it.size
} }
} }
} }
Mode.HISTORY -> { Mode.HISTORY -> {
when { when {
query.isEmpty() -> { query.isEmpty() -> {
histories.toList().apply { histories.toList().also {
totalItems = size totalItems = it.size
} }
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
histories.filter { result.binarySearch(it) >= 0 }.apply { histories.filter { result.binarySearch(it) >= 0 }.also {
totalItems = size totalItems = it.size
} }
} }
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file -> val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists() file.isDirectory && file.name.toIntOrNull() != null
}?.sortedByDescending {
it.lastModified()
}?.map { }?.map {
it.name.toInt() it.name.toInt()
} ?: emptyList() } ?: emptyList()
when { when {
query.isEmpty() -> downloads.apply { query.isEmpty() -> downloads.also {
totalItems = size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
downloads.filter { result.binarySearch(it) >= 0 }.apply { downloads.filter { result.binarySearch(it) >= 0 }.also {
totalItems = size totalItems = it.size
} }
} }
} }
} }
Mode.FAVORITE -> { Mode.FAVORITE -> {
when { when {
query.isEmpty() -> favorites.toList().apply { query.isEmpty() -> favorites.toList().also {
totalItems = size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
favorites.filter { result.binarySearch(it) >= 0 }.apply { favorites.filter { result.binarySearch(it) >= 0 }.also {
totalItems = size totalItems = it.size
} }
} }
} }
@@ -1004,9 +1028,16 @@ class MainActivity : AppCompatActivity() {
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25 val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
loadingJob = CoroutineScope(Dispatchers.IO).launch { loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = galleryIDs?.await() val galleryIDs = try {
galleryIDs!!.await().also {
if (it.isEmpty())
throw Exception("No result")
}
} catch (e: Exception) {
if (Fabric.isInitialized() && e.message != "No result")
Crashlytics.logException(e)
if (galleryIDs.isNullOrEmpty()) { //No result
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
main_noresult.visibility = View.VISIBLE main_noresult.visibility = View.VISIBLE
main_progressbar.hide() main_progressbar.hide()

View File

@@ -32,13 +32,13 @@ import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.* import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.* import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.serialization.ImplicitReflectionSerializer
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -141,7 +141,6 @@ class ReaderActivity : AppCompatActivity() {
super.onResume() super.onResume()
} }
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu) menuInflater.inflate(R.menu.reader, menu)
@@ -159,10 +158,10 @@ class ReaderActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId) { when(item?.itemId) {
R.id.reader_menu_page_indicator -> { R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
with(view.dialog_number_picker) { with(view.dialog_number_picker) {
minValue=1 minValue=1
maxValue=reader_recyclerview?.adapter?.itemCount ?: 0 maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
value=currentPage value=currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
@@ -256,11 +255,17 @@ class ReaderActivity : AppCompatActivity() {
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
if (reader != null) { if (reader != null) {
title = reader.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}" with (reader_recyclerview.adapter as ReaderAdapter) {
this.reader = reader
notifyDataSetChanged()
}
title = reader.galleryInfo.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
when (reader.code) { when (reader.code) {
@@ -282,7 +287,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply { adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID).apply {
onItemClickListener = { onItemClickListener = {
if (isScroll) { if (isScroll) {
isScroll = false isScroll = false
@@ -296,7 +301,6 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
//addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() { addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@@ -357,6 +361,8 @@ class ReaderActivity : AppCompatActivity() {
window.attributes = this window.attributes = this
} }
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
} }
private fun scrollMode(isScroll: Boolean) { private fun scrollMode(isScroll: Boolean) {
@@ -382,7 +388,7 @@ class ReaderActivity : AppCompatActivity() {
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post { post {
setImageResource(R.drawable.ic_download) setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download) labelText = getString(R.string.reader_fab_download_cancel)
} }
else // Or continue animate else // Or continue animate
post { post {

View File

@@ -30,9 +30,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.* import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.list
import kotlinx.serialization.json.Json import kotlinx.serialization.serializer
import kotlinx.serialization.parseList
import net.rdrei.android.dirchooser.DirectoryChooserActivity import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -79,7 +78,6 @@ class SettingsActivity : AppCompatActivity() {
return true return true
} }
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) { when(requestCode) {
REQUEST_LOCK -> { REQUEST_LOCK -> {
@@ -96,13 +94,13 @@ class SettingsActivity : AppCompatActivity() {
val uri = data?.data ?: return val uri = data?.data ?: return
try { try {
val json = contentResolver.openInputStream(uri).use { inputStream -> val str = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!! inputStream!!
inputStream.readBytes().toString(Charset.defaultCharset()) inputStream.readBytes().toString(Charset.defaultCharset())
} }
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).also { (application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
Snackbar.make( Snackbar.make(
window.decorView, window.decorView,
getString(R.string.settings_restore_successful, it.size), getString(R.string.settings_restore_successful, it.size),
@@ -158,6 +156,43 @@ class SettingsActivity : AppCompatActivity() {
.apply() .apply()
} }
} }
REQUEST_IMPORT_OLD_GALLERIES -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int =
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
val file = uri.toFile(this)
if (file?.canRead() != true)
Snackbar.make(
settings,
resources.getText(R.string.import_old_galleries_folder_not_readable),
Snackbar.LENGTH_LONG
).show()
else
importOldGalleries(this, file)
}
}
}
REQUEST_IMPORT_OLD_GALLERIES_OLD -> {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canRead())
Snackbar.make(
settings,
resources.getText(R.string.import_old_galleries_folder_not_readable),
Snackbar.LENGTH_LONG
).show()
else {
importOldGalleries(this, File(directory))
}
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }

View File

@@ -46,16 +46,12 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val excludeBL = "-male:yaoi" private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro") private val excludeGuro = listOf("-female:guro", "-male:guro")
private lateinit var dialogView : View
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.default_query_dialog_title) setTitle(R.string.default_query_dialog_title)
setView(dialogView) setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString()) val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
@@ -79,15 +75,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun initDialog() { private fun build() : View {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val tags = Tags.parse( val tags = Tags.parse(
preferences.getString("default_query", "") ?: "" preferences.getString("default_query", "") ?: ""
) )
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null) val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
with(dialogView.default_query_dialog_language_selector) { with(view.default_query_dialog_language_selector) {
adapter = adapter =
ArrayAdapter( ArrayAdapter(
context, context,
@@ -110,13 +106,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
} }
with(dialogView.default_query_dialog_BL_checkbox) { with(view.default_query_dialog_BL_checkbox) {
isChecked = tags.contains(excludeBL) isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL)) if (tags.contains(excludeBL))
tags.remove(excludeBL) tags.remove(excludeBL)
} }
with(dialogView.default_query_dialog_guro_checkbox) { with(view.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) } isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) }) if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach { excludeGuro.forEach {
@@ -124,7 +120,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
} }
with(dialogView.default_query_dialog_edittext) { with(view.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE) setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged( override fun beforeTextChanged(
@@ -149,6 +145,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
}) })
} }
return view
} }
} }

View File

@@ -26,6 +26,7 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -36,10 +37,7 @@ import kotlinx.android.synthetic.main.item_dl_location.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.REQUEST_WRITE_PERMISSION_AND_SAF
import xyz.quaver.pupil.util.byteToString
import java.io.File import java.io.File
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@@ -49,6 +47,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val buttons = mutableListOf<Pair<RadioButton, File?>>() private val buttons = mutableListOf<Pair<RadioButton, File?>>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.settings_dl_location)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun build() : View {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null) val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
@@ -115,25 +123,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
buttons.add(button to null) buttons.add(button to null)
}) })
val pref = preference.getString("dl_location", null) externalFilesDirs.indexOfFirst {
val index = externalFilesDirs.indexOfFirst { it.canonicalPath == getDownloadDirectory(context).canonicalPath
it.canonicalPath == pref }.let { index ->
}
if (index < 0) if (index < 0)
buttons.last().first.isChecked = true buttons.first().first.isChecked = true
else else
buttons[index].first.isChecked = true buttons[index].first.isChecked = true
setTitle(R.string.settings_dl_location)
setView(view)
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
dismiss()
} }
super.onCreate(savedInstanceState) return view
} }
} }

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -29,7 +30,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.* import kotlinx.android.synthetic.main.dialog_gallery.*
@@ -53,7 +54,7 @@ import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) { class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map { private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split -> it.split("|").let { split ->
@@ -61,8 +62,6 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
} }
}.toMap() }.toMap()
private val glide = Glide.with(context)
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -90,7 +89,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
try { try {
val gallery = getGallery(galleryID) val gallery = getGallery(galleryID)
launch(Dispatchers.Main) { gallery_cover.post {
gallery_progressbar.visibility = View.GONE gallery_progressbar.visibility = View.GONE
gallery_title.text = gallery.title gallery_title.text = gallery.title
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() } gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
@@ -112,7 +111,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
} }
} }
Glide.with(context) glide
.load(gallery.cover) .load(gallery.cover)
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
@@ -226,7 +225,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val galleries = ArrayList<GalleryBlock>() val galleries = ArrayList<GalleryBlock>()
val adapter = GalleryBlockAdapter(context, galleries).apply { val adapter = GalleryBlockAdapter(glide, galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler -> this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
@@ -264,6 +263,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
.setOnItemLongClickListener { _, position, _ -> .setOnItemLongClickListener { _, position, _ ->
GalleryDialog( GalleryDialog(
context, context,
glide,
galleries[position].id galleries[position].id
).apply { ).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->

View File

@@ -22,6 +22,7 @@ import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
@@ -56,21 +57,17 @@ class MirrorDialog(context: Context) : AlertDialog(context) {
} }
} }
private lateinit var recyclerView: RecyclerView
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.settings_mirror_title) setTitle(R.string.settings_mirror_title)
setView(recyclerView) setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> } setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
private fun initDialog() { private fun build() : View {
recyclerView = RecyclerView(context).apply recyclerview@{ return RecyclerView(context).apply recyclerview@{
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = MirrorAdapter(context).apply adapter@{ adapter = MirrorAdapter(context).apply adapter@{

View File

@@ -0,0 +1,133 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.dialog_proxy.view.*
import xyz.quaver.proxy
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.ProxyInfo
import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.json
import java.net.Proxy
class ProxyDialog(context: Context) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
val view = build()
setTitle(R.string.settings_proxy_title)
setContentView(view)
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
super.onCreate(savedInstanceState)
}
@SuppressLint("InflateParams")
private fun build() : View {
val proxyInfo = getProxyInfo(context)
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
val enabler = { enable: Boolean ->
view?.proxy_addr?.isEnabled = enable
view?.proxy_port?.isEnabled = enable
view?.proxy_username?.isEnabled = enable
view?.proxy_password?.isEnabled = enable
if (!enable) {
view?.proxy_addr?.text = null
view?.proxy_port?.text = null
view?.proxy_username?.text = null
view?.proxy_password?.text = null
}
}
with(view.proxy_type_selector) {
adapter = ArrayAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
context.resources.getStringArray(R.array.proxy_type)
)
setSelection(proxyInfo.type.ordinal)
onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
enabler.invoke(position != 0)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
view.proxy_addr.setText(proxyInfo.host)
view.proxy_port.setText(proxyInfo.port?.toString())
view.proxy_username.setText(proxyInfo.username)
view.proxy_password.setText(proxyInfo.password)
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
view.proxy_cancel.setOnClickListener {
dismiss()
}
view.proxy_ok.setOnClickListener {
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
val addr = view.proxy_addr.text?.toString()
val port = view.proxy_port.text?.toString()?.toIntOrNull()
val username = view.proxy_username.text?.toString()
val password = view.proxy_password.text?.toString()
if (type != Proxy.Type.DIRECT) {
if (addr == null || addr.isEmpty())
view.proxy_addr.error = context.getText(R.string.proxy_dialog_error)
if (port == null)
view.proxy_port.error = context.getText(R.string.proxy_dialog_error)
if (addr == null || addr.isEmpty() || port == null)
return@setOnClickListener
}
ProxyInfo(type, addr, port, username, password).let {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString("proxy",
json.stringify(ProxyInfo.serializer(), it)
).apply()
proxy = it.proxy()
}
dismiss()
}
return view
}
}

View File

@@ -18,17 +18,23 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
@@ -36,6 +42,7 @@ import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.ui.dialog.ProxyDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
@@ -146,6 +153,10 @@ class SettingsFragment :
MirrorDialog(context) MirrorDialog(context)
.show() .show()
} }
"proxy" -> {
ProxyDialog(context)
.show()
}
"backup" -> { "backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"), File(getDownloadDirectory(context), "favorites.json"),
@@ -163,6 +174,31 @@ class SettingsFragment :
activity?.startActivityForResult(intent, REQUEST_RESTORE) activity?.startActivityForResult(intent, REQUEST_RESTORE)
} }
"old_import_galleries" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(activity!!, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES)
}
} else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder()
.newDirectoryName("Pupil")
.allowNewDirectoryNameModification(true)
.build()
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
}
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES_OLD)
}
}
else -> return false else -> return false
} }
} }
@@ -189,9 +225,18 @@ class SettingsFragment :
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
key ?: return
with(findPreference<Preference>(key)) {
this ?: return
when (key) { when (key) {
"proxy" -> {
summary = getProxyInfo(context).type.name
}
"dl_location" -> { "dl_location" -> {
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).canonicalPath summary = getDownloadDirectory(context!!).canonicalPath
}
} }
} }
} }
@@ -245,8 +290,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"default_query" -> { "default_query" -> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: ""
summary = preferences.getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
@@ -270,6 +314,11 @@ class SettingsFragment :
"mirrors" -> { "mirrors" -> {
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"proxy" -> {
summary = getProxyInfo(context).type.name
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> { "dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }
@@ -279,6 +328,9 @@ class SettingsFragment :
"restore" -> { "restore" -> {
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"old_import_galleries" -> {
onPreferenceClickListener = this@SettingsFragment
}
} }
} }

View File

@@ -18,8 +18,22 @@
package xyz.quaver.pupil.util 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_LOCK = 38238
const val REQUEST_RESTORE = 16546 const val REQUEST_RESTORE = 16546
const val REQUEST_IMPORT_OLD_GALLERIES = 6458
const val REQUEST_IMPORT_OLD_GALLERIES_OLD = 5946
const val REQUEST_DOWNLOAD_FOLDER = 3874 const val REQUEST_DOWNLOAD_FOLDER = 3874
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425 const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900 const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
const val NOTIFICATION_ID_UPDATE = 2345
val json = Json(JsonConfiguration.Stable)

View File

@@ -22,25 +22,46 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import android.util.SparseArray
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.async import kotlinx.io.InputStream
import kotlinx.coroutines.withContext
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.proxy
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf
import xyz.quaver.pupil.util.json
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.URL import java.net.URL
import java.util.concurrent.Executors
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
class Cache(context: Context) : ContextWrapper(context) { class Cache(context: Context) : ContextWrapper(context) {
companion object {
private val moving = mutableListOf<Int>()
}
private val locks = SparseArray<Lock>()
private fun lock(galleryID: Int) {
synchronized(locks) {
if (locks.indexOfKey(galleryID) < 0)
locks.put(galleryID, ReentrantLock())
}
locks[galleryID].lock()
}
private fun unlock(galleryID: Int) {
locks[galleryID]?.unlock()
}
private val preference = PreferenceManager.getDefaultSharedPreferences(this) private val preference = PreferenceManager.getDefaultSharedPreferences(this)
// Search in this order // Search in this order
@@ -50,7 +71,6 @@ class Cache(context: Context) : ContextWrapper(context) {
it.mkdirs() it.mkdirs()
} }
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? { fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = File(getCachedGallery(galleryID), ".metadata") val file = File(getCachedGallery(galleryID), ".metadata")
@@ -58,7 +78,7 @@ class Cache(context: Context) : ContextWrapper(context) {
return null return null
return try { return try {
Json.parse(file.readText()) json.parse(Metadata.serializer(), file.readText())
} catch (e: Exception) { } catch (e: Exception) {
//File corrupted //File corrupted
file.delete() file.delete()
@@ -66,14 +86,13 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
} }
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) { fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = File(getCachedGallery(galleryID), ".metadata").also { val file = File(getCachedGallery(galleryID), ".metadata").also {
if (!it.exists()) if (!it.exists())
it.createNewFile() it.createNewFile()
} }
file.writeText(Json.stringify(metadata)) file.writeText(json.stringify(Metadata.serializer(), metadata))
} }
suspend fun getThumbnail(galleryID: Int): String? { suspend fun getThumbnail(galleryID: Int): String? {
@@ -83,7 +102,9 @@ class Cache(context: Context) : ContextWrapper(context) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails val thumbnails = getGalleryBlock(galleryID)?.thumbnails
try { try {
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT) Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
it.readBytes()
}, Base64.DEFAULT)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -102,21 +123,29 @@ class Cache(context: Context) : ContextWrapper(context) {
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? { suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID) val metadata = Cache(this).getCachedMetadata(galleryID)
val source = mapOf( val sources = listOf(
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) }, { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) } { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
) )
val galleryBlock = if (metadata?.galleryBlock == null) val galleryBlock = if (metadata?.galleryBlock == null) {
source.entries.map {
CoroutineScope(Dispatchers.IO).async { CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching { var galleryBlock: GalleryBlock? = null
it.value.invoke()
}.getOrNull() for (source in sources) {
galleryBlock = try {
source.invoke()
} catch (e: Exception) {
null
}
if (galleryBlock != null)
break
}
galleryBlock
}.await() ?: return null
} }
}.firstOrNull {
it.await() != null
}?.await()
else else
metadata.galleryBlock metadata.galleryBlock
@@ -155,20 +184,22 @@ class Cache(context: Context) : ContextWrapper(context) {
var retval: Reader? = null var retval: Reader? = null
for (source in sources) { for (source in sources) {
retval = kotlin.runCatching { retval = try {
source.value.invoke() source.value.invoke()
}.getOrNull() } catch (e: Exception) {
Crashlytics.logException(e)
null
}
if (retval != null) if (retval != null)
break break
} }
retval retval
}.await() }.await() ?: return null
} else } else
metadata.reader metadata.reader
if (reader != null)
setCachedMetadata( setCachedMetadata(
galleryID, galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader) Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
@@ -177,39 +208,74 @@ class Cache(context: Context) : ContextWrapper(context) {
return reader return reader
} }
val imageNameRegex = Regex("""^\d+\..+$""")
fun getImages(galleryID: Int): List<File?>? { fun getImages(galleryID: Int): List<File?>? {
val started = System.currentTimeMillis()
val gallery = getCachedGallery(galleryID) val gallery = getCachedGallery(galleryID)
val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles() ?: return null
Log.i("PUPILD", "${System.currentTimeMillis() - started} ms") return gallery.list { _, name ->
return reader.galleryInfo.indices.map { index -> imageNameRegex.matches(name)
images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) } }?.map {
File(gallery, it)
} }
} }
fun putImage(galleryID: Int, name: String, data: ByteArray) { val imageExtensions = listOf(
val cache = File(getCachedGallery(galleryID), name).also { "png",
"jpg",
"webp",
"gif"
)
fun getImage(galleryID: Int, index: Int): File? {
val gallery = getCachedGallery(galleryID)
for (ext in imageExtensions) {
File(gallery, "%05d.$ext".format(index)).let {
if (it.exists())
return it
}
}
return null
}
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
if (!it.exists()) if (!it.exists())
it.createNewFile() it.createNewFile()
} }
if (!Regex("""^[0-9]+.+$""").matches(name)) data.use {
throw IllegalArgumentException("File name is not a number") it.copyTo(FileOutputStream(cache))
}
cache.writeBytes(data)
} }
fun moveToDownload(galleryID: Int) { fun moveToDownload(galleryID: Int) {
if (moving.contains(galleryID))
return
CoroutineScope(Dispatchers.IO).launch {
val cache = getCachedGallery(galleryID).also { val cache = getCachedGallery(galleryID).also {
if (!it.exists()) if (!it.exists())
return return@launch
} }
val download = File(getDownloadDirectory(this), galleryID.toString()) val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
cache.copyRecursively(download, true) 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() cache.deleteRecursively()
Log.i("PUPILD", "DELETED ${cache.canonicalPath}")
}
} }
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true

View File

@@ -23,6 +23,7 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -36,15 +37,18 @@ import okio.*
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.proxy
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) { class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
@@ -145,58 +149,55 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private val loop = loop() private val loop = loop()
private val worker = SparseArray<Job?>() private val worker = SparseArray<Job?>()
@Volatile var nRunners = 0
private val client = OkHttpClient.Builder() val interceptor = Interceptor { chain ->
.addInterceptor { chain ->
val request = chain.request() val request = chain.request()
var response = chain.proceed(request) val response = chain.proceed(request)
var retry = preferences.getInt("retry", 3)
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder() response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener)) .body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build() .build()
} }
val client =
OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.readTimeout(0, TimeUnit.SECONDS)
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4))) .dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.proxy(proxy)
.build() .build()
fun stop() { fun stop() {
queue.clear() queue.clear()
loop.cancel() loop.cancel()
for (i in 0..worker.size()) { for (i in 0 until worker.size()) {
val galleryID = worker.keyAt(i) val galleryID = worker.keyAt(i)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
worker[galleryID]?.cancel() worker[galleryID]?.cancel()
} }
client.dispatcher().cancelAll() client.dispatcher().queuedCalls().filter {
it.request().tag() is Pair<*, *>
}.forEach {
it.cancel()
}
progress.clear() progress.clear()
exception.clear() exception.clear()
notification.clear() notification.clear()
notificationManager.cancelAll() notificationManager.cancelAll()
nRunners = 0
} }
fun cancel(galleryID: Int) { fun cancel(galleryID: Int) {
queue.remove(galleryID) queue.remove(galleryID)
worker[galleryID]?.cancel() worker[galleryID]?.cancel()
client.dispatcher().queuedCalls() client.dispatcher().queuedCalls().filter {
.filter { ((it.request().tag() as Pair<*, *>).first as Int) == galleryID
@Suppress("UNCHECKED_CAST") }.forEach {
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
}
.forEach {
it.cancel() it.cancel()
} }
@@ -205,10 +206,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notification.remove(galleryID) notification.remove(galleryID)
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0) { if (progress.indexOfKey(galleryID) >= 0)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
}
} }
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
@@ -220,10 +219,10 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
when (reader.code) { when (reader.code) {
Code.HITOMI -> { Code.HITOMI -> {
url( url(
urlFromUrlFromHash( imageUrlFromImage(
galleryID, galleryID,
reader.galleryInfo[index], reader.galleryInfo.files[index],
if (lowQuality) "webp" else null lowQuality
) )
) )
addHeader("Referer", getReferer(galleryID)) addHeader("Referer", getReferer(galleryID))
@@ -252,24 +251,23 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
exception.put(galleryID, null) exception.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
return@launch return@launch
} }
val cache = Cache(this@DownloadWorker).getImages(galleryID) val cache = Cache(this@DownloadWorker).getImages(galleryID)
progress.put(galleryID, reader.galleryInfo.indices.map { index -> progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
if (cache?.get(index) != null) if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
Float.POSITIVE_INFINITY Float.POSITIVE_INFINITY
else else
0F 0F
}.toMutableList()) }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList()) exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
if (notification[galleryID] == null) if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
notification[galleryID].setContentTitle(reader.title) notification[galleryID].setContentTitle(reader.galleryInfo.title)
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
@@ -279,15 +277,15 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setDownloading(galleryID, false) setDownloading(galleryID, false)
} }
} }
nRunners--
return@launch return@launch
} }
for (i in reader.galleryInfo.indices) { for (i in reader.galleryInfo.files.indices) {
val callback = object : Callback { val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
if (Fabric.isInitialized()) Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
if (Fabric.isInitialized() && e.message != "Canceled")
Crashlytics.logException(e) Crashlytics.logException(e)
progress[galleryID]?.set(i, Float.NaN) progress[galleryID]?.set(i, Float.NaN)
@@ -295,6 +293,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID) notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) { with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) { if (isDownloading(galleryID)) {
@@ -302,22 +301,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setDownloading(galleryID, false) setDownloading(galleryID, false)
} }
} }
nRunners-- }
} }
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
response.body().use { Log.i("PUPILD", "OK ${call.request().tag()}")
val res = it.bytes()
val ext =
call.request().url().encodedPath().split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res) val ext = call.request().url().encodedPath().split('.').last()
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
try {
response.body().use {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
} }
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
notify(galleryID) notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) { with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) { if (isDownloading(galleryID)) {
@@ -325,13 +326,41 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setDownloading(galleryID, false) setDownloading(galleryID, false)
} }
} }
nRunners-- }
}
Log.i("PUPILD", "SUCCESS ${call.request().tag()}")
} catch (e: Exception) {
progress[galleryID]?.set(i, Float.NaN)
exception[galleryID]?.set(i, e)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
} }
} }
} }
if (progress[galleryID]?.get(i)?.isFinite() == true) File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})")
}
}
}
if (progress[galleryID]?.get(i)?.isFinite() == true) {
queueDownload(galleryID, reader, i, callback) queueDownload(galleryID, reader, i, callback)
Log.i("PUPILD", "$galleryID QUEUED $i")
} else {
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
}
} }
} }
@@ -339,11 +368,17 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0 val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
if (isCompleted(galleryID)) Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
if (isCompleted(galleryID)) {
notification[galleryID] notification[galleryID]
?.setContentText(getString(R.string.reader_notification_complete)) ?.setContentText(getString(R.string.reader_notification_complete))
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
?.setProgress(0, 0, false) ?.setProgress(0, 0, false)
else ?.setOngoing(false)
notificationManager.cancel(galleryID)
} else
notification[galleryID] notification[galleryID]
?.setProgress(max, progress, false) ?.setProgress(max, progress, false)
?.setContentText("$progress/$max") ?.setContentText("$progress/$max")
@@ -360,7 +395,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
val pendingIntent = TaskStackBuilder.create(this).run { val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent) addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
} }
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply { notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
@@ -369,24 +404,30 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
setProgress(0, 0, true) setProgress(0, 0, true)
setOngoing(true)
}) })
} }
private fun loop() = CoroutineScope(Dispatchers.Default).launch { private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) { while (true) {
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4)) if (queue.isEmpty())
continue continue
val galleryID = queue.poll() ?: continue val galleryID = queue.peek() ?: continue
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading! if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue cancel(galleryID)
if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID)) if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build()) notificationManager.notify(galleryID, notification[galleryID].build())
Log.i("PUPILD", "QUEUED $galleryID")
worker.put(galleryID, download(galleryID)) worker.put(galleryID, download(galleryID))
nRunners++ queue.poll()
} }
} }

View File

@@ -212,3 +212,6 @@ fun Uri.toFile(context: Context): File? {
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName) return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
} }
fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath)

View File

@@ -18,15 +18,14 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.list
import kotlinx.serialization.json.Json import kotlinx.serialization.serializer
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.parseList
import kotlinx.serialization.stringify
import java.io.File import java.io.File
class Histories(private val file: File) : ArrayList<Int>() { class Histories(private val file: File) : ArrayList<Int>() {
val serializer = Int.serializer().list
init { init {
if (!file.exists()) if (!file.exists())
file.parentFile?.mkdirs() file.parentFile?.mkdirs()
@@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
} }
} }
@UseExperimental(ImplicitReflectionSerializer::class)
fun load() : Histories { fun load() : Histories {
return apply { return apply {
super.clear() super.clear()
addAll( addAll(
Json(JsonConfiguration.Stable).parseList( json.parse(
serializer,
file.bufferedReader().use { it.readText() } file.bufferedReader().use { it.readText() }
) )
) )
} }
} }
@UseExperimental(ImplicitReflectionSerializer::class)
fun save() { fun save() {
file.writeText(Json(JsonConfiguration.Stable).stringify(this)) file.writeText(json.stringify(serializer, this))
} }
override fun add(element: Int): Boolean { override fun add(element: Int): Boolean {

View File

@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.serialization.* import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import java.io.File import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
load() load()
} }
@UseExperimental(ImplicitReflectionSerializer::class)
private fun load() { private fun load() {
val lock = File(ContextCompat.getDataDir(this), "lock.json") val lock = File(ContextCompat.getDataDir(this), "lock.json")
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
lock.writeText("[]") lock.writeText("[]")
} }
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText())) locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
} }
@UseExperimental(ImplicitReflectionSerializer::class)
private fun save() { private fun save() {
val lock = File(ContextCompat.getDataDir(this), "lock.json") val lock = File(ContextCompat.getDataDir(this), "lock.json")
if (!lock.exists()) if (!lock.exists())
lock.createNewFile() lock.createNewFile()
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf())) lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
} }
fun add(lock: Lock) { fun add(lock: Lock) {

View File

@@ -0,0 +1,63 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import android.content.Context
import androidx.preference.PreferenceManager
import kotlinx.serialization.Serializable
import okhttp3.Authenticator
import okhttp3.Credentials
import java.net.InetSocketAddress
import java.net.Proxy
@Serializable
data class ProxyInfo(
val type: Proxy.Type,
val host: String? = null,
val port: Int? = null,
val username: String? = null,
val password: String? = null
) {
fun proxy() : Proxy {
return if (host == null || port == null)
return Proxy.NO_PROXY
else
Proxy(type, InetSocketAddress.createUnresolved(host, port))
}
fun authenticator() = Authenticator { _, response ->
val credential = Credentials.basic(username, password)
response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
}
fun getProxy(context: Context) =
getProxyInfo(context).proxy()
fun getProxyInfo(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context).getString("proxy", null).let {
if (it == null)
ProxyInfo(Proxy.Type.DIRECT)
else
json.parse(ProxyInfo.serializer(), it)
}

View File

@@ -18,32 +18,45 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.webkit.MimeTypeMap import android.net.Uri
import android.util.Base64
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.JsonArray
import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.* import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.content
import okhttp3.*
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.proxy
import xyz.quaver.pupil.BroadcastReciever
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.Metadata
import java.io.File import java.io.File
import java.io.IOException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
fun getReleases(url: String) : JsonArray { fun getReleases(url: String) : JsonArray {
return try { return try {
URL(url).readText().let { URL(url).readText().let {
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it) json.parse(JsonArray.serializer(), it)
} }
} catch (e: Exception) { } catch (e: Exception) {
JsonArray(emptyList()) JsonArray(emptyList())
@@ -78,7 +91,7 @@ fun getApkUrl(releases: JsonObject) : String? {
} }
const val UPDATE_NOTIFICATION_ID = 384823 const val UPDATE_NOTIFICATION_ID = 384823
fun checkUpdate(context: AppCompatActivity, force: Boolean = false) { fun checkUpdate(context: Context, force: Boolean = false) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0) val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
@@ -140,53 +153,27 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setMessage(Markwon.create(context).toMarkdown(msg)) setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ -> setPositiveButton(android.R.string.yes) { _, _ ->
val notificationManager = NotificationManagerCompat.from(context) val preference = PreferenceManager.getDefaultSharedPreferences(context)
val builder = NotificationCompat.Builder(context, "download").apply {
setContentTitle(context.getString(R.string.update_notification_description)) val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
setSmallIcon(android.R.drawable.stat_sys_download)
priority = NotificationCompat.PRIORITY_LOW //Cancel any download queued before
val id = preference.getLong("update_download_id", -1)
if (id != -1L)
downloadManager.remove(id)
val target = File(context.getExternalFilesDir(null), "Pupil.apk").also {
it.delete()
} }
CoroutineScope(Dispatchers.IO).launch io@{ val request = DownloadManager.Request(Uri.parse(url))
val target = File(getDownloadDirectory(context), "Pupil.apk") .setTitle(context.getText(R.string.update_notification_description))
.setDestinationUri(Uri.fromFile(target))
try { downloadManager.enqueue(request).also {
URL(url).download(target) { progress, fileSize -> preference.edit().putLong("update_download_id", it).apply()
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
} catch (e: Exception) {
builder.apply {
setContentText(context.getString(R.string.update_failed))
setMessage(context.getString(R.string.update_failed_message))
setSmallIcon(android.R.drawable.stat_sys_download_done)
}
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))
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
if (context.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
context.startActivity(install)
else
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
} }
} }
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ -> setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
@@ -202,3 +189,148 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
} }
} }
} }
var cancelImport = false
@SuppressLint("RestrictedApi")
fun importOldGalleries(context: Context, folder: File) = CoroutineScope(Dispatchers.IO).launch {
val client = OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.proxy(proxy)
.build()
val cancelIntent = Intent(context, BroadcastReciever::class.java).apply {
action = BroadcastReciever.ACTION_CANCEL_IMPORT
putExtra(BroadcastReciever.EXTRA_IMPORT_NOTIFICATION_ID, 0)
}
val pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, 0)
val notificationManager = NotificationManagerCompat.from(context)
val notificationBuilder = NotificationCompat.Builder(context, "import").apply {
setContentTitle(context.getText(R.string.import_old_galleries_notification))
setProgress(0, 0, true)
setSmallIcon(R.drawable.ic_notification)
addAction(0, context.getText(android.R.string.cancel), pendingIntent)
setOngoing(true)
}
notificationManager.notify(0, notificationBuilder.build())
if (!folder.isDirectory)
return@launch
val galleryRegex = Regex("""[0-9]+$""")
val imageRegex = Regex("""^[0-9]+\..+$""")
var size = 0
fun setProgress(progress: Int) {
notificationBuilder.apply {
setContentText(
context.getString(
R.string.import_old_galleries_notification_text,
progress,
size
)
)
setProgress(size, progress, false)
}
notificationManager.notify(0, notificationBuilder.build())
}
folder.listFiles { _, name ->
galleryRegex.matches(name)
}?.also {
size = it.size
setProgress(0)
}?.forEachIndexed { index, gallery ->
if (cancelImport)
return@forEachIndexed
setProgress(index)
val galleryID = gallery.name.toIntOrNull() ?: return@forEachIndexed
File(getDownloadDirectory(context), galleryID.toString()).mkdirs()
val reader = async {
kotlin.runCatching {
json.parse(Reader.serializer(), File(gallery, "reader.json").readText())
}.getOrElse {
getReader(galleryID)
}
}
val galleryBlock = async {
kotlin.runCatching {
json.parse(GalleryBlock.serializer(), File(gallery, "galleryBlock.json").readText())
}.getOrElse {
getGalleryBlock(galleryID)
}
}
@Suppress("NAME_SHADOWING")
val thumbnail = async thumbnail@{
val galleryBlock = galleryBlock.await()
Base64.encodeToString(try {
File(gallery, "thumbnail.jpg").readBytes()
} catch (e: Exception) {
val url = galleryBlock?.thumbnails?.firstOrNull()
if (url == null)
null
else {
val request = Request.Builder().url(url).build()
var done = false
var result: ByteArray? = null
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call?, e: IOException?) {
done = true
}
override fun onResponse(call: Call?, response: Response?) {
result = response?.body()?.use {
it.bytes()
}
done = true
}
})
if (!done)
yield()
result
}
} ?: return@thumbnail null, Base64.DEFAULT)
}
Cache(context).setCachedMetadata(galleryID,
Metadata(
thumbnail.await(),
galleryBlock.await(),
reader.await()
)
)
File(gallery, "images").listFiles { _, name ->
imageRegex.matches(name)
}?.forEach {
if (cancelImport)
return@forEach
@Suppress("NAME_SHADOWING")
val index = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
Cache(context).putImage(galleryID, index, it.extension, it.inputStream())
}
}
notificationBuilder.apply {
setContentText(context.getText(R.string.import_old_galleries_notification_done))
setProgress(0, 0, false)
setOngoing(false)
mActions.clear()
}
notificationManager.notify(0, notificationBuilder.build())
cancelImport = false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1,34 @@
<?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/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="44dp"
android:topRightRadius="44dp"
android:bottomLeftRadius="44dp" />
<padding
android:paddingLeft="22dp"
android:paddingRight="22dp" />
<solid android:color="@color/colorPrimary" />
</shape>

View File

@@ -0,0 +1,27 @@
<?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_pressed="true"
android:drawable="@drawable/thumb"/>
<item
android:drawable="@drawable/thumb"/>
</selector>

View File

@@ -0,0 +1,30 @@
<?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/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<padding
android:top="10dp"
android:left="10dp"
android:right="10dp"
android:bottom="10dp"/>
</shape>

View File

@@ -0,0 +1,27 @@
<?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_pressed="true"
android:drawable="@drawable/track"/>
<item
android:drawable="@drawable/track"/>
</selector>

View File

@@ -71,7 +71,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingTop="64dp" android:paddingTop="64dp"
android:clipToPadding="false" android:clipToPadding="false"
android:scrollbars="vertical" app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<com.github.clans.fab.FloatingActionMenu <com.github.clans.fab.FloatingActionMenu
@@ -82,6 +86,13 @@
android:layout_margin="16dp" android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent"> app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump" android:id="@+id/main_fab_jump"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -30,6 +30,11 @@
android:id="@+id/reader_recyclerview" android:id="@+id/reader_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<LinearLayout <LinearLayout

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp">
<TextView
android:id="@+id/proxy_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
style="@style/TextAppearance.AppCompat.Large"
android:text="@string/settings_proxy_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:id="@+id/proxy_type_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:text="@string/proxy_dialog_type"
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
<Spinner
android:id="@+id/proxy_type_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_type_text"/>
<TextView
android:id="@+id/proxy_server_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_type_selector"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:text="@string/proxy_dialog_server"
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
<LinearLayout
android:id="@+id/proxy_address_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_server_text">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_addr"
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="wrap_content"
android:hint="@string/proxy_dialog_addr_hint"/>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_port"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:hint="@string/proxy_dialog_port_hint"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_address_layout"
android:hint="@string/proxy_dialog_username_hint"
android:enabled="false"/>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_username"
android:hint="@string/proxy_dialog_password_hint"
android:enabled="false"/>
<Button
android:id="@+id/proxy_cancel"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
app:layout_constraintTop_toBottomOf="@id/proxy_password"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/proxy_ok"
app:layout_constraintRight_toLeftOf="@id/proxy_ok"/>
<Button
android:id="@+id/proxy_ok"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
app:layout_constraintTop_toBottomOf="@id/proxy_password"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -25,10 +25,13 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHeight_max="2000dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="@drawable/reader_item_boundary"> android:background="@drawable/reader_item_boundary">
<LinearLayout <LinearLayout
@@ -61,9 +64,12 @@
<com.github.chrisbanes.photoview.PhotoView <com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image" android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description" android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:paddingBottom="8dp"/> android:paddingBottom="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -20,6 +20,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/main_menu_thin"
android:title="@string/main_menu_thin"/>
<item <item
android:id="@+id/main_menu_sort" android:id="@+id/main_menu_sort"
android:title="@string/main_menu_sort"> android:title="@string/main_menu_sort">

View File

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

View File

@@ -122,4 +122,22 @@
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string> <string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_dl_location_custom">手動で設定</string> <string name="settings_dl_location_custom">手動で設定</string>
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string> <string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_proxy_title">プロクシ</string>
<string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">プロクシタイプ</string>
<string name="proxy_dialog_port_hint">ポート</string>
<string name="proxy_dialog_password_hint">パスワード</string>
<string name="proxy_dialog_error">エラー</string>
<string name="proxy_dialog_addr_hint">サーバーアドレス</string>
<string name="proxy_dialog_server">サーバー</string>
<string name="main_menu_thin">簡単モード</string>
<string name="main_fab_cancel">すべてのダウンロードキャンセル</string>
<string name="channel_update">アップデート</string>
<string name="channel_update_description">アップデートの進行状態を表示</string>
<string name="channel_import">インポート</string>
<string name="channel_import_description">インポート状態を表示</string>
<string name="settings_import_old_galleries">旧ギャラリーインポート</string>
<string name="import_old_galleries_folder_not_readable">フォルダを読めません</string>
<string name="import_old_galleries_notification">旧ギャラリーインポート中…</string>
<string name="import_old_galleries_notification_done">インポート完了</string>
</resources> </resources>

View File

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

View File

@@ -122,4 +122,22 @@
<string name="settings_mirror_title">미러 설정</string> <string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</string> <string name="settings_dl_location_custom">직접 설정</string>
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string> <string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
<string name="settings_proxy_title">프록시</string>
<string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">프록시 타입</string>
<string name="proxy_dialog_port_hint">포트</string>
<string name="proxy_dialog_password_hint">비밀번호</string>
<string name="proxy_dialog_error">잘못된 값</string>
<string name="proxy_dialog_addr_hint">서버 주소</string>
<string name="proxy_dialog_server">서버</string>
<string name="main_menu_thin">간단히 보기 모드</string>
<string name="main_fab_cancel">다운로드 모두 취소</string>
<string name="channel_update">업데이트</string>
<string name="channel_update_description">업데이트 진행상황 표시</string>
<string name="channel_import">가져오기</string>
<string name="channel_import_description">가져오기 상태 표시</string>
<string name="settings_import_old_galleries">이전 버전 갤러리 가져오기</string>
<string name="import_old_galleries_folder_not_readable">폴더를 읽을 수 없습니다</string>
<string name="import_old_galleries_notification">이전 버전 갤러리 가져오는 중…</string>
<string name="import_old_galleries_notification_done">가져오기 완료</string>
</resources> </resources>

View File

@@ -62,4 +62,10 @@
<item>HIYOBI|hiyobi.me</item> <item>HIYOBI|hiyobi.me</item>
</string-array> </string-array>
<string-array name="proxy_type">
<item>Direct</item>
<item>HTTP</item>
<item>SOCKS</item>
</string-array>
</resources> </resources>

View File

@@ -10,4 +10,7 @@
<dimen name="nav_header_height">176dp</dimen> <dimen name="nav_header_height">176dp</dimen>
<dimen name="thumbnail_margin">8dp</dimen> <dimen name="thumbnail_margin">8dp</dimen>
<dimen name="galleryblock_thumbnail_thin">100dp</dimen>
<dimen name="galleryblock_thumbnail_normal">150dp</dimen>
</resources> </resources>

View File

@@ -35,6 +35,12 @@
<string name="channel_download">Download</string> <string name="channel_download">Download</string>
<string name="channel_download_description">Shows download status</string> <string name="channel_download_description">Shows download status</string>
<string name="channel_update">Update</string>
<string name="channel_update_description">Shows update progress</string>
<string name="channel_import">Import</string>
<string name="channel_import_description">Shows progress of import</string>
<string name="unable_to_connect">Unable to connect to hitomi.la</string> <string name="unable_to_connect">Unable to connect to hitomi.la</string>
<string name="lock_corrupted">Lock file corrupted! Please re-install Pupil</string> <string name="lock_corrupted">Lock file corrupted! Please re-install Pupil</string>
@@ -53,6 +59,8 @@
<string name="main_drawer_group_contact_email">Email me!</string> <string name="main_drawer_group_contact_email">Email me!</string>
<string name="main_drawer_grouop_contact_discord">Discord</string> <string name="main_drawer_grouop_contact_discord">Discord</string>
<string name="main_menu_thin">Toggle Thin Mode</string>
<string name="main_menu_sort">Sort</string> <string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string> <string name="main_menu_sort_newest">Newest</string>
<string name="main_menu_sort_popular">Popular</string> <string name="main_menu_sort_popular">Popular</string>
@@ -61,6 +69,7 @@
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string> <string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
<string name="main_open_gallery_by_id">Open Gallery by ID</string> <string name="main_open_gallery_by_id">Open Gallery by ID</string>
<string name="reader_failed_to_find_gallery">Failed to open gallery</string> <string name="reader_failed_to_find_gallery">Failed to open gallery</string>
<string name="main_fab_cancel">Cancel all downloads</string>
<string name="main_move">Move to page %1$d</string> <string name="main_move">Move to page %1$d</string>
@@ -154,6 +163,7 @@
<string name="settings_miscellaneous_title">Miscellaneous</string> <string name="settings_miscellaneous_title">Miscellaneous</string>
<string name="settings_mirror_summary">Load images from mirrors</string> <string name="settings_mirror_summary">Load images from mirrors</string>
<string name="settings_proxy_title">Proxy</string>
<string name="settings_security_mode_title">Enable security mode</string> <string name="settings_security_mode_title">Enable security mode</string>
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string> <string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
<string name="settings_dark_mode_title">Dark mode</string> <string name="settings_dark_mode_title">Dark mode</string>
@@ -166,6 +176,7 @@
<string name="settings_restore_title">Restore favorites</string> <string name="settings_restore_title">Restore favorites</string>
<string name="settings_restore_failed">Restore failed</string> <string name="settings_restore_failed">Restore failed</string>
<string name="settings_restore_successful">%1$d entries restored</string> <string name="settings_restore_successful">%1$d entries restored</string>
<string name="settings_import_old_galleries">Import old galleries</string>
<!-- SETTINGS/APP LOCK ACTIVITY --> <!-- SETTINGS/APP LOCK ACTIVITY -->
@@ -189,4 +200,19 @@
<string name="default_query_dialog_language_selector_none">Any</string> <string name="default_query_dialog_language_selector_none">Any</string>
<string name="settings_mirror_title">Mirrors</string> <string name="settings_mirror_title">Mirrors</string>
<!-- PROXY DIALOG -->
<string name="proxy_dialog_type">type</string>
<string name="proxy_dialog_addr_hint">address</string>
<string name="proxy_dialog_port_hint">port</string>
<string name="proxy_dialog_username_hint">username</string>
<string name="proxy_dialog_password_hint">password</string>
<string name="proxy_dialog_error">Wrong value</string>
<string name="proxy_dialog_server">server</string>
<!-- IMPORT OLD GALLERIES -->
<string name="import_old_galleries_folder_not_readable">This folder is not readable</string>
<string name="import_old_galleries_notification">Importing old galleries…</string>
<string name="import_old_galleries_notification_text" translatable="false">%1$d/%2$d</string>
<string name="import_old_galleries_notification_done">Importing completed</string>
</resources> </resources>

View File

@@ -48,7 +48,7 @@
app:title="@string/settings_dl_location"/> app:title="@string/settings_dl_location"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="dl_low_quality" app:key="low_quality"
app:title="@string/settings_low_quality" app:title="@string/settings_low_quality"
app:summary="@string/settings_low_quality_summary"/> app:summary="@string/settings_low_quality_summary"/>
@@ -71,6 +71,10 @@
app:title="@string/settings_mirror_title" app:title="@string/settings_mirror_title"
app:summary="@string/settings_mirror_summary"/> app:summary="@string/settings_mirror_summary"/>
<Preference
app:key="proxy"
app:title="@string/settings_proxy_title"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="security_mode" app:key="security_mode"
app:title="@string/settings_security_mode_title" app:title="@string/settings_security_mode_title"
@@ -95,6 +99,10 @@
app:key="restore" app:key="restore"
app:title="@string/settings_restore_title"/> app:title="@string/settings_restore_title"/>
<Preference
app:key="old_import_galleries"
app:title="@string/settings_import_old_galleries"/>
</PreferenceCategory> </PreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View File

@@ -34,8 +34,6 @@ class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
val arr = SparseArray<Float>() val arr = SparseArray<Float>()
print(arr.indexOfKey(34))
} }
} }

View File

@@ -8,7 +8,7 @@ buildscript {
maven { url 'https://maven.fabric.io/public' } maven { url 'https://maven.fabric.io/public' }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.6.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"

5555
dependencies.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
#Fri Aug 23 08:21:15 KST 2019 #Sat Feb 29 09:07:20 KST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

@@ -11,8 +11,6 @@ dependencies {
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
} }
sourceCompatibility = "7"
targetCompatibility = "7"
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()

View File

@@ -16,6 +16,10 @@
package xyz.quaver package xyz.quaver
import java.net.Proxy
var proxy = Proxy.NO_PROXY
fun availableInHiyobi(galleryID: Int) : Boolean { fun availableInHiyobi(galleryID: Int) : Boolean {
return try { return try {
xyz.quaver.hiyobi.getReader(galleryID) xyz.quaver.hiyobi.getReader(galleryID)

View File

@@ -17,7 +17,7 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.list import xyz.quaver.proxy
import java.net.URL import java.net.URL
const val protocol = "https:" const val protocol = "https:"
@@ -25,10 +25,10 @@ const val protocol = "https:"
@Suppress("EXPERIMENTAL_API_USAGE") @Suppress("EXPERIMENTAL_API_USAGE")
fun getGalleryInfo(galleryID: Int) = fun getGalleryInfo(galleryID: Int) =
Json.nonstrict.parse( Json.nonstrict.parse(
GalleryInfo.serializer().list, GalleryInfo.serializer(),
Regex("""\[.+]""").find( URL("$protocol//$domain/galleries/$galleryID.js").openConnection(proxy).getInputStream().use {
URL("$protocol//$domain/galleries/$galleryID.js").readText() it.reader().readText()
)?.value ?: "[]" }.replace("var galleryinfo = ", "")
) )
//common.js //common.js
@@ -68,6 +68,7 @@ fun urlFromURL(url: String, base: String? = null) : String {
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/") return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
} }
fun fullPathFromHash(hash: String?) : String? { fun fullPathFromHash(hash: String?) : String? {
return when { return when {
(hash?.length ?: 0) < 3 -> hash (hash?.length ?: 0) < 3 -> hash
@@ -76,11 +77,20 @@ fun fullPathFromHash(hash: String?) : String? {
} }
@Suppress("NAME_SHADOWING", "UNUSED_PARAMETER") @Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
fun urlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null) : String { fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
val ext = ext ?: dir ?: image.name.split('.').last() val ext = ext ?: dir ?: image.name.split('.').last()
val dir = dir ?: "images" val dir = dir ?: "images"
return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext" return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
} }
fun urlFromUrlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null, base: String? = null) = fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
urlFromURL(urlFromHash(galleryID, image, dir, ext), base) urlFromURL(urlFromHash(galleryID, image, dir, ext), base)
fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
val webp = if (image.hash != null && image.haswebp != 0 && !noWebp)
"webp"
else
null
return urlFromUrlFromHash(galleryID, image, webp)
}

View File

@@ -18,6 +18,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.proxy
import java.net.URLDecoder import java.net.URLDecoder
@Serializable @Serializable
@@ -36,7 +37,7 @@ data class Gallery(
val thumbnails: List<String> val thumbnails: List<String>
) )
fun getGallery(galleryID: Int) : Gallery { fun getGallery(galleryID: Int) : Gallery {
val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").get() val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").proxy(proxy).get()
.select("a").attr("href") .select("a").attr("href")
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).get()
@@ -70,7 +71,7 @@ fun getGallery(galleryID: Int) : Gallery {
href.slice(5 until href.indexOf('-')) href.slice(5 until href.indexOf('-'))
} }
val thumbnails = getGalleryInfo(galleryID).map { galleryInfo -> val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo ->
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn") urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
} }

View File

@@ -19,6 +19,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.proxy
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -34,7 +35,6 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension" else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
} }
try {
with(URL(url).openConnection() as HttpsURLConnection) { with(URL(url).openConnection() as HttpsURLConnection) {
requestMethod = "GET" requestMethod = "GET"
@@ -61,9 +61,6 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
return Pair(nozomi, totalItems) return Pair(nozomi, totalItems)
} }
} catch (e: Exception) {
return Pair(emptyList(), 0)
}
} }
@Serializable @Serializable
@@ -82,8 +79,7 @@ data class GalleryBlock(
fun getGalleryBlock(galleryID: Int) : GalleryBlock? { fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension" val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
try { val doc = Jsoup.connect(url).proxy(proxy).get()
val doc = Jsoup.connect(url).get()
val galleryUrl = doc.selectFirst(".lillie").attr("href") val galleryUrl = doc.selectFirst(".lillie").attr("href")
@@ -105,7 +101,4 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
} }
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags) return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) {
return null
}
} }

View File

@@ -17,28 +17,35 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html" fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
@Serializable @Serializable
data class GalleryInfo( data class GalleryInfo(
val language_localname: String? = null,
val language: String? = null,
val date: String? = null,
val files: List<GalleryFiles>,
val id: Int? = null,
val type: String? = null,
val title: String? = null
)
@Serializable
data class GalleryFiles(
val width: Int, val width: Int,
val hash: String? = null, val hash: String? = null,
val haswebp: Int = 0, val haswebp: Int = 0,
val name: String, val name: String,
val height: Int val height: Int,
val hasavif: Int = 0
) )
@Serializable @Serializable
data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>) data class Reader(val code: Code, val galleryInfo: GalleryInfo)
//Set header `Referer` to reader url to avoid 403 error //Set header `Referer` to reader url to avoid 403 error
fun getReader(galleryID: Int) : Reader { fun getReader(galleryID: Int) : Reader {
val readerUrl = "https://hitomi.la/reader/$galleryID.html" return Reader(Code.HITOMI, getGalleryInfo(galleryID))
val doc = Jsoup.connect(readerUrl).get()
return Reader(Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
} }

View File

@@ -16,6 +16,7 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import xyz.quaver.proxy
import java.net.URL import java.net.URL
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@@ -49,8 +50,9 @@ fun sanitize(input: String) : String {
fun getIndexVersion(name: String) : String { fun getIndexVersion(name: String) : String {
return try { return try {
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}") URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").openConnection(proxy).getInputStream().use {
.readText() it.reader().readText()
}
} catch (e: Exception) { } catch (e: Exception) {
"" ""
} }
@@ -173,8 +175,13 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
} }
try { val bytes = try {
val bytes = URL(nozomiAddress).readBytes() URL(nozomiAddress).openConnection(proxy).getInputStream().use {
it.readBytes()
}
} catch (e: Exception) {
return emptyList()
}
val nozomi = ArrayList<Int>() val nozomi = ArrayList<Int>()
@@ -186,9 +193,6 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
nozomi.add(arrayBuffer.int) nozomi.add(arrayBuffer.int)
return nozomi return nozomi
} catch (e: Exception) {
return emptyList()
}
} }
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> { fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
@@ -242,7 +246,7 @@ fun getNodeAtAddress(field: String, address: Long) : Node? {
fun getURLAtRange(url: String, range: LongRange) : ByteArray? { fun getURLAtRange(url: String, range: LongRange) : ByteArray? {
try { try {
with (URL(url).openConnection() as HttpsURLConnection) { with (URL(url).openConnection(proxy) as HttpsURLConnection) {
requestMethod = "GET" requestMethod = "GET"
setRequestProperty("Range", "bytes=${range.first}-${range.last}") setRequestProperty("Range", "bytes=${range.first}-${range.last}")

View File

@@ -20,12 +20,12 @@ import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.protocol import xyz.quaver.hitomi.protocol
import xyz.quaver.proxy
fun getGalleryBlock(galleryID: Int) : GalleryBlock? { fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$hiyobi/search/$galleryID" val url = "$protocol//$hiyobi/info/$galleryID"
try { val doc = Jsoup.connect(url).proxy(proxy).get()
val doc = Jsoup.connect(url).get()
val galleryBlock = doc.selectFirst(".gallery-content") val galleryBlock = doc.selectFirst(".gallery-content")
@@ -43,7 +43,4 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') } val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags) return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) {
return null
}
} }

View File

@@ -16,17 +16,21 @@
package xyz.quaver.hiyobi package xyz.quaver.hiyobi
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.list import kotlinx.serialization.list
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryFiles
import xyz.quaver.hitomi.GalleryInfo import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.protocol import xyz.quaver.hitomi.protocol
import xyz.quaver.proxy
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
const val hiyobi = "hiyobi.me" const val hiyobi = "hiyobi.me"
const val primary_img_domain = "cdn.hiyobi.me"
const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"
var cookie: String = "" var cookie: String = ""
@@ -47,7 +51,7 @@ fun renewCookie() : String {
val url = "https://$hiyobi/" val url = "https://$hiyobi/"
try { try {
with(URL(url).openConnection() as HttpsURLConnection) { with(URL(url).openConnection(proxy) as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
connectTimeout = 2000 connectTimeout = 2000
connect() connect()
@@ -58,16 +62,16 @@ fun renewCookie() : String {
} }
} }
@UseExperimental(UnstableDefault::class)
fun getReader(galleryID: Int) : Reader { fun getReader(galleryID: Int) : Reader {
val reader = "https://$hiyobi/reader/$galleryID" val reader = "https://$hiyobi/reader/$galleryID"
val url = "https://$hiyobi/data/json/${galleryID}_list.json" val url = "https://cdn.hiyobi.me/data/json/${galleryID}_list.json"
val title = Jsoup.connect(reader).get().title() val title = Jsoup.connect(reader).proxy(proxy).get().title()
@Suppress("EXPERIMENTAL_API_USAGE") val galleryFiles = Json.nonstrict.parse(
val galleryInfo = Json.parse( GalleryFiles.serializer().list,
GalleryInfo.serializer().list, with(URL(url).openConnection(proxy) as HttpsURLConnection) {
with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
connectTimeout = 2000 connectTimeout = 2000
@@ -77,14 +81,14 @@ fun getReader(galleryID: Int) : Reader {
} }
) )
return Reader(Code.HIYOBI, title, galleryInfo) return Reader(Code.HIYOBI, GalleryInfo(title = title, files = galleryFiles))
} }
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) = fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
if (lowQuality) if (lowQuality)
reader.galleryInfo.map { reader.galleryInfo.files.map {
val name = it.name.replace(Regex("/.[^/.]+$"), "") + ".jpg" val name = it.name.replace(Regex("""\.[^/.]+$"""), "")
Images("$protocol//$hiyobi/data/$galleryID/$name.jpg", galleryID, it.name) Images("$protocol//$primary_img_domain/data_r/$galleryID/$name.jpg", galleryID, it.name)
} }
else else
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) } reader.galleryInfo.files.map { Images("$protocol//$primary_img_domain/data/$galleryID/${it.name}", galleryID, it.name) }

View File

@@ -75,21 +75,21 @@ class UnitTest {
@Test @Test
fun test_getReader() { fun test_getReader() {
val reader = getReader(1567569) val reader = getReader(1574736)
print(reader) print(reader)
} }
@Test @Test
fun test_hiyobi() { fun test_hiyobi() {
val reader = xyz.quaver.hiyobi.getReader(10000062) val reader = xyz.quaver.hiyobi.getReader(1574736)
print(reader) print(reader)
} }
@Test @Test
fun test_urlFromUrlFromHash() { fun test_urlFromUrlFromHash() {
val url = urlFromUrlFromHash(1531795, GalleryInfo( val url = urlFromUrlFromHash(1531795, GalleryFiles(
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300 212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
), "webp") ), "webp")