Compare commits

...

32 Commits

Author SHA1 Message Date
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
69e85f8b90 Bug fix 2020-02-24 21:10:10 +09:00
Pupil
65e9557d9f Bug fix 2020-02-24 20:02:44 +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
48 changed files with 6440 additions and 266 deletions

View File

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

3
.idea/gradle.xml generated
View File

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

View File

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

View File

@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
if (file("src/google-services.json").exists() && file("src/debug/google-services.json").exists()) { if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
logger.lifecycle("Firebase Enabled") logger.lifecycle("Firebase Enabled")
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
@@ -19,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 42 versionCode 46
versionName "4.6-beta1" versionName "4.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -68,20 +68,18 @@ dependencies {
implementation "androidx.biometric:biometric:1.0.1" implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.2.0-alpha04' implementation 'com.google.android.material:material:1.2.0-alpha05'
implementation 'com.google.firebase:firebase-core:17.2.2' implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.google.firebase:firebase-perf:19.0.5' implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0' kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false transitive = false
} }
implementation 'net.rdrei.android.dirchooser:library:3.2@aar' implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":42,"versionName":"4.6-beta1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":46,"versionName":"4.10","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}]

View File

@@ -105,9 +105,9 @@ class ExampleInstrumentedTest {
val galleryID = 1561552 val galleryID = 1561552
runBlocking { runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null") Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
} }
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null") Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
} }
} }

View File

@@ -6,8 +6,9 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -18,7 +19,8 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:replace="android:theme"> tools:replace="android:theme"
android:requestLegacyExternalStorage="true">
<provider <provider
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View File

@@ -31,7 +31,9 @@ import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import xyz.quaver.proxy
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getProxy
import java.io.File import java.io.File
class Pupil : MultiDexApplication() { class Pupil : MultiDexApplication() {
@@ -46,8 +48,10 @@ class Pupil : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
proxy = getProxy(this)
try { try {
PreferenceManager.getDefaultSharedPreferences(this).getInt("dl_location", 0) preference.getString("dl_location", null)
} catch (e: Exception) { } catch (e: Exception) {
preference.edit().remove("dl_location").apply() preference.edit().remove("dl_location").apply()
} }
@@ -58,11 +62,6 @@ class Pupil : MultiDexApplication() {
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false) FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
val file = preference.getString("dl_location", null)
if (file?.startsWith("content") == true)
preference.edit().remove("dl_location").apply()
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)
} catch (e: GooglePlayServicesRepairableException) { } catch (e: GooglePlayServicesRepairableException) {

View File

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

View File

@@ -28,7 +28,6 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.* import kotlinx.android.synthetic.main.item_reader.view.*
@@ -52,20 +51,19 @@ class ReaderAdapter(private val context: Context,
//region Glide.RecyclerView //region Glide.RecyclerView
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position -> val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(position)?.let { Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.getOrNull(position)?.let {
arrayOf(it.width, it.height).toIntArray() arrayOf(it.width, it.height).toIntArray()
} }
} }
val modelProvider = object: ListPreloader.PreloadModelProvider<File> { val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
override fun getPreloadItems(position: Int): MutableList<File> { override fun getPreloadItems(position: Int): MutableList<File> {
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf()) return listOf(Cache(context).getImages(galleryID)?.getOrNull(position)).filterNotNullTo(mutableListOf())
} }
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? { override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
return glide return glide
.load(item) .load(item)
.diskCacheStrategy(DiskCacheStrategy.NONE) .fitCenter()
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
@@ -83,15 +81,6 @@ class ReaderAdapter(private val context: Context,
var onItemClickListener : ((Int) -> (Unit))? = null var onItemClickListener : ((Int) -> (Unit))? = null
init {
CoroutineScope(Dispatchers.IO).launch {
reader = Cache(context).getReader(galleryID)
launch(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -111,6 +100,9 @@ class ReaderAdapter(private val context: Context,
} else { } else {
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
holder.view.container.layoutParams.height = 0 holder.view.container.layoutParams.height = 0
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
} }
holder.view.image.setOnPhotoTapListener { _, _, _ -> holder.view.image.setOnPhotoTapListener { _, _, _ ->
@@ -121,28 +113,26 @@ class ReaderAdapter(private val context: Context,
onItemClickListener?.invoke(position) onItemClickListener?.invoke(position)
} }
if (!isFullScreen)
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
holder.view.reader_index.text = (position+1).toString() holder.view.reader_index.text = (position+1).toString()
val images = Cache(context).getImages(galleryID) 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)
}
if (images?.get(position) != null) {
glide
.load(images[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.dontTransform()
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
} else { } else {
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position) holder.view.reader_item_progressbar.visibility = View.VISIBLE
glide.clear(holder.view.image)
if (progress?.isNaN() == true) { if (progress?.isNaN() == true) {
if (Fabric.isInitialized()) if (Fabric.isInitialized())
@@ -171,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

@@ -25,7 +25,6 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.* import android.text.*
import android.text.style.AlignmentSpan import android.text.style.AlignmentSpan
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -120,7 +119,6 @@ class MainActivity : AppCompatActivity() {
val lockManager = try { val lockManager = try {
LockManager(this) LockManager(this)
} catch (e: Exception) { } catch (e: Exception) {
Log.i("PUPILD", e.toString())
android.app.AlertDialog.Builder(this).apply { android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
setMessage(R.string.lock_corrupted) setMessage(R.string.lock_corrupted)
@@ -332,6 +330,13 @@ class MainActivity : AppCompatActivity() {
true true
} }
with(main_fab_cancel) {
setImageResource(R.drawable.cancel)
setOnClickListener {
DownloadWorker.getInstance(context).stop()
}
}
with(main_fab_jump) { with(main_fab_jump) {
setImageResource(R.drawable.ic_jump) setImageResource(R.drawable.ic_jump)
setOnClickListener { setOnClickListener {
@@ -410,7 +415,7 @@ class MainActivity : AppCompatActivity() {
if (!completeFlag.get(galleryID, false)) { if (!completeFlag.get(galleryID, false)) {
val worker = DownloadWorker.getInstance(context) val worker = DownloadWorker.getInstance(context)
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress if (Cache(context).isDownloading(galleryID)) //download in progress
worker.cancel(galleryID) worker.cancel(galleryID)
else { else {
Cache(context).setDownloading(galleryID, true) Cache(context).setDownloading(galleryID, true)
@@ -726,6 +731,15 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener { setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS) R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> { R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST sortMode = SortMode.NEWEST
it.isChecked = true it.isChecked = true
@@ -937,58 +951,60 @@ class MainActivity : AppCompatActivity() {
when(sortMode) { when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all") else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply { }.also {
totalItems = size totalItems = it.size
} }
} }
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply { else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = size totalItems = it.size
} }
} }
} }
Mode.HISTORY -> { Mode.HISTORY -> {
when { when {
query.isEmpty() -> { query.isEmpty() -> {
histories.toList().apply { histories.toList().also {
totalItems = size totalItems = it.size
} }
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
histories.filter { result.binarySearch(it) >= 0 }.apply { histories.filter { result.binarySearch(it) >= 0 }.also {
totalItems = size totalItems = it.size
} }
} }
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file -> val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists() file.isDirectory && file.name.toIntOrNull() != null
}?.sortedByDescending {
it.lastModified()
}?.map { }?.map {
it.name.toInt() it.name.toInt()
} ?: emptyList() } ?: emptyList()
when { when {
query.isEmpty() -> downloads.apply { query.isEmpty() -> downloads.also {
totalItems = size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
downloads.filter { result.binarySearch(it) >= 0 }.apply { downloads.filter { result.binarySearch(it) >= 0 }.also {
totalItems = size totalItems = it.size
} }
} }
} }
} }
Mode.FAVORITE -> { Mode.FAVORITE -> {
when { when {
query.isEmpty() -> favorites.toList().apply { query.isEmpty() -> favorites.toList().also {
totalItems = size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
favorites.filter { result.binarySearch(it) >= 0 }.apply { favorites.filter { result.binarySearch(it) >= 0 }.also {
totalItems = size totalItems = it.size
} }
} }
} }
@@ -1003,10 +1019,13 @@ class MainActivity : AppCompatActivity() {
loadingJob = CoroutineScope(Dispatchers.IO).launch { loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = try { val galleryIDs = try {
galleryIDs!!.await() galleryIDs!!.await().also {
if (it.isEmpty())
throw Exception("No result")
}
} catch (e: Exception) { } catch (e: Exception) {
if (Fabric.isInitialized()) if (Fabric.isInitialized() && e.message != "No result")
Crashlytics.logException(e) Crashlytics.logException(e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@@ -254,11 +254,17 @@ class ReaderActivity : AppCompatActivity() {
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
if (reader != null) { if (reader != null) {
title = reader.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}" with (reader_recyclerview.adapter as ReaderAdapter) {
this.reader = reader
notifyDataSetChanged()
}
title = reader.galleryInfo.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
when (reader.code) { when (reader.code) {
@@ -294,7 +300,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
//addOnScrollListener((adapter as ReaderAdapter).preloader) addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() { addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@@ -355,6 +361,8 @@ class ReaderActivity : AppCompatActivity() {
window.attributes = this window.attributes = this
} }
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
} }
private fun scrollMode(isScroll: Boolean) { private fun scrollMode(isScroll: Boolean) {
@@ -380,7 +388,7 @@ class ReaderActivity : AppCompatActivity() {
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post { post {
setImageResource(R.drawable.ic_download) setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download) labelText = getString(R.string.reader_fab_download_cancel)
} }
else // Or continue animate else // Or continue animate
post { post {

View File

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

View File

@@ -26,6 +26,7 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -46,6 +47,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val buttons = mutableListOf<Pair<RadioButton, File?>>() private val buttons = mutableListOf<Pair<RadioButton, File?>>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.settings_dl_location)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun build() : View {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null) val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
@@ -115,18 +126,13 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
externalFilesDirs.indexOfFirst { externalFilesDirs.indexOfFirst {
it.canonicalPath == getDownloadDirectory(context).canonicalPath it.canonicalPath == getDownloadDirectory(context).canonicalPath
}.let { index -> }.let { index ->
buttons[index].first.isChecked = true if (index < 0)
buttons.first().first.isChecked = true
else
buttons[index].first.isChecked = true
} }
setTitle(R.string.settings_dl_location) return view
setView(view)
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
dismiss()
}
super.onCreate(savedInstanceState)
} }
} }

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.ui.dialog.ProxyDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
@@ -146,6 +147,10 @@ class SettingsFragment :
MirrorDialog(context) MirrorDialog(context)
.show() .show()
} }
"proxy" -> {
ProxyDialog(context)
.show()
}
"backup" -> { "backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"), File(getDownloadDirectory(context), "favorites.json"),
@@ -189,9 +194,18 @@ class SettingsFragment :
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { key ?: return
"dl_location" -> {
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).canonicalPath with(findPreference<Preference>(key)) {
this ?: return
when (key) {
"proxy" -> {
summary = getProxyInfo(context).type.name
}
"dl_location" -> {
summary = getDownloadDirectory(context!!).canonicalPath
}
} }
} }
} }
@@ -245,8 +259,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"default_query" -> { "default_query" -> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: ""
summary = preferences.getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
@@ -270,6 +283,11 @@ class SettingsFragment :
"mirrors" -> { "mirrors" -> {
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"proxy" -> {
summary = getProxyInfo(context).type.name
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> { "dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }

View File

@@ -21,22 +21,42 @@ package xyz.quaver.pupil.util.download
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.util.SparseArray
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.async import kotlinx.io.InputStream
import kotlinx.coroutines.withContext
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.proxy
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf
import xyz.quaver.pupil.util.json import xyz.quaver.pupil.util.json
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.URL import java.net.URL
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
class Cache(context: Context) : ContextWrapper(context) { 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) private val preference = PreferenceManager.getDefaultSharedPreferences(this)
// Search in this order // Search in this order
@@ -77,7 +97,9 @@ class Cache(context: Context) : ContextWrapper(context) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails val thumbnails = getGalleryBlock(galleryID)?.thumbnails
try { try {
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT) Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
it.readBytes()
}, Base64.DEFAULT)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -106,9 +128,11 @@ class Cache(context: Context) : ContextWrapper(context) {
var galleryBlock: GalleryBlock? = null var galleryBlock: GalleryBlock? = null
for (source in sources) { for (source in sources) {
galleryBlock = kotlin.runCatching { galleryBlock = try {
source.invoke() source.invoke()
}.getOrNull() } catch (e: Exception) {
null
}
if (galleryBlock != null) if (galleryBlock != null)
break break
@@ -155,9 +179,12 @@ class Cache(context: Context) : ContextWrapper(context) {
var retval: Reader? = null var retval: Reader? = null
for (source in sources) { for (source in sources) {
retval = kotlin.runCatching { retval = try {
source.value.invoke() source.value.invoke()
}.getOrNull() } catch (e: Exception) {
Crashlytics.logException(e)
null
}
if (retval != null) if (retval != null)
break break
@@ -176,37 +203,69 @@ class Cache(context: Context) : ContextWrapper(context) {
return reader return reader
} }
val imageNameRegex = Regex("""^\d+\..+$""")
fun getImages(galleryID: Int): List<File?>? { fun getImages(galleryID: Int): List<File?>? {
val gallery = getCachedGallery(galleryID) val gallery = getCachedGallery(galleryID)
val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles() ?: return null
return reader.galleryInfo.indices.map { index -> return gallery.list { _, name ->
images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) } imageNameRegex.matches(name)
}?.map {
File(gallery, it)
} }
} }
fun putImage(galleryID: Int, name: String, data: ByteArray) { val imageExtensions = listOf(
val cache = File(getCachedGallery(galleryID), name).also { "png",
"jpg",
"webp",
"gif"
)
fun getImage(galleryID: Int, index: Int): File? {
val gallery = getCachedGallery(galleryID)
for (ext in imageExtensions) {
File(gallery, "%05d.$ext".format(index)).let {
if (it.exists())
return it
}
}
return null
}
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
if (!it.exists()) if (!it.exists())
it.createNewFile() it.createNewFile()
} }
if (!Regex("""^[0-9]+.+$""").matches(name)) data.use {
throw IllegalArgumentException("File name is not a number") it.copyTo(FileOutputStream(cache))
}
cache.writeBytes(data)
} }
fun moveToDownload(galleryID: Int) { fun moveToDownload(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
val cache = getCachedGallery(galleryID).also { val cache = getCachedGallery(galleryID).also {
if (!it.exists()) if (!it.exists())
return return@launch
} }
val download = File(getDownloadDirectory(this), galleryID.toString()) val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
cache.copyRecursively(download, true) if (download.isParentOf(cache))
return@launch
Log.i("PUPILD", "MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
cache.copyRecursively(download, true) { file, err ->
Log.i("PUPILD", "MOVING ERROR ${file.canonicalPath} ${err.message}")
OnErrorAction.SKIP
}
Log.i("PUPILD", "MOVED ${cache.canonicalPath}")
Log.i("PUPILD", "DELETING ${cache.canonicalPath}")
cache.deleteRecursively() cache.deleteRecursively()
Log.i("PUPILD", "DELETED ${cache.canonicalPath}")
} }
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true

View File

@@ -23,6 +23,7 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -36,15 +37,18 @@ import okio.*
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.proxy
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) { class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
@@ -145,37 +149,36 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private val loop = loop() private val loop = loop()
private val worker = SparseArray<Job?>() private val worker = SparseArray<Job?>()
val clients = SparseArray<OkHttpClient>()
val interceptor = Interceptor { chain -> val interceptor = Interceptor { chain ->
val request = chain.request() val request = chain.request()
val response = chain.proceed(request) val response = chain.proceed(request)
response.newBuilder() response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener)) .body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build() .build()
} }
fun buildClient() = val client =
OkHttpClient.Builder() OkHttpClient.Builder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4))) .dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.proxy(proxy)
.build() .build()
fun stop() { fun stop() {
queue.clear() queue.clear()
loop.cancel() loop.cancel()
for (i in 0..worker.size()) { for (i in 0 until worker.size()) {
val galleryID = worker.keyAt(i) val galleryID = worker.keyAt(i)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
worker[galleryID]?.cancel() worker[galleryID]?.cancel()
} }
for (i in 0 until clients.size()) { client.dispatcher().cancelAll()
clients.valueAt(i).dispatcher().cancelAll()
}
clients.clear()
progress.clear() progress.clear()
exception.clear() exception.clear()
@@ -187,24 +190,19 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
queue.remove(galleryID) queue.remove(galleryID)
worker[galleryID]?.cancel() worker[galleryID]?.cancel()
clients[galleryID]?.dispatcher()?.queuedCalls() client.dispatcher().queuedCalls().filter {
?.filter { ((it.request().tag() as Pair<*, *>).first as Int) == galleryID
@Suppress("UNCHECKED_CAST") }.forEach {
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID it.cancel()
} }
?.forEach {
it.cancel()
}
clients.remove(galleryID)
progress.remove(galleryID) progress.remove(galleryID)
exception.remove(galleryID) exception.remove(galleryID)
notification.remove(galleryID) notification.remove(galleryID)
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0) { if (progress.indexOfKey(galleryID) >= 0)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
}
} }
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
@@ -216,10 +214,10 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
when (reader.code) { when (reader.code) {
Code.HITOMI -> { Code.HITOMI -> {
url( url(
urlFromUrlFromHash( imageUrlFromImage(
galleryID, galleryID,
reader.galleryInfo[index], reader.galleryInfo.files[index],
if (lowQuality) "webp" else null lowQuality
) )
) )
addHeader("Referer", getReferer(galleryID)) addHeader("Referer", getReferer(galleryID))
@@ -236,7 +234,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
tag(galleryID to index) tag(galleryID to index)
}.build() }.build()
clients[galleryID].newCall(request).enqueue(callback) client.newCall(request).enqueue(callback)
} }
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch { private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
@@ -253,18 +251,18 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val cache = Cache(this@DownloadWorker).getImages(galleryID) val cache = Cache(this@DownloadWorker).getImages(galleryID)
progress.put(galleryID, reader.galleryInfo.indices.map { index -> progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
if (cache?.get(index) != null) if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
Float.POSITIVE_INFINITY Float.POSITIVE_INFINITY
else else
0F 0F
}.toMutableList()) }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList()) exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
if (notification[galleryID] == null) if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
notification[galleryID].setContentTitle(reader.title) notification[galleryID].setContentTitle(reader.galleryInfo.title)
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
@@ -278,12 +276,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
return@launch return@launch
} }
clients.put(galleryID, buildClient()) for (i in reader.galleryInfo.files.indices) {
for (i in reader.galleryInfo.indices) {
val callback = object : Callback { val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
if (Fabric.isInitialized()) Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
if (Fabric.isInitialized() && e.message != "Canceled")
Crashlytics.logException(e) Crashlytics.logException(e)
progress[galleryID]?.set(i, Float.NaN) progress[galleryID]?.set(i, Float.NaN)
@@ -291,43 +288,74 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { CoroutineScope(Dispatchers.IO).launch {
with(Cache(this@DownloadWorker)) { if (isCompleted(galleryID)) {
if (isDownloading(galleryID)) { with(Cache(this@DownloadWorker)) {
moveToDownload(galleryID) if (isDownloading(galleryID)) {
setDownloading(galleryID, false) moveToDownload(galleryID)
setDownloading(galleryID, false)
}
} }
} }
clients.remove(galleryID)
} }
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
response.body().use { Log.i("PUPILD", "OK ${call.request().tag()}")
val res = it.bytes()
val ext =
call.request().url().encodedPath().split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res) val ext = call.request().url().encodedPath().split('.').last()
try {
response.body().use {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
}
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY) progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
}
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { CoroutineScope(Dispatchers.IO).launch {
with(Cache(this@DownloadWorker)) { if (isCompleted(galleryID)) {
if (isDownloading(galleryID)) { with(Cache(this@DownloadWorker)) {
moveToDownload(galleryID) if (isDownloading(galleryID)) {
setDownloading(galleryID, false) moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
} }
} }
clients.remove(galleryID)
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) if (progress[galleryID]?.get(i)?.isFinite() == true) {
queueDownload(galleryID, reader, i, callback) queueDownload(galleryID, reader, i, callback)
Log.i("PUPILD", "$galleryID QUEUED $i")
} else {
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
}
} }
} }
@@ -335,13 +363,17 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0 val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
if (isCompleted(galleryID)) Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
if (isCompleted(galleryID)) {
notification[galleryID] notification[galleryID]
?.setContentText(getString(R.string.reader_notification_complete)) ?.setContentText(getString(R.string.reader_notification_complete))
?.setSmallIcon(android.R.drawable.stat_sys_download_done) ?.setSmallIcon(android.R.drawable.stat_sys_download_done)
?.setProgress(0, 0, false) ?.setProgress(0, 0, false)
?.setOngoing(false) ?.setOngoing(false)
else
notificationManager.cancel(galleryID)
} else
notification[galleryID] notification[galleryID]
?.setProgress(max, progress, false) ?.setProgress(max, progress, false)
?.setContentText("$progress/$max") ?.setContentText("$progress/$max")
@@ -358,7 +390,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
val pendingIntent = TaskStackBuilder.create(this).run { val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent) addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
} }
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply { notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
@@ -373,18 +405,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private fun loop() = CoroutineScope(Dispatchers.Default).launch { private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) { while (true) {
if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4)) if (queue.isEmpty())
continue continue
val galleryID = queue.poll() ?: continue val galleryID = queue.peek() ?: continue
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading! if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue continue
initNotification(galleryID) if (notification[galleryID] == null)
initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID)) if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build()) notificationManager.notify(galleryID, notification[galleryID].build())
Log.i("PUPILD", "QUEUED $galleryID")
worker.put(galleryID, download(galleryID)) worker.put(galleryID, download(galleryID))
queue.poll()
} }
} }

View File

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

View File

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

@@ -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" android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent"> app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump" android:id="@+id/main_fab_jump"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -122,4 +122,14 @@
<string name="settings_mirror_title">미러 설정</string> <string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</string> <string name="settings_dl_location_custom">직접 설정</string>
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string> <string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
<string name="settings_proxy_title">프록시</string>
<string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">프록시 타입</string>
<string name="proxy_dialog_port_hint">포트</string>
<string name="proxy_dialog_password_hint">비밀번호</string>
<string name="proxy_dialog_error">잘못된 값</string>
<string name="proxy_dialog_addr_hint">서버 주소</string>
<string name="proxy_dialog_server">서버</string>
<string name="main_menu_thin">간단히 보기 모드</string>
<string name="main_fab_cancel">다운로드 모두 취소</string>
</resources> </resources>

View File

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

View File

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

View File

@@ -53,6 +53,8 @@
<string name="main_drawer_group_contact_email">Email me!</string> <string name="main_drawer_group_contact_email">Email me!</string>
<string name="main_drawer_grouop_contact_discord">Discord</string> <string name="main_drawer_grouop_contact_discord">Discord</string>
<string name="main_menu_thin">Toggle Thin Mode</string>
<string name="main_menu_sort">Sort</string> <string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string> <string name="main_menu_sort_newest">Newest</string>
<string name="main_menu_sort_popular">Popular</string> <string name="main_menu_sort_popular">Popular</string>
@@ -61,6 +63,7 @@
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string> <string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
<string name="main_open_gallery_by_id">Open Gallery by ID</string> <string name="main_open_gallery_by_id">Open Gallery by ID</string>
<string name="reader_failed_to_find_gallery">Failed to open gallery</string> <string name="reader_failed_to_find_gallery">Failed to open gallery</string>
<string name="main_fab_cancel">Cancel all downloads</string>
<string name="main_move">Move to page %1$d</string> <string name="main_move">Move to page %1$d</string>
@@ -154,6 +157,7 @@
<string name="settings_miscellaneous_title">Miscellaneous</string> <string name="settings_miscellaneous_title">Miscellaneous</string>
<string name="settings_mirror_summary">Load images from mirrors</string> <string name="settings_mirror_summary">Load images from mirrors</string>
<string name="settings_proxy_title">Proxy</string>
<string name="settings_security_mode_title">Enable security mode</string> <string name="settings_security_mode_title">Enable security mode</string>
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string> <string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
<string name="settings_dark_mode_title">Dark mode</string> <string name="settings_dark_mode_title">Dark mode</string>
@@ -189,4 +193,13 @@
<string name="default_query_dialog_language_selector_none">Any</string> <string name="default_query_dialog_language_selector_none">Any</string>
<string name="settings_mirror_title">Mirrors</string> <string name="settings_mirror_title">Mirrors</string>
<!-- PROXY DIALOG -->
<string name="proxy_dialog_type">type</string>
<string name="proxy_dialog_addr_hint">address</string>
<string name="proxy_dialog_port_hint">port</string>
<string name="proxy_dialog_username_hint">username</string>
<string name="proxy_dialog_password_hint">password</string>
<string name="proxy_dialog_error">Wrong value</string>
<string name="proxy_dialog_server">server</string>
</resources> </resources>

View File

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

View File

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

5555
dependencies.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,5 +14,4 @@
kotlin.code.style=official kotlin.code.style=official
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M" org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
android.useAndroidX=true android.useAndroidX=true
android.enableR8.fullMode=true

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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.proxy
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -78,7 +79,7 @@ data class GalleryBlock(
fun getGalleryBlock(galleryID: Int) : GalleryBlock? { fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension" val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).proxy(proxy).get()
val galleryUrl = doc.selectFirst(".lillie").attr("href") val galleryUrl = doc.selectFirst(".lillie").attr("href")

View File

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

View File

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

View File

@@ -20,11 +20,12 @@ import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.protocol import xyz.quaver.hitomi.protocol
import xyz.quaver.proxy
fun getGalleryBlock(galleryID: Int) : GalleryBlock? { fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$hiyobi/info/$galleryID" val url = "$protocol//$hiyobi/info/$galleryID"
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).proxy(proxy).get()
val galleryBlock = doc.selectFirst(".gallery-content") val galleryBlock = doc.selectFirst(".gallery-content")

View File

@@ -16,13 +16,16 @@
package xyz.quaver.hiyobi package xyz.quaver.hiyobi
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.list import kotlinx.serialization.list
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryFiles
import xyz.quaver.hitomi.GalleryInfo import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.protocol import xyz.quaver.hitomi.protocol
import xyz.quaver.proxy
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -47,7 +50,7 @@ fun renewCookie() : String {
val url = "https://$hiyobi/" val url = "https://$hiyobi/"
try { try {
with(URL(url).openConnection() as HttpsURLConnection) { with(URL(url).openConnection(proxy) as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
connectTimeout = 2000 connectTimeout = 2000
connect() connect()
@@ -58,16 +61,16 @@ fun renewCookie() : String {
} }
} }
@UseExperimental(UnstableDefault::class)
fun getReader(galleryID: Int) : Reader { fun getReader(galleryID: Int) : Reader {
val reader = "https://$hiyobi/reader/$galleryID" val reader = "https://$hiyobi/reader/$galleryID"
val url = "https://$hiyobi/data/json/${galleryID}_list.json" val url = "https://cdn.hiyobi.me/data/json/${galleryID}_list.json"
val title = Jsoup.connect(reader).get().title() val title = Jsoup.connect(reader).proxy(proxy).get().title()
@Suppress("EXPERIMENTAL_API_USAGE") val galleryFiles = Json.nonstrict.parse(
val galleryInfo = Json.parse( GalleryFiles.serializer().list,
GalleryInfo.serializer().list, with(URL(url).openConnection(proxy) as HttpsURLConnection) {
with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
connectTimeout = 2000 connectTimeout = 2000
@@ -77,14 +80,14 @@ fun getReader(galleryID: Int) : Reader {
} }
) )
return Reader(Code.HIYOBI, title, galleryInfo) return Reader(Code.HIYOBI, GalleryInfo(title = title, files = galleryFiles))
} }
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) = fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
if (lowQuality) if (lowQuality)
reader.galleryInfo.map { reader.galleryInfo.files.map {
val name = it.name.replace(Regex("/.[^/.]+$"), "") + ".jpg" val name = it.name.replace(Regex("""\.[^/.]+$"""), "")
Images("$protocol//$hiyobi/data/$galleryID/$name.jpg", galleryID, it.name) Images("$protocol//$hiyobi/data_r/$galleryID/$name.jpg", galleryID, it.name)
} }
else else
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) } reader.galleryInfo.files.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }

View File

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