Merge pull request #64 from tom5079/development

Version 5.0
This commit is contained in:
Pupil
2020-02-08 20:38:51 +09:00
committed by GitHub
58 changed files with 1999 additions and 896 deletions

View File

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

7
.idea/kotlinCodeInsightSettings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinCodeInsightWorkspaceSettings">
<option name="addUnambiguousImportsOnTheFly" value="true" />
<option name="optimizeImportsOnTheFly" value="true" />
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
</project>

1
.idea/vcs.xml generated
View File

@@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/gh-pages" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -19,15 +19,16 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 32 versionCode 33
versionName "4.3-hotfix1" versionName "5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
buildTypes.each { buildTypes.each {
@@ -41,6 +42,9 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildToolsVersion = '29.0.2' buildToolsVersion = '29.0.2'
} }
@@ -70,9 +74,11 @@ dependencies {
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.10.0' implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") { implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
transitive = false transitive = false
} }
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.gu:option:1.3'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":32,"versionName":"4.3-hotfix1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":33,"versionName":"5.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
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.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
@@ -37,6 +38,8 @@ 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.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File import java.io.File
@@ -118,4 +121,37 @@ class ExampleInstrumentedTest {
updateOldReaderGalleries(context) updateOldReaderGalleries(context)
} }
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 515515
val worker = DownloadWorker.getInstance(context)
worker.queue.add(galleryID)
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { !it.isFinite() } == true)
break
}
Log.i("PUPILD", "DONE!!")
}
@Test
fun test_getReaderOrNull() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
}
} }

View File

@@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="21" />
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -116,6 +119,7 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
</application> </application>
</manifest> </manifest>

View File

@@ -22,6 +22,7 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -30,17 +31,12 @@ 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() {
lateinit var histories: Histories lateinit var histories: Histories
lateinit var downloads: Histories
lateinit var favorites: Histories lateinit var favorites: Histories
init { init {
@@ -51,9 +47,15 @@ class Pupil : MultiDexApplication() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json")) histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json")) favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
val download = preference.getString("dl_location", null)
if (download == null) {
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
}
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)
} catch (e: GooglePlayServicesRepairableException) { } catch (e: GooglePlayServicesRepairableException) {
@@ -64,7 +66,7 @@ class Pupil : MultiDexApplication() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply { val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
description = getString(R.string.channel_download_description) description = getString(R.string.channel_download_description)
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
@@ -73,15 +75,12 @@ class Pupil : MultiDexApplication() {
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
} }
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) { AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
true -> AppCompatDelegate.MODE_NIGHT_YES true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO false -> AppCompatDelegate.MODE_NIGHT_NO
}) })
CoroutineScope(Dispatchers.IO).launch {
updateOldReaderGalleries(this@Pupil)
}
super.onCreate() super.onCreate()
} }

View File

@@ -18,7 +18,9 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Base64
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -27,9 +29,10 @@ import android.widget.LinearLayout
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.Animatable2Compat 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.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
@@ -37,28 +40,22 @@ 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
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig 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.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
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>>>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface { class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType { enum class ViewType {
NEXT, NEXT,
@@ -66,10 +63,56 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
PREV PREV
} }
private val glide = Glide.with(context)
private lateinit var favorites: Histories private lateinit var favorites: Histories
val timer = Timer()
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) { inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Pair<GalleryBlock, Deferred<String>>) { var timerTask: TimerTask? = null
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
val cache = Cache(context).getCachedGallery(galleryID)
val reader = Cache(context).getReaderOrNull(galleryID)
if (reader == null) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
return@launch
}
with(view.galleryblock_progressbar) {
progress = cache?.listFiles()?.count { file ->
Regex("^[0-9]+.+\$").matches(file.name!!)
} ?: 0
if (visibility == View.GONE) {
visibility = View.VISIBLE
max = reader.galleryInfo.size
}
if (progress == max) {
if (completeFlag.get(galleryID, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryID, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
}
}
fun bind(galleryBlock: GalleryBlock) {
with(view) { with(view) {
val resources = context.resources val resources = context.resources
val languages = resources.getStringArray(R.array.languages).map { val languages = resources.getStringArray(R.array.languages).map {
@@ -78,16 +121,18 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
} }
}.toMap() }.toMap()
val (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
val artists = galleryBlock.artists val artists = galleryBlock.artists
val series = galleryBlock.series val series = galleryBlock.series
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
it.start()
})
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val cache = thumbnail.await() val thumbnail = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT)
glide glide
.load(cache) .load(thumbnail)
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
@@ -99,75 +144,28 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
} }
//Check cache //Check cache
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") } val cache = Cache(context).getCachedGallery(galleryBlock.id)
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") } val reader = Cache(context).getReaderOrNull(galleryBlock.id)
try { if (cache != null && reader != null) {
Json(JsonConfiguration.Stable) val count = cache.listFiles().count {
.parse(Reader.serializer(), readerCache.invoke().readText()) Regex("^[0-9]+.+\$").matches(it.name!!)
} catch(e: Exception) { }
readerCache.invoke().delete()
}
if (readerCache.invoke().exists()) {
val reader = Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText())
with(galleryblock_progressbar) { with(galleryblock_progressbar) {
max = reader.galleryInfo.size max = reader.galleryInfo.size
progress = imageCache.invoke().list()?.size ?: 0 progress = count
visibility = View.VISIBLE visibility = View.VISIBLE
} }
} else { } else
galleryblock_progressbar.visibility = View.GONE galleryblock_progressbar.visibility = View.GONE
}
if (refreshTasks[this@GalleryViewHolder] == null) { if (timerTask == null)
val refresh = Timer(false).schedule(0, 1000) { timerTask = timer.schedule(0, 1000) {
post { updateProgress(context, galleryBlock.id)
with(view.galleryblock_progressbar) {
progress = imageCache.invoke().list()?.size ?: 0
if (!readerCache.invoke().exists()) {
visibility = View.GONE
max = 0
progress = 0
view.galleryblock_progress_complete.visibility = View.INVISIBLE
} else {
if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText())
max = reader.galleryInfo.size
visibility = View.VISIBLE
}
if (progress == max) {
if (completeFlag.get(galleryBlock.id, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryBlock.id, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
}
}
}
} }
refreshTasks[this@GalleryViewHolder] = refresh
}
galleryblock_title.text = galleryBlock.title galleryblock_title.text = galleryBlock.title
with(galleryblock_artist) { with(galleryblock_artist) {
text = artists.joinToString(", ") { it.wordCapitalize() } text = artists.joinToString(", ") { it.wordCapitalize() }
@@ -277,7 +275,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
} }
} }
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
val completeFlag = SparseBooleanArray() val completeFlag = SparseBooleanArray()
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
@@ -336,10 +333,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
override fun onStartOpen(layout: SwipeLayout?) { override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout) mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = when(GalleryDownloader.get(gallery.first.id)) { holder.view.galleryblock_download.text =
null -> holder.view.context.getString(R.string.main_download) if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
else -> holder.view.context.getString(android.R.string.cancel) holder.view.context.getString(R.string.main_download)
} else
holder.view.context.getString(android.R.string.cancel)
} }
override fun onClose(layout: SwipeLayout?) {} override fun onClose(layout: SwipeLayout?) {}
@@ -355,10 +353,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
super.onViewDetachedFromWindow(holder) super.onViewDetachedFromWindow(holder)
if (holder is GalleryViewHolder) { if (holder is GalleryViewHolder) {
val task = refreshTasks[holder] ?: return holder.timerTask?.cancel()
holder.timerTask = null
task.cancel()
refreshTasks.remove(holder)
} }
} }

View File

@@ -0,0 +1,85 @@
/*
* 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.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_mirrors.view.*
import xyz.quaver.pupil.R
import java.util.*
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
it.split('|').let { split ->
Pair(split.first(), split.last())
}
}.toMap()
val list = mirrors.keys.toMutableList().apply {
PreferenceManager.getDefaultSharedPreferences(context)
.getString("mirrors", "")!!
.split(">")
.reversed()
.forEach {
if (this.contains(it)) {
this.remove(it)
this.add(0, it)
}
}
}
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
Collections.swap(list, from, to)
notifyItemMoved(from, to)
onItemMoved?.invoke(list)
}
var onStartDrag : ((ViewHolder) -> Unit)? = null
var onItemMoved : ((List<String>) -> (Unit))? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder.view) {
mirror_name.text = mirrors[list.elementAt(position)]
mirror_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN)
onStartDrag?.invoke(holder)
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate(
R.layout.item_mirrors, parent, false
).let {
ViewHolder(it)
}
}
override fun getItemCount() = mirrors.size
}

View File

@@ -18,24 +18,48 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.Context
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 androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Reader
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.download.Cache
import java.io.File import xyz.quaver.pupil.util.download.DownloadWorker
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class ReaderAdapter(private val glide: RequestManager, class ReaderAdapter(private val context: Context,
private val galleryID: Int, private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var isFullScreen = false var isFullScreen = false
var reader: Reader? = null
private val glide = Glide.with(context)
val timer = Timer()
var onItemClickListener : ((Int) -> (Unit))? = null
init {
CoroutineScope(Dispatchers.IO).launch {
reader = Cache(context).getReader(galleryID)
launch(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -47,26 +71,75 @@ class ReaderAdapter(private val glide: RequestManager,
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ImageView holder.view as ConstraintLayout
if (isFullScreen) if (isFullScreen)
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
else else
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
glide holder.view.image.setOnPhotoTapListener { _, _, _ ->
.load(File(getCachedGallery(holder.view.context, galleryID), images[position])) onItemClickListener?.invoke(position)
.diskCacheStrategy(DiskCacheStrategy.NONE) }
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant) holder.view.container.setOnClickListener {
.dontTransform() onItemClickListener?.invoke(position)
.apply { }
if (BuildConfig.CENSOR)
override(5, 8) (holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
holder.view.reader_index.text = (position+1).toString()
val images = Cache(context).getImages(galleryID)
if (images?.get(position) != null) {
glide
.load(images[position]?.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
} else {
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
if (progress?.isNaN() == true) {
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
Snackbar
.make(
holder.view,
DownloadWorker.getInstance(context).exception[galleryID]!![position]?.message
?: context.getText(R.string.default_error_msg),
Snackbar.LENGTH_INDEFINITE
)
.show()
return
} }
.into(holder.view)
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null)
timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch {
notifyItemChanged(position)
}
}
}
} }
override fun getItemCount() = images.size override fun getItemCount() = reader?.galleryInfo?.size ?: 0
} }

View File

@@ -45,7 +45,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import com.arlib.floatingsearchview.util.view.SearchInputView
import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.android.synthetic.main.activity_main_content.*
@@ -55,7 +54,10 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list import kotlinx.serialization.list
import kotlinx.serialization.stringify import kotlinx.serialization.stringify
import xyz.quaver.hitomi.* import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
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
@@ -64,11 +66,10 @@ import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.ui.dialog.GalleryDialog import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.* import java.util.*
import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
@@ -89,7 +90,7 @@ class MainActivity : AppCompatActivity() {
POPULAR POPULAR
} }
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>() private val galleries = ArrayList<GalleryBlock>()
private var query = "" private var query = ""
set(value) { set(value) {
@@ -112,7 +113,6 @@ class MainActivity : AppCompatActivity() {
private var currentPage = 0 private var currentPage = 0
private lateinit var histories: Histories private lateinit var histories: Histories
private lateinit var downloads: Histories
private lateinit var favorites: Histories private lateinit var favorites: Histories
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -150,7 +150,6 @@ class MainActivity : AppCompatActivity() {
with(application as Pupil) { with(application as Pupil) {
this@MainActivity.histories = histories this@MainActivity.histories = histories
this@MainActivity.downloads = downloads
this@MainActivity.favorites = favorites this@MainActivity.favorites = favorites
} }
@@ -176,6 +175,12 @@ class MainActivity : AppCompatActivity() {
} }
} }
override fun onDestroy() {
super.onDestroy()
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
}
override fun onResume() { override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -386,7 +391,7 @@ class MainActivity : AppCompatActivity() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(main_recyclerview) { with(main_recyclerview) {
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply { adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
query = it.toQuery() query = it.toQuery()
@@ -399,16 +404,18 @@ class MainActivity : AppCompatActivity() {
} }
} }
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { position ->
val galleryID = galleries[position].first.id val galleryID = galleries[position].id
if (!completeFlag.get(galleryID, false)) { if (!completeFlag.get(galleryID, false)) {
val downloader = GalleryDownloader.get(galleryID) val worker = DownloadWorker.getInstance(context)
if (downloader == null) if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
GalleryDownloader(context, galleryID, true).start() worker.cancel(galleryID)
else { else {
downloader.cancel() Cache(context).setDownloading(galleryID, true)
downloader.clearNotification()
if (!worker.queue.contains(galleryID))
worker.queue.add(galleryID)
} }
} }
@@ -416,39 +423,27 @@ class MainActivity : AppCompatActivity() {
} }
onDeleteClickedHandler = { position -> onDeleteClickedHandler = { position ->
val galleryID = galleries[position].first.id val galleryID = galleries[position].id
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryID]) { DownloadWorker.getInstance(context).cancel(galleryID)
this?.cancelAndJoin()
this?.clearNotification()
}
val cache = File(cacheDir, "imageCache/${galleryID}")
val data = getCachedGallery(context, galleryID)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryID) var cache = Cache(context).getCachedGallery(galleryID)
if (this@MainActivity.mode == Mode.DOWNLOAD) { while (cache != null) {
runOnUiThread { cache.deleteRecursively()
cancelFetch() cache = Cache(context).getCachedGallery(galleryID)
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
} }
histories.remove(galleryID) histories.remove(galleryID)
if (this@MainActivity.mode == Mode.HISTORY) { if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
}
completeFlag.put(galleryID, false) completeFlag.put(galleryID, false)
} }
@@ -462,7 +457,7 @@ class MainActivity : AppCompatActivity() {
return@setOnItemClickListener return@setOnItemClickListener
val intent = Intent(this@MainActivity, ReaderActivity::class.java) val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position].first val gallery = galleries[position]
intent.putExtra("galleryID", gallery.id) intent.putExtra("galleryID", gallery.id)
//TODO: Maybe sprinkling some transitions will be nice :D //TODO: Maybe sprinkling some transitions will be nice :D
@@ -474,7 +469,7 @@ class MainActivity : AppCompatActivity() {
if (v !is CardView) if (v !is CardView)
return@setOnItemLongClickListener true return@setOnItemLongClickListener true
val galleryID = galleries[position].first.id val galleryID = galleries[position].id
GalleryDialog( GalleryDialog(
this@MainActivity, this@MainActivity,
@@ -973,8 +968,14 @@ class MainActivity : AppCompatActivity() {
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file ->
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
}?.map {
it.name!!.toInt()
}?: listOf()
when { when {
query.isEmpty() -> downloads.toList().apply { query.isEmpty() -> downloads.apply {
totalItems = size totalItems = size
} }
else -> { else -> {
@@ -1022,48 +1023,7 @@ class MainActivity : AppCompatActivity() {
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
async { async {
try { Cache(this@MainActivity).getGalleryBlock(galleryID)
val json = Json(JsonConfiguration.Stable)
val serializer = GalleryBlock.serializer()
val galleryBlock =
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
when {
cache.exists() -> json.parse(serializer, cache.readText())
else -> {
getGalleryBlock(galleryID).apply {
this ?: return@apply
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, this))
}
}
}
} ?: return@async null
val thumbnail = async {
val ext = galleryBlock.thumbnails[0].split('.').last()
File(getCachedGallery(this@MainActivity, galleryBlock.id), "thumbnail.$ext").apply {
if (!exists())
try {
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
if (this@apply.parentFile?.exists() == false)
this@apply.parentFile!!.mkdirs()
inputStream.copyTo(FileOutputStream(this@apply))
}
} catch (e: Exception) {
delete()
}
}.absolutePath
}
Pair(galleryBlock, thumbnail)
} catch (e: Exception) {
null
}
} }
}.forEach { }.forEach {
val galleryBlock = it.await() val galleryBlock = it.await()

View File

@@ -21,40 +21,37 @@ package xyz.quaver.pupil.ui
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.* import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.* import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import xyz.quaver.Code
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.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.util.*
import kotlin.concurrent.schedule
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
private var galleryID = 0 private var galleryID = 0
private val images = ArrayList<String>()
private var gallerySize = 0
private var currentPage = 0 private var currentPage = 0
private var isScroll = true private var isScroll = true
@@ -70,7 +67,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
private lateinit var downloader: GalleryDownloader private val timer = Timer()
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
@@ -102,12 +99,8 @@ class ReaderActivity : AppCompatActivity() {
return return
} }
initDownloader()
initView() initView()
initDownloader()
if (!downloader.download)
downloader.start()
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@@ -169,7 +162,7 @@ class ReaderActivity : AppCompatActivity() {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
with(view.dialog_number_picker) { with(view.dialog_number_picker) {
minValue=1 minValue=1
maxValue=gallerySize maxValue=reader_recyclerview.adapter?.itemCount ?: 0
value=currentPage value=currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
@@ -202,8 +195,11 @@ class ReaderActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if (::downloader.isInitialized && !downloader.download) timer.cancel()
downloader.cancel() (reader_recyclerview.adapter as ReaderAdapter).timer.cancel()
if (!Cache(this).isDownloading(galleryID))
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
} }
override fun onBackPressed() { override fun onBackPressed() {
@@ -239,101 +235,66 @@ class ReaderActivity : AppCompatActivity() {
} }
private fun initDownloader() { private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryID) val worker = DownloadWorker.getInstance(this).apply {
queue.add(galleryID)
if (d == null)
d = GalleryDownloader(this, galleryID)
downloader = d.apply {
onReaderLoadedHandler = {
CoroutineScope(Dispatchers.Main).launch {
title = it.title
with(reader_download_progressbar) {
max = it.galleryInfo.size
progress = 0
}
with(reader_progressbar) {
max = it.galleryInfo.size
progress = 0
}
gallerySize = it.galleryInfo.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.galleryInfo.size}"
}
}
onProgressHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_download_progressbar.progress = it
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi
}
}
onDownloadedHandler = {
val item = it.toList()
CoroutineScope(Dispatchers.Main).launch {
if (images.isEmpty()) {
images.addAll(item)
reader_recyclerview.adapter?.notifyDataSetChanged()
} else {
images.add(item.last())
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
}
}
}
onErrorHandler = {
Snackbar
.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.reader_help) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
}
.show()
downloader.download = false
}
onCompleteHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_download_progressbar.visibility = View.GONE
}
}
onNotifyChangedHandler = { notify ->
val fab = reader_fab_download
runOnUiThread {
if (notify) {
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
if (downloader.download)
fab.post {
icon.start()
fab.labelText = getString(R.string.reader_fab_download_cancel)
}
else
fab.post {
fab.setImageResource(R.drawable.ic_download)
fab.labelText = getString(R.string.reader_fab_download)
}
}
})
fab.setImageDrawable(icon)
icon?.start()
} else {
runOnUiThread {
fab.setImageResource(R.drawable.ic_download)
}
}
}
}
} }
if (downloader.download) { timer.schedule(0, 1000) {
downloader.invokeOnReaderLoaded() if (worker.progress.indexOfKey(galleryID) < 0) //loading
downloader.invokeOnNotifyChanged() return@schedule
if (worker.progress[galleryID] == null) { //Gallery not found
timer.cancel()
Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
.show()
}
runOnUiThread {
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress = worker.progress[galleryID]?.count { !it.isFinite() } ?: 0
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) {
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
if (reader != null) {
title = reader.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
when (reader.code) {
Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
})
}
}
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished
reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false)
}
}
} }
} }
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images) adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = {
if (isScroll) {
isScroll = false
isFullscreen = true
scrollMode(false)
fullscreen(true)
} else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
}
}
}
addOnScrollListener(object: RecyclerView.OnScrollListener() { addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -349,32 +310,24 @@ class ReaderActivity : AppCompatActivity() {
if (layoutManager.findFirstVisibleItemPosition() == -1) if (layoutManager.findFirstVisibleItemPosition() == -1)
return return
currentPage = layoutManager.findFirstVisibleItemPosition()+1 currentPage = layoutManager.findFirstVisibleItemPosition()+1
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize" menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
this@ReaderActivity.reader_progressbar.progress = currentPage this@ReaderActivity.reader_progressbar.progress = currentPage
} }
}) })
ItemClickSupport.addTo(this)
.setOnItemClickListener { _, _, _ ->
if (isScroll) {
isScroll = false
isFullscreen = true
scrollMode(false)
fullscreen(true)
} else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
}
}
} }
with(reader_fab_download) { with(reader_fab_download) {
setImageResource(R.drawable.ic_download) animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
setOnClickListener {
downloader.download = !downloader.download
if (!downloader.download) setOnClickListener {
downloader.clearNotification() if (Cache(context).isDownloading(galleryID)) {
Cache(context).setDownloading(galleryID, false)
animateDownloadFAB(false)
} else {
Cache(context).setDownloading(galleryID, true)
animateDownloadFAB(true)
}
} }
} }
@@ -416,4 +369,34 @@ class ReaderActivity : AppCompatActivity() {
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0) (reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
} }
private fun animateDownloadFAB(animate: Boolean) {
with(reader_fab_download) {
if (animate) {
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
}
else // Or continue animate
post {
icon.start()
labelText = getString(R.string.reader_fab_download_cancel)
}
}
})
setImageDrawable(icon)
icon?.start()
} else {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
}
}
}
} }

View File

@@ -20,26 +20,33 @@ package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList import kotlinx.serialization.parseList
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.LockFragment import xyz.quaver.pupil.ui.fragment.LockFragment
import xyz.quaver.pupil.ui.fragment.SettingsFragment import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.REQUEST_LOCK
import xyz.quaver.pupil.util.REQUEST_RESTORE
import java.io.File
import java.nio.charset.Charset import java.nio.charset.Charset
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
val REQUEST_LOCK = 38238
val REQUEST_RESTORE = 16546
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -114,6 +121,35 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
} }
REQUEST_DOWNLOAD_FOLDER -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", uri.toString())
.apply()
}
}
}
REQUEST_DOWNLOAD_FOLDER_OLD -> {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite())
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", Uri.fromFile(File(directory)).toString())
.apply()
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
@@ -28,6 +29,7 @@ import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.dialog_default_query.*
import kotlinx.android.synthetic.main.dialog_default_query.view.* import kotlinx.android.synthetic.main.dialog_default_query.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
@@ -50,21 +52,41 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.default_query_dialog_title)
setView(dialogView)
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
with(default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
initView()
setContentView(dialogView)
} }
private fun initView() { @SuppressLint("InflateParams")
private fun initDialog() {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val tags = Tags.parse( val tags = Tags.parse(
preferences.getString("default_query", "") ?: "" preferences.getString("default_query", "") ?: ""
) )
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
with(dialogView.default_query_dialog_language_selector) { with(dialogView.default_query_dialog_language_selector) {
adapter = adapter =
ArrayAdapter( ArrayAdapter(
@@ -105,7 +127,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
with(dialogView.default_query_dialog_edittext) { with(dialogView.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE) setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
@@ -113,29 +141,14 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
s ?: return s ?: return
if (s.any { it.isUpperCase() }) if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(java.util.Locale.getDefault())) s.replace(
0,
s.length,
s.toString().toLowerCase(java.util.Locale.getDefault())
)
} }
}) })
} }
dialogView.default_query_dialog_ok.setOnClickListener {
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
with(dialogView.default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
} }
} }

View File

@@ -19,28 +19,37 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.* 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.R
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
class DownloadLocationDialog(context: Context) : AlertDialog(context) { class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context) private val preference = PreferenceManager.getDefaultSharedPreferences(context)
private val buttons = mutableListOf<RadioButton>() private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
var onDownloadLocationChangedListener : ((Int) -> (Unit))? = null
init { override fun onCreate(savedInstanceState: Bundle?) {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
ContextCompat.getExternalFilesDirs(context, null).forEachIndexed { index, dir -> val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
externalFilesDirs.forEachIndexed { index, dir ->
dir ?: return@forEachIndexed dir ?: return@forEachIndexed
@@ -54,17 +63,58 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
byteToString(dir.freeSpace) byteToString(dir.freeSpace)
) )
setOnClickListener { setOnClickListener {
buttons.forEach { button -> buttons.forEach { pair ->
button.isChecked = false pair.first.isChecked = false
} }
button.performClick() button.performClick()
onDownloadLocationChangedListener?.invoke(index) preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
} }
buttons.add(button) buttons.add(button to Uri.fromFile(dir))
}) })
} }
buttons[preference.getInt("dl_location", 0)].isChecked = true view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
location_type.text = context.getString(R.string.settings_dl_location_custom)
setOnClickListener {
buttons.forEach { pair ->
pair.first.isChecked = false
}
button.performClick()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
dismiss()
} else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder()
.newDirectoryName("Pupil")
.allowNewDirectoryNameModification(true)
.build()
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
dismiss()
}
}
buttons.add(button to null)
})
val pref = Uri.parse(preference.getString("dl_location", null))
val index = externalFilesDirs.indexOfFirst {
Uri.fromFile(it).toString() == pref.toString()
}
if (index < 0)
buttons.last().first.isChecked = true
else
buttons[index].first.isChecked = true
setTitle(R.string.settings_dl_location) setTitle(R.string.settings_dl_location)
@@ -73,6 +123,8 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
dismiss() dismiss()
} }
super.onCreate(savedInstanceState)
} }
} }

View File

@@ -32,14 +32,16 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
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_gallery.*
import kotlinx.android.synthetic.main.gallery_details.view.* import kotlinx.android.synthetic.main.gallery_details.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.* import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Gallery 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.pupil.BuildConfig 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
@@ -48,6 +50,7 @@ import xyz.quaver.pupil.adapters.ThumbnailAdapter
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) { class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
@@ -64,7 +67,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_galleryblock) setContentView(R.layout.dialog_gallery)
window?.attributes.apply { window?.attributes.apply {
this ?: return@apply this ?: return@apply
@@ -183,6 +186,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
else -> tag.tag.wordCapitalize() else -> tag.tag.wordCapitalize()
} }
setEnsureMinTouchTargetSize(false)
setOnClickListener { setOnClickListener {
onChipClickedHandler.forEach { handler -> onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
@@ -219,9 +224,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
private fun addRelated(gallery: Gallery) { private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>() val galleries = ArrayList<GalleryBlock>()
val adapter = GalleryBlockAdapter(glide, galleries).apply { val adapter = GalleryBlockAdapter(context, galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler -> this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
@@ -232,11 +237,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
gallery.related.forEachIndexed { i, galleryID -> gallery.related.forEachIndexed { i, galleryID ->
async(Dispatchers.IO) { async(Dispatchers.IO) {
getGalleryBlock(galleryID) Cache(context).getGalleryBlock(galleryID)
}.let { }.let {
val galleryBlock = it.await() ?: return@let val galleryBlock = it.await() ?: return@let
galleries.add(Pair(galleryBlock, GlobalScope.async { galleryBlock.thumbnails.first() })) galleries.add(galleryBlock)
adapter.notifyItemInserted(i) adapter.notifyItemInserted(i)
} }
} }
@@ -252,14 +257,14 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
ItemClickSupport.addTo(this) ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, _ -> .setOnItemClickListener { _, position, _ ->
context.startActivity(Intent(context, ReaderActivity::class.java).apply { context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleries[position].first.id) putExtra("galleryID", galleries[position].id)
}) })
(context.applicationContext as Pupil).histories.add(galleries[position].first.id) (context.applicationContext as Pupil).histories.add(galleries[position].id)
} }
.setOnItemLongClickListener { _, position, _ -> .setOnItemLongClickListener { _, position, _ ->
GalleryDialog( GalleryDialog(
context, context,
galleries[position].first.id galleries[position].id
).apply { ).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) } this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }

View File

@@ -0,0 +1,97 @@
/*
* 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 androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.MirrorAdapter
class MirrorDialog(context: Context) : AlertDialog(context) {
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
var onMoveItem : ((Int, Int) -> (Unit))? = null
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
private lateinit var recyclerView: RecyclerView
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.settings_mirror_title)
setView(recyclerView)
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun initDialog() {
recyclerView = RecyclerView(context).apply recyclerview@{
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(context)
adapter = MirrorAdapter(context).apply adapter@{
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
onMoveItem = this@adapter.onItemMove
}).apply {
attachToRecyclerView(this@recyclerview)
}
onStartDrag = {
itemTouchHelper.startDrag(it)
}
onItemMoved = {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString("mirrors", it.joinToString(">"))
.apply()
}
}
}
}
}

View File

@@ -19,10 +19,12 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@@ -34,6 +36,7 @@ import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
@@ -41,7 +44,14 @@ import java.io.File
class SettingsFragment : class SettingsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
Preference.OnPreferenceClickListener, Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener { Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@@ -61,7 +71,7 @@ class SettingsFragment :
} }
} }
private fun getDirSize(dir: File) : String { private fun getDirSize(dir: DocumentFile) : String {
val size = dir.walk().map { it.length() }.sum() val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size)) return getString(R.string.settings_clear_summary, byteToString(size))
@@ -76,7 +86,7 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true) checkUpdate(activity as SettingsActivity, true)
} }
"delete_cache" -> { "delete_cache" -> {
val dir = File(context.cacheDir, "imageCache") val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
@@ -91,7 +101,7 @@ class SettingsFragment :
}.show() }.show()
} }
"delete_downloads" -> { "delete_downloads" -> {
val dir = getDownloadDirectory(context) val dir = getDownloadDirectory(context)!!
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
@@ -100,10 +110,6 @@ class SettingsFragment :
if (dir.exists()) if (dir.exists())
dir.deleteRecursively() dir.deleteRecursively()
val downloads = (activity!!.application as Pupil).downloads
downloads.clear()
summary = getDirSize(dir) summary = getDirSize(dir)
} }
setNegativeButton(android.R.string.no) { _, _ -> } setNegativeButton(android.R.string.no) { _, _ -> }
@@ -123,33 +129,28 @@ class SettingsFragment :
}.show() }.show()
} }
"dl_location" -> { "dl_location" -> {
DownloadLocationDialog(context).apply { DownloadLocationDialog(activity!!).show()
onDownloadLocationChangedListener = { value ->
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(key, value)
.apply()
summary = getDownloadDirectory(context).absolutePath
}
}.show()
} }
"default_query" -> { "default_query" -> {
DefaultQueryDialog(context).apply { DefaultQueryDialog(context).apply {
onPositiveButtonClickListener = { newTags -> onPositiveButtonClickListener = { newTags ->
sharedPreferences.edit().putString("default_query", newTags.toString()).apply() sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = newTags.toString() summary = newTags.toString()
dismiss() //This sucks
// TODO: make dialog dissmiss itself :P
} }
}.show() }.show()
} }
"app_lock" -> { "app_lock" -> {
val intent = Intent(context, LockActivity::class.java) val intent = Intent(context, LockActivity::class.java)
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK) activity?.startActivityForResult(intent, REQUEST_LOCK)
}
"mirrors" -> {
MirrorDialog(context)
.show()
} }
"backup" -> { "backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"), context,
true getDownloadDirectory(context)?.createFile("null", "favorites.json")!!
) )
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG) Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
@@ -161,7 +162,7 @@ class SettingsFragment :
type = "*/*" type = "*/*"
} }
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_RESTORE) activity?.startActivityForResult(intent, REQUEST_RESTORE)
} }
else -> return false else -> return false
} }
@@ -188,6 +189,15 @@ class SettingsFragment :
return true return true
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
"dl_location" -> {
findPreference<Preference>(key)?.summary =
FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
@@ -214,13 +224,13 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"delete_cache" -> { "delete_cache" -> {
val dir = File(context.cacheDir, "imageCache") val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
summary = getDirSize(dir) summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"delete_downloads" -> { "delete_downloads" -> {
val dir = getDownloadDirectory(context) val dir = getDownloadDirectory(context)!!
summary = getDirSize(dir) summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
@@ -232,7 +242,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"dl_location" -> { "dl_location" -> {
summary = getDownloadDirectory(context).absolutePath summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
@@ -259,6 +269,9 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"mirrors" -> {
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> { "dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }

View File

@@ -0,0 +1,24 @@
/*
* 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
const val REQUEST_LOCK = 38238
const val REQUEST_RESTORE = 16546
const val REQUEST_DOWNLOAD_FOLDER = 3874
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425

View File

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

View File

@@ -1,327 +0,0 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.util
import android.app.PendingIntent
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.*
import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule
class GalleryDownloader(
base: Context,
private val galleryID: Int,
_notify: Boolean = false
) : ContextWrapper(base) {
private val downloads = (applicationContext as Pupil).downloads
var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
var download: Boolean = false
set(value) {
if (value) {
field = true
notificationManager.notify(galleryID, notificationBuilder.build())
if (reader?.isActive == false && downloadJob?.isActive != true) {
val data = File(getDownloadDirectory(this), galleryID.toString())
val cache = File(cacheDir, "imageCache/$galleryID")
if (File(cache, "images").exists() && !data.exists()) {
cache.copyRecursively(data, true)
cache.deleteRecursively()
}
field = false
}
downloads.add(galleryID)
} else {
field = false
}
onNotifyChangedHandler?.invoke(value)
}
private val reader: Deferred<Reader?>?
private var downloadJob: Job? = null
private lateinit var notificationBuilder: NotificationCompat.Builder
private lateinit var notificationManager: NotificationManagerCompat
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
var onProgressHandler: ((Int) -> Unit)? = null
var onDownloadedHandler: ((List<String>) -> Unit)? = null
var onErrorHandler: ((Exception) -> Unit)? = null
var onCompleteHandler: (() -> Unit)? = null
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
companion object : SparseArray<GalleryDownloader>()
init {
put(galleryID, this)
initNotification()
reader = CoroutineScope(Dispatchers.IO).async {
try {
download = _notify
val json = Json(JsonConfiguration.Stable)
val serializer = Reader.serializer()
//Check cache
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
try {
json.parse(serializer, cache.readText())
} catch(e: Exception) {
cache.delete()
}
if (cache.exists()) {
val cached = json.parse(serializer, cache.readText())
if (cached.galleryInfo.isNotEmpty()) {
useHiyobi = cached.code == Reader.Code.HIYOBI
onReaderLoadedHandler?.invoke(cached)
return@async cached
}
}
//Cache doesn't exist. Load from internet
val reader = when {
useHiyobi -> {
try {
xyz.quaver.hiyobi.getReader(galleryID)
} catch(e: Exception) {
useHiyobi = false
getReader(galleryID)
}
}
else -> {
getReader(galleryID)
}
}
if (reader.galleryInfo.isNotEmpty()) {
//Save cache
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, reader))
}
reader
} catch (e: Exception) {
Crashlytics.logException(e)
onErrorHandler?.invoke(e)
null
}
}
}
fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await() ?: return@launch
val lowQuality = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader)
.getBoolean("low_quality", false)
notificationBuilder.setContentTitle(reader.title)
val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader)
notificationBuilder
.setProgress(reader.galleryInfo.size, 0, false)
.setContentText("0/${reader.galleryInfo.size}")
reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunk ->
chunk.mapIndexed { i, galleryInfo ->
val index = chunkIndex*4+i
async(Dispatchers.IO) {
val url = when(useHiyobi) {
true -> createImgList(galleryID, reader, lowQuality)[index].path
false -> when {
(!galleryInfo.hash.isNullOrBlank()) && (galleryInfo.haswebp == 1) && lowQuality ->
urlFromUrlFromHash(galleryID, galleryInfo, "webp")
else ->
urlFromUrlFromHash(galleryID, galleryInfo)
}
}
val name = "$index".padStart(4, '0')
val ext = url.split('.').last()
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "images/$name.$ext")
if (!cache.exists())
try {
with(URL(url).openConnection() as HttpsURLConnection) {
if (useHiyobi) {
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
} else
setRequestProperty("Referer", getReferer(galleryID))
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
inputStream.copyTo(FileOutputStream(cache))
}
} catch (e: Exception) {
cache.delete()
onErrorHandler?.invoke(e)
notificationBuilder
.setContentTitle(reader.title)
.setContentText(getString(R.string.reader_notification_error))
.setProgress(0, 0, false)
notificationManager.notify(galleryID, notificationBuilder.build())
}
"images/$name.$ext"
}
}.forEach {
list.add(it.await())
val index = list.size
onProgressHandler?.invoke(index)
notificationBuilder
.setProgress(reader.galleryInfo.size, index, false)
.setContentText("$index/${reader.galleryInfo.size}")
if (download)
notificationManager.notify(galleryID, notificationBuilder.build())
onDownloadedHandler?.invoke(list)
}
}
Timer(false).schedule(1000) {
notificationBuilder
.setContentTitle(reader.title)
.setContentText(getString(R.string.reader_notification_complete))
.setProgress(0, 0, false)
if (download) {
File(cacheDir, "imageCache/${galleryID}").let {
if (it.exists()) {
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
if (!target.exists())
target.mkdirs()
it.copyRecursively(target, true)
it.deleteRecursively()
}
}
notificationManager.notify(galleryID, notificationBuilder.build())
download = false
}
onCompleteHandler?.invoke()
}
remove(galleryID)
}
}
fun cancel() {
downloadJob?.cancel()
remove(galleryID)
}
suspend fun cancelAndJoin() {
downloadJob?.cancelAndJoin()
remove(galleryID)
}
fun invokeOnReaderLoaded() {
CoroutineScope(Dispatchers.Default).launch {
onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
}
}
fun clearNotification() {
notificationManager.cancel(galleryID)
}
fun invokeOnNotifyChanged() {
onNotifyChangedHandler?.invoke(download)
}
private fun initNotification() {
val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
notificationManager = NotificationManagerCompat.from(this)
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(android.R.drawable.stat_sys_download)
setContentIntent(pendingIntent)
setProgress(0, 0, true)
priority = NotificationCompat.PRIORITY_LOW
}
}
}

View File

@@ -0,0 +1,220 @@
/*
* 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.download
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.util.*
import java.io.File
import java.net.URL
class Cache(context: Context) : ContextWrapper(context) {
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
// Search in this order
// Download -> Cache
fun getCachedGallery(galleryID: Int) : DocumentFile? {
var file = getDownloadDirectory(this)?.findFile(galleryID.toString())
if (file?.exists() == true)
return file
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
return if (file.exists())
file
else
null
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
if (file?.exists() != true)
return null
return try {
Json.parse(file.readText(this))
} catch (e: Exception) {
//File corrupted
file.delete()
null
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
if (!it.exists())
it.mkdirs()
}).createFile("null", ".metadata") ?: return
file.writeText(this, Json.stringify(metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val thumbnail = if (metadata?.thumbnail == null)
withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
try {
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
} catch (e: Exception) {
null
}
}
else
metadata.thumbnail
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
)
return thumbnail
}
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val galleryBlock = if (metadata?.galleryBlock == null)
listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
).map {
CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching {
it.invoke()
}.getOrNull()
}
}.awaitAll().filterNotNull()
else
metadata.galleryBlock
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return galleryBlock.firstOrNull {
mirrors.contains(it.code.name)
} ?: galleryBlock.firstOrNull()
}
fun getReaderOrNull(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return metadata?.readers?.firstOrNull {
mirrors.contains(it.code.name)
} ?: metadata?.readers?.firstOrNull()
}
suspend fun getReader(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
val readers = if (metadata?.readers == null) {
listOf(
{ xyz.quaver.hitomi.getReader(galleryID) },
{ xyz.quaver.hiyobi.getReader(galleryID) }
).map {
CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching {
it.invoke()
}.getOrNull()
}
}.awaitAll().filterNotNull()
} else {
metadata.readers
}
if (readers.isNotEmpty())
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers)
)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return readers.firstOrNull {
mirrors.contains(it.code.name)
} ?: readers.firstOrNull()
}
fun getImages(galleryID: Int): List<DocumentFile?>? {
val gallery = getCachedGallery(galleryID) ?: return null
val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles()
return reader.galleryInfo.indices.map { index ->
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true }
}
}
fun putImage(galleryID: Int, name: String, data: ByteArray) {
val cache = getCachedGallery(galleryID) ?:
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
if (!it.exists())
it.mkdirs()
}) ?: return
if (!Regex("""^[0-9]+.+$""").matches(name))
throw IllegalArgumentException("File name is not a number")
cache.createFile("null", name)?.writeBytes(this, data)
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID)
if (cache != null) {
val download = getDownloadDirectory(this)!!
if (!download.isParentOf(cache)) {
cache.copyRecursively(this, download)
cache.deleteRecursively()
}
} else
getDownloadDirectory(this)?.createDirectory(galleryID.toString())
}
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
fun setDownloading(galleryID: Int, isDownloading: Boolean) {
setCachedMetadata(galleryID, Metadata(getCachedMetadata(galleryID), isDownloading = isDownloading))
}
}

View File

@@ -0,0 +1,388 @@
/*
* 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.download
import android.app.PendingIntent
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.SharedPreferences
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.*
import okhttp3.*
import okio.*
import xyz.quaver.Code
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
@UseExperimental(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
//region ProgressListener
@Suppress("UNCHECKED_CAST")
private val progressListener = object: ProgressListener {
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
}
}
interface ProgressListener {
fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
}
class ProgressResponseBody(
val tag: Any?,
val responseBody: ResponseBody,
val progressListener : ProgressListener
) : ResponseBody() {
private var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType() ?: null
override fun source(): BufferedSource {
if (bufferedSource == null)
bufferedSource = Okio.buffer(source(responseBody.source()))
return bufferedSource!!
}
private fun source(source: Source) = object: ForwardingSource(source) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
progressListener.update(tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
return bytesRead
}
}
}
//endregion
//region Singleton
companion object {
@Volatile private var instance: DownloadWorker? = null
fun getInstance(context: Context) =
instance ?: synchronized(this) {
instance ?: DownloadWorker(context).also { instance = it }
}
}
//endregion
val notificationManager = NotificationManagerCompat.from(context)
val queue = LinkedBlockingQueue<Int>()
/*
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress
* null -> Loading / Gallery doesn't exist
* SECONDARY VALUE
* 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed
* Float.NaN -> Exception
*/
val progress = SparseArray<MutableList<Float>?>()
/*
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress / Loading
* null -> Gallery doesn't exist
* SECONDARY VALUE
* Throwable -> Exception
* null -> Download in progress / Loading
*/
val exception = SparseArray<MutableList<Throwable?>?>()
val notification = SparseArray<NotificationCompat.Builder>()
private val loop = loop()
private val worker = SparseArray<Job?>()
@Volatile var nRunners = 0
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request()
var response = chain.proceed(request)
var retry = preferences.getInt("retry", 3)
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build()
}
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor()))
.build()
fun stop() {
queue.clear()
loop.cancel()
for (i in 0..worker.size()) {
val galleryID = worker.keyAt(i)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
worker[galleryID]?.cancel()
}
client.dispatcher().cancelAll()
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()
}
progress.remove(galleryID)
exception.remove(galleryID)
notification.remove(galleryID)
notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0) {
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
}
}
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
val cache = Cache(this@DownloadWorker).getImages(galleryID)
val lowQuality = preferences.getBoolean("low_quality", false)
//Cache exists :P
cache?.get(index)?.let {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
nRunners--
}
return
}
val request = Request.Builder().apply {
when (reader.code) {
Code.HITOMI -> {
url(
urlFromUrlFromHash(
galleryID,
reader.galleryInfo[index],
if (lowQuality) "webp" else null
)
)
addHeader("Referer", getReferer(galleryID))
}
Code.HIYOBI -> {
url(createImgList(galleryID, reader, lowQuality)[index].path)
addHeader("User-Agent", user_agent)
addHeader("Cookie", cookie)
}
else -> {
//shouldn't be called anyway
}
}
tag(galleryID to index)
}.build()
client.newCall(request).enqueue(callback)
}
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
val reader = Cache(this@DownloadWorker).getReader(galleryID)
//gallery doesn't exist
if (reader == null) {
progress.put(galleryID, null)
exception.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
return@launch
}
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
notification[galleryID].setContentTitle(reader.title)
notify(galleryID)
for (i in reader.galleryInfo.indices) {
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
if (Fabric.isInitialized())
Crashlytics.logException(e)
progress[galleryID]?.set(i, Float.NaN)
exception[galleryID]?.set(i, e)
notify(galleryID)
if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker)
if (cache.isDownloading(galleryID)) {
cache.moveToDownload(galleryID)
cache.setDownloading(galleryID, false)
}
nRunners--
}
}
override fun onResponse(call: Call, response: Response) {
response.body().use {
val res = it.bytes()
val ext =
call.request().url().encodedPath().split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
}
notify(galleryID)
if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker)
if (cache.isDownloading(galleryID)) {
cache.moveToDownload(galleryID)
cache.setDownloading(galleryID, false)
}
nRunners--
}
}
}
queueDownload(galleryID, reader, i, callback)
}
}
private fun notify(galleryID: Int) {
val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
if (isCompleted(galleryID))
notification[galleryID]
?.setContentText(getString(R.string.reader_notification_complete))
?.setProgress(0, 0, false)
else
notification[galleryID]
?.setProgress(max, progress, false)
?.setContentText("$progress/$max")
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
notificationManager.notify(galleryID, notification[galleryID].build())
else
notificationManager.cancel(galleryID)
}
private fun initNotification(galleryID: Int) {
val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text))
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)
})
}
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
continue
val galleryID = queue.poll() ?: continue
if (progress.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++
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.download
import kotlinx.serialization.Serializable
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
@Serializable
data class Metadata(
val thumbnail: String? = null,
val galleryBlock: List<GalleryBlock>? = null,
val readers: List<Reader>? = null,
val isDownloading: Boolean? = null
) {
constructor(
metadata: Metadata?,
thumbnail: String? = null,
galleryBlock: List<GalleryBlock>? = null,
readers: List<Reader>? = null,
isDownloading: Boolean? = null
) : this(
thumbnail ?: metadata?.thumbnail,
galleryBlock ?: metadata?.galleryBlock,
readers ?: metadata?.readers,
isDownloading ?: metadata?.isDownloading
)
}

View File

@@ -19,28 +19,32 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import java.nio.charset.Charset
import java.util.*
fun getCachedGallery(context: Context, galleryID: Int): File { fun getCachedGallery(context: Context, galleryID: Int) =
return File(getDownloadDirectory(context), galleryID.toString()).let { getDownloadDirectory(context)?.findFile(galleryID.toString()) ?:
when { DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
it.exists() -> it
else -> File(context.cacheDir, "imageCache/$galleryID") fun getDownloadDirectory(context: Context) : DocumentFile? {
} val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
Uri.parse(it)
} }
return if (uri.toString().startsWith("file"))
DocumentFile.fromFile(File(uri.path!!))
else
DocumentFile.fromTreeUri(context, uri)
} }
fun getDownloadDirectory(context: Context): File { fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
val dlLocation = PreferenceManager.getDefaultSharedPreferences(context).getInt("dl_location", 0) context.contentResolver.openOutputStream(to.uri).use { out ->
out!!
return ContextCompat.getExternalFilesDirs(context, null)[dlLocation]
}
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
to.outputStream().use { out ->
with(openConnection()) { with(openConnection()) {
val fileSize = contentLength.toLong() val fileSize = contentLength.toLong()
@@ -49,6 +53,7 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
var bytesCopied: Long = 0 var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE) val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = it.read(buffer) var bytes = it.read(buffer)
while (bytes >= 0) { while (bytes >= 0) {
out.write(buffer, 0, bytes) out.write(buffer, 0, bytes)
@@ -62,3 +67,70 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
} }
} }
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
var parent = file?.parentFile
while (parent != null) {
if (this.uri.path == parent.uri.path)
return true
parent = parent.parentFile
}
return false
}
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
fun DocumentFile.copyRecursively(
context: Context,
target: DocumentFile
) {
if (!exists())
throw Exception("The source file doesn't exist.")
if (this.isFile)
target.createFile("null", name!!)!!.writeBytes(
context,
readBytes(context)
)
else if (this.isDirectory) {
target.createDirectory(name!!).also { newTarget ->
listFiles().forEach { child ->
child.copyRecursively(context, newTarget!!)
}
}
}
}
fun DocumentFile.deleteRecursively() {
if (this.isDirectory)
listFiles().forEach {
it.deleteRecursively()
}
this.delete()
}
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
if (state.isEmpty())
state.push(this)
listFiles().forEach {
state.push(it)
if (it.isDirectory) {
it.walk(state)
}
}
return state
}
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())

View File

@@ -21,27 +21,20 @@ package xyz.quaver.pupil.util
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.internal.EnumSerializer
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import xyz.quaver.availableInHiyobi
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import java.io.File
import java.net.URL import java.net.URL
import java.util.* import java.util.*
@@ -153,10 +146,10 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
} }
CoroutineScope(Dispatchers.IO).launch io@{ CoroutineScope(Dispatchers.IO).launch io@{
val target = File(getDownloadDirectory(context), "Pupil.apk") val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
try { try {
URL(url).download(target) { progress, fileSize -> URL(url).download(context, target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false) builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build()) notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
} }
@@ -175,15 +168,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val install = Intent(Intent.ACTION_VIEW).apply { val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(FileProvider.getUriForFile( setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
context,
context.applicationContext.packageName + ".fileprovider",
target
), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
if (resolveActivity(context.packageManager) == null)
setDataAndType(Uri.fromFile(target),
MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
} }
builder.apply { builder.apply {
@@ -215,58 +200,3 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
} }
} }
} }
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
try {
Json(JsonConfiguration.Stable).parseJson(readerFile.readText())
.jsonObject.let { reader ->
if (!reader.contains("code"))
oldGallery.add(gallery)
}
} catch (e: Exception) {
// do nothing
}
}
}
}
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.toIntOrNull() ?: return@forEach)
-> json.toJson(codeSerializer, Reader.Code.HIYOBI)
else -> json.toJson(codeSerializer, Reader.Code.HITOMI)
}
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#fff" <vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> <path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>

View File

@@ -0,0 +1,8 @@
<!-- drawable/menu.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
</vector>

View File

@@ -0,0 +1,34 @@
<?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/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp">
<shape android:shape="rectangle" >
<stroke
android:width="1dp"
android:color="#555555" />
<solid android:color="@color/transparent" />
</shape>
</item>
</layer-list>

View File

@@ -18,21 +18,12 @@
--> -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"> android:padding="16dp">
<TextView
android:id="@+id/default_query_dialog_title"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/default_query_dialog_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<EditText <EditText
tools:ignore="Autofill" tools:ignore="Autofill"
android:inputType="text" android:inputType="text"
@@ -40,7 +31,7 @@
android:id="@+id/default_query_dialog_edittext" android:id="@+id/default_query_dialog_edittext"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_title" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
@@ -116,14 +107,4 @@
</LinearLayout> </LinearLayout>
<Button
android:id="@+id/default_query_dialog_ok"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@android:string/ok"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -35,6 +35,6 @@
android:id="@+id/gallery_details_tags" android:id="@+id/gallery_details_tags"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipSpacingVertical="8dp"/> app:chipSpacingVertical="4dp"/>
</LinearLayout> </LinearLayout>

View File

@@ -177,7 +177,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:chipSpacing="2dp" app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding" app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"

View File

@@ -0,0 +1,49 @@
<?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="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingStart="32dp"
android:paddingLeft="32dp"
android:paddingEnd="32dp"
android:paddingRight="32dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/mirror_name"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/mirror_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/menu"
app:tint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -36,7 +36,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
app:srcCompat="@drawable/ic_navigate_next_black_24dp" app:srcCompat="@drawable/ic_navigate_next_black_24dp"
android:tint="@color/colorAccent" app:tint="@color/colorAccent"
android:rotation="180"/> android:rotation="180"/>
<TextView <TextView

View File

@@ -36,7 +36,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
app:srcCompat="@drawable/ic_navigate_next_black_24dp" app:srcCompat="@drawable/ic_navigate_next_black_24dp"
android:tint="@color/colorAccent" app:tint="@color/colorAccent"
android:rotation="180"/> android:rotation="180"/>
<TextView <TextView

View File

@@ -17,9 +17,55 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>. ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<com.github.chrisbanes.photoview.PhotoView xmlns:android="http://schemas.android.com/apk/res/android"
android:contentDescription="@string/reader_imageview_description" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="100dp" xmlns:app="http://schemas.android.com/apk/res-auto">
android:paddingBottom="8dp"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/reader_item_boundary">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ProgressBar
android:id="@+id/reader_item_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:progressBarStyleHorizontal"
android:indeterminate="false"
android:progress="0"
android:max="100"
android:visibility="visible"/>
<TextView
android:id="@+id/reader_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Caption"/>
</LinearLayout>
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -25,11 +25,9 @@
android:icon="@drawable/avd_star" android:icon="@drawable/avd_star"
app:showAsAction="always"/> app:showAsAction="always"/>
<item android:id="@+id/reader_menu_use_hiyobi" <item android:id="@+id/reader_type"
android:title="" android:title=""
android:icon="@drawable/ic_hiyobi" app:showAsAction="ifRoom"/>
app:showAsAction="ifRoom"
android:visible="false"/>
<item android:id="@+id/reader_menu_page_indicator" <item android:id="@+id/reader_menu_page_indicator"
android:title="@string/page_indicator_placeholder" android:title="@string/page_indicator_placeholder"

View File

@@ -18,7 +18,7 @@
<string name="update_title">新しいアップデートがあります</string> <string name="update_title">新しいアップデートがあります</string>
<string name="warning">注意</string> <string name="warning">注意</string>
<string name="settings_miscellaneous_title">その他</string> <string name="settings_miscellaneous_title">その他</string>
<string name="settings_use_hiyobi_title">hiyobi.meからロード</string> <string name="settings_mirror_title">ミラーサーバー</string>
<string name="settings_clear_history">履歴を削除</string> <string name="settings_clear_history">履歴を削除</string>
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string> <string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string> <string name="settings_clear_history_summary">履歴数: %1$d</string>
@@ -61,10 +61,10 @@
<string name="main_export_error">エクスポートエラーが発生しました</string> <string name="main_export_error">エクスポートエラーが発生しました</string>
<string name="settings_clear_downloads">ダウンロード削除</string> <string name="settings_clear_downloads">ダウンロード削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string> <string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string>
<string name="settings_use_hiyobi_summary">ロード速度を向上させるため可能であればhiyobi.meからイメージロード</string> <string name="settings_mirror_summary">ミラーサーバからイメージロード</string>
<string name="main_drawer_favorite">お気に入り</string> <string name="main_drawer_favorite">お気に入り</string>
<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="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string> <string name="settings_storage">ストレージ</string>
<string name="main_drawer_grouop_contact_discord">ディスコード</string> <string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string> <string name="settings_app_lock">アプリロック</string>
@@ -120,4 +120,6 @@
<string name="settings_app_version_description">v%s</string> <string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string> <string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string> <string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_dl_location_custom">手動で設定</string>
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
</resources> </resources>

View File

@@ -18,7 +18,6 @@
<string name="main_no_result">결과 없음</string> <string name="main_no_result">결과 없음</string>
<string name="main_search">검색</string> <string name="main_search">검색</string>
<string name="settings_miscellaneous_title">기타</string> <string name="settings_miscellaneous_title">기타</string>
<string name="settings_use_hiyobi_title">hiyobi.me 사용</string>
<string name="settings_clear_history">기록 삭제</string> <string name="settings_clear_history">기록 삭제</string>
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string> <string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string> <string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
@@ -61,10 +60,9 @@
<string name="main_export_error">내보내기 오류가 발생했습니다</string> <string name="main_export_error">내보내기 오류가 발생했습니다</string>
<string name="settings_clear_downloads">다운로드 삭제</string> <string name="settings_clear_downloads">다운로드 삭제</string>
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string> <string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
<string name="settings_use_hiyobi_summary">속도 향상을 위해 가능하면 hiyobi.me에서 이미지 로드</string>
<string name="main_drawer_favorite">즐겨찾기</string> <string name="main_drawer_favorite">즐겨찾기</string>
<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="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
<string name="settings_storage">저장 공간</string> <string name="settings_storage">저장 공간</string>
<string name="main_drawer_grouop_contact_discord">디스코드</string> <string name="main_drawer_grouop_contact_discord">디스코드</string>
<string name="settings_app_lock">앱 잠금</string> <string name="settings_app_lock">앱 잠금</string>
@@ -120,4 +118,8 @@
<string name="settings_app_version_description">v%s</string> <string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">저해상도 이미지</string> <string name="settings_low_quality">저해상도 이미지</string>
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string> <string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
<string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</string>
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
</resources> </resources>

View File

@@ -57,4 +57,9 @@
<item>japanese|日本語</item> <item>japanese|日本語</item>
</string-array> </string-array>
<string-array name="mirrors">
<item>HITOMI|hitomi.la</item>
<item>HIYOBI|hiyobi.me</item>
</string-array>
</resources> </resources>

View File

@@ -60,7 +60,7 @@
<string name="main_jump_title">Jump to page</string> <string name="main_jump_title">Jump to page</string>
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string> <string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
<string name="main_open_gallery_by_id">Open Gallery by ID</string> <string name="main_open_gallery_by_id">Open Gallery by ID</string>
<string name="main_open_gallery_by_id_error">Failed to open gallery</string> <string name="reader_failed_to_find_gallery">Failed to open gallery</string>
<string name="main_move">Move to page %1$d</string> <string name="main_move">Move to page %1$d</string>
@@ -140,6 +140,8 @@
<string name="settings_dl_location_removable">Removable Storage</string> <string name="settings_dl_location_removable">Removable Storage</string>
<string name="settings_dl_location_internal">Internal Storage</string> <string name="settings_dl_location_internal">Internal Storage</string>
<string name="settings_dl_location_available">%s available</string> <string name="settings_dl_location_available">%s available</string>
<string name="settings_dl_location_custom">Custom Location</string>
<string name="settings_dl_location_not_writable">This folder is not writable. Please select another folder.</string>
<string name="settings_low_quality">Low quality images</string> <string name="settings_low_quality">Low quality images</string>
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string> <string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
@@ -151,8 +153,7 @@
<!-- SETTINGS/MISCELLANEOUS --> <!-- SETTINGS/MISCELLANEOUS -->
<string name="settings_miscellaneous_title">Miscellaneous</string> <string name="settings_miscellaneous_title">Miscellaneous</string>
<string name="settings_use_hiyobi_title">Use hiyobi.me</string> <string name="settings_mirror_summary">Load images from mirrors</string>
<string name="settings_use_hiyobi_summary">Load images from hiyobi.me to improve loading speed (if available)</string>
<string name="settings_security_mode_title">Enable security mode</string> <string name="settings_security_mode_title">Enable security mode</string>
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string> <string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
<string name="settings_dark_mode_title">Dark mode</string> <string name="settings_dark_mode_title">Dark mode</string>
@@ -186,5 +187,6 @@
<string name="default_query_dialog_filter_BL">Filter BL</string> <string name="default_query_dialog_filter_BL">Filter BL</string>
<string name="default_query_dialog_filter_guro">Filter Guro</string> <string name="default_query_dialog_filter_guro">Filter Guro</string>
<string name="default_query_dialog_language_selector_none">Any</string> <string name="default_query_dialog_language_selector_none">Any</string>
<string name="settings_mirror_title">Mirrors</string>
</resources> </resources>

View File

@@ -66,10 +66,10 @@
<PreferenceCategory <PreferenceCategory
app:title="@string/settings_miscellaneous_title"> app:title="@string/settings_miscellaneous_title">
<SwitchPreferenceCompat <Preference
app:key="use_hiyobi" app:key="mirrors"
app:title="@string/settings_use_hiyobi_title" app:title="@string/settings_mirror_title"
app:summary="@string/settings_use_hiyobi_summary"/> app:summary="@string/settings_mirror_summary"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="security_mode" app:key="security_mode"

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@file:Suppress("UNUSED_VARIABLE") @file:Suppress("UNUSED_VARIABLE", "IncorrectScope")
package xyz.quaver.pupil package xyz.quaver.pupil
@@ -26,20 +26,16 @@ package xyz.quaver.pupil
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
import android.util.SparseArray
import org.junit.Test import org.junit.Test
import xyz.quaver.pupil.util.download
import java.io.File
import java.net.URL
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
URL("https://github.om/tom5079/Pupil/releases/download/4.2-beta2-hotfix2/Pupil-v4.2-beta2-hotfix2.apk").download( val arr = SparseArray<Float>()
File(System.getenv("USERPROFILE"), "Pupil.apk")
) { downloaded, fileSize -> print(arr.indexOfKey(34))
println("%.1f%%".format(downloaded*100.0/fileSize))
}
} }
} }

View File

@@ -15,7 +15,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.3.3' 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.31.0'
classpath 'com.google.firebase:perf-plugin:1.3.1' classpath 'com.google.firebase:perf-plugin:1.3.1'
} }
} }
@@ -25,6 +25,7 @@ allprojects {
google() google()
jcenter() jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url 'http://guardian.github.com/maven/repo-releases' }
} }
} }

View File

@@ -1,18 +1,17 @@
# Project-wide Gradle settings. ## For more details on how to configure your build environment visit
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m # Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete": #Thu Jan 30 12:29:48 KST 2020
kotlin.code.style=official kotlin.code.style=official
android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
android.useAndroidX=true

View File

@@ -5,10 +5,10 @@ apply plugin: 'kotlinx-serialization'
dependencies { 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.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.jsoup:jsoup:1.12.1'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
} }
sourceCompatibility = "7" sourceCompatibility = "7"

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2020 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
enum class Code {
HITOMI,
HIYOBI,
SORALA
}

View File

@@ -16,9 +16,11 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URLDecoder import java.net.URLDecoder
@Serializable
data class Gallery( data class Gallery(
val related: List<Int>, val related: List<Int>,
val langList: List<Pair<String, String>>, val langList: List<Pair<String, String>>,

View File

@@ -18,6 +18,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -67,6 +68,7 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
@Serializable @Serializable
data class GalleryBlock( data class GalleryBlock(
val code: Code,
val id: Int, val id: Int,
val galleryUrl: String, val galleryUrl: String,
val thumbnails: List<String>, val thumbnails: List<String>,
@@ -102,7 +104,7 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
href.slice(5 until href.indexOf("-all")) href.slice(5 until href.indexOf("-all"))
} }
return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags) return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }

View File

@@ -18,6 +18,7 @@ package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html" fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
@@ -31,13 +32,7 @@ data class GalleryInfo(
) )
@Serializable @Serializable
data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>) { 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 {
@@ -45,5 +40,5 @@ fun getReader(galleryID: Int) : Reader {
val doc = Jsoup.connect(readerUrl).get() val doc = Jsoup.connect(readerUrl).get()
return Reader(Reader.Code.HITOMI, doc.title(), getGalleryInfo(galleryID)) return Reader(Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
} }

View File

@@ -26,7 +26,7 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : List<Int> {
val terms = query val terms = query
.trim() .trim()
.replace(Regex("""^\?"""), "") .replace(Regex("""^\?"""), "")
.toLowerCase() .toLowerCase(Locale.US)
.split(Regex("\\s+")) .split(Regex("\\s+"))
.map { .map {
it.replace('_', ' ') it.replace('_', ' ')

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2020 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.hiyobi
import org.jsoup.Jsoup
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.protocol
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
val url = "$protocol//$hiyobi/search/$galleryID"
try {
val doc = Jsoup.connect(url).get()
val galleryBlock = doc.selectFirst(".gallery-content")
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
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 language = "korean"
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) {
return null
}
}

View File

@@ -19,6 +19,7 @@ package xyz.quaver.hiyobi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.list import kotlinx.serialization.list
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryInfo import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.protocol import xyz.quaver.hitomi.protocol
@@ -76,7 +77,7 @@ fun getReader(galleryID: Int) : Reader {
} }
) )
return Reader(Reader.Code.HIYOBI, title, galleryInfo) return Reader(Code.HIYOBI, title, galleryInfo)
} }
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) = fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =

View File

@@ -102,4 +102,11 @@ class UnitTest {
print(result) print(result)
} }
@Test
fun test_hiyobi_galleryBlock() {
val galleryBlock = xyz.quaver.hiyobi.getGalleryBlock(10000027)
print(galleryBlock)
}
} }