Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8bff0b98 | ||
|
|
a4336cd954 | ||
|
|
4f0dbead79 | ||
|
|
c0e7c87ca4 | ||
|
|
b967bf9a26 | ||
|
|
764a265053 | ||
|
|
68c2b2dbfa | ||
|
|
061f1263f4 | ||
|
|
2a27355479 | ||
|
|
ae2a8e8ada | ||
|
|
68dcc2333b | ||
|
|
66fb2e9a62 | ||
|
|
1dbfc64f37 | ||
|
|
98d1f88579 | ||
|
|
bb6fadc182 | ||
|
|
ac1ca71299 | ||
|
|
0d93785581 | ||
|
|
69a9d63e1d | ||
|
|
5dea35343b | ||
|
|
5c768d2121 | ||
|
|
4d5834821a | ||
|
|
ca077c4fee | ||
|
|
85d01f60f1 | ||
|
|
066d73b217 | ||
|
|
ba069d8f8e | ||
|
|
275684c9ce | ||
|
|
49d87a08d2 | ||
|
|
04c500f3d8 | ||
|
|
d05c1e4d08 | ||
|
|
bb63959678 | ||
|
|
842148647f |
@@ -1,7 +1,10 @@
|
||||
# Pupil
|
||||
|
||||

|
||||
*Pupil, Hitomi.la viewer for Android*
|
||||
*Pupil, Hitomi.la viewer for Android*
|
||||
|
||||
[](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
|
||||

|
||||
|
||||
@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
if (file("google-services.json").exists()) {
|
||||
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
||||
logger.lifecycle("Firebase Enabled")
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'io.fabric'
|
||||
@@ -19,19 +19,27 @@ android {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 41
|
||||
versionName "4.5"
|
||||
versionCode 42
|
||||
versionName "4.6"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
debug {
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-DEBUG"
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
buildTypes.each {
|
||||
it.buildConfigField('boolean', 'CENSOR', 'false')
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
@@ -57,26 +65,27 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
implementation "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'
|
||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") {
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
|
||||
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
@@ -18,4 +18,13 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":41,"versionName":"4.5","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":42,"versionName":"4.6","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
22
app/src/debug/res/values/strings.xml
Normal file
22
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
|
||||
</resources>
|
||||
@@ -6,8 +6,8 @@
|
||||
<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 +18,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"
|
||||
|
||||
@@ -30,7 +30,10 @@ import androidx.preference.PreferenceManager
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import xyz.quaver.proxy
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.getProxy
|
||||
import java.io.File
|
||||
|
||||
class Pupil : MultiDexApplication() {
|
||||
@@ -45,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()
|
||||
}
|
||||
@@ -54,10 +59,8 @@ class Pupil : MultiDexApplication() {
|
||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
|
||||
val file = preference.getString("dl_location", null)
|
||||
|
||||
if (file?.startsWith("content") == true)
|
||||
preference.edit().remove("dl_location").apply()
|
||||
if (BuildConfig.DEBUG)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(this)
|
||||
|
||||
@@ -71,15 +71,15 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
var timerTask: TimerTask? = null
|
||||
|
||||
private fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||
private fun updateProgress(context: Context, galleryID: Int) {
|
||||
val cache = Cache(context).getCachedGallery(galleryID)
|
||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||
|
||||
launch(Dispatchers.Main) main@{
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (reader == null) {
|
||||
view.galleryblock_progressbar.visibility = View.GONE
|
||||
view.galleryblock_progress_complete.visibility = View.GONE
|
||||
return@main
|
||||
return@launch
|
||||
}
|
||||
|
||||
with(view.galleryblock_progressbar) {
|
||||
@@ -90,7 +90,7 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
|
||||
if (visibility == View.GONE) {
|
||||
visibility = View.VISIBLE
|
||||
max = reader.galleryInfo.size
|
||||
max = reader.galleryInfo.files.size
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
@@ -160,7 +160,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
|
||||
|
||||
@@ -48,21 +48,17 @@ import kotlin.math.roundToInt
|
||||
class ReaderAdapter(private val context: Context,
|
||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
|
||||
val glide = Glide.with(context)
|
||||
|
||||
//region Glide.RecyclerView
|
||||
inner class SizeProvider : ListPreloader.PreloadSizeProvider<File> {
|
||||
|
||||
override fun getPreloadSize(item: File, adapterPosition: Int, itemPosition: Int): IntArray? {
|
||||
return Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(itemPosition)?.let {
|
||||
arrayOf(it.width, it.height).toIntArray()
|
||||
}
|
||||
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
|
||||
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.getOrNull(position)?.let {
|
||||
arrayOf(it.width, it.height).toIntArray()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class ModelProvider : ListPreloader.PreloadModelProvider<File> {
|
||||
|
||||
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<*>? {
|
||||
@@ -76,31 +72,17 @@ class ReaderAdapter(private val context: Context,
|
||||
override(5, 8)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
|
||||
//endregion
|
||||
|
||||
var reader: Reader? = null
|
||||
val glide = Glide.with(context)
|
||||
val timer = Timer()
|
||||
|
||||
val sizeProvider = SizeProvider()
|
||||
val modelProvider = ModelProvider()
|
||||
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
|
||||
|
||||
var isFullScreen = false
|
||||
|
||||
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 {
|
||||
@@ -114,10 +96,13 @@ class ReaderAdapter(private val context: Context,
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.view as ConstraintLayout
|
||||
|
||||
if (isFullScreen)
|
||||
if (isFullScreen) {
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||
else
|
||||
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||
holder.view.container.layoutParams.height = 0
|
||||
}
|
||||
|
||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||
onItemClickListener?.invoke(position)
|
||||
@@ -127,57 +112,55 @@ class ReaderAdapter(private val context: Context,
|
||||
onItemClickListener?.invoke(position)
|
||||
}
|
||||
|
||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
||||
if (!isFullScreen)
|
||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||
|
||||
holder.view.reader_index.text = (position+1).toString()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val images = Cache(context).getImages(galleryID)
|
||||
val images = Cache(context).getImage(galleryID, position)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
if (images?.get(position) != null) {
|
||||
glide
|
||||
.load(images[position])
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
.into(holder.view.image)
|
||||
} else {
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
if (images != null) {
|
||||
glide
|
||||
.load(images)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
.into(holder.view.image)
|
||||
} else {
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
|
||||
if (progress?.isNaN() == true) {
|
||||
if (progress?.isNaN() == true) {
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||
glide
|
||||
.load(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
|
||||
glide
|
||||
.load(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
} else {
|
||||
holder.view.reader_item_progressbar.progress =
|
||||
if (progress?.isInfinite() == true)
|
||||
100
|
||||
else
|
||||
progress?.roundToInt() ?: 0
|
||||
return
|
||||
} else {
|
||||
holder.view.reader_item_progressbar.progress =
|
||||
if (progress?.isInfinite() == true)
|
||||
100
|
||||
else
|
||||
progress?.roundToInt() ?: 0
|
||||
|
||||
holder.view.image.setImageDrawable(null)
|
||||
}
|
||||
holder.view.image.setImageDrawable(null)
|
||||
}
|
||||
|
||||
timer.schedule(1000) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
timer.schedule(1000) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = reader?.galleryInfo?.size ?: 0
|
||||
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
|
||||
|
||||
}
|
||||
@@ -45,15 +45,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.arlib.floatingsearchview.FloatingSearchView
|
||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import kotlinx.serialization.stringify
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.doSearch
|
||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||
@@ -179,7 +177,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
|
||||
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -693,7 +691,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private var suggestionJob : Job? = null
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun setupSearchBar() {
|
||||
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||
//Change upper case letters to lower case
|
||||
@@ -717,12 +714,11 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
with(main_searchview as FloatingSearchView) {
|
||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = Tag.serializer().list
|
||||
|
||||
if (!favoritesFile.exists()) {
|
||||
favoritesFile.createNewFile()
|
||||
favoritesFile.writeText(json.stringify(Tags(listOf())))
|
||||
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
@@ -840,7 +836,7 @@ class MainActivity : AppCompatActivity() {
|
||||
favorites.add(tag)
|
||||
}
|
||||
|
||||
favoritesFile.writeText(json.stringify(favorites))
|
||||
favoritesFile.writeText(json.stringify(serializer, favorites))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,26 +935,26 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -971,26 +967,26 @@ class MainActivity : AppCompatActivity() {
|
||||
} ?: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1004,9 +1000,16 @@ class MainActivity : AppCompatActivity() {
|
||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
||||
|
||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val galleryIDs = galleryIDs?.await()
|
||||
val galleryIDs = try {
|
||||
galleryIDs!!.await().also {
|
||||
if (it.isEmpty())
|
||||
throw Exception("No result")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
if (Fabric.isInitialized() && e.message != "No result")
|
||||
Crashlytics.logException(e)
|
||||
|
||||
if (galleryIDs.isNullOrEmpty()) { //No result
|
||||
withContext(Dispatchers.Main) {
|
||||
main_noresult.visibility = View.VISIBLE
|
||||
main_progressbar.hide()
|
||||
|
||||
@@ -38,7 +38,6 @@ import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
@@ -141,7 +140,6 @@ class ReaderActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.reader, menu)
|
||||
|
||||
@@ -256,11 +254,17 @@ class ReaderActivity : AppCompatActivity() {
|
||||
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
|
||||
if (title == getString(R.string.reader_loading)) {
|
||||
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
|
||||
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
|
||||
|
||||
if (reader != null) {
|
||||
title = reader.title
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
|
||||
|
||||
with (reader_recyclerview.adapter as ReaderAdapter) {
|
||||
this.reader = reader
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
title = reader.galleryInfo.title
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
|
||||
|
||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
||||
when (reader.code) {
|
||||
@@ -296,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)
|
||||
|
||||
@@ -30,9 +30,8 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.parseList
|
||||
import kotlinx.serialization.list
|
||||
import kotlinx.serialization.serializer
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
@@ -79,7 +78,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when(requestCode) {
|
||||
REQUEST_LOCK -> {
|
||||
@@ -96,13 +94,13 @@ class SettingsActivity : AppCompatActivity() {
|
||||
val uri = data?.data ?: return
|
||||
|
||||
try {
|
||||
val json = contentResolver.openInputStream(uri).use { inputStream ->
|
||||
val str = contentResolver.openInputStream(uri).use { inputStream ->
|
||||
inputStream!!
|
||||
|
||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||
}
|
||||
|
||||
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).also {
|
||||
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
||||
Snackbar.make(
|
||||
window.decorView,
|
||||
getString(R.string.settings_restore_successful, it.size),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -36,10 +37,7 @@ import kotlinx.android.synthetic.main.item_dl_location.view.*
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
||||
import xyz.quaver.pupil.util.REQUEST_WRITE_PERMISSION_AND_SAF
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -49,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,25 +123,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||
buttons.add(button to null)
|
||||
})
|
||||
|
||||
val pref = preference.getString("dl_location", null)
|
||||
val index = externalFilesDirs.indexOfFirst {
|
||||
it.canonicalPath == pref
|
||||
externalFilesDirs.indexOfFirst {
|
||||
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
||||
}.let { index ->
|
||||
if (index < 0)
|
||||
buttons.first().first.isChecked = true
|
||||
else
|
||||
buttons[index].first.isChecked = true
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
buttons.last().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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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@{
|
||||
|
||||
133
app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
Normal file
133
app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -18,8 +18,13 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
|
||||
const val REQUEST_LOCK = 38238
|
||||
const val REQUEST_RESTORE = 16546
|
||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
@@ -21,21 +21,18 @@ package xyz.quaver.pupil.util.download
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.parse
|
||||
import kotlinx.serialization.stringify
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||
import xyz.quaver.pupil.util.json
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
@@ -50,7 +47,6 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||
val file = File(getCachedGallery(galleryID), ".metadata")
|
||||
|
||||
@@ -58,7 +54,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
return null
|
||||
|
||||
return try {
|
||||
Json.parse(file.readText())
|
||||
json.parse(Metadata.serializer(), file.readText())
|
||||
} catch (e: Exception) {
|
||||
//File corrupted
|
||||
file.delete()
|
||||
@@ -66,14 +62,13 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||
if (!it.exists())
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
file.writeText(Json.stringify(metadata))
|
||||
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
||||
}
|
||||
|
||||
suspend fun getThumbnail(galleryID: Int): String? {
|
||||
@@ -102,21 +97,29 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||
|
||||
val source = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
val sources = listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
val galleryBlock = if (metadata?.galleryBlock == null)
|
||||
source.entries.map {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
kotlin.runCatching {
|
||||
it.value.invoke()
|
||||
}.getOrNull()
|
||||
}
|
||||
}.firstOrNull {
|
||||
it.await() != null
|
||||
}?.await()
|
||||
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
galleryBlock = try {
|
||||
source.invoke()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (galleryBlock != null)
|
||||
break
|
||||
}
|
||||
|
||||
galleryBlock
|
||||
}.await() ?: return null
|
||||
}
|
||||
else
|
||||
metadata.galleryBlock
|
||||
|
||||
@@ -155,40 +158,60 @@ 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
|
||||
}
|
||||
|
||||
retval
|
||||
}.await()
|
||||
}.await() ?: return null
|
||||
} else
|
||||
metadata.reader
|
||||
|
||||
if (reader != null)
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||
)
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||
)
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
val imageNameRegex = Regex("""^\d+\..+$""")
|
||||
fun getImages(galleryID: Int): List<File?>? {
|
||||
val started = System.currentTimeMillis()
|
||||
val gallery = getCachedGallery(galleryID)
|
||||
val reader = getReaderOrNull(galleryID) ?: return null
|
||||
val images = gallery.listFiles() ?: return null
|
||||
|
||||
Log.i("PUPILD", "${System.currentTimeMillis() - started} ms")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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, name: String, data: ByteArray) {
|
||||
val cache = File(getCachedGallery(galleryID), name).also {
|
||||
if (!it.exists())
|
||||
|
||||
@@ -40,6 +40,7 @@ import xyz.quaver.hitomi.urlFromUrlFromHash
|
||||
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.IOException
|
||||
@@ -145,25 +146,22 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
|
||||
private val loop = loop()
|
||||
private val worker = SparseArray<Job?>()
|
||||
@Volatile var nRunners = 0
|
||||
val clients = SparseArray<OkHttpClient>()
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
val interceptor = Interceptor { chain ->
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
var retry = preferences.getInt("retry", 3)
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
}
|
||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||
response.newBuilder()
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
}
|
||||
fun buildClient() =
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
|
||||
fun stop() {
|
||||
queue.clear()
|
||||
@@ -176,29 +174,23 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
worker[galleryID]?.cancel()
|
||||
}
|
||||
|
||||
client.dispatcher().cancelAll()
|
||||
for (i in 0 until clients.size()) {
|
||||
clients.valueAt(i).dispatcher().cancelAll()
|
||||
}
|
||||
clients.clear()
|
||||
|
||||
progress.clear()
|
||||
exception.clear()
|
||||
notification.clear()
|
||||
notificationManager.cancelAll()
|
||||
|
||||
nRunners = 0
|
||||
|
||||
}
|
||||
|
||||
fun cancel(galleryID: Int) {
|
||||
queue.remove(galleryID)
|
||||
worker[galleryID]?.cancel()
|
||||
|
||||
client.dispatcher().queuedCalls()
|
||||
.filter {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
||||
}
|
||||
.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
clients[galleryID]?.dispatcher()?.cancelAll()
|
||||
clients.remove(galleryID)
|
||||
|
||||
progress.remove(galleryID)
|
||||
exception.remove(galleryID)
|
||||
@@ -207,7 +199,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
|
||||
if (progress.indexOfKey(galleryID) >= 0) {
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
nRunners--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +213,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
url(
|
||||
urlFromUrlFromHash(
|
||||
galleryID,
|
||||
reader.galleryInfo[index],
|
||||
reader.galleryInfo.files[index],
|
||||
if (lowQuality) "webp" else null
|
||||
)
|
||||
)
|
||||
@@ -240,7 +231,10 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
tag(galleryID to index)
|
||||
}.build()
|
||||
|
||||
client.newCall(request).enqueue(callback)
|
||||
if (clients.get(galleryID) == null)
|
||||
clients.put(galleryID, buildClient())
|
||||
|
||||
clients[galleryID]?.newCall(request)?.enqueue(callback)
|
||||
}
|
||||
|
||||
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||
@@ -252,24 +246,23 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
exception.put(galleryID, null)
|
||||
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
nRunners--
|
||||
return@launch
|
||||
}
|
||||
|
||||
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?.getOrNull(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)) {
|
||||
@@ -279,15 +272,14 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
nRunners--
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
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())
|
||||
if (Fabric.isInitialized() && e.message != "Canceled")
|
||||
Crashlytics.logException(e)
|
||||
|
||||
progress[galleryID]?.set(i, Float.NaN)
|
||||
@@ -302,7 +294,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
nRunners--
|
||||
clients.remove(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +317,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
nRunners--
|
||||
clients.remove(galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,7 +334,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
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
|
||||
notification[galleryID]
|
||||
?.setProgress(max, progress, false)
|
||||
@@ -369,24 +363,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
||||
setContentIntent(pendingIntent)
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
})
|
||||
}
|
||||
|
||||
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (true) {
|
||||
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
|
||||
if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4))
|
||||
continue
|
||||
|
||||
val galleryID = queue.poll() ?: continue
|
||||
|
||||
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||
continue
|
||||
|
||||
initNotification(galleryID)
|
||||
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||
worker.put(galleryID, download(galleryID))
|
||||
nRunners++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,14 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.parseList
|
||||
import kotlinx.serialization.stringify
|
||||
import kotlinx.serialization.list
|
||||
import kotlinx.serialization.serializer
|
||||
import java.io.File
|
||||
|
||||
class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
val serializer = Int.serializer().list
|
||||
|
||||
init {
|
||||
if (!file.exists())
|
||||
file.parentFile?.mkdirs()
|
||||
@@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun load() : Histories {
|
||||
return apply {
|
||||
super.clear()
|
||||
addAll(
|
||||
Json(JsonConfiguration.Stable).parseList(
|
||||
json.parse(
|
||||
serializer,
|
||||
file.bufferedReader().use { it.readText() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun save() {
|
||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
||||
file.writeText(json.stringify(serializer, this))
|
||||
}
|
||||
|
||||
override fun add(element: Int): Boolean {
|
||||
|
||||
@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
load()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun load() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
lock.writeText("[]")
|
||||
}
|
||||
|
||||
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText()))
|
||||
locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun save() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
if (!lock.exists())
|
||||
lock.createNewFile()
|
||||
|
||||
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf()))
|
||||
lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
|
||||
}
|
||||
|
||||
fun add(lock: Lock) {
|
||||
|
||||
63
app/src/main/java/xyz/quaver/pupil/util/proxy.kt
Normal file
63
app/src/main/java/xyz/quaver/pupil/util/proxy.kt
Normal 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)
|
||||
}
|
||||
@@ -32,7 +32,10 @@ import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.content
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
@@ -43,7 +46,7 @@ import java.util.*
|
||||
fun getReleases(url: String) : JsonArray {
|
||||
return try {
|
||||
URL(url).readText().let {
|
||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
||||
json.parse(JsonArray.serializer(), it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
JsonArray(emptyList())
|
||||
@@ -145,6 +148,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
setContentTitle(context.getString(R.string.update_notification_description))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
setOngoing(true)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch io@{
|
||||
@@ -160,6 +164,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
setContentText(context.getString(R.string.update_failed))
|
||||
setMessage(context.getString(R.string.update_failed_message))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOngoing(false)
|
||||
}
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||
@@ -179,6 +184,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setContentTitle(context.getString(R.string.update_download_completed))
|
||||
setContentText(context.getString(R.string.update_download_completed_description))
|
||||
setOngoing(false)
|
||||
}
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||
|
||||
123
app/src/main/res/layout/dialog_proxy.xml
Normal file
123
app/src/main/res/layout/dialog_proxy.xml
Normal 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>
|
||||
@@ -62,6 +62,8 @@
|
||||
<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"
|
||||
android:paddingBottom="8dp"/>
|
||||
|
||||
26
app/src/main/res/values-ja/arrays.xml
Normal file
26
app/src/main/res/values-ja/arrays.xml
Normal 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>
|
||||
@@ -122,4 +122,12 @@
|
||||
<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>
|
||||
</resources>
|
||||
26
app/src/main/res/values-ko/arrays.xml
Normal file
26
app/src/main/res/values-ko/arrays.xml
Normal 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>
|
||||
@@ -122,4 +122,12 @@
|
||||
<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>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -10,4 +10,7 @@
|
||||
<dimen name="nav_header_height">176dp</dimen>
|
||||
|
||||
<dimen name="thumbnail_margin">8dp</dimen>
|
||||
|
||||
<dimen name="galleryblock_thumbnail_thin">50dp</dimen>
|
||||
<dimen name="galleryblock_thumbnail_normal">150dp</dimen>
|
||||
</resources>
|
||||
@@ -154,6 +154,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 +190,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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -31,4 +31,4 @@ allprojects {
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
}
|
||||
@@ -15,3 +15,4 @@ kotlin.code.style=official
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
||||
android.useAndroidX=true
|
||||
android.enableR8.fullMode=true
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package xyz.quaver.hitomi
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.proxy
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.nio.ByteBuffer
|
||||
@@ -34,35 +35,31 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
|
||||
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
try {
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
requestMethod = "GET"
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
requestMethod = "GET"
|
||||
|
||||
if (start != -1 && count != -1) {
|
||||
val startByte = start*4
|
||||
val endByte = (start+count)*4-1
|
||||
if (start != -1 && count != -1) {
|
||||
val startByte = start*4
|
||||
val endByte = (start+count)*4-1
|
||||
|
||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
||||
}
|
||||
|
||||
connect()
|
||||
|
||||
val totalItems = getHeaderField("Content-Range")
|
||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(inputStream.readBytes())
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return Pair(nozomi, totalItems)
|
||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Pair(emptyList(), 0)
|
||||
|
||||
connect()
|
||||
|
||||
val totalItems = getHeaderField("Content-Range")
|
||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(inputStream.readBytes())
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return Pair(nozomi, totalItems)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,30 +79,26 @@ data class GalleryBlock(
|
||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||
|
||||
try {
|
||||
val doc = Jsoup.connect(url).get()
|
||||
val doc = Jsoup.connect(url).proxy(proxy).get()
|
||||
|
||||
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
||||
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
||||
|
||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||
|
||||
val title = doc.selectFirst("h1.lillie > a").text()
|
||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||
val title = doc.selectFirst("h1.lillie > a").text()
|
||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||
|
||||
val language = {
|
||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||
href.slice(7 until href.indexOf("-1"))
|
||||
}.invoke()
|
||||
val language = {
|
||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||
href.slice(7 until href.indexOf("-1"))
|
||||
}.invoke()
|
||||
|
||||
val relatedTags = doc.select(".relatedtags a").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf("-all"))
|
||||
}
|
||||
|
||||
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
val relatedTags = doc.select(".relatedtags a").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf("-all"))
|
||||
}
|
||||
|
||||
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,22 +175,20 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
|
||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
try {
|
||||
val bytes = URL(nozomiAddress).readBytes()
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(bytes)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return nozomi
|
||||
} catch (e: Exception) {
|
||||
return emptyList()
|
||||
val bytes = URL(nozomiAddress).openConnection(proxy).getInputStream().use {
|
||||
it.readBytes()
|
||||
}
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(bytes)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return nozomi
|
||||
}
|
||||
|
||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
||||
@@ -242,7 +242,7 @@ fun getNodeAtAddress(field: String, address: Long) : Node? {
|
||||
|
||||
fun getURLAtRange(url: String, range: LongRange) : ByteArray? {
|
||||
try {
|
||||
with (URL(url).openConnection() as HttpsURLConnection) {
|
||||
with (URL(url).openConnection(proxy) as HttpsURLConnection) {
|
||||
requestMethod = "GET"
|
||||
|
||||
setRequestProperty("Range", "bytes=${range.first}-${range.last}")
|
||||
|
||||
@@ -20,30 +20,27 @@ import org.jsoup.Jsoup
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.protocol
|
||||
import xyz.quaver.proxy
|
||||
|
||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||
val url = "$protocol//$hiyobi/search/$galleryID"
|
||||
val url = "$protocol//$hiyobi/info/$galleryID"
|
||||
|
||||
try {
|
||||
val doc = Jsoup.connect(url).get()
|
||||
val doc = Jsoup.connect(url).proxy(proxy).get()
|
||||
|
||||
val galleryBlock = doc.selectFirst(".gallery-content")
|
||||
val galleryBlock = doc.selectFirst(".gallery-content")
|
||||
|
||||
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
||||
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
||||
|
||||
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
||||
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
||||
|
||||
val title = galleryBlock.selectFirst("b").text()
|
||||
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.text() }
|
||||
val series = galleryBlock.select("tr:matches(원작) a").map { it.attr("href").substringAfter("series:").replace('_', ' ') }
|
||||
val type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
||||
val title = galleryBlock.selectFirst("b").text()
|
||||
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.text() }
|
||||
val series = galleryBlock.select("tr:matches(원작) a").map { it.attr("href").substringAfter("series:").replace('_', ' ') }
|
||||
val type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
||||
|
||||
val language = "korean"
|
||||
val language = "korean"
|
||||
|
||||
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
||||
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
||||
|
||||
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user