Compare commits

...

43 Commits

Author SHA1 Message Date
Pupil
3159c343c1 Merge pull request #34 from tom5079/development
Version 4.2-beta1
2019-12-13 20:10:55 +09:00
tom5079
ceaa930623 bug fix 2019-12-13 20:03:11 +09:00
tom5079
6a8539106b bug fix 2019-12-13 20:01:45 +09:00
tom5079
7a24c3c08e bug fix 2019-12-13 19:50:14 +09:00
tom5079
251abeb090 Merge remote-tracking branch 'origin/development' into development 2019-12-13 19:43:42 +09:00
Pupil
a61fe9f98c Merge pull request #33 from tom5079/Pupil-29
Pupil-29
2019-12-13 19:42:44 +09:00
tom5079
d29c7bf91a Apply update on startup 2019-12-13 19:30:19 +09:00
tom5079
ed4911c441 Updated serialization library 2019-12-13 18:39:12 +09:00
tom5079
d40b4f3748 Added update logic for outdated readers 2019-12-12 20:14:55 +09:00
tom5079
f3c4fe1914 Merge remote-tracking branch 'origin/development' into development 2019-12-11 20:23:45 +09:00
tom5079
55ee841bd0 resolves #31 2019-12-11 20:23:18 +09:00
tom5079
657fb488ee fixed #31 on libpupil
#TODO: fix app side to completely resolve the issue
2019-12-11 20:23:18 +09:00
tom5079
4eef0b93fb bug fix 2019-12-11 20:23:17 +09:00
Pupil
f2be56435c Merge pull request #32 from tom5079/Pupil-31 2019-12-11 20:08:45 +09:00
tom5079
fa6b3ad7ba resolves #31 2019-12-11 20:03:55 +09:00
tom5079
52c05e6888 fixed #31 on libpupil
#TODO: fix app side to completely resolve the issue
2019-12-11 19:52:25 +09:00
tom5079
865bf0ba83 Added code for differentiating readers 2019-12-09 10:33:26 +09:00
tom5079
3f827d1bad bug fix 2019-12-09 10:15:53 +09:00
tom5079
0561d5f55c Added code for saved reader 2019-12-09 09:36:36 +09:00
Pupil
1bf2e1dacc Merge pull request #30 from tom5079/Pupil-24
fixed #24
2019-12-08 18:45:21 +09:00
tom5079
db5a221b56 kotlin plugin update 2019-12-08 18:10:35 +09:00
tom5079
295285f132 Turned off development only option 2019-12-02 19:04:09 +09:00
tom5079
5052b6c074 Removed folder opening feature due to its unstability 2019-12-02 18:55:38 +09:00
tom5079
f98f45dc54 Pupil-24 Absence of backing up favorites feature 2019-12-01 16:58:29 +09:00
tom5079
8d16950f46 Issue #27 fix 2019-11-30 16:10:47 +09:00
tom5079
74033b9f4a Issue #27 fix 2019-11-30 16:10:09 +09:00
tom5079
e497d47374 Fix for bug caused by changed hiyobi domain 2019-11-30 15:10:25 +09:00
tom5079
a97af59260 Potential fix for memory issues 2019-11-30 15:09:53 +09:00
tom5079
2197de98ea Potential fix for too large bitmap crash 2019-11-25 19:39:17 +09:00
tom5079
c004c7f71a Fixed bug fetching old galleries from hiyobi 2019-11-15 19:47:09 +09:00
tom5079
69fc3ad4e8 typo 2019-11-02 21:50:03 +09:00
tom5079
678a8f0914 Merge pull request #21 from tom5079/development
Fixed bug with missing hash
2019-11-02 21:46:11 +09:00
tom5079
08c4c0bf1f Fixed bug with missing hash
Version 4.1
2019-11-02 21:45:27 +09:00
tom5079
f2a2656837 Merge pull request #20 from tom5079/development
Development
2019-11-02 20:30:04 +09:00
tom5079
2011572270 Merge remote-tracking branch 'origin/development' into development 2019-11-02 20:29:24 +09:00
tom5079
3b682667e1 Fixed bug caused by updated hitomi server structure
Version 4.0
2019-11-02 20:25:03 +09:00
tom5079
6da8de6463 Merge pull request #19 from tom5079/master
merge readme
2019-11-02 20:08:28 +09:00
tom5079
039d415871 Update README.md
r/engrish
2019-08-31 23:59:41 +09:00
tom5079
776f53bde0 Update README.md 2019-08-30 22:28:07 +09:00
tom5079
58e535595e Update README.md 2019-08-30 22:27:32 +09:00
tom5079
96ad5f6a6c Update README.md 2019-08-30 22:27:08 +09:00
tom5079
043f7bedd8 Added quick download/delete 2019-08-30 15:24:51 +09:00
tom5079
8a58564812 Version 3.2 2019-08-29 12:10:51 +09:00
37 changed files with 820 additions and 368 deletions

View File

@@ -1,5 +1,8 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
@@ -14,6 +17,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:android</NAME> <NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -24,6 +28,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:.*</NAME> <NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -35,6 +40,7 @@
<match> <match>
<AND> <AND>
<NAME>.*:id</NAME> <NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -45,6 +51,7 @@
<match> <match>
<AND> <AND>
<NAME>.*:name</NAME> <NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -55,6 +62,7 @@
<match> <match>
<AND> <AND>
<NAME>name</NAME> <NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -65,6 +73,7 @@
<match> <match>
<AND> <AND>
<NAME>style</NAME> <NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -75,6 +84,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -86,6 +96,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -97,6 +108,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE> <XML_NAMESPACE>.*</XML_NAMESPACE>
</AND> </AND>
</match> </match>

View File

@@ -1,2 +1,27 @@
# Pupil # Pupil
Hitomi.la viewer for Android
![Banner](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/pupil-banner.png?raw=true)
*Pupil, Hitomi.la viewer for Android*
# Screenshot
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true)
*Main Screen*
![Reader Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/reader-screenshot.png?raw=true)
*Reader Screen*
Images are censored to be SFW
# Installation
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
or Build app yourself
# Manual
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
# Contribution
Any kind of contribution is appriciated. Feel free to leave PR!

View File

@@ -13,8 +13,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 27 versionCode 31
versionName "3.2-beta2" versionName "4.2-beta1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -25,7 +25,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
buildTypes.each { buildTypes.each {
it.buildConfigField('boolean', 'PRERELEASE', 'true') it.buildConfigField('boolean', 'PRERELEASE', 'false')
it.buildConfigField('boolean', 'CENSOR', 'false') it.buildConfigField('boolean', 'CENSOR', 'false')
} }
} }
@@ -46,19 +46,21 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.0"
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation 'com.google.android.material:material:1.1.0-alpha09' implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.firebase:firebase-core:17.1.0' implementation 'com.google.android.material:material:1.2.0-alpha02'
implementation 'com.google.firebase:firebase-perf:19.0.0' implementation 'com.google.firebase:firebase-core:17.2.1'
implementation 'com.google.firebase:firebase-perf:19.0.3'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
@@ -67,7 +69,6 @@ dependencies {
transitive = false transitive = false
} }
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation 'com.jsibbold:zoomage:1.3.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.9.0' kapt 'com.github.bumptech.glide:compiler:4.9.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":25,"versionName":"3.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":31,"versionName":"4.2-beta1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -25,15 +25,21 @@ import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.JsonObject
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hitomi.fetchNozomi
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -49,9 +55,8 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
assertEquals("xyz.quaver.pupil", appContext.packageName) assertEquals("xyz.quaver.pupil", appContext.packageName)
Log.d("Pupil", fetchNozomi().first.size.toString())
} }
@Test @Test
@@ -60,17 +65,15 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
activityTestRule.launchActivity(Intent()) activityTestRule.launchActivity(Intent())
while(true);
} }
@Test @Test
fun test_doSearch() { fun test_doSearch() {
val reader = getReader(1426382) val reader = getReader( 1426382)
val data: ByteArray val data: ByteArray
with(URL(reader.readerItems[0].url).openConnection() as HttpsURLConnection) { with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
@@ -79,4 +82,38 @@ class ExampleInstrumentedTest {
Log.d("Pupil", data.size.toString()) Log.d("Pupil", data.size.toString())
} }
}
@UseExperimental(ImplicitReflectionSerializer::class)
@Test
fun test_deleteCodeFromReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val json = Json(JsonConfiguration.Stable)
listOf(
getDownloadDirectory(context),
File(context.cacheDir, "imageCache")
).forEach { root ->
root.listFiles()?.forEach gallery@{ gallery ->
val reader = json.parseJson(File(gallery, "reader.json").apply {
if (!exists())
return@gallery
}.readText())
.jsonObject.toMutableMap()
Log.d("PUPILD", gallery.name)
reader.remove("code")
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
}
}
}
@Test
fun test_updateOldReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
updateOldReaderGalleries(context)
}
}

View File

@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="xyz.quaver.pupil"> package="xyz.quaver.pupil">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/> android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -15,7 +17,20 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
tools:replace="android:theme">
<provider
android:authorities="${applicationId}.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<activity android:name=".ui.LockActivity"/> <activity android:name=".ui.LockActivity"/>
<activity <activity

View File

@@ -30,7 +30,11 @@ import androidx.preference.PreferenceManager
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File import java.io.File
class Pupil : MultiDexApplication() { class Pupil : MultiDexApplication() {
@@ -78,6 +82,10 @@ class Pupil : MultiDexApplication() {
false -> AppCompatDelegate.MODE_NIGHT_NO false -> AppCompatDelegate.MODE_NIGHT_NO
}) })
CoroutineScope(Dispatchers.IO).launch {
updateOldReaderGalleries(this@Pupil)
}
super.onCreate() super.onCreate()
} }

View File

@@ -31,6 +31,9 @@ import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.item_galleryblock.view.* import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -45,6 +48,7 @@ import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
@@ -54,7 +58,7 @@ import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType { enum class ViewType {
NEXT, NEXT,
@@ -64,7 +68,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
private lateinit var favorites: Histories private lateinit var favorites: Histories
inner class GalleryViewHolder(val view: CardView) : RecyclerView.ViewHolder(view) { inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Pair<GalleryBlock, Deferred<String>>) { fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
with(view) { with(view) {
val resources = context.resources val resources = context.resources
@@ -91,6 +95,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
override(5, 8) override(5, 8)
} }
.fitCenter()
.into(galleryblock_thumbnail) .into(galleryblock_thumbnail)
} }
@@ -110,7 +115,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
.parse(Reader.serializer(), readerCache.invoke().readText()) .parse(Reader.serializer(), readerCache.invoke().readText())
with(galleryblock_progressbar) { with(galleryblock_progressbar) {
max = reader.readerItems.size max = reader.galleryInfo.size
progress = imageCache.invoke().list()?.size ?: 0 progress = imageCache.invoke().list()?.size ?: 0
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -135,7 +140,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
if (visibility == View.GONE) { if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable) val reader = Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText()) .parse(Reader.serializer(), readerCache.invoke().readText())
max = reader.readerItems.size max = reader.galleryInfo.size
visibility = View.VISIBLE visibility = View.VISIBLE
} }
@@ -276,6 +281,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
val completeFlag = SparseBooleanArray() val completeFlag = SparseBooleanArray()
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
var onDownloadClickedHandler: ((Int) -> Unit)? = null
var onDeleteClickedHandler: ((Int) -> Unit)? = null
var showNext = false var showNext = false
var showPrev = false var showPrev = false
@@ -301,8 +308,47 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GalleryViewHolder) if (holder is GalleryViewHolder) {
holder.bind(galleries[position-(if (showPrev) 1 else 0)]) val gallery = galleries[position-(if (showPrev) 1 else 0)]
holder.bind(gallery)
with(holder.view.galleryblock_primary) {
setOnClickListener {
holder.view.performClick()
}
setOnLongClickListener {
holder.view.performLongClick()
}
}
holder.view.galleryblock_download.setOnClickListener {
onDownloadClickedHandler?.invoke(position)
}
holder.view.galleryblock_delete.setOnClickListener {
onDeleteClickedHandler?.invoke(position)
}
mItemManger.bindView(holder.view, position)
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = when(GalleryDownloader.get(gallery.first.id)) {
null -> holder.view.context.getString(R.string.main_download)
else -> holder.view.context.getString(android.R.string.cancel)
}
}
override fun onClose(layout: SwipeLayout?) {}
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
override fun onOpen(layout: SwipeLayout?) {}
override fun onStartClose(layout: SwipeLayout?) {}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
})
}
} }
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
@@ -328,4 +374,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
else -> ViewType.GALLERY else -> ViewType.GALLERY
}.ordinal }.ordinal
} }
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
} }

View File

@@ -18,18 +18,13 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getCachedGallery
@@ -40,7 +35,6 @@ class ReaderAdapter(private val glide: RequestManager,
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var isFullScreen = false var isFullScreen = false
private var prev : Drawable? = null
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
@@ -64,29 +58,8 @@ class ReaderAdapter(private val glide: RequestManager,
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
override(5, 8) override(5, 8)
if (isFullScreen)
placeholder(prev)
} }
.listener(object: RequestListener<Drawable> { .fitCenter()
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
) = false
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
prev = resource?.constantState?.newDrawable()?.mutate()
return false
}
})
.into(holder.view) .into(holder.view)
} }

View File

@@ -39,6 +39,7 @@ class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
override(5, 8) override(5, 8)
} }
.fitCenter()
.into(holder.view) .into(holder.view)
} }

View File

@@ -22,19 +22,14 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.gridlayout.widget.GridLayout
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_galleryblock.* import kotlinx.android.synthetic.main.dialog_galleryblock.*
@@ -45,6 +40,7 @@ import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGallery import xyz.quaver.hitomi.getGallery
import xyz.quaver.hitomi.getGalleryBlock import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
@@ -113,8 +109,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
} }
Glide.with(context) Glide.with(context)
.load(gallery.thumbnails.firstOrNull()) .load(gallery.cover)
.into(gallery_thumbnail) .apply {
if (BuildConfig.CENSOR)
override(5, 8)
}.into(gallery_cover)
addDetails(gallery) addDetails(gallery)
addThumbnails(gallery) addThumbnails(gallery)

View File

@@ -371,7 +371,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun checkPermissions() { private fun checkPermissions() {
if (this.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) if (!hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489) ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489)
} }
@@ -448,7 +448,7 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
} }
R.id.main_drawer_kakaotalk -> { R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.kakaotalk)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
} }
} }
} }
@@ -535,6 +535,63 @@ class MainActivity : AppCompatActivity() {
loadBlocks() loadBlocks()
} }
} }
onDownloadClickedHandler = { position ->
val galleryID = galleries[position].first.id
if (!completeFlag.get(galleryID, false)) {
val downloader = GalleryDownloader.get(galleryID)
if (downloader == null)
GalleryDownloader(context, galleryID, true).start()
else {
downloader.cancel()
downloader.clearNotification()
}
}
closeAllItems()
}
onDeleteClickedHandler = { position ->
val galleryID = galleries[position].first.id
CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryID]) {
this?.cancelAndJoin()
this?.clearNotification()
}
val cache = File(cacheDir, "imageCache/${galleryID}")
val data = getCachedGallery(context, galleryID)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryID)
if (this@MainActivity.mode == Mode.DOWNLOAD) {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
histories.remove(galleryID)
if (this@MainActivity.mode == Mode.HISTORY) {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
completeFlag.put(galleryID, false)
}
closeAllItems()
}
} }
ItemClickSupport.addTo(this) ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, v -> .setOnItemClickListener { _, position, v ->

View File

@@ -29,8 +29,6 @@ import com.andrognito.patternlockview.utils.PatternLockUtils
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.* import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.hash
import xyz.quaver.pupil.util.hashWithSalt
class PatternLockFragment : Fragment(), PatternLockViewListener { class PatternLockFragment : Fragment(), PatternLockViewListener {

View File

@@ -249,16 +249,16 @@ class ReaderActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
title = it.title title = it.title
with(reader_download_progressbar) { with(reader_download_progressbar) {
max = it.readerItems.size max = it.galleryInfo.size
progress = 0 progress = 0
} }
with(reader_progressbar) { with(reader_progressbar) {
max = it.readerItems.size max = it.galleryInfo.size
progress = 0 progress = 0
} }
gallerySize = it.readerItems.size gallerySize = it.galleryInfo.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.readerItems.size}" menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.galleryInfo.size}"
} }
} }
onProgressHandler = { onProgressHandler = {
@@ -282,7 +282,7 @@ class ReaderActivity : AppCompatActivity() {
onErrorHandler = { onErrorHandler = {
Snackbar Snackbar
.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE) .make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.reader_help) { _ -> .setAction(R.string.reader_help) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
} }
.show() .show()

View File

@@ -32,10 +32,15 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_default_query.view.* import kotlinx.android.synthetic.main.dialog_default_query.view.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
@@ -43,10 +48,13 @@ import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import java.io.File import java.io.File
import java.nio.charset.Charset
import java.util.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
val REQUEST_LOCK = 38238 val REQUEST_LOCK = 38238
val REQUEST_RESTORE = 16546
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -154,7 +162,7 @@ class SettingsActivity : AppCompatActivity() {
with(findPreference<Preference>("delete_downloads")) { with(findPreference<Preference>("delete_downloads")) {
this!! this!!
val dir = getDownloadDirectory(context)!! val dir = getDownloadDirectory(context)
summary = getDirSize(dir) summary = getDirSize(dir)
@@ -278,7 +286,7 @@ class SettingsActivity : AppCompatActivity() {
s ?: return s ?: return
if (s.any { it.isUpperCase() }) if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase()) s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
} }
}) })
} }
@@ -352,6 +360,37 @@ class SettingsActivity : AppCompatActivity() {
true true
} }
} }
with(findPreference<Preference>("backup")) {
this!!
onPreferenceClickListener = Preference.OnPreferenceClickListener {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"),
true
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
.show()
true
}
}
with(findPreference<Preference>("restore")) {
this!!
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_RESTORE)
true
}
}
} }
} }
@@ -415,6 +454,7 @@ class SettingsActivity : AppCompatActivity() {
return true return true
} }
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) { when(requestCode) {
REQUEST_LOCK -> { REQUEST_LOCK -> {
@@ -426,6 +466,33 @@ class SettingsActivity : AppCompatActivity() {
.commitAllowingStateLoss() .commitAllowingStateLoss()
} }
} }
REQUEST_RESTORE -> {
if (resultCode == Activity.RESULT_OK) {
val uri = data?.data ?: return
try {
val json = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!!
inputStream.readBytes().toString(Charset.defaultCharset())
}
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).also {
Snackbar.make(
window.decorView,
getString(R.string.settings_restore_successful, it.size),
Snackbar.LENGTH_LONG
).show()
})
} catch (e: Exception) {
Snackbar.make(
window.decorView,
R.string.settings_restore_failed,
Snackbar.LENGTH_LONG
).show()
}
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }

View File

@@ -29,13 +29,14 @@ import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.io.IOException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReader import xyz.quaver.hitomi.getReader
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -83,7 +84,7 @@ class GalleryDownloader(
onNotifyChangedHandler?.invoke(value) onNotifyChangedHandler?.invoke(value)
} }
private val reader: Deferred<Reader>? private val reader: Deferred<Reader?>?
private var downloadJob: Job? = null private var downloadJob: Job? = null
private lateinit var notificationBuilder: NotificationCompat.Builder private lateinit var notificationBuilder: NotificationCompat.Builder
@@ -121,11 +122,8 @@ class GalleryDownloader(
if (cache.exists()) { if (cache.exists()) {
val cached = json.parse(serializer, cache.readText()) val cached = json.parse(serializer, cache.readText())
if (cached.readerItems.isNotEmpty()) { if (cached.galleryInfo.isNotEmpty()) {
useHiyobi = when { useHiyobi = cached.code == Reader.Code.HIYOBI
cached.readerItems[0].url.contains("hitomi.la") -> false
else -> true
}
onReaderLoadedHandler?.invoke(cached) onReaderLoadedHandler?.invoke(cached)
@@ -148,7 +146,7 @@ class GalleryDownloader(
} }
} }
if (reader.readerItems.isNotEmpty()) { if (reader.galleryInfo.isNotEmpty()) {
//Save cache //Save cache
if (cache.parentFile?.exists() == false) if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs() cache.parentFile!!.mkdirs()
@@ -159,7 +157,8 @@ class GalleryDownloader(
reader reader
} catch (e: Exception) { } catch (e: Exception) {
Crashlytics.logException(e) Crashlytics.logException(e)
Reader("", listOf()) onErrorHandler?.invoke(e)
null
} }
} }
} }
@@ -168,29 +167,32 @@ class GalleryDownloader(
fun start() { fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch { downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await() val reader = reader!!.await() ?: return@launch
notificationBuilder.setContentTitle(reader.title) notificationBuilder.setContentTitle(reader.title)
if (reader.readerItems.isEmpty()) {
onErrorHandler?.invoke(IOException(getString(R.string.unable_to_connect)))
return@launch
}
val list = ArrayList<String>() val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader) onReaderLoadedHandler?.invoke(reader)
notificationBuilder notificationBuilder
.setProgress(reader.readerItems.size, 0, false) .setProgress(reader.galleryInfo.size, 0, false)
.setContentText("0/${reader.readerItems.size}") .setContentText("0/${reader.galleryInfo.size}")
reader.readerItems.chunked(4).forEachIndexed { chunkIndex, chunked -> reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunked ->
chunked.mapIndexed { i, it -> chunked.mapIndexed { i, galleryInfo ->
val index = chunkIndex*4+i val index = chunkIndex*4+i
async(Dispatchers.IO) { async(Dispatchers.IO) {
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url val url = when(useHiyobi) {
true -> createImgList(galleryID, reader)[index].path
false -> when {
(!galleryInfo.hash.isNullOrBlank()) and (galleryInfo.haswebp == 1) ->
urlFromUrlFromHash(galleryID, galleryInfo, "webp")
else ->
urlFromUrlFromHash(galleryID, galleryInfo)
}
}
val name = "$index".padStart(4, '0') val name = "$index".padStart(4, '0')
val ext = url.split('.').last() val ext = url.split('.').last()
@@ -234,8 +236,8 @@ class GalleryDownloader(
onProgressHandler?.invoke(index) onProgressHandler?.invoke(index)
notificationBuilder notificationBuilder
.setProgress(reader.readerItems.size, index, false) .setProgress(reader.galleryInfo.size, index, false)
.setContentText("$index/${reader.readerItems.size}") .setContentText("$index/${reader.galleryInfo.size}")
if (download) if (download)
notificationManager.notify(galleryID, notificationBuilder.build()) notificationManager.notify(galleryID, notificationBuilder.build())

View File

@@ -21,8 +21,6 @@ package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import androidx.core.content.ContextCompat
import java.io.File import java.io.File
fun getCachedGallery(context: Context, galleryID: Int): File { fun getCachedGallery(context: Context, galleryID: Int): File {
@@ -35,9 +33,9 @@ fun getCachedGallery(context: Context, galleryID: Int): File {
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun getDownloadDirectory(context: Context): File? { fun getDownloadDirectory(context: Context): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getExternalFilesDir("Pupil") context.getExternalFilesDir("Pupil")!!
else else
File(Environment.getExternalStorageDirectory(), "Pupil") File(Environment.getExternalStorageDirectory(), "Pupil")
} }

View File

@@ -18,8 +18,14 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.content.Context
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.internal.EnumSerializer
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import xyz.quaver.availableInHiyobi
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import java.io.File
import java.net.URL import java.net.URL
fun getReleases(url: String) : JsonArray { fun getReleases(url: String) : JsonArray {
@@ -54,11 +60,60 @@ fun checkUpdate(url: String) : JsonObject? {
fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? { fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
return releases["assets"]?.jsonArray?.firstOrNull { return releases["assets"]?.jsonArray?.firstOrNull {
Regex("Pupil-v(\\d+\\.)+\\d+\\.apk").matches(it.jsonObject["name"]?.content ?: "") Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
}.let { }.let {
if (it == null) if (it == null)
null null
else else
Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content) Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
} }
}
fun getOldReaderGalleries(context: Context) : List<File> {
val oldGallery = mutableListOf<File>()
listOf(
getDownloadDirectory(context),
File(context.cacheDir, "imageCache")
).forEach { root ->
root.listFiles()?.forEach { gallery ->
File(gallery, "reader.json").let { readerFile ->
if (!readerFile.exists())
return@let
Json(JsonConfiguration.Stable).parseJson(readerFile.readText()).jsonObject.let { reader ->
if (!reader.contains("code"))
oldGallery.add(gallery)
}
}
}
}
return oldGallery
}
@UseExperimental(InternalSerializationApi::class)
fun updateOldReaderGalleries(context: Context) {
val json = Json(JsonConfiguration.Stable)
getOldReaderGalleries(context).forEach { gallery ->
val reader = json.parseJson(File(gallery, "reader.json").apply {
if (!exists())
return@forEach
}.readText())
.jsonObject.toMutableMap()
val codeSerializer = EnumSerializer(Reader.Code::class)
reader["code"] = when {
(File(gallery, "images").list()?.
all { !it.endsWith("webp") } ?: return@forEach) &&
availableInHiyobi(gallery.name.toInt()) -> json.toJson(codeSerializer, Reader.Code.HIYOBI)
else -> json.toJson(codeSerializer, Reader.Code.HITOMI)
}
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
}
} }

View File

@@ -29,7 +29,29 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/lock_button_layout"/> app:layout_constraintBottom_toTopOf="@id/lock_fingerprint_layout"/>
<LinearLayout
android:id="@+id/lock_fingerprint_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="32dp"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/lock_content"
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/lock_button_layout" android:id="@+id/lock_button_layout"
@@ -37,7 +59,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="32dp" android:layout_marginBottom="32dp"
app:layout_constraintTop_toBottomOf="@id/lock_content" app:layout_constraintTop_toBottomOf="@id/lock_fingerprint_layout"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"> android:gravity="center">
@@ -59,16 +81,6 @@
app:backgroundTint="@color/dark_gray" app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_password" android:id="@+id/lock_password"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -40,7 +40,7 @@
android:padding="8dp"> android:padding="8dp">
<ImageView <ImageView
android:id="@+id/gallery_thumbnail" android:id="@+id/gallery_cover"
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
@@ -55,7 +55,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_thumbnail" app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
@@ -66,7 +66,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/gallery_title" app:layout_constraintTop_toBottomOf="@id/gallery_title"
app:layout_constraintLeft_toRightOf="@id/gallery_thumbnail" app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
@@ -83,7 +83,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_thumbnail" app:layout_constraintLeft_toRightOf="@id/gallery_cover"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>

View File

@@ -23,161 +23,203 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
android:foreground="?attr/selectableItemBackground" android:clipChildren="true">
android:focusable="true"
android:clickable="true">
<LinearLayout <com.daimajia.swipe.SwipeLayout
android:id="@+id/galleryblock_swipe_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> app:drag_edge="right"
app:show_mode="pull_out">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_progress_complete"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<View
android:id="@+id/galleryblock_padding"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="2dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="8dp"
android:background="@android:color/darker_gray"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/galleryblock_secondary"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:paddingLeft="8dp" android:layout_height="match_parent">
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/galleryblock_id" android:id="@+id/galleryblock_download"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white"
android:text="@string/main_download"
android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"/>
<View <TextView
android:layout_width="0dp" android:id="@+id/galleryblock_delete"
android:layout_height="1dp" android:layout_width="wrap_content"
android:layout_weight="1"/> android:layout_height="match_parent"
android:minWidth="70dp"
<ImageView android:padding="8dp"
android:id="@+id/galleryblock_favorite" android:gravity="center"
android:contentDescription="@string/app_name" android:background="@android:color/holo_red_dark"
android:layout_width="32dp" android:textColor="@android:color/white"
android:layout_height="32dp" android:text="@string/main_delete"
app:srcCompat="@drawable/ic_star_empty"/> android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <LinearLayout
android:id="@+id/galleryblock_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_progress_complete"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<View
android:id="@+id/galleryblock_padding"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="2dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="8dp"
android:background="@android:color/darker_gray"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/galleryblock_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<ImageView
android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name"
android:layout_width="32dp"
android:layout_height="32dp"
app:srcCompat="@drawable/ic_star_empty"/>
</LinearLayout>
</LinearLayout>
</com.daimajia.swipe.SwipeLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@@ -53,7 +53,7 @@
android:title="@string/main_drawer_group_contact_email" android:title="@string/main_drawer_group_contact_email"
android:icon="@drawable/ic_email"/> android:icon="@drawable/ic_email"/>
<item android:id="@+id/main_drawer_kakaotalk" <item android:id="@+id/main_drawer_kakaotalk"
android:title="@string/main_drawer_grouop_contact_kakaotalk" android:title="@string/main_drawer_grouop_contact_discord"
android:icon="@drawable/ic_message"/> android:icon="@drawable/ic_message"/>
</menu> </menu>
</item> </item>

View File

@@ -66,7 +66,7 @@
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string> <string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
<string name="main_open_gallery_by_id_error">エラーが発生しました</string> <string name="main_open_gallery_by_id_error">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string> <string name="settings_storage">ストレージ</string>
<string name="main_drawer_grouop_contact_kakaotalk">カカオトーク</string> <string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string> <string name="settings_app_lock">アプリロック</string>
<string name="settings_app_lock_type">アップロックの種類</string> <string name="settings_app_lock_type">アップロックの種類</string>
<string name="settings_app_version_title">バージョン</string> <string name="settings_app_version_title">バージョン</string>
@@ -99,7 +99,15 @@
<string name="gallery_tags">タグ</string> <string name="gallery_tags">タグ</string>
<string name="gallery_thumbnails">サムネイル</string> <string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string> <string name="gallery_related">おすすめ</string>
<string name="settings_nomedia_summary">イメージを隠す</string> <string name="settings_nomedia_summary">イメージをギャラリーから見えなくする</string>
<string name="settings_nomedia_title">イメージをギャラリーから見えなくする</string> <string name="settings_nomedia_title">イメージを隠す</string>
<string name="reader_help">ヘルプ</string> <string name="reader_help">ヘルプ</string>
<string name="main_delete">削除</string>
<string name="main_download">ダウンロード</string>
<string name="settings_backup_title">お気に入りバックアップ</string>
<string name="settings_restore_title">お気に入り復元</string>
<string name="settings_backup_snackbar">バックアップファイルを作成しました</string>
<string name="settings_backup_checkout">確認</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_successful">%1$d項目を復元しました</string>
</resources> </resources>

View File

@@ -66,7 +66,7 @@
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string> <string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
<string name="main_open_gallery_by_id_error">갤러리를 찾지 못했습니다</string> <string name="main_open_gallery_by_id_error">갤러리를 찾지 못했습니다</string>
<string name="settings_storage">저장 공간</string> <string name="settings_storage">저장 공간</string>
<string name="main_drawer_grouop_contact_kakaotalk">카카오톡 오픈채팅방</string> <string name="main_drawer_grouop_contact_discord">디스코드</string>
<string name="settings_app_lock">앱 잠금</string> <string name="settings_app_lock">앱 잠금</string>
<string name="settings_app_lock_type">앱 잠금 종류</string> <string name="settings_app_lock_type">앱 잠금 종류</string>
<string name="settings_app_version_title">앱 버전</string> <string name="settings_app_version_title">앱 버전</string>
@@ -99,7 +99,15 @@
<string name="gallery_tags">태그</string> <string name="gallery_tags">태그</string>
<string name="gallery_related">관련 갤러리</string> <string name="gallery_related">관련 갤러리</string>
<string name="gallery_thumbnails">미리보기</string> <string name="gallery_thumbnails">미리보기</string>
<string name="settings_nomedia_summary">이미지 숨기기</string> <string name="settings_nomedia_summary">갤러리에서 이미지 검색이 되지 않도록 합니다</string>
<string name="settings_nomedia_title">갤러리에서 이미지 검색이 되지 않도록 합니다</string> <string name="settings_nomedia_title">이미지 숨기기</string>
<string name="reader_help">도움말</string> <string name="reader_help">도움말</string>
<string name="main_delete">삭제</string>
<string name="main_download">다운로드</string>
<string name="settings_backup_title">즐겨찾기 백업</string>
<string name="settings_restore_title">즐겨찾기 복원</string>
<string name="settings_backup_snackbar">백업 파일을 생성하였습니다</string>
<string name="settings_backup_checkout">확인</string>
<string name="settings_restore_failed">복원에 실패했습니다</string>
<string name="settings_restore_successful">%1$d개 항목을 복원했습니다</string>
</resources> </resources>

View File

@@ -7,9 +7,9 @@
<string name="home_page" translatable="false">http://bit.ly/2EZDClw</string> <string name="home_page" translatable="false">http://bit.ly/2EZDClw</string>
<string name="update" translatable="false">http://bit.ly/2ZlOjXJ</string> <string name="update" translatable="false">http://bit.ly/2ZlOjXJ</string>
<string name="help" translatable="false">http://bit.ly/2Z7lNZE</string> <string name="help" translatable="false">http://bit.ly/2Z7lNZE</string>
<string name="github" translatable="false">https://github.com/tom5079/Pupil-issue/issues/new/choose</string> <string name="github" translatable="false">https://github.com/tom5079/Pupil/</string>
<string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string> <string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string>
<string name="kakaotalk" translatable="false">https://open.kakao.com/o/gvNrncsb</string> <string name="discord" translatable="false">https://discord.gg/Stj4b5v</string>
<string name="error_help" translatable="false">http://bit.ly/2KYYhto</string> <string name="error_help" translatable="false">http://bit.ly/2KYYhto</string>
<string name="main_settings" translatable="false">Settings</string> <string name="main_settings" translatable="false">Settings</string>
@@ -51,7 +51,7 @@
<string name="main_drawer_group_contact_homepage">Visit homepage</string> <string name="main_drawer_group_contact_homepage">Visit homepage</string>
<string name="main_drawer_group_contact_github">Visit github</string> <string name="main_drawer_group_contact_github">Visit github</string>
<string name="main_drawer_group_contact_email">Email me!</string> <string name="main_drawer_group_contact_email">Email me!</string>
<string name="main_drawer_grouop_contact_kakaotalk">Kakaotalk</string> <string name="main_drawer_grouop_contact_discord">Discord</string>
<string name="main_menu_sort">Sort</string> <string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string> <string name="main_menu_sort_newest">Newest</string>
@@ -71,6 +71,9 @@
<string name="main_export_open_folder">Open Folder</string> <string name="main_export_open_folder">Open Folder</string>
<string name="main_export_error">Error occurred during export</string> <string name="main_export_error">Error occurred during export</string>
<string name="main_download">DOWNLOAD</string>
<string name="main_delete">DELETE</string>
<string name="update_title">Update available</string> <string name="update_title">Update available</string>
<string name="update_download_started">Download started</string> <string name="update_download_started">Download started</string>
<string name="update_notification_description">Downloading apk&#8230;</string> <string name="update_notification_description">Downloading apk&#8230;</string>
@@ -129,6 +132,12 @@
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</string> <string name="settings_dark_mode_summary">Protect yourself against light attacks!</string>
<string name="settings_nomedia_title">Hide image from gallery</string> <string name="settings_nomedia_title">Hide image from gallery</string>
<string name="settings_nomedia_summary">Hides image from gallery</string> <string name="settings_nomedia_summary">Hides image from gallery</string>
<string name="settings_backup_title">Backup favorites</string>
<string name="settings_backup_snackbar">Backup file created</string>
<string name="settings_backup_checkout">Check out</string>
<string name="settings_restore_title">Restore favorites</string>
<string name="settings_restore_failed">Restore failed</string>
<string name="settings_restore_successful">%1$d entries restored</string>
<string name="settings_lock_none">None</string> <string name="settings_lock_none">None</string>
<string name="settings_lock_pattern">Pattern</string> <string name="settings_lock_pattern">Pattern</string>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 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/>.
-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external" path="/"/>
</paths>

View File

@@ -73,6 +73,15 @@
app:key="nomedia" app:key="nomedia"
app:title="@string/settings_nomedia_title" app:title="@string/settings_nomedia_title"
app:summary="@string/settings_nomedia_title"/> app:summary="@string/settings_nomedia_title"/>
<Preference
app:key="backup"
app:title="@string/settings_backup_title"/>
<Preference
app:key="restore"
app:title="@string/settings_restore_title"/>
</PreferenceCategory> </PreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View File

@@ -20,22 +20,19 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import org.junit.Test
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
import org.junit.Test
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
val current = "0.1"
val latest = "0.2"
print(current < latest)
} }
} }

View File

@@ -1,20 +1,18 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.3.61'
repositories { repositories {
google() google()
jcenter() jcenter()
maven { maven { url 'https://maven.fabric.io/public' }
url 'https://maven.fabric.io/public'
}
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.1' classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath 'io.fabric.tools:gradle:1.29.0' classpath 'io.fabric.tools:gradle:1.29.0'

View File

@@ -6,7 +6,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'org.jsoup:jsoup:1.11.3' implementation 'org.jsoup:jsoup:1.11.3'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
} }

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver
fun availableInHiyobi(galleryID: Int) : Boolean {
return try {
xyz.quaver.hiyobi.getReader(galleryID)
true
} catch (e: Exception) {
false
}
}

View File

@@ -16,11 +16,25 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import java.net.URL
const val protocol = "https:" const val protocol = "https:"
fun getGalleryInfo(galleryID: Int): List<GalleryInfo> {
return Json(JsonConfiguration.Stable).parse(
GalleryInfo.serializer().list,
Regex("""\[.+]""").find(
URL("$protocol//$domain/galleries/$galleryID.js").readText()
)?.value ?: "[]"
)
}
//common.js //common.js
var adapose = false var adapose = false
const val numberOfFrontends = 2 const val numberOfFrontends = 3
const val domain = "ltn.hitomi.la" const val domain = "ltn.hitomi.la"
const val galleryblockdir = "galleryblock" const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi" const val nozomiextension = ".nozomi"
@@ -37,20 +51,22 @@ fun subdomainFromGalleryID(g: Int) : String {
fun subdomainFromURL(url: String, base: String? = null) : String { fun subdomainFromURL(url: String, base: String? = null) : String {
var retval = "a" var retval = "a"
if (base != null) if (!base.isNullOrBlank())
retval = base retval = base
val r = Regex("""/\d*(\d)/""") val r = Regex("""/galleries/\d*(\d)/""")
val m = r.find(url) var m = r.find(url)
var b = 10
m ?: return retval if (m == null) {
b = 16
val r2 = Regex("""/[0-9a-f]/([0-9a-f]{2})/""")
m = r2.find(url)
if (m == null)
return retval
}
var g = m.groups[1]!!.value.toIntOrNull() val g = m.groupValues[1].toIntOrNull(b) ?: return retval
g ?: return retval
if (g == 1)
g = 0
retval = subdomainFromGalleryID(g) + retval retval = subdomainFromGalleryID(g) + retval
@@ -58,5 +74,25 @@ fun subdomainFromURL(url: String, base: String? = null) : String {
} }
fun urlFromURL(url: String, base: String? = null) : String { fun urlFromURL(url: String, base: String? = null) : String {
return url.replace(Regex("//..?\\.hitomi\\.la/"), "//${subdomainFromURL(url, base)}.hitomi.la/") return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
} }
fun fullPathFromHash(hash: String?) : String? {
return when {
(hash?.length ?: 0) < 3 -> hash
else -> hash!!.replace(Regex("^.*(..)(.)$"), "$2/$1/$hash")
}
}
fun urlFromHash(galleryID: Int, image: GalleryInfo, webp: String? = null) : String {
val ext = webp ?: image.name.split('.').last()
return when {
image.hash.isNullOrBlank() ->
"$protocol//a.hitomi.la/galleries/$galleryID/${image.name}"
else ->
"$protocol//a.hitomi.la/${webp?:"images"}/${fullPathFromHash(image.hash)}.$ext"
}
}
fun urlFromUrlFromHash(galleryID: Int, image: GalleryInfo, webp: String? = null) =
urlFromURL(urlFromHash(galleryID, image, webp))

View File

@@ -17,7 +17,6 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
data class Gallery( data class Gallery(
@@ -35,7 +34,8 @@ data class Gallery(
val thumbnails: List<String> val thumbnails: List<String>
) )
fun getGallery(galleryID: Int) : Gallery { fun getGallery(galleryID: Int) : Gallery {
val url = "https://hitomi.la/galleries/$galleryID.html" val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").get()
.select("a").attr("href")
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).get()
@@ -46,7 +46,7 @@ fun getGallery(galleryID: Int) : Gallery {
}.toList() }.toList()
val langList = doc.select("#lang-list a").map { val langList = doc.select("#lang-list a").map {
Pair(it.text(), it.attr("href").replace(".html", "")) Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
} }
val cover = protocol + doc.selectFirst(".cover img").attr("src") val cover = protocol + doc.selectFirst(".cover img").attr("src")
@@ -68,11 +68,9 @@ fun getGallery(galleryID: Int) : Gallery {
href.slice(5 until href.indexOf('-')) href.slice(5 until href.indexOf('-'))
} }
val thumbnails = Regex("'(//tn.hitomi.la/smalltn/\\d+/.+)',") val thumbnails = getGalleryInfo(galleryID).map {
.findAll(doc.select("script").last().html()) "$protocol//tn.hitomi.la/smalltn/$galleryID/${it.name}.jpg"
.map { }
protocol + it.groups[1]!!.value
}.toList()
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails) return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
} }

View File

@@ -18,7 +18,6 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import sun.rmi.runtime.Log
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -69,6 +68,7 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
@Serializable @Serializable
data class GalleryBlock( data class GalleryBlock(
val id: Int, val id: Int,
val galleryUrl: String,
val thumbnails: List<String>, val thumbnails: List<String>,
val title: String, val title: String,
val artists: List<String>, val artists: List<String>,
@@ -83,6 +83,8 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
try { try {
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).get()
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 title = doc.selectFirst("h1.lillie > a").text()
@@ -100,7 +102,7 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
href.slice(5 until href.indexOf("-all")) href.slice(5 until href.indexOf("-all"))
} }
return GalleryBlock(galleryID, thumbnails, title, artists, series, type, language, relatedTags) return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }

View File

@@ -17,72 +17,34 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.hiyobi.HiyobiReader
import java.net.URL
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html" fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp" fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
fun webpReaderFromReader(reader: Reader) : Reader {
if (reader is HiyobiReader)
return reader
return Reader(reader.title, reader.readerItems.map {
ReaderItem(
if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url,
it.galleryInfo
)
})
}
@Serializable @Serializable
data class GalleryInfo( data class GalleryInfo(
val width: Int, val width: Int,
val haswebp: Int, val hash: String? = null,
val haswebp: Int = 0,
val name: String, val name: String,
val height: Int val height: Int
) )
@Serializable
data class ReaderItem(
val url: String,
val galleryInfo: GalleryInfo?
)
@Serializable @Serializable
open class Reader(val title: String, val readerItems: List<ReaderItem>) data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>) {
enum class Code {
HITOMI,
HIYOBI,
SORALA
}
}
//Set header `Referer` to reader url to avoid 403 error //Set header `Referer` to reader url to avoid 403 error
fun getReader(galleryID: Int) : Reader { fun getReader(galleryID: Int) : Reader {
val readerUrl = "https://hitomi.la/reader/$galleryID.html" val readerUrl = "https://hitomi.la/reader/$galleryID.html"
val galleryInfoUrl = "https://ltn.hitomi.la/galleries/$galleryID.js"
val doc = Jsoup.connect(readerUrl).get() val doc = Jsoup.connect(readerUrl).get()
val title = doc.title() return Reader(Reader.Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
val images = doc.select(".img-url").map {
protocol + urlFromURL(it.text())
}
val galleryInfo = ArrayList<GalleryInfo?>()
galleryInfo.addAll(
Json(JsonConfiguration.Stable).parse(
GalleryInfo.serializer().list,
Regex("""\[.+]""").find(
URL(galleryInfoUrl).readText()
)?.value ?: "[]"
)
)
if (images.size > galleryInfo.size)
galleryInfo.addAll(arrayOfNulls(images.size - galleryInfo.size))
return Reader(title, (images zip galleryInfo).map {
ReaderItem(it.first, it.second)
})
} }

View File

@@ -18,18 +18,17 @@ package xyz.quaver.hiyobi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.content import kotlinx.serialization.list
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.ReaderItem import xyz.quaver.hitomi.protocol
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
const val hiyobi = "xn--9w3b15m8vo.asia" const val hiyobi = "hiyobi.me"
const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"
class HiyobiReader(title: String, readerItems: List<ReaderItem>) : Reader(title, readerItems)
var cookie: String = "" var cookie: String = ""
get() { get() {
if (field.isEmpty()) if (field.isEmpty())
@@ -38,6 +37,12 @@ var cookie: String = ""
return field return field
} }
data class Images(
val path: String,
val no: Int,
val name: String
)
fun renewCookie() : String { fun renewCookie() : String {
val url = "https://$hiyobi/" val url = "https://$hiyobi/"
@@ -59,7 +64,8 @@ fun getReader(galleryID: Int) : Reader {
val title = Jsoup.connect(reader).get().title() val title = Jsoup.connect(reader).get().title()
val json = Json(JsonConfiguration.Stable).parseJson( val galleryInfo = Json(JsonConfiguration.Stable).parse(
GalleryInfo.serializer().list,
with(URL(url).openConnection() as HttpsURLConnection) { with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
@@ -70,8 +76,8 @@ fun getReader(galleryID: Int) : Reader {
} }
) )
return Reader(title, json.jsonArray.map { return Reader(Reader.Code.HIYOBI, title, galleryInfo)
val name = it.jsonObject["name"]!!.content }
ReaderItem("https://$hiyobi/data/$galleryID/$name", null)
}) fun createImgList(galleryID: Int, reader: Reader) =
} reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }

View File

@@ -18,12 +18,17 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import xyz.quaver.availableInHiyobi
class UnitTest { class UnitTest {
@Test @Test
fun test() { fun test() {
assertEquals(
"6/2d/c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6",
fullPathFromHash("c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6")
)
} }
@Test @Test
@@ -63,7 +68,7 @@ class UnitTest {
@Test @Test
fun test_getGallery() { fun test_getGallery() {
val gallery = getGallery(1405267) val gallery = getGallery(1510566)
print(gallery) print(gallery)
} }
@@ -77,6 +82,22 @@ class UnitTest {
@Test @Test
fun test_hiyobi() { fun test_hiyobi() {
xyz.quaver.hiyobi.getReader(1510567)
}
@Test
fun test_urlFromUrlFromHash() {
val url = urlFromUrlFromHash(1531795, GalleryInfo(
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
), "webp")
print(url)
}
@Test
fun test_availableInHiyobi() {
val result = availableInHiyobi(1272781)
print(result)
} }
} }