Compare commits

...

67 Commits
5.2 ... 4.13

Author SHA1 Message Date
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
Pupil
19308d840a Merge branch 'master' into hotfix 2020-02-12 18:51:17 +09:00
Pupil
46bd1318cd Merge branch 'master' into old 2020-02-12 18:49:35 +09:00
Pupil
9d1998fe52 deleted deleteRecursively 2020-02-12 09:03:47 +09:00
Pupil
a714a8230b ugh 2020-02-11 21:23:34 +09:00
Pupil
b5432cd0b4 ugh 2020-02-11 09:29:58 +09:00
Pupil
5634e94f3e DocumentFileX 2020-02-10 16:28:13 +09:00
Pupil
c1a71b0db3 hotfix 2020-02-10 12:17:05 +09:00
Pupil
d93e7f8834 Fixed renamed file 2020-02-09 18:26:52 +09:00
Pupil
3175b2c45c Fixed renamed file 2020-02-09 18:17:21 +09:00
Pupil
547b6e8e3b Fixed renamed file 2020-02-09 18:16:45 +09:00
Pupil
d88ac27e72 Fixed renamed file 2020-02-09 18:15:46 +09:00
Pupil
e551a40d08 Fixed renamed file 2020-02-09 18:13:16 +09:00
Pupil
e810abe33a Bug fix 2020-02-09 17:57:18 +09:00
Pupil
6172a73719 Bug fix 2020-02-09 17:36:28 +09:00
Pupil
7455e68a45 Bug fix 2020-02-09 17:35:34 +09:00
Pupil
748495ca64 Downloader thread number to 4 2020-02-09 17:25:55 +09:00
Pupil
f6d9c7f550 Bug fix
Networking optimized
2020-02-09 17:11:35 +09:00
58 changed files with 7043 additions and 911 deletions

View File

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

3
.idea/gradle.xml generated
View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="false" />
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<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)
*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
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true)
*Main Screen*

View File

@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
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")
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
@@ -19,19 +19,27 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 33
versionName "5.0"
versionCode 48
versionName "4.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
minifyEnabled false
debug {
debuggable true
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
buildTypes.each {
it.buildConfigField('boolean', 'CENSOR', 'false')
release {
minifyEnabled true
shrinkResources true
buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
kotlinOptions {
@@ -41,9 +49,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildToolsVersion = '29.0.2'
}
@@ -60,28 +65,25 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.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 'com.android.support:multidex:1.0.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.2.0-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-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false
}
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.gu:option:1.3'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.10.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0'

View File

@@ -19,3 +19,17 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-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":33,"versionName":"5.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":48,"versionName":"4.12","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}]

View File

@@ -26,11 +26,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.JsonObject
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import xyz.quaver.hiyobi.cookie
@@ -40,9 +35,6 @@ import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File
import java.net.URL
import javax.net.ssl.HttpsURLConnection
@@ -58,8 +50,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
assertEquals("xyz.quaver.pupil", appContext.packageName)
}
@Test
@@ -88,40 +78,6 @@ class ExampleInstrumentedTest {
Log.d("Pupil", data.size.toString())
}
@UseExperimental(ImplicitReflectionSerializer::class)
@Test
fun test_deleteCodeFromReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val json = Json(JsonConfiguration.Stable)
listOf(
getDownloadDirectory(context),
File(context.cacheDir, "imageCache")
).forEach { root ->
root.listFiles()?.forEach gallery@{ gallery ->
val reader = json.parseJson(File(gallery, "reader.json").apply {
if (!exists())
return@gallery
}.readText())
.jsonObject.toMutableMap()
Log.d("PUPILD", gallery.name)
reader.remove("code")
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
}
}
}
@Test
fun test_updateOldReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
updateOldReaderGalleries(context)
}
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -149,9 +105,9 @@ class ExampleInstrumentedTest {
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
}
}

View File

@@ -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,9 +6,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="21" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".Pupil"
@@ -19,10 +19,11 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:replace="android:theme">
tools:replace="android:theme"
android:requestLegacyExternalStorage="true">
<provider
android:authorities="${applicationId}.fileprovider"
android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">

View File

@@ -22,7 +22,6 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
@@ -31,7 +30,10 @@ import androidx.preference.PreferenceManager
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics
import xyz.quaver.proxy
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getProxy
import java.io.File
class Pupil : MultiDexApplication() {
@@ -46,15 +48,19 @@ class Pupil : MultiDexApplication() {
override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
proxy = getProxy(this)
try {
preference.getString("dl_location", null)
} catch (e: Exception) {
preference.edit().remove("dl_location").apply()
}
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
val download = preference.getString("dl_location", null)
if (download == null) {
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
}
if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
try {
ProviderInstaller.installIfNeeded(this)

View File

@@ -49,13 +49,12 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.wordCapitalize
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
class GalleryBlockAdapter(private val context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType {
NEXT,
@@ -68,13 +67,15 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
val timer = Timer()
var isThin = false
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var timerTask: TimerTask? = null
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
val cache = Cache(context).getCachedGallery(galleryID)
private fun updateProgress(context: Context, galleryID: Int) {
val reader = Cache(context).getReaderOrNull(galleryID)
CoroutineScope(Dispatchers.Main).launch {
if (reader == null) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
@@ -83,13 +84,11 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
with(view.galleryblock_progressbar) {
progress = cache?.listFiles()?.count { file ->
Regex("^[0-9]+.+\$").matches(file.name!!)
} ?: 0
progress = Cache(context).getImages(galleryID)?.size ?: 0
if (visibility == View.GONE) {
visibility = View.VISIBLE
max = reader.galleryInfo.size
max = reader.galleryInfo.files.size
}
if (progress == max) {
@@ -111,6 +110,7 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
view.galleryblock_progress_complete.visibility = View.INVISIBLE
}
}
}
fun bind(galleryBlock: GalleryBlock) {
with(view) {
@@ -124,13 +124,23 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
val artists = galleryBlock.artists
val series = galleryBlock.series
if (isThin)
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
R.dimen.galleryblock_thumbnail_thin
)
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
it.start()
})
CoroutineScope(Dispatchers.Main).launch {
val thumbnail = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT)
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
if (it != null)
Base64.decode(it, Base64.DEFAULT)
else
null
}
galleryblock_thumbnail.post {
glide
.load(thumbnail)
.skipMemoryCache(true)
@@ -142,18 +152,19 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
}
.into(galleryblock_thumbnail)
}
}
//Check cache
val cache = Cache(context).getCachedGallery(galleryBlock.id)
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
if (cache != null && reader != null) {
val count = cache.listFiles().count {
Regex("^[0-9]+.+\$").matches(it.name!!)
}
if (reader != null) {
val count = cache.listFiles()?.count {
Regex("^[0-9]+.+\$").matches(it.name)
} ?: 0
with(galleryblock_progressbar) {
max = reader.galleryInfo.size
max = reader.galleryInfo.files.size
progress = count
visibility = View.VISIBLE
@@ -257,6 +268,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
}
}
}
}
@@ -334,10 +353,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text =
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
holder.view.context.getString(R.string.main_download)
else
if (Cache(context).isDownloading(gallery.id))
holder.view.context.getString(android.R.string.cancel)
else
holder.view.context.getString(R.string.main_download)
}
override fun onClose(layout: SwipeLayout?) {}

View File

@@ -25,8 +25,11 @@ import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.snackbar.Snackbar
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -36,6 +39,7 @@ import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@@ -43,22 +47,39 @@ import kotlin.math.roundToInt
class ReaderAdapter(private val context: Context,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var isFullScreen = false
val glide = Glide.with(context)
//region Glide.RecyclerView
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.getOrNull(position)?.let {
arrayOf(it.width, it.height).toIntArray()
}
}
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
override fun getPreloadItems(position: Int): MutableList<File> {
return listOf(Cache(context).getImages(galleryID)?.getOrNull(position)).filterNotNullTo(mutableListOf())
}
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
return glide
.load(item)
.fitCenter()
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
}
}
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
//endregion
var reader: Reader? = null
private val glide = Glide.with(context)
val timer = Timer()
var onItemClickListener : ((Int) -> (Unit))? = null
var isFullScreen = false
init {
CoroutineScope(Dispatchers.IO).launch {
reader = Cache(context).getReader(galleryID)
launch(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
var onItemClickListener : ((Int) -> (Unit))? = null
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
@@ -73,10 +94,16 @@ class ReaderAdapter(private val context: Context,
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout
if (isFullScreen)
if (isFullScreen) {
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.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 { _, _, _ ->
onItemClickListener?.invoke(position)
@@ -86,43 +113,37 @@ class ReaderAdapter(private val context: Context,
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()
val images = Cache(context).getImages(galleryID)
if (images?.get(position) != null) {
glide
.load(images[position]?.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
} else {
val images = Cache(context).getImage(galleryID, position)
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
if (progress?.isInfinite() == true && images != null) {
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
holder.view.image.post {
glide
.load(images)
.fitCenter()
.error(R.drawable.image_broken_variant)
.into(holder.view.image)
}
} else {
holder.view.reader_item_progressbar.visibility = View.VISIBLE
glide.clear(holder.view.image)
if (progress?.isNaN() == true) {
if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
Snackbar
.make(
holder.view,
DownloadWorker.getInstance(context).exception[galleryID]!![position]?.message
?: context.getText(R.string.default_error_msg),
Snackbar.LENGTH_INDEFINITE
)
.show()
return
}
} else {
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
@@ -130,7 +151,7 @@ class ReaderAdapter(private val context: Context,
progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null)
}
timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch {
@@ -140,6 +161,6 @@ 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,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import com.crashlytics.android.Crashlytics
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_content.*
import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
@@ -179,7 +177,7 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
}
override fun onResume() {
@@ -332,6 +330,13 @@ class MainActivity : AppCompatActivity() {
true
}
with(main_fab_cancel) {
setImageResource(R.drawable.cancel)
setOnClickListener {
DownloadWorker.getInstance(context).stop()
}
}
with(main_fab_jump) {
setImageResource(R.drawable.ic_jump)
setOnClickListener {
@@ -410,7 +415,7 @@ class MainActivity : AppCompatActivity() {
if (!completeFlag.get(galleryID, false)) {
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)
else {
Cache(context).setDownloading(galleryID, true)
@@ -429,12 +434,7 @@ class MainActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Default).launch {
DownloadWorker.getInstance(context).cancel(galleryID)
var cache = Cache(context).getCachedGallery(galleryID)
while (cache != null) {
cache.deleteRecursively()
cache = Cache(context).getCachedGallery(galleryID)
}
Cache(context).getCachedGallery(galleryID).deleteRecursively()
histories.remove(galleryID)
@@ -698,7 +698,6 @@ class MainActivity : AppCompatActivity() {
}
private var suggestionJob : Job? = null
@UseExperimental(ImplicitReflectionSerializer::class)
private fun setupSearchBar() {
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
//Change upper case letters to lower case
@@ -722,17 +721,25 @@ class MainActivity : AppCompatActivity() {
with(main_searchview as FloatingSearchView) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
val json = Json(JsonConfiguration.Stable)
val serializer = Tag.serializer().list
if (!favoritesFile.exists()) {
favoritesFile.createNewFile()
favoritesFile.writeText(json.stringify(Tags(listOf())))
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
}
setOnMenuItemClickListener {
when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
it.isChecked = true
@@ -845,7 +852,7 @@ class MainActivity : AppCompatActivity() {
favorites.add(tag)
}
favoritesFile.writeText(json.stringify(favorites))
favoritesFile.writeText(json.stringify(serializer, favorites))
}
}
@@ -944,58 +951,60 @@ class MainActivity : AppCompatActivity() {
when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply {
totalItems = size
}.also {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
totalItems = size
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = it.size
}
}
}
Mode.HISTORY -> {
when {
query.isEmpty() -> {
histories.toList().apply {
totalItems = size
histories.toList().also {
totalItems = it.size
}
}
else -> {
val result = doSearch(query).sorted()
histories.filter { result.binarySearch(it) >= 0 }.apply {
totalItems = size
histories.filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file ->
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
file.isDirectory && file.name.toIntOrNull() != null
}?.sortedByDescending {
it.lastModified()
}?.map {
it.name!!.toInt()
}?: listOf()
it.name.toInt()
} ?: emptyList()
when {
query.isEmpty() -> downloads.apply {
totalItems = size
query.isEmpty() -> downloads.also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
downloads.filter { result.binarySearch(it) >= 0 }.apply {
totalItems = size
downloads.filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.FAVORITE -> {
when {
query.isEmpty() -> favorites.toList().apply {
totalItems = size
query.isEmpty() -> favorites.toList().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
favorites.filter { result.binarySearch(it) >= 0 }.apply {
totalItems = size
favorites.filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
@@ -1009,9 +1018,16 @@ class MainActivity : AppCompatActivity() {
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
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) {
main_noresult.visibility = View.VISIBLE
main_progressbar.hide()

View File

@@ -38,7 +38,6 @@ import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.serialization.ImplicitReflectionSerializer
import xyz.quaver.Code
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
@@ -141,7 +140,6 @@ class ReaderActivity : AppCompatActivity() {
super.onResume()
}
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu)
@@ -162,7 +160,7 @@ class ReaderActivity : AppCompatActivity() {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
with(view.dialog_number_picker) {
minValue=1
maxValue=reader_recyclerview.adapter?.itemCount ?: 0
maxValue=reader_recyclerview?.adapter?.itemCount ?: 0
value=currentPage
}
val dialog = AlertDialog.Builder(this).apply {
@@ -196,7 +194,7 @@ class ReaderActivity : AppCompatActivity() {
super.onDestroy()
timer.cancel()
(reader_recyclerview.adapter as ReaderAdapter).timer.cancel()
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
if (!Cache(this).isDownloading(galleryID))
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
@@ -239,7 +237,7 @@ class ReaderActivity : AppCompatActivity() {
queue.add(galleryID)
}
timer.schedule(0, 1000) {
timer.schedule(1000, 1000) {
if (worker.progress.indexOfKey(galleryID) < 0) //loading
return@schedule
@@ -256,11 +254,17 @@ class ReaderActivity : AppCompatActivity() {
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
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) {
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,
when (reader.code) {
@@ -296,6 +300,7 @@ class ReaderActivity : AppCompatActivity() {
}
}
addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
@@ -356,6 +361,8 @@ class ReaderActivity : AppCompatActivity() {
window.attributes = this
}
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
}
private fun scrollMode(isScroll: Boolean) {
@@ -381,7 +388,7 @@ class ReaderActivity : AppCompatActivity() {
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
labelText = getString(R.string.reader_fab_download_cancel)
}
else // Or continue animate
post {

View File

@@ -18,30 +18,26 @@
package xyz.quaver.pupil.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList
import kotlinx.serialization.list
import kotlinx.serialization.serializer
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.LockFragment
import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.REQUEST_LOCK
import xyz.quaver.pupil.util.REQUEST_RESTORE
import xyz.quaver.pupil.util.*
import java.io.File
import java.nio.charset.Charset
@@ -82,7 +78,6 @@ class SettingsActivity : AppCompatActivity() {
return true
}
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
REQUEST_LOCK -> {
@@ -99,13 +94,13 @@ class SettingsActivity : AppCompatActivity() {
val uri = data?.data ?: return
try {
val json = contentResolver.openInputStream(uri).use { inputStream ->
val str = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!!
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(
window.decorView,
getString(R.string.settings_restore_successful, it.size),
@@ -124,16 +119,23 @@ class SettingsActivity : AppCompatActivity() {
REQUEST_DOWNLOAD_FOLDER -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
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)
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
val file = uri.toFile(this)
if (file?.canWrite() != true)
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", uri.toString())
.putString("dl_location", file.canonicalPath)
.apply()
}
}
@@ -143,14 +145,33 @@ class SettingsActivity : AppCompatActivity() {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite())
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", Uri.fromFile(File(directory)).toString())
.putString("dl_location", File(directory).canonicalPath)
.apply()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
@SuppressLint("InlinedApi")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
REQUEST_WRITE_PERMISSION_AND_SAF -> {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
}
}
}
}
}

View File

@@ -46,16 +46,12 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro")
private lateinit var dialogView : View
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.default_query_dialog_title)
setView(dialogView)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
@@ -79,15 +75,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
}
@SuppressLint("InflateParams")
private fun initDialog() {
private fun build() : View {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val tags = Tags.parse(
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 =
ArrayAdapter(
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)
if (tags.contains(excludeBL))
tags.remove(excludeBL)
}
with(dialogView.default_query_dialog_guro_checkbox) {
with(view.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) })
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)
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
@@ -149,6 +145,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
}
})
}
return view
}
}

View File

@@ -18,33 +18,45 @@
package xyz.quaver.pupil.ui.dialog
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.*
import java.io.File
@SuppressLint("InflateParams")
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.settings_dl_location)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun build() : View {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
@@ -67,9 +79,9 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
pair.first.isChecked = false
}
button.performClick()
preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
preference.edit().putString("dl_location", dir.canonicalPath).apply()
}
buttons.add(button to Uri.fromFile(dir))
buttons.add(button to dir)
})
}
@@ -82,11 +94,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
button.performClick()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
}
dismiss()
} else { // Can't use SAF on old Androids!
@@ -106,25 +123,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
buttons.add(button to null)
})
val pref = Uri.parse(preference.getString("dl_location", null))
val index = externalFilesDirs.indexOfFirst {
Uri.fromFile(it).toString() == pref.toString()
}
externalFilesDirs.indexOfFirst {
it.canonicalPath == getDownloadDirectory(context).canonicalPath
}.let { index ->
if (index < 0)
buttons.last().first.isChecked = true
buttons.first().first.isChecked = true
else
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

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

@@ -24,7 +24,6 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
@@ -37,6 +36,7 @@ import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.ui.dialog.ProxyDialog
import xyz.quaver.pupil.util.*
import java.io.File
@@ -71,7 +71,7 @@ class SettingsFragment :
}
}
private fun getDirSize(dir: DocumentFile) : String {
private fun getDirSize(dir: File) : String {
val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size))
@@ -86,7 +86,7 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true)
}
"delete_cache" -> {
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
val dir = File(context.cacheDir, "imageCache")
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
@@ -101,7 +101,7 @@ class SettingsFragment :
}.show()
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)!!
val dir = getDownloadDirectory(context)
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
@@ -147,10 +147,14 @@ class SettingsFragment :
MirrorDialog(context)
.show()
}
"proxy" -> {
ProxyDialog(context)
.show()
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
context,
getDownloadDirectory(context)?.createFile("null", "favorites.json")!!
File(getDownloadDirectory(context), "favorites.json"),
true
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
@@ -190,10 +194,18 @@ class SettingsFragment :
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
key ?: return
with(findPreference<Preference>(key)) {
this ?: return
when (key) {
"proxy" -> {
summary = getProxyInfo(context).type.name
}
"dl_location" -> {
findPreference<Preference>(key)?.summary =
FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
summary = getDownloadDirectory(context!!).canonicalPath
}
}
}
}
@@ -224,13 +236,13 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
val dir = File(context.cacheDir, "imageCache")
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)!!
val dir = getDownloadDirectory(context)
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
@@ -242,13 +254,12 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri)
summary = getDownloadDirectory(context).canonicalPath
onPreferenceClickListener = this@SettingsFragment
}
"default_query" -> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
summary = preferences.getString("default_query", "") ?: ""
summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment
}
@@ -272,6 +283,11 @@ class SettingsFragment :
"mirrors" -> {
onPreferenceClickListener = this@SettingsFragment
}
"proxy" -> {
summary = getProxyInfo(context).type.name
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment
}

View File

@@ -18,7 +18,13 @@
package xyz.quaver.pupil.util
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
const val REQUEST_LOCK = 38238
const val REQUEST_RESTORE = 16546
const val REQUEST_DOWNLOAD_FOLDER = 3874
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
val json = Json(JsonConfiguration.Stable)

View File

@@ -1,178 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util;
/*
* Copyright (C) 2007-2008 OpenIntents.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
/**
* @version 2009-07-03
* @author Peli
* @version 2013-12-11
* @author paulburke (ipaulpro)
*/
public class FileUtils {
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
public static String getPath(final Context context, final Uri uri) {
// DocumentProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}

View File

@@ -21,48 +21,59 @@ package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import androidx.documentfile.provider.DocumentFile
import android.util.Log
import android.util.SparseArray
import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import kotlinx.io.InputStream
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.util.*
import xyz.quaver.proxy
import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf
import xyz.quaver.pupil.util.json
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
class Cache(context: Context) : ContextWrapper(context) {
private val locks = SparseArray<Lock>()
private fun lock(galleryID: Int) {
synchronized(locks) {
if (locks.indexOfKey(galleryID) < 0)
locks.put(galleryID, ReentrantLock())
}
locks[galleryID].lock()
}
private fun unlock(galleryID: Int) {
locks[galleryID]?.unlock()
}
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
// Search in this order
// Download -> Cache
fun getCachedGallery(galleryID: Int) : DocumentFile? {
var file = getDownloadDirectory(this)?.findFile(galleryID.toString())
if (file?.exists() == true)
return file
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
return if (file.exists())
file
else
null
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
if (!it.exists())
it.mkdirs()
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
val file = File(getCachedGallery(galleryID), ".metadata")
if (file?.exists() != true)
if (!file.exists())
return null
return try {
Json.parse(file.readText(this))
json.parse(Metadata.serializer(), file.readText())
} catch (e: Exception) {
//File corrupted
file.delete()
@@ -70,15 +81,13 @@ class Cache(context: Context) : ContextWrapper(context) {
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
val file = File(getCachedGallery(galleryID), ".metadata").also {
if (!it.exists())
it.mkdirs()
}).createFile("null", ".metadata") ?: return
it.createNewFile()
}
file.writeText(this, Json.stringify(metadata))
file.writeText(json.stringify(Metadata.serializer(), metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
@@ -88,7 +97,9 @@ class Cache(context: Context) : ContextWrapper(context) {
withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
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) {
null
}
@@ -107,17 +118,29 @@ class Cache(context: Context) : ContextWrapper(context) {
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val galleryBlock = if (metadata?.galleryBlock == null)
listOf(
val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
).map {
)
val galleryBlock = if (metadata?.galleryBlock == null) {
CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching {
it.invoke()
}.getOrNull()
var galleryBlock: GalleryBlock? = null
for (source in sources) {
galleryBlock = try {
source.invoke()
} catch (e: Exception) {
null
}
if (galleryBlock != null)
break
}
galleryBlock
}.await() ?: return null
}
}.awaitAll().filterNotNull()
else
metadata.galleryBlock
@@ -126,89 +149,123 @@ class Cache(context: Context) : ContextWrapper(context) {
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return galleryBlock.firstOrNull {
mirrors.contains(it.code.name)
} ?: galleryBlock.firstOrNull()
return galleryBlock
}
fun getReaderOrNull(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return metadata?.readers?.firstOrNull {
mirrors.contains(it.code.name)
} ?: metadata?.readers?.firstOrNull()
return getCachedMetadata(galleryID)?.reader
}
suspend fun getReader(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
val readers = if (metadata?.readers == null) {
listOf(
{ xyz.quaver.hitomi.getReader(galleryID) },
{ xyz.quaver.hiyobi.getReader(galleryID) }
).map {
val sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).let {
if (mirrors.isNotEmpty())
it.toSortedMap(
Comparator { o1, o2 ->
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
}
)
else
it
}
val reader = if (metadata?.reader == null) {
CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching {
it.invoke()
}.getOrNull()
}
}.awaitAll().filterNotNull()
} else {
metadata.readers
var retval: Reader? = null
for (source in sources) {
retval = try {
source.value.invoke()
} catch (e: Exception) {
Crashlytics.logException(e)
null
}
if (readers.isNotEmpty())
if (retval != null)
break
}
retval
}.await() ?: return null
} else
metadata.reader
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers)
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return readers.firstOrNull {
mirrors.contains(it.code.name)
} ?: readers.firstOrNull()
return reader
}
fun getImages(galleryID: Int): List<DocumentFile?>? {
val gallery = getCachedGallery(galleryID) ?: return null
val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles()
val imageNameRegex = Regex("""^\d+\..+$""")
fun getImages(galleryID: Int): List<File?>? {
val gallery = getCachedGallery(galleryID)
return reader.galleryInfo.indices.map { index ->
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true }
return gallery.list { _, name ->
imageNameRegex.matches(name)
}?.map {
File(gallery, it)
}
}
fun putImage(galleryID: Int, name: String, data: ByteArray) {
val cache = getCachedGallery(galleryID) ?:
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
val imageExtensions = listOf(
"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())
it.mkdirs()
}) ?: return
if (!Regex("""^[0-9]+.+$""").matches(name))
throw IllegalArgumentException("File name is not a number")
cache.createFile("null", name)?.writeBytes(this, data)
it.createNewFile()
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID)
data.use {
it.copyTo(FileOutputStream(cache))
}
}
if (cache != null) {
val download = getDownloadDirectory(this)!!
fun moveToDownload(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
val cache = getCachedGallery(galleryID).also {
if (!it.exists())
return@launch
}
val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
if (!download.isParentOf(cache)) {
cache.copyRecursively(this, download)
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()
}
} else
getDownloadDirectory(this)?.createDirectory(galleryID.toString())
Log.i("PUPILD", "DELETED ${cache.canonicalPath}")
}
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.Intent
import android.content.SharedPreferences
import android.util.Log
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -36,15 +37,18 @@ import okio.*
import xyz.quaver.Code
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.proxy
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
@UseExperimental(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
@@ -145,31 +149,29 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private val loop = loop()
private val worker = SparseArray<Job?>()
@Volatile var nRunners = 0
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val interceptor = Interceptor { chain ->
val request = chain.request()
var response = chain.proceed(request)
var retry = preferences.getInt("retry", 3)
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
val response = chain.proceed(request)
response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build()
}
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor()))
val client =
OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.proxy(proxy)
.build()
fun stop() {
queue.clear()
loop.cancel()
for (i in 0..worker.size()) {
for (i in 0 until worker.size()) {
val galleryID = worker.keyAt(i)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
@@ -182,21 +184,15 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
exception.clear()
notification.clear()
notificationManager.cancelAll()
nRunners = 0
}
fun cancel(galleryID: Int) {
queue.remove(galleryID)
worker[galleryID]?.cancel()
client.dispatcher().queuedCalls()
.filter {
@Suppress("UNCHECKED_CAST")
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
}
.forEach {
client.dispatcher().queuedCalls().filter {
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
}.forEach {
it.cancel()
}
@@ -205,45 +201,23 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notification.remove(galleryID)
notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0) {
if (progress.indexOfKey(galleryID) >= 0)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
}
}
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
val cache = Cache(this@DownloadWorker).getImages(galleryID)
val lowQuality = preferences.getBoolean("low_quality", false)
//Cache exists :P
cache?.get(index)?.let {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
nRunners--
}
return
}
val request = Request.Builder().apply {
when (reader.code) {
Code.HITOMI -> {
url(
urlFromUrlFromHash(
imageUrlFromImage(
galleryID,
reader.galleryInfo[index],
if (lowQuality) "webp" else null
reader.galleryInfo.files[index],
lowQuality
)
)
addHeader("Referer", getReferer(galleryID))
@@ -272,20 +246,41 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
exception.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
return@launch
}
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
val cache = Cache(this@DownloadWorker).getImages(galleryID)
notification[galleryID].setContentTitle(reader.title)
progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
Float.POSITIVE_INFINITY
else
0F
}.toMutableList())
exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
if (notification[galleryID] == null)
initNotification(galleryID)
notification[galleryID].setContentTitle(reader.galleryInfo.title)
notify(galleryID)
for (i in reader.galleryInfo.indices) {
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
return@launch
}
for (i in reader.galleryInfo.files.indices) {
val callback = object : Callback {
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)
progress[galleryID]?.set(i, Float.NaN)
@@ -293,40 +288,74 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker)
if (cache.isDownloading(galleryID)) {
cache.moveToDownload(galleryID)
cache.setDownloading(galleryID, false)
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
}
nRunners--
}
}
override fun onResponse(call: Call, response: Response) {
response.body().use {
val res = it.bytes()
val ext =
call.request().url().encodedPath().split('.').last()
Log.i("PUPILD", "OK ${call.request().tag()}")
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
val ext = call.request().url().encodedPath().split('.').last()
try {
response.body().use {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
}
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker)
if (cache.isDownloading(galleryID)) {
cache.moveToDownload(galleryID)
cache.setDownloading(galleryID, false)
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
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)
}
}
}
}
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)
Log.i("PUPILD", "$galleryID QUEUED $i")
} else {
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
}
}
}
@@ -334,11 +363,17 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val max = progress[galleryID]?.size ?: 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]
?.setContentText(getString(R.string.reader_notification_complete))
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
?.setProgress(0, 0, false)
else
?.setOngoing(false)
notificationManager.cancel(galleryID)
} else
notification[galleryID]
?.setProgress(max, progress, false)
?.setContentText("$progress/$max")
@@ -355,7 +390,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
}
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
@@ -364,24 +399,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
setContentIntent(pendingIntent)
setProgress(0, 0, true)
setOngoing(true)
})
}
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
if (queue.isEmpty())
continue
val galleryID = queue.poll() ?: continue
val galleryID = queue.peek() ?: continue
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue
if (notification[galleryID] == null)
initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build())
Log.i("PUPILD", "QUEUED $galleryID")
worker.put(galleryID, download(galleryID))
nRunners++
queue.poll()
}
}

View File

@@ -25,20 +25,20 @@ import xyz.quaver.hitomi.Reader
@Serializable
data class Metadata(
val thumbnail: String? = null,
val galleryBlock: List<GalleryBlock>? = null,
val readers: List<Reader>? = null,
val galleryBlock: GalleryBlock? = null,
val reader: Reader? = null,
val isDownloading: Boolean? = null
) {
constructor(
metadata: Metadata?,
thumbnail: String? = null,
galleryBlock: List<GalleryBlock>? = null,
readers: List<Reader>? = null,
galleryBlock: GalleryBlock? = null,
readers: Reader? = null,
isDownloading: Boolean? = null
) : this(
thumbnail ?: metadata?.thumbnail,
galleryBlock ?: metadata?.galleryBlock,
readers ?: metadata?.readers,
readers ?: metadata?.reader,
isDownloading ?: metadata?.isDownloading
)
}

View File

@@ -18,33 +18,45 @@
package xyz.quaver.pupil.util
import android.annotation.TargetApi
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import android.os.Build
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import java.io.File
import java.io.FileOutputStream
import java.lang.reflect.Array
import java.net.URL
import java.nio.charset.Charset
import java.util.*
fun getCachedGallery(context: Context, galleryID: Int) =
getDownloadDirectory(context)?.findFile(galleryID.toString()) ?:
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
fun getDownloadDirectory(context: Context) : DocumentFile? {
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
Uri.parse(it)
}
return if (uri.toString().startsWith("file"))
DocumentFile.fromFile(File(uri.path!!))
File(getDownloadDirectory(context), galleryID.toString()).let {
if (it.exists())
it
else
DocumentFile.fromTreeUri(context, uri)
File(context.cacheDir, "imageCache/$galleryID")
}
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
context.contentResolver.openOutputStream(to.uri).use { out ->
out!!
fun getDownloadDirectory(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
if (it != null && !it.startsWith("content"))
File(it)
else
context.getExternalFilesDir(null)!!
}
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
if (to.parentFile?.exists() == false)
to.parentFile!!.mkdirs()
if (!to.exists())
to.createNewFile()
FileOutputStream(to).use { out ->
with(openConnection()) {
val fileSize = contentLength.toLong()
@@ -68,69 +80,138 @@ fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long,
}
}
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
var parent = file?.parentFile
while (parent != null) {
if (this.uri.path == parent.uri.path)
return true
parent = parent.parentFile
}
return false
}
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
fun DocumentFile.copyRecursively(
context: Context,
target: DocumentFile
) {
if (!exists())
throw Exception("The source file doesn't exist.")
if (this.isFile)
target.createFile("null", name!!)!!.writeBytes(
context,
readBytes(context)
)
else if (this.isDirectory) {
target.createDirectory(name!!).also { newTarget ->
listFiles().forEach { child ->
child.copyRecursively(context, newTarget!!)
fun getExtSdCardPaths(context: Context) =
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
runCatching {
File(path).canonicalPath
}.getOrElse {
path
}
}
}
const val PRIMARY_VOLUME_NAME = "primary"
fun getVolumePath(context: Context, volumeID: String?): String? {
return runCatching {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
val getUUID = storageVolumeClass.getMethod("getUuid")
val getPath = storageVolumeClass.getMethod("getPath")
val isPrimary = storageVolumeClass.getMethod("isPrimary")
val result = getVolumeList.invoke(storageManager)!!
val length = Array.getLength(result)
for (i in 0 until length) {
val storageVolumeElement = Array.get(result, i)
val uuid = getUUID.invoke(storageVolumeElement) as? String
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
// primary volume?
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
return@runCatching getPath.invoke(storageVolumeElement) as? String
// other volumes?
if (volumeID == uuid) {
return@runCatching getPath.invoke(storageVolumeElement) as? String
}
}
return@runCatching null
}.getOrNull()
}
fun DocumentFile.deleteRecursively() {
if (this.isDirectory)
listFiles().forEach {
it.deleteRecursively()
// Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun getVolumeIdFromTreeUri(uri: Uri) =
DocumentsContract.getTreeDocumentId(uri).split(':').let {
if (it.isNotEmpty())
it[0]
else
null
}
this.delete()
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun getDocumentPathFromTreeUri(uri: Uri) =
DocumentsContract.getTreeDocumentId(uri).split(':').let {
if (it.size >= 2)
it[1]
else
File.separator
}
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
if (state.isEmpty())
state.push(this)
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
it ?: return File.separator
listFiles().forEach {
state.push(it)
if (it.endsWith(File.separator))
it.dropLast(1)
else
it
}
if (it.isDirectory) {
it.walk(state)
val documentPath = getDocumentPathFromTreeUri(uri).let {
if (it.endsWith(File.separator))
it.dropLast(1)
else
it
}
return if (documentPath.isNotEmpty()) {
if (documentPath.startsWith(File.separator))
volumePath + documentPath
else
volumePath + File.separator + documentPath
} else
volumePath
}
// Huge thanks to avluis(https://github.com/avluis)
// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
fun Uri.toFile(context: Context): File? {
val path = this.path ?: return null
val pathSeparator = path.indexOf(':')
val folderName = path.substring(pathSeparator+1)
// Determine whether the designated file is
// - on a removable media (e.g. SD card, OTG)
// or
// - on the internal phone memory
val removableMediaFolderRoots = getExtSdCardPaths(context)
/* First test is to compare root names with known roots of removable media
* In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
* (e.g. /storage/3437-3934 vs. /tree/3437-3934)
* This is what the following block is trying to do
*/
for (s in removableMediaFolderRoots) {
val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
val root = path.substring(0, pathSeparator).let {
it.substring(it.lastIndexOf(File.separatorChar))
}
if (sRoot.equals(root, true)) {
return File(s + File.separatorChar + folderName)
}
}
/* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
* We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
* and compare it to the known removable media volume names
*/
val root = getFullPathFromTreeUri(context, this)
for (s in removableMediaFolderRoots) {
if (root?.startsWith(s) == true) {
return File(root)
}
}
return state
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
}
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath)

View File

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

View File

@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
import android.content.Context
import android.content.ContextWrapper
import androidx.core.content.ContextCompat
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import java.io.File
import java.security.MessageDigest
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
load()
}
@UseExperimental(ImplicitReflectionSerializer::class)
private fun load() {
val lock = File(ContextCompat.getDataDir(this), "lock.json")
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
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() {
val lock = File(ContextCompat.getDataDir(this), "lock.json")
if (!lock.exists())
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) {

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

@@ -26,22 +26,27 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.*
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.content
import ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import java.io.File
import java.net.URL
import java.util.*
fun getReleases(url: String) : JsonArray {
return try {
URL(url).readText().let {
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
json.parse(JsonArray.serializer(), it)
}
} catch (e: Exception) {
JsonArray(emptyList())
@@ -143,13 +148,14 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setContentTitle(context.getString(R.string.update_notification_description))
setSmallIcon(android.R.drawable.stat_sys_download)
priority = NotificationCompat.PRIORITY_LOW
setOngoing(true)
}
CoroutineScope(Dispatchers.IO).launch io@{
val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
val target = File(getDownloadDirectory(context), "Pupil.apk")
try {
URL(url).download(context, target) { progress, fileSize ->
URL(url).download(target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
@@ -158,6 +164,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setContentText(context.getString(R.string.update_failed))
setMessage(context.getString(R.string.update_failed_message))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOngoing(false)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
@@ -168,7 +175,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}
builder.apply {
@@ -177,6 +184,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentTitle(context.getString(R.string.update_download_completed))
setContentText(context.getString(R.string.update_download_completed_description))
setOngoing(false)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)

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

@@ -82,6 +82,13 @@
android:layout_margin="16dp"
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
android:id="@+id/main_fab_jump"
android:layout_width="wrap_content"

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

View File

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

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,14 @@
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_dl_location_custom">手動で設定</string>
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_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>
</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,14 @@
<string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</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>
</resources>

View File

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

View File

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

View File

@@ -53,6 +53,8 @@
<string name="main_drawer_group_contact_email">Email me!</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_newest">Newest</string>
<string name="main_menu_sort_popular">Popular</string>
@@ -61,6 +63,7 @@
<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="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>
@@ -154,6 +157,7 @@
<string name="settings_miscellaneous_title">Miscellaneous</string>
<string name="settings_mirror_summary">Load images from mirrors</string>
<string name="settings_proxy_title">Proxy</string>
<string name="settings_security_mode_title">Enable security mode</string>
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
<string name="settings_dark_mode_title">Dark mode</string>
@@ -189,4 +193,13 @@
<string name="default_query_dialog_language_selector_none">Any</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>
</resources>

View File

@@ -17,6 +17,7 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<external-path name="external" path="/"/>
<external-files-path name="files" path="/"/>
</paths>

View File

@@ -48,7 +48,7 @@
app:title="@string/settings_dl_location"/>
<SwitchPreferenceCompat
app:key="dl_low_quality"
app:key="low_quality"
app:title="@string/settings_low_quality"
app:summary="@string/settings_low_quality_summary"/>
@@ -71,6 +71,10 @@
app:title="@string/settings_mirror_title"
app:summary="@string/settings_mirror_summary"/>
<Preference
app:key="proxy"
app:title="@string/settings_proxy_title"/>
<SwitchPreferenceCompat
app:key="security_mode"
app:title="@string/settings_security_mode_title"

View File

@@ -8,7 +8,7 @@ buildscript {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.6.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$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
#Tue Feb 25 09:55:23 KST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

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

View File

@@ -17,7 +17,7 @@
package xyz.quaver.hitomi
import kotlinx.serialization.json.Json
import kotlinx.serialization.list
import xyz.quaver.proxy
import java.net.URL
const val protocol = "https:"
@@ -25,10 +25,10 @@ const val protocol = "https:"
@Suppress("EXPERIMENTAL_API_USAGE")
fun getGalleryInfo(galleryID: Int) =
Json.nonstrict.parse(
GalleryInfo.serializer().list,
Regex("""\[.+]""").find(
URL("$protocol//$domain/galleries/$galleryID.js").readText()
)?.value ?: "[]"
GalleryInfo.serializer(),
URL("$protocol//$domain/galleries/$galleryID.js").openConnection(proxy).getInputStream().use {
it.reader().readText()
}.replace("var galleryinfo = ", "")
)
//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/")
}
fun fullPathFromHash(hash: String?) : String? {
return when {
(hash?.length ?: 0) < 3 -> hash
@@ -76,11 +77,20 @@ fun fullPathFromHash(hash: String?) : String? {
}
@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 dir = dir ?: "images"
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)
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 org.jsoup.Jsoup
import xyz.quaver.proxy
import java.net.URLDecoder
@Serializable
@@ -36,7 +37,7 @@ data class Gallery(
val thumbnails: List<String>
)
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")
val doc = Jsoup.connect(url).get()
@@ -70,7 +71,7 @@ fun getGallery(galleryID: Int) : Gallery {
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")
}

View File

@@ -19,6 +19,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import xyz.quaver.Code
import xyz.quaver.proxy
import java.net.URL
import java.net.URLDecoder
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"
}
try {
with(URL(url).openConnection() as HttpsURLConnection) {
requestMethod = "GET"
@@ -61,9 +61,6 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
return Pair(nozomi, totalItems)
}
} catch (e: Exception) {
return Pair(emptyList(), 0)
}
}
@Serializable
@@ -82,8 +79,7 @@ data class GalleryBlock(
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
try {
val doc = Jsoup.connect(url).get()
val doc = Jsoup.connect(url).proxy(proxy).get()
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)
} catch (e: Exception) {
return null
}
}

View File

@@ -17,28 +17,35 @@
package xyz.quaver.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import xyz.quaver.Code
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
@Serializable
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 hash: String? = null,
val haswebp: Int = 0,
val name: String,
val height: Int
val height: Int,
val hasavif: Int = 0
)
@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
fun getReader(galleryID: Int) : Reader {
val readerUrl = "https://hitomi.la/reader/$galleryID.html"
val doc = Jsoup.connect(readerUrl).get()
return Reader(Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
return Reader(Code.HITOMI, getGalleryInfo(galleryID))
}

View File

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

View File

@@ -20,12 +20,12 @@ import org.jsoup.Jsoup
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.protocol
import xyz.quaver.proxy
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$hiyobi/search/$galleryID"
val url = "$protocol//$hiyobi/info/$galleryID"
try {
val doc = Jsoup.connect(url).get()
val doc = Jsoup.connect(url).proxy(proxy).get()
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('_', ' ') }
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
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import kotlinx.serialization.list
import org.jsoup.Jsoup
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryFiles
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.protocol
import xyz.quaver.proxy
import java.net.URL
import javax.net.ssl.HttpsURLConnection
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"
var cookie: String = ""
@@ -47,7 +51,7 @@ fun renewCookie() : String {
val url = "https://$hiyobi/"
try {
with(URL(url).openConnection() as HttpsURLConnection) {
with(URL(url).openConnection(proxy) as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent)
connectTimeout = 2000
connect()
@@ -58,16 +62,16 @@ fun renewCookie() : String {
}
}
@UseExperimental(UnstableDefault::class)
fun getReader(galleryID: Int) : Reader {
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 galleryInfo = Json.parse(
GalleryInfo.serializer().list,
with(URL(url).openConnection() as HttpsURLConnection) {
val galleryFiles = Json.nonstrict.parse(
GalleryFiles.serializer().list,
with(URL(url).openConnection(proxy) as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
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) =
if (lowQuality)
reader.galleryInfo.map {
val name = it.name.replace(Regex("/.[^/.]+$"), "") + ".jpg"
Images("$protocol//$hiyobi/data/$galleryID/$name.jpg", galleryID, it.name)
reader.galleryInfo.files.map {
val name = it.name.replace(Regex("""\.[^/.]+$"""), "")
Images("$protocol//$primary_img_domain/data_r/$galleryID/$name.jpg", galleryID, it.name)
}
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
fun test_getReader() {
val reader = getReader(1442740)
val reader = getReader(1567569)
print(reader)
}
@Test
fun test_hiyobi() {
val reader = xyz.quaver.hiyobi.getReader(10000062)
val reader = xyz.quaver.hiyobi.getReader(1574736)
print(reader)
}
@Test
fun test_urlFromUrlFromHash() {
val url = urlFromUrlFromHash(1531795, GalleryInfo(
val url = urlFromUrlFromHash(1531795, GalleryFiles(
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
), "webp")