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">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

3
.idea/gradle.xml generated
View File

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

View File

@@ -1,7 +1,10 @@
# Pupil
![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
![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: '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")
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
@@ -19,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 42
versionName "4.6-beta1"
versionCode 46
versionName "4.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -68,20 +68,18 @@ dependencies {
implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.2.0-alpha04'
implementation 'com.google.android.material:material:1.2.0-alpha05'
implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false
}
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'

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
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.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".Pupil"
@@ -18,7 +19,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:replace="android:theme">
tools:replace="android:theme"
android:requestLegacyExternalStorage="true">
<provider
android:authorities="${applicationId}.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.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics
import xyz.quaver.proxy
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getProxy
import java.io.File
class Pupil : MultiDexApplication() {
@@ -46,8 +48,10 @@ class Pupil : MultiDexApplication() {
override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
proxy = getProxy(this)
try {
PreferenceManager.getDefaultSharedPreferences(this).getInt("dl_location", 0)
preference.getString("dl_location", null)
} catch (e: Exception) {
preference.edit().remove("dl_location").apply()
}
@@ -58,11 +62,6 @@ class Pupil : MultiDexApplication() {
if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
val file = preference.getString("dl_location", null)
if (file?.startsWith("content") == true)
preference.edit().remove("dl_location").apply()
try {
ProviderInstaller.installIfNeeded(this)
} catch (e: GooglePlayServicesRepairableException) {

View File

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

View File

@@ -28,7 +28,6 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.*
@@ -52,20 +51,19 @@ class ReaderAdapter(private val context: Context,
//region Glide.RecyclerView
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()
}
}
val modelProvider = object: ListPreloader.PreloadModelProvider<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<*>? {
return glide
.load(item)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.fitCenter()
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
@@ -83,15 +81,6 @@ class ReaderAdapter(private val context: Context,
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)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -111,6 +100,9 @@ class ReaderAdapter(private val context: Context,
} else {
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
holder.view.container.layoutParams.height = 0
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
}
holder.view.image.setOnPhotoTapListener { _, _, _ ->
@@ -121,28 +113,26 @@ class ReaderAdapter(private val context: Context,
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()
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 {
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 (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.text.*
import android.text.style.AlignmentSpan
import android.util.Log
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
@@ -120,7 +119,6 @@ class MainActivity : AppCompatActivity() {
val lockManager = try {
LockManager(this)
} catch (e: Exception) {
Log.i("PUPILD", e.toString())
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
@@ -332,6 +330,13 @@ class MainActivity : AppCompatActivity() {
true
}
with(main_fab_cancel) {
setImageResource(R.drawable.cancel)
setOnClickListener {
DownloadWorker.getInstance(context).stop()
}
}
with(main_fab_jump) {
setImageResource(R.drawable.ic_jump)
setOnClickListener {
@@ -410,7 +415,7 @@ class MainActivity : AppCompatActivity() {
if (!completeFlag.get(galleryID, false)) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
if (Cache(context).isDownloading(galleryID)) //download in progress
worker.cancel(galleryID)
else {
Cache(context).setDownloading(galleryID, true)
@@ -726,6 +731,15 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener {
when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
it.isChecked = true
@@ -937,58 +951,60 @@ class MainActivity : AppCompatActivity() {
when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply {
totalItems = size
}.also {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
totalItems = size
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = it.size
}
}
}
Mode.HISTORY -> {
when {
query.isEmpty() -> {
histories.toList().apply {
totalItems = size
histories.toList().also {
totalItems = it.size
}
}
else -> {
val result = doSearch(query).sorted()
histories.filter { result.binarySearch(it) >= 0 }.apply {
totalItems = size
histories.filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists()
file.isDirectory && file.name.toIntOrNull() != null
}?.sortedByDescending {
it.lastModified()
}?.map {
it.name.toInt()
} ?: emptyList()
when {
query.isEmpty() -> downloads.apply {
totalItems = size
query.isEmpty() -> downloads.also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
downloads.filter { result.binarySearch(it) >= 0 }.apply {
totalItems = size
downloads.filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.FAVORITE -> {
when {
query.isEmpty() -> favorites.toList().apply {
totalItems = size
query.isEmpty() -> favorites.toList().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
favorites.filter { result.binarySearch(it) >= 0 }.apply {
totalItems = size
favorites.filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
@@ -1003,10 +1019,13 @@ class MainActivity : AppCompatActivity() {
loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = try {
galleryIDs!!.await()
galleryIDs!!.await().also {
if (it.isEmpty())
throw Exception("No result")
}
} catch (e: Exception) {
if (Fabric.isInitialized())
if (Fabric.isInitialized() && e.message != "No result")
Crashlytics.logException(e)
withContext(Dispatchers.Main) {

View File

@@ -254,11 +254,17 @@ class ReaderActivity : AppCompatActivity() {
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) {
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
if (reader != null) {
title = reader.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
with (reader_recyclerview.adapter as ReaderAdapter) {
this.reader = reader
notifyDataSetChanged()
}
title = reader.galleryInfo.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
when (reader.code) {
@@ -294,7 +300,7 @@ class ReaderActivity : AppCompatActivity() {
}
}
//addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
@@ -355,6 +361,8 @@ class ReaderActivity : AppCompatActivity() {
window.attributes = this
}
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
}
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
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
labelText = getString(R.string.reader_fab_download_cancel)
}
else // Or continue animate
post {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5555
dependencies.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,5 +14,4 @@
kotlin.code.style=official
android.enableJetifier=true
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
android.useAndroidX=true
android.enableR8.fullMode=true
android.useAndroidX=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
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import xyz.quaver.Code
import xyz.quaver.proxy
import java.net.URL
import java.net.URLDecoder
import java.nio.ByteBuffer
@@ -78,7 +79,7 @@ data class GalleryBlock(
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
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")

View File

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

View File

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

View File

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

View File

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

View File

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