@@ -29,6 +29,7 @@ dependencies {
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"
|
||||
@@ -40,7 +41,10 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
implementation 'com.shawnlin:number-picker:2.4.8'
|
||||
implementation 'com.github.clans:fab:1.6.4'
|
||||
implementation('com.finotes:finotescore:2.5.7@aar') {
|
||||
transitive = true
|
||||
}
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
|
||||
9
app/proguard-rules.pro
vendored
@@ -18,4 +18,11 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keep class com.finotes.android.finotescore.* { *; }
|
||||
|
||||
-keepclassmembers class * {
|
||||
@com.finotes.android.finotescore.annotation.Observe *;
|
||||
}
|
||||
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
@@ -11,7 +11,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
android:name=".Pupil">
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -24,7 +25,8 @@
|
||||
</provider>
|
||||
|
||||
<activity android:name=".ReaderActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"/>
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"/>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/settings_title" />
|
||||
|
||||
@@ -6,9 +6,10 @@ import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.text.*
|
||||
import android.text.style.AlignmentSpan
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
@@ -21,19 +22,20 @@ import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||
import kotlinx.android.synthetic.main.dialog_galleryblock.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.content
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.hitomi.*
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.types.TagSuggestion
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.SetLineOverlap
|
||||
import xyz.quaver.pupil.util.checkUpdate
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.collections.ArrayList
|
||||
@@ -44,6 +46,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var query = ""
|
||||
|
||||
private val SETTINGS = 45162
|
||||
|
||||
private var galleryIDs: Deferred<List<Int>>? = null
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
@@ -125,8 +129,8 @@ class MainActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
|
||||
setupRecyclerView()
|
||||
setupSearchBar()
|
||||
setupRecyclerView()
|
||||
fetchGalleries(query)
|
||||
loadBlocks()
|
||||
}
|
||||
@@ -134,6 +138,17 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onBackPressed() {
|
||||
if (main_drawer_layout.isDrawerOpen(GravityCompat.START))
|
||||
main_drawer_layout.closeDrawer(GravityCompat.START)
|
||||
else if (query.isNotEmpty()) {
|
||||
runOnUiThread {
|
||||
query = ""
|
||||
findViewById<SearchInputView>(R.id.search_bar_text).setText(query, TextView.BufferType.EDITABLE)
|
||||
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query)
|
||||
loadBlocks()
|
||||
}
|
||||
}
|
||||
else
|
||||
super.onBackPressed()
|
||||
}
|
||||
@@ -150,6 +165,20 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when(requestCode) {
|
||||
SETTINGS -> {
|
||||
runOnUiThread {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query)
|
||||
loadBlocks()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUpdate() {
|
||||
|
||||
fun extractReleaseNote(update: JsonObject, locale: String) : String {
|
||||
@@ -216,7 +245,20 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
with(main_recyclerview) {
|
||||
adapter = GalleryBlockAdapter(galleries)
|
||||
adapter = GalleryBlockAdapter(galleries).apply {
|
||||
onChipClickedHandler.add {
|
||||
post {
|
||||
query = it.toQuery()
|
||||
this@MainActivity.findViewById<SearchInputView>(R.id.search_bar_text)
|
||||
.setText(query, TextView.BufferType.EDITABLE)
|
||||
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query)
|
||||
loadBlocks()
|
||||
}
|
||||
}
|
||||
}
|
||||
addOnScrollListener(
|
||||
object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
@@ -230,17 +272,63 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
)
|
||||
ItemClickSupport.addTo(this).setOnItemClickListener { _, position, _ ->
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||
val gallery = galleries[position].first
|
||||
intent.putExtra("GALLERY_ID", gallery.id)
|
||||
intent.putExtra("GALLERY_TITLE", gallery.title)
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, position, _ ->
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||
val gallery = galleries[position].first
|
||||
intent.putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), gallery))
|
||||
|
||||
//TODO: Maybe sprinke some transitions will be nice :D
|
||||
startActivity(intent)
|
||||
//TODO: Maybe sprinke some transitions will be nice :D
|
||||
startActivity(intent)
|
||||
|
||||
Histories.default.add(gallery.id)
|
||||
}
|
||||
Histories.default.add(gallery.id)
|
||||
}.setOnItemLongClickListener { recyclerView, position, v ->
|
||||
val galleryBlock = galleries[position].first
|
||||
val view = LayoutInflater.from(this@MainActivity)
|
||||
.inflate(R.layout.dialog_galleryblock, recyclerView, false)
|
||||
|
||||
val dialog = AlertDialog.Builder(this@MainActivity).apply {
|
||||
setView(view)
|
||||
}.create()
|
||||
|
||||
with(view.main_dialog_download) {
|
||||
text = when(GalleryDownloader.get(galleryBlock.id)) {
|
||||
null -> getString(R.string.reader_fab_download)
|
||||
else -> getString(R.string.reader_fab_download_cancel)
|
||||
}
|
||||
isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(galleryBlock.id, false)
|
||||
setOnClickListener {
|
||||
val downloader = GalleryDownloader.get(galleryBlock.id)
|
||||
if (downloader == null) {
|
||||
GalleryDownloader(context, galleryBlock, true).start()
|
||||
Histories.default.add(galleryBlock.id)
|
||||
} else {
|
||||
downloader.cancel()
|
||||
downloader.clearNotification()
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
view.main_dialog_delete.setOnClickListener {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
with(GalleryDownloader[galleryBlock.id]) {
|
||||
this?.cancelAndJoin()
|
||||
this?.clearNotification()
|
||||
}
|
||||
val cache = File(cacheDir, "imageCache/${galleryBlock.id}/images/")
|
||||
cache.deleteRecursively()
|
||||
|
||||
dialog.dismiss()
|
||||
(adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +356,7 @@ class MainActivity : AppCompatActivity() {
|
||||
with(main_searchview as FloatingSearchView) {
|
||||
setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
|
||||
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), SETTINGS)
|
||||
R.id.main_menu_search -> setSearchFocused(true)
|
||||
}
|
||||
}
|
||||
@@ -378,7 +466,12 @@ class MainActivity : AppCompatActivity() {
|
||||
private fun clearGalleries() {
|
||||
galleries.clear()
|
||||
|
||||
main_recyclerview.adapter?.notifyDataSetChanged()
|
||||
with(main_recyclerview.adapter as GalleryBlockAdapter?) {
|
||||
this ?: return@with
|
||||
|
||||
this.completeFlag.clear()
|
||||
this.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
main_noresult.visibility = View.INVISIBLE
|
||||
main_progressbar.show()
|
||||
@@ -436,25 +529,48 @@ class MainActivity : AppCompatActivity() {
|
||||
galleryIDs
|
||||
else ->
|
||||
galleryIDs.slice(galleries.size until Math.min(galleries.size+perPage, galleryIDs.size))
|
||||
}.chunked(4).let { chunks ->
|
||||
}.chunked(5).let { chunks ->
|
||||
for (chunk in chunks)
|
||||
chunk.map {
|
||||
async {
|
||||
try {
|
||||
val galleryBlock = getGalleryBlock(it)
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = GalleryBlock.serializer()
|
||||
|
||||
val galleryBlock =
|
||||
File(cacheDir, "imageCache/$it/galleryBlock.json").let { cache ->
|
||||
when {
|
||||
cache.exists() -> json.parse(serializer, cache.readText())
|
||||
else -> {
|
||||
getGalleryBlock(it).apply {
|
||||
this ?: return@apply
|
||||
|
||||
if (!cache.parentFile.exists())
|
||||
cache.parentFile.mkdirs()
|
||||
|
||||
cache.writeText(json.stringify(serializer, this))
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: return@async null
|
||||
|
||||
val thumbnail = async {
|
||||
val cache = File(cacheDir, "imageCache/$it/thumbnail.${galleryBlock.thumbnails[0].path.split('.').last()}")
|
||||
val ext = galleryBlock.thumbnails[0].split('.').last()
|
||||
File(cacheDir, "imageCache/$it/thumbnail.$ext").apply {
|
||||
val cache = this
|
||||
|
||||
if (!cache.exists())
|
||||
with(galleryBlock.thumbnails[0].openConnection() as HttpsURLConnection) {
|
||||
if (!cache.parentFile.exists())
|
||||
cache.parentFile.mkdirs()
|
||||
if (!cache.exists())
|
||||
try {
|
||||
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
|
||||
if (!cache.parentFile.exists())
|
||||
cache.parentFile.mkdirs()
|
||||
|
||||
inputStream.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
|
||||
cache.absolutePath
|
||||
inputStream.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cache.delete()
|
||||
}
|
||||
}.absolutePath
|
||||
}
|
||||
|
||||
Pair(galleryBlock, thumbnail)
|
||||
@@ -463,16 +579,19 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}.forEach {
|
||||
val galleryBlock = it.await() ?: return@forEach
|
||||
val galleryBlock = it.await()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
main_progressbar.hide()
|
||||
|
||||
galleries.add(galleryBlock)
|
||||
main_recyclerview.adapter?.notifyItemInserted(galleries.size - 1)
|
||||
if (galleryBlock != null) {
|
||||
galleries.add(galleryBlock)
|
||||
|
||||
main_recyclerview.adapter?.notifyItemInserted(galleries.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
app/src/main/java/xyz/quaver/pupil/Pupil.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.SparseArray
|
||||
import com.finotes.android.finotescore.Fn
|
||||
import com.finotes.android.finotescore.ObservableApplication
|
||||
import com.finotes.android.finotescore.Severity
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class Pupil : ObservableApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
super.onCreate()
|
||||
Fn.init(this)
|
||||
|
||||
Fn.enableFrameDetection()
|
||||
|
||||
if (!preference.getBoolean("channel_created", false)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_download_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
preference.edit().putBoolean("channel_created", true).apply()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
@@ -7,32 +8,37 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||
import xyz.quaver.pupil.util.GalleryDownloader
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
private val images = ArrayList<String>()
|
||||
private var galleryID = 0
|
||||
private var gallerySize: Int = 0
|
||||
private var currentPage: Int = 0
|
||||
private lateinit var reader: Deferred<Reader>
|
||||
private var loadJob: Job? = null
|
||||
private lateinit var galleryBlock: GalleryBlock
|
||||
private var gallerySize = 0
|
||||
private var currentPage = 0
|
||||
|
||||
private lateinit var snapHelper: PagerSnapHelper
|
||||
private var isScroll = true
|
||||
private var isFullscreen = false
|
||||
|
||||
private lateinit var downloader: GalleryDownloader
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
||||
private var menu: Menu? = null
|
||||
|
||||
@@ -45,49 +51,20 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
supportActionBar?.title = intent.getStringExtra("GALLERY_TITLE")
|
||||
galleryBlock = Json(JsonConfiguration.Stable).parse(
|
||||
GalleryBlock.serializer(),
|
||||
intent.getStringExtra("galleryblock")
|
||||
)
|
||||
|
||||
galleryID = intent.getIntExtra("GALLERY_ID", 0)
|
||||
CoroutineScope(Dispatchers.Unconfined).launch {
|
||||
reader = async(Dispatchers.IO) {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this@ReaderActivity)
|
||||
if (preference.getBoolean("use_hiyobi", false)) {
|
||||
try {
|
||||
xyz.quaver.hiyobi.getReader(galleryID)
|
||||
} catch (e: Exception) {
|
||||
getReader(galleryID)
|
||||
}
|
||||
}
|
||||
getReader(galleryID)
|
||||
}
|
||||
}
|
||||
supportActionBar?.title = galleryBlock.title
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
|
||||
snapHelper = PagerSnapHelper()
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val attrs = window.attributes
|
||||
|
||||
if (preferences.getBoolean("reader_fullscreen", false)) {
|
||||
attrs.flags = attrs.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
supportActionBar?.hide()
|
||||
} else {
|
||||
attrs.flags = attrs.flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||
supportActionBar?.show()
|
||||
}
|
||||
|
||||
window.attributes = attrs
|
||||
|
||||
if (preferences.getBoolean("reader_one_by_one", false)) {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
} else {
|
||||
snapHelper.attachToRecyclerView(null)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
}
|
||||
initDownloader()
|
||||
|
||||
initView()
|
||||
loadImages()
|
||||
|
||||
if (!downloader.notify)
|
||||
downloader.start()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -135,7 +112,103 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
loadJob?.cancel()
|
||||
|
||||
if (!downloader.notify)
|
||||
downloader.cancel()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (isScroll and !isFullscreen)
|
||||
super.onBackPressed()
|
||||
|
||||
if (isFullscreen) {
|
||||
isFullscreen = false
|
||||
fullscreen(false)
|
||||
}
|
||||
|
||||
if (!isScroll) {
|
||||
isScroll = true
|
||||
scrollMode(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDownloader() {
|
||||
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id)
|
||||
|
||||
if (d == null) {
|
||||
d = GalleryDownloader(this, galleryBlock)
|
||||
}
|
||||
|
||||
downloader = d.apply {
|
||||
onReaderLoadedHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
with(reader_progressbar) {
|
||||
max = it.size
|
||||
progress = 0
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
gallerySize = it.size
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}"
|
||||
}
|
||||
}
|
||||
onProgressHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reader_progressbar.progress = it
|
||||
}
|
||||
}
|
||||
onDownloadedHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (images.isEmpty()) {
|
||||
images.addAll(it)
|
||||
reader_recyclerview.adapter?.notifyDataSetChanged()
|
||||
} else {
|
||||
images.add(it.last())
|
||||
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
onErrorHandler = {
|
||||
downloader.notify = false
|
||||
}
|
||||
onCompleteHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reader_progressbar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
onNotifyChangedHandler = { notify ->
|
||||
val fab = reader_fab_download
|
||||
|
||||
if (notify) {
|
||||
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
|
||||
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
if (downloader.notify)
|
||||
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 {
|
||||
fab.setImageResource(R.drawable.ic_download)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloader.notify) {
|
||||
downloader.invokeOnReaderLoaded()
|
||||
downloader.invokeOnNotifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
@@ -155,100 +228,65 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
})
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, _, _ ->
|
||||
val attrs = window.attributes
|
||||
val fullscreen = preferences.getBoolean("reader_fullscreen", false)
|
||||
if (isScroll) {
|
||||
isScroll = false
|
||||
isFullscreen = true
|
||||
|
||||
if (fullscreen) {
|
||||
attrs.flags = attrs.flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||
supportActionBar?.show()
|
||||
scrollMode(false)
|
||||
fullscreen(true)
|
||||
} else {
|
||||
attrs.flags = attrs.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
supportActionBar?.hide()
|
||||
val smoothScroller = object : LinearSmoothScroller(context) {
|
||||
override fun getVerticalSnapPreference() = SNAP_TO_START
|
||||
}.apply {
|
||||
targetPosition = currentPage
|
||||
}
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.startSmoothScroll(smoothScroller)
|
||||
}
|
||||
|
||||
window.attributes = attrs
|
||||
|
||||
preferences.edit().putBoolean("reader_fullscreen", !fullscreen).apply()
|
||||
}.setOnItemLongClickListener { _, _, _ ->
|
||||
val oneByOne = preferences.getBoolean("reader_one_by_one", false)
|
||||
if (oneByOne) {
|
||||
snapHelper.attachToRecyclerView(null)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
else {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||
|
||||
preferences.edit().putBoolean("reader_one_by_one", !oneByOne).apply()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
reader_fab_fullscreen.setOnClickListener {
|
||||
isFullscreen = true
|
||||
fullscreen(isFullscreen)
|
||||
|
||||
reader_fab.close(true)
|
||||
}
|
||||
|
||||
reader_fab_download.setOnClickListener {
|
||||
downloader.notify = !downloader.notify
|
||||
|
||||
if (!downloader.notify)
|
||||
downloader.clearNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImages() {
|
||||
fun webpUrlFromUrl(url: URL) = URL(url.toString().replace("/galleries/", "/webp/") + ".webp")
|
||||
|
||||
loadJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
val reader = reader.await()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
with(reader_progressbar) {
|
||||
max = reader.size
|
||||
progress = 0
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
gallerySize = reader.size
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
|
||||
private fun fullscreen(isFullscreen: Boolean) {
|
||||
with(window.attributes) {
|
||||
if (isFullscreen) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
supportActionBar?.hide()
|
||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
||||
} else {
|
||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||
supportActionBar?.show()
|
||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
reader.chunked(8).forEach { chunked ->
|
||||
chunked.map {
|
||||
async(Dispatchers.IO) {
|
||||
val url = if (it.second?.haswebp == 1) webpUrlFromUrl(it.first) else it.first
|
||||
|
||||
val fileName: String
|
||||
|
||||
with(url.path) {
|
||||
fileName = substring(lastIndexOf('/')+1)
|
||||
}
|
||||
|
||||
val cache = File(cacheDir, "/imageCache/$galleryID/$fileName")
|
||||
|
||||
if (!cache.exists())
|
||||
with(url.openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("Referer", getReferer(galleryID))
|
||||
|
||||
if (!cache.parentFile.exists())
|
||||
cache.parentFile.mkdirs()
|
||||
|
||||
inputStream.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
|
||||
cache.absolutePath
|
||||
}
|
||||
}.forEach {
|
||||
val cache = it.await()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
images.add(cache)
|
||||
reader_recyclerview.adapter?.notifyItemInserted(images.size - 1)
|
||||
reader_progressbar.progress++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
reader_progressbar.visibility = View.GONE
|
||||
}
|
||||
window.attributes = this
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollMode(isScroll: Boolean) {
|
||||
if (isScroll) {
|
||||
snapHelper.attachToRecyclerView(null)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
} else {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,14 @@ import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.*
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
@@ -140,7 +138,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
setOnPreferenceClickListener {
|
||||
val dialogView = LayoutInflater.from(context).inflate(
|
||||
R.layout.dialog_default_query,
|
||||
null
|
||||
LinearLayout(context),
|
||||
false
|
||||
)
|
||||
|
||||
val tags = Tags.parse(
|
||||
@@ -164,6 +163,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
val tag = languages[tags.first { it.area == "language" }.tag]
|
||||
if (tag != null) {
|
||||
setSelection(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(adapter as ArrayAdapter<String>).getPosition(tag)
|
||||
)
|
||||
tags.removeByArea("language")
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.PorterDuff
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.toTag
|
||||
import xyz.quaver.hitomi.ReaderItem
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
@@ -32,9 +48,13 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
|
||||
}
|
||||
|
||||
var noMore = false
|
||||
private val refreshTasks = SparseArray<TimerTask>()
|
||||
val completeFlag = SparseBooleanArray()
|
||||
|
||||
class ViewHolder(val view: CardView) : RecyclerView.ViewHolder(view)
|
||||
class ProgressViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||
|
||||
class ViewHolder(val view: CardView, var galleryID: Int? = null) : RecyclerView.ViewHolder(view)
|
||||
class ProgressViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
when(viewType) {
|
||||
@@ -68,16 +88,92 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
|
||||
}.toMap()
|
||||
val (gallery, thumbnail) = galleries[position]
|
||||
|
||||
holder.galleryID = gallery.id
|
||||
|
||||
val artists = gallery.artists
|
||||
val series = gallery.series
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val cache = thumbnail.await()
|
||||
|
||||
if (!File(cache).exists())
|
||||
return@launch
|
||||
|
||||
val bitmap = BitmapFactory.decodeFile(thumbnail.await())
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
galleryblock_thumbnail.setImageBitmap(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
//Check cache
|
||||
val readerCache = File(context.cacheDir, "imageCache/${gallery.id}/reader.json")
|
||||
val imageCache = File(context.cacheDir, "imageCache/${gallery.id}/images")
|
||||
|
||||
if (readerCache.exists()) {
|
||||
val reader = Json(JsonConfiguration.Stable)
|
||||
.parse(ReaderItem.serializer().list, readerCache.readText())
|
||||
|
||||
with(galleryblock_progressbar) {
|
||||
max = reader.size
|
||||
progress = imageCache.list()?.size ?: 0
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
galleryblock_progressbar.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (refreshTasks.get(gallery.id) == null) {
|
||||
val refresh = Timer(false).schedule(0, 1000) {
|
||||
post {
|
||||
with(galleryblock_progressbar) {
|
||||
progress = imageCache.list()?.size ?: 0
|
||||
|
||||
if (!readerCache.exists()) {
|
||||
visibility = View.GONE
|
||||
max = 0
|
||||
progress = 0
|
||||
|
||||
holder.view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
} else {
|
||||
if (visibility == View.GONE) {
|
||||
val reader = Json(JsonConfiguration.Stable)
|
||||
.parse(ReaderItem.serializer().list, readerCache.readText())
|
||||
max = reader.size
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
if (completeFlag.get(gallery.id, false)) {
|
||||
with(holder.view.galleryblock_progress_complete) {
|
||||
setImageResource(R.drawable.ic_progressbar)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
val drawable = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete)
|
||||
with(holder.view.galleryblock_progress_complete) {
|
||||
setImageDrawable(drawable)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
drawable?.start()
|
||||
completeFlag.put(gallery.id, true)
|
||||
}
|
||||
} else {
|
||||
with(holder.view.galleryblock_progress_complete) {
|
||||
visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshTasks.put(gallery.id, refresh)
|
||||
}
|
||||
|
||||
galleryblock_title.text = gallery.title
|
||||
with(galleryblock_artist) {
|
||||
text = artists.joinToString(", ") { it.wordCapitalize() }
|
||||
@@ -108,17 +204,57 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
|
||||
|
||||
galleryblock_tag_group.removeAllViews()
|
||||
gallery.relatedTags.forEach {
|
||||
galleryblock_tag_group.addView(
|
||||
Chip(context).apply {
|
||||
text = it.toTag().wordCapitalize()
|
||||
val tag = Tag.parse(it)
|
||||
|
||||
val chip = LayoutInflater.from(context)
|
||||
.inflate(R.layout.tag_chip, holder.view, false) as Chip
|
||||
|
||||
val icon = when(tag.area) {
|
||||
"male" -> {
|
||||
chip.setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
|
||||
}
|
||||
)
|
||||
"female" -> {
|
||||
chip.setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
chip.chipIcon = icon
|
||||
chip.text = tag.tag.wordCapitalize()
|
||||
chip.setOnClickListener {
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(tag)
|
||||
}
|
||||
|
||||
galleryblock_tag_group.addView(chip)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (holder is ProgressViewHolder) {
|
||||
holder.view.visibility = when(noMore) {
|
||||
true -> View.GONE
|
||||
false -> View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = if (galleries.isEmpty()) 0 else galleries.size+(if (noMore) 0 else 1)
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewDetachedFromWindow(holder)
|
||||
|
||||
if (holder is ViewHolder) {
|
||||
val galleryID = holder.galleryID ?: return
|
||||
val task = refreshTasks.get(galleryID) ?: return
|
||||
|
||||
refreshTasks.remove(galleryID)
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = if (galleries.isEmpty()) 0 else galleries.size+1
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when {
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -2,7 +2,7 @@ package xyz.quaver.pupil.types
|
||||
|
||||
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
|
||||
companion object {
|
||||
fun parseTag(tag: String) : Tag {
|
||||
fun parse(tag: String) : Tag {
|
||||
if (tag.first() == '-') {
|
||||
tag.substring(1).split(Regex(":"), 2).let {
|
||||
return when(it.size) {
|
||||
@@ -27,6 +27,10 @@ data class Tag(val area: String?, val tag: String, val isNegative: Boolean = fal
|
||||
}
|
||||
}
|
||||
|
||||
fun toQuery(): String {
|
||||
return toString().replace(' ', '_')
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Tag)
|
||||
return false
|
||||
@@ -49,7 +53,7 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
return Tags(
|
||||
tags.split(' ').map {
|
||||
if (it.isNotEmpty())
|
||||
Tag.parseTag(it)
|
||||
Tag.parse(it)
|
||||
else
|
||||
null
|
||||
}
|
||||
@@ -74,7 +78,7 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
}
|
||||
|
||||
fun add(element: String): Boolean {
|
||||
return super.add(Tag.parseTag(element))
|
||||
return super.add(Tag.parse(element))
|
||||
}
|
||||
|
||||
fun remove(element: String) {
|
||||
|
||||
245
app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt
Normal file
@@ -0,0 +1,245 @@
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.io.IOException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import xyz.quaver.hitomi.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.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 galleryBlock: GalleryBlock,
|
||||
_notify: Boolean = false
|
||||
) : ContextWrapper(base) {
|
||||
|
||||
var notify: Boolean = false
|
||||
set(value) {
|
||||
if (value) {
|
||||
field = true
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
|
||||
if (!reader.isActive && downloadJob?.isActive != true)
|
||||
field = false
|
||||
} 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: (() -> Unit)? = null
|
||||
var onCompleteHandler: (() -> Unit)? = null
|
||||
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
|
||||
|
||||
companion object : SparseArray<GalleryDownloader>()
|
||||
|
||||
init {
|
||||
put(galleryBlock.id, this)
|
||||
|
||||
initNotification()
|
||||
|
||||
reader = CoroutineScope(Dispatchers.IO).async {
|
||||
notify = _notify
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = ReaderItem.serializer().list
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader)
|
||||
val useHiyobi = preference.getBoolean("use_hiyobi", false)
|
||||
|
||||
//Check cache
|
||||
val cache = File(cacheDir, "imageCache/${galleryBlock.id}/reader.json")
|
||||
|
||||
if (cache.exists()) {
|
||||
val cached = json.parse(serializer, cache.readText())
|
||||
|
||||
if (cached.isNotEmpty()) {
|
||||
onReaderLoadedHandler?.invoke(cached)
|
||||
return@async cached
|
||||
}
|
||||
}
|
||||
|
||||
//Cache doesn't exist. Load from internet
|
||||
val reader = when {
|
||||
useHiyobi -> {
|
||||
xyz.quaver.hiyobi.getReader(galleryBlock.id).let {
|
||||
when {
|
||||
it.isEmpty() -> getReader(galleryBlock.id)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
getReader(galleryBlock.id)
|
||||
}
|
||||
}
|
||||
|
||||
//Could not retrieve reader
|
||||
if (reader.isEmpty())
|
||||
throw IOException("Can't retrieve Reader")
|
||||
|
||||
//Save cache
|
||||
if (!cache.parentFile.exists())
|
||||
cache.parentFile.mkdirs()
|
||||
|
||||
cache.writeText(json.stringify(serializer, reader))
|
||||
|
||||
reader
|
||||
}
|
||||
}
|
||||
|
||||
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
|
||||
|
||||
fun start() {
|
||||
downloadJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
val reader = reader.await()
|
||||
|
||||
val list = ArrayList<String>()
|
||||
|
||||
onReaderLoadedHandler?.invoke(reader)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(reader.size, 0, false)
|
||||
.setContentText("0/${reader.size}")
|
||||
|
||||
reader.chunked(4).forEachIndexed { chunkIndex, chunked ->
|
||||
chunked.mapIndexed { i, it ->
|
||||
val index = chunkIndex*4+i
|
||||
|
||||
onProgressHandler?.invoke(index)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(reader.size, index, false)
|
||||
.setContentText("$index/${reader.size}")
|
||||
|
||||
if (notify)
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
|
||||
async(Dispatchers.IO) {
|
||||
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
|
||||
|
||||
val name = "$index".padStart(4, '0')
|
||||
val ext = url.split('.').last()
|
||||
|
||||
val cache = File(cacheDir, "/imageCache/${galleryBlock.id}/images/$name.$ext")
|
||||
|
||||
if (!cache.exists())
|
||||
try {
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("Referer", getReferer(galleryBlock.id))
|
||||
|
||||
if (!cache.parentFile.exists())
|
||||
cache.parentFile.mkdirs()
|
||||
|
||||
inputStream.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cache.delete()
|
||||
|
||||
onErrorHandler?.invoke()
|
||||
|
||||
notificationBuilder
|
||||
.setContentTitle(galleryBlock.title)
|
||||
.setContentText(getString(R.string.reader_notification_error))
|
||||
.setProgress(0, 0, false)
|
||||
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
}
|
||||
|
||||
cache.absolutePath
|
||||
}
|
||||
}.forEach {
|
||||
list.add(it.await())
|
||||
onDownloadedHandler?.invoke(list)
|
||||
}
|
||||
}
|
||||
|
||||
onCompleteHandler?.invoke()
|
||||
|
||||
Timer(false).schedule(1000) {
|
||||
notificationBuilder
|
||||
.setContentTitle(galleryBlock.title)
|
||||
.setContentText(getString(R.string.reader_notification_complete))
|
||||
.setProgress(0, 0, false)
|
||||
|
||||
if (notify)
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
|
||||
notify = false
|
||||
}
|
||||
|
||||
remove(galleryBlock.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
downloadJob?.cancel()
|
||||
|
||||
remove(galleryBlock.id)
|
||||
}
|
||||
|
||||
suspend fun cancelAndJoin() {
|
||||
downloadJob?.cancelAndJoin()
|
||||
}
|
||||
|
||||
fun invokeOnReaderLoaded() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
onReaderLoadedHandler?.invoke(reader.await())
|
||||
}
|
||||
}
|
||||
|
||||
fun clearNotification() {
|
||||
notificationManager.cancel(galleryBlock.id)
|
||||
}
|
||||
|
||||
fun invokeOnNotifyChanged() {
|
||||
onNotifyChangedHandler?.invoke(notify)
|
||||
}
|
||||
|
||||
private fun initNotification() {
|
||||
val intent = Intent(this, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), galleryBlock))
|
||||
}
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(galleryBlock.title)
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(R.drawable.ic_download)
|
||||
setContentIntent(pendingIntent)
|
||||
setProgress(0, 0, true)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import kotlinx.serialization.parseList
|
||||
import kotlinx.serialization.stringify
|
||||
import java.io.File
|
||||
|
||||
|
||||
class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
init {
|
||||
@@ -23,10 +22,6 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
companion object {
|
||||
lateinit var default: Histories
|
||||
|
||||
fun load(file: File) : Histories {
|
||||
return Histories(file).load()
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.io.IOException
|
||||
import kotlinx.serialization.json.*
|
||||
import java.net.URL
|
||||
|
||||
fun getReleases(url: String) : JsonArray {
|
||||
return URL(url).readText().let {
|
||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
||||
return try {
|
||||
URL(url).readText().let {
|
||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
app/src/main/res/drawable-anydpi/ic_downloading.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
tools:ignore="NewApi">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z"
|
||||
android:fillColor="#fff"
|
||||
android:strokeWidth="1"/>
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 5 19 L 19 19"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#fff"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_2">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathEnd"
|
||||
android:duration="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0.8"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathStart"
|
||||
android:startOffset="500"
|
||||
android:duration="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0.8"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathOffset"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0.2"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
10
app/src/main/res/drawable-anydpi/ic_fullscreen.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable-anydpi/ic_placeholder.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
|
||||
</vector>
|
||||
18
app/src/main/res/drawable-anydpi/ic_progressbar_complete.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:drawable="@drawable/ic_progressbar"
|
||||
tools:ignore="NewApi">
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathEnd"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_fullscreen.png
Normal file
|
After Width: | Height: | Size: 145 B |
BIN
app/src/main/res/drawable-hdpi/ic_placeholder.png
Normal file
|
After Width: | Height: | Size: 361 B |
BIN
app/src/main/res/drawable-mdpi/ic_fullscreen.png
Normal file
|
After Width: | Height: | Size: 99 B |
BIN
app/src/main/res/drawable-mdpi/ic_placeholder.png
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
app/src/main/res/drawable-xhdpi/ic_fullscreen.png
Normal file
|
After Width: | Height: | Size: 125 B |
BIN
app/src/main/res/drawable-xhdpi/ic_placeholder.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_fullscreen.png
Normal file
|
After Width: | Height: | Size: 149 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_placeholder.png
Normal file
|
After Width: | Height: | Size: 602 B |
8
app/src/main/res/drawable/github_circle.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- drawable/github-circle.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="#000" android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_download.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#fff"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ff000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
</vector>
|
||||
8
app/src/main/res/drawable/ic_gender_female_white.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- drawable/gender_female.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#fff" android:pathData="M12,4A6,6 0 0,1 18,10C18,12.97 15.84,15.44 13,15.92V18H15V20H13V22H11V20H9V18H11V15.92C8.16,15.44 6,12.97 6,10A6,6 0 0,1 12,4M12,6A4,4 0 0,0 8,10A4,4 0 0,0 12,14A4,4 0 0,0 16,10A4,4 0 0,0 12,6Z" />
|
||||
</vector>
|
||||
8
app/src/main/res/drawable/ic_gender_male_white.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- drawable/gender-male.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="M9,9C10.29,9 11.5,9.41 12.47,10.11L17.58,5H13V3H21V11H19V6.41L13.89,11.5C14.59,12.5 15,13.7 15,15A6,6 0 0,1 9,21A6,6 0 0,1 3,15A6,6 0 0,1 9,9M9,11A4,4 0 0,0 5,15A4,4 0 0,0 9,19A4,4 0 0,0 13,15A4,4 0 0,0 9,11Z" />
|
||||
</vector>
|
||||
21
app/src/main/res/drawable/ic_progressbar.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 0 12 L 24 12"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#b9f6ca"
|
||||
android:strokeWidth="24"/>
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 0 12 L 24 12"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#00c853"
|
||||
android:strokeWidth="24"/>
|
||||
</vector>
|
||||
@@ -4,14 +4,21 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/dark_gray"
|
||||
tools:context=".ReaderActivity">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reader_recyclerview"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reader_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/reader_framelayout"
|
||||
@@ -26,5 +33,31 @@
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<com.github.clans.fab.FloatingActionMenu
|
||||
android:id="@+id/reader_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:menu_colorNormal="@color/colorAccent">
|
||||
|
||||
<com.github.clans.fab.FloatingActionButton
|
||||
android:id="@+id/reader_fab_download"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_downloading"
|
||||
android:tint="@android:color/white"
|
||||
app:fab_label="@string/reader_fab_download"
|
||||
app:fab_size="mini"/>
|
||||
|
||||
<com.github.clans.fab.FloatingActionButton
|
||||
android:id="@+id/reader_fab_fullscreen"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_fullscreen"
|
||||
app:fab_label="@string/reader_fab_fullscreen"
|
||||
app:fab_size="mini"/>
|
||||
|
||||
</com.github.clans.fab.FloatingActionMenu>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
23
app/src/main/res/layout/dialog_galleryblock.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:padding="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/main_dialog_download"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/main_dialog_delete"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/main_dialog_delete"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_dialog_download"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -18,6 +18,23 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:id="@+id/galleryblock_progressbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/galleryblock_progress_complete"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:visibility="invisible"
|
||||
android:scaleType="fitXY"
|
||||
android:contentDescription="@string/reader_imageview_description"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/galleryblock_thumbnail"
|
||||
android:layout_width="150dp"
|
||||
@@ -25,7 +42,7 @@
|
||||
android:contentDescription="@string/galleryblock_thumbnail_description"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<TextView
|
||||
|
||||
9
app/src/main/res/layout/tag_chip.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.chip.Chip
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
app:chipIconSize="16dp"
|
||||
app:chipStartPadding="8dp"
|
||||
app:chipCornerRadius="100dp"/>
|
||||
@@ -21,7 +21,7 @@
|
||||
<string name="settings_miscellaneous_title">その他</string>
|
||||
<string name="settings_use_hiyobi_summary">ロード速度を向上させるためhiyobi.meからイメージロード</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_summary">履歴数: %1$d</string>
|
||||
<string name="main_drawer_history">履歴</string>
|
||||
@@ -43,4 +43,13 @@
|
||||
<string name="main_drawer_group_contact_email">メールを送る</string>
|
||||
<string name="help_dialog_title">準備中</string>
|
||||
<string name="help_dialog_message">準備中です。</string>
|
||||
<string name="reader_fab_fullscreen">フルスクリーン</string>
|
||||
<string name="channel_download">ダウンロード</string>
|
||||
<string name="channel_download_description">ダウンロードの進行を通知</string>
|
||||
<string name="reader_fab_download">バックグラウンドダウンロード</string>
|
||||
<string name="reader_notification_text">ダウンロード中…</string>
|
||||
<string name="reader_notification_complete">ダウンロード完了</string>
|
||||
<string name="reader_notification_error">ダウンロードエラー</string>
|
||||
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
|
||||
<string name="main_dialog_delete">このギャラリーを削除</string>
|
||||
</resources>
|
||||
@@ -21,10 +21,10 @@
|
||||
<string name="settings_miscellaneous_title">기타</string>
|
||||
<string name="settings_use_hiyobi_summary">속도 향상을 위해 가능하면 hiyobi.me에서 이미지 로드</string>
|
||||
<string name="settings_use_hiyobi_title">hiyobi.me 사용</string>
|
||||
<string name="settings_clear_history">히스토리 삭제</string>
|
||||
<string name="settings_clear_history_alert_message">히스토리를 삭제하시겠습니까?</string>
|
||||
<string name="settings_clear_history_summary">히스토리 %1$d개 저장됨</string>
|
||||
<string name="main_drawer_history">히스토리</string>
|
||||
<string name="settings_clear_history">기록 삭제</string>
|
||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
||||
<string name="main_drawer_history">기록</string>
|
||||
<string name="main_drawer_home">홈</string>
|
||||
<string name="update_download_started">다운로드 중</string>
|
||||
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
|
||||
@@ -42,5 +42,14 @@
|
||||
<string name="main_drawer_group_contact_homepage">홈페이지</string>
|
||||
<string name="main_drawer_group_contact_title">문의</string>
|
||||
<string name="help_dialog_title">준비 중</string>
|
||||
<string name="help_dialog_message">준비중입니다.\n만화 화면에서 사진을 길게 누르면 스크롤 방식이 바뀝니다. 알고 계셨나요? :)</string>
|
||||
<string name="help_dialog_message">준비중입니다.</string>
|
||||
<string name="reader_fab_fullscreen">전체 화면</string>
|
||||
<string name="channel_download">다운로드</string>
|
||||
<string name="channel_download_description">다운로드 상태 알림</string>
|
||||
<string name="reader_fab_download">백그라운드 다운로드</string>
|
||||
<string name="reader_notification_text">다운로드 중…</string>
|
||||
<string name="reader_notification_complete">다운로드 완료</string>
|
||||
<string name="reader_notification_error">다운로드 오류</string>
|
||||
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string>
|
||||
<string name="main_dialog_delete">갤러리 삭제</string>
|
||||
</resources>
|
||||
@@ -4,4 +4,8 @@
|
||||
<color name="colorPrimaryDark">#0093c4</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
<color name="appbar">#FFFFFF</color>
|
||||
|
||||
<color name="material_pink_600">#d81b60</color>
|
||||
<color name="material_blue_700">#1976d2</color>
|
||||
<color name="material_green_a700">#00c853</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Pupil</string>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name" translatable="false" tools:override="true">Pupil</string>
|
||||
|
||||
<string name="release_url" translatable="false">https://api.github.com/repos/tom5079/Pupil-issue/releases</string>
|
||||
<string name="release_name" translatable="false">Pupil-v(\\d+\\.)+\\d+\\.apk</string>
|
||||
@@ -22,6 +22,9 @@
|
||||
|
||||
<string name="permission_explain">Denying any permission can deactivate some functions</string>
|
||||
|
||||
<string name="channel_download">Download</string>
|
||||
<string name="channel_download_description">Shows download status</string>
|
||||
|
||||
<string name="main_search">Search</string>
|
||||
<string name="main_no_result">No result</string>
|
||||
|
||||
@@ -33,8 +36,10 @@
|
||||
<string name="main_drawer_group_contact_github">Visit github</string>
|
||||
<string name="main_drawer_group_contact_email">Email me!</string>
|
||||
|
||||
<string name="main_dialog_delete">Delete this gallery</string>
|
||||
|
||||
<string name="help_dialog_title">WIP</string>
|
||||
<string name="help_dialog_message">While in progress!\nOne thing might you don\'t know:\nLong tap an image in reader will change scrolling mode! Go try it :)</string>
|
||||
<string name="help_dialog_message">While in progress!</string>
|
||||
|
||||
<string name="update_title">Update available</string>
|
||||
<string name="update_download_started">Download started</string>
|
||||
@@ -48,6 +53,12 @@
|
||||
<string name="galleryblock_language">Language: %1$s</string>
|
||||
|
||||
<string name="reader_go_to_page">Go to page</string>
|
||||
<string name="reader_fab_fullscreen">Fullscreen</string>
|
||||
<string name="reader_fab_download">Background download</string>
|
||||
<string name="reader_fab_download_cancel">Cancel background download</string>
|
||||
<string name="reader_notification_text">Downloading…</string>
|
||||
<string name="reader_notification_complete">Download complete</string>
|
||||
<string name="reader_notification_error">Download error</string>
|
||||
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_search_title">Search Settings</string>
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.junit.Test
|
||||
class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
@ImplicitReflectionSerializer
|
||||
fun test() {
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url "s3://finotescore-android/release"
|
||||
credentials(AwsCredentials) {
|
||||
accessKey = "AKIAJ7TPIN63PV5SWK3A"
|
||||
secretKey = "YP6hNd9YSAkCSHUNVFxlcrtqSUWUGBaVdrRtVMxb"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,6 @@ package xyz.quaver.hitomi
|
||||
|
||||
const val protocol = "https:"
|
||||
|
||||
fun String.toTag() : String {
|
||||
if (this.indexOf(':') > -1) {
|
||||
val split = this.split(':')
|
||||
|
||||
val field = split[0]
|
||||
val term = split[1]
|
||||
|
||||
when(field) {
|
||||
"male" -> return "$term ♂"
|
||||
"female" -> return "$term ♀"
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
//common.js
|
||||
var adapose = false
|
||||
const val numberOfFrontends = 2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package xyz.quaver.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
@@ -39,13 +40,14 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
|
||||
return nozomi
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return listOf()
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GalleryBlock(
|
||||
val id: Int,
|
||||
val thumbnails: List<URL>,
|
||||
val thumbnails: List<String>,
|
||||
val title: String,
|
||||
val artists: List<String>,
|
||||
val series: List<String>,
|
||||
@@ -53,27 +55,31 @@ data class GalleryBlock(
|
||||
val language: String,
|
||||
val relatedTags: List<String>
|
||||
)
|
||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock {
|
||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||
|
||||
val doc = Jsoup.connect(url).get()
|
||||
try {
|
||||
val doc = Jsoup.connect(url).get()
|
||||
|
||||
val thumbnails = doc.select("img").map { URL(protocol + it.attr("data-src")) }
|
||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||
|
||||
val title = doc.selectFirst("h1.lillie > a").text()
|
||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||
val title = doc.selectFirst("h1.lillie > a").text()
|
||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||
|
||||
val language = {
|
||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||
href.slice(7 until href.indexOf("-1"))
|
||||
}.invoke()
|
||||
val language = {
|
||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||
href.slice(7 until href.indexOf("-1"))
|
||||
}.invoke()
|
||||
|
||||
val relatedTags = doc.select(".relatedtags a").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf('-'))
|
||||
val relatedTags = doc.select(".relatedtags a").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf('-'))
|
||||
}
|
||||
|
||||
return GalleryBlock(galleryID, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
||||
return GalleryBlock(galleryID, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
}
|
||||
@@ -16,31 +16,42 @@ data class GalleryInfo(
|
||||
val name: String,
|
||||
val height: Int
|
||||
)
|
||||
typealias Reader = List<Pair<URL, GalleryInfo?>>
|
||||
@Serializable
|
||||
data class ReaderItem(
|
||||
val url: String,
|
||||
val galleryInfo: GalleryInfo?
|
||||
)
|
||||
typealias Reader = List<ReaderItem>
|
||||
//Set header `Referer` to reader url to avoid 403 error
|
||||
fun getReader(galleryID: Int) : Reader {
|
||||
val readerUrl = "https://hitomi.la/reader/$galleryID.html"
|
||||
val galleryInfoUrl = "https://ltn.hitomi.la/galleries/$galleryID.js"
|
||||
|
||||
val doc = Jsoup.connect(readerUrl).get()
|
||||
try {
|
||||
val doc = Jsoup.connect(readerUrl).get()
|
||||
|
||||
val images = doc.select(".img-url").map {
|
||||
URL(protocol + urlFromURL(it.text()))
|
||||
}
|
||||
val images = doc.select(".img-url").map {
|
||||
protocol + urlFromURL(it.text())
|
||||
}
|
||||
|
||||
val galleryInfo = ArrayList<GalleryInfo?>()
|
||||
val galleryInfo = ArrayList<GalleryInfo?>()
|
||||
|
||||
galleryInfo.addAll(
|
||||
Json(JsonConfiguration.Stable).parse(
|
||||
GalleryInfo.serializer().list,
|
||||
Regex("""\[.+]""").find(
|
||||
URL(galleryInfoUrl).readText()
|
||||
)?.value ?: "[]"
|
||||
galleryInfo.addAll(
|
||||
Json(JsonConfiguration.Stable).parse(
|
||||
GalleryInfo.serializer().list,
|
||||
Regex("""\[.+]""").find(
|
||||
URL(galleryInfoUrl).readText()
|
||||
)?.value ?: "[]"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (images.size > galleryInfo.size)
|
||||
galleryInfo.addAll(arrayOfNulls(images.size - galleryInfo.size))
|
||||
if (images.size > galleryInfo.size)
|
||||
galleryInfo.addAll(arrayOfNulls(images.size - galleryInfo.size))
|
||||
|
||||
return images zip galleryInfo
|
||||
return (images zip galleryInfo).map {
|
||||
ReaderItem(it.first, it.second)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ const val max_node_size = 464
|
||||
const val B = 16
|
||||
const val compressed_nozomi_prefix = "n"
|
||||
|
||||
val tag_index_version = getIndexVersion("tagindex")
|
||||
val galleries_index_version = getIndexVersion("galleriesindex")
|
||||
var tag_index_version = getIndexVersion("tagindex")
|
||||
var galleries_index_version = getIndexVersion("galleriesindex")
|
||||
|
||||
fun sha256(data: ByteArray) : ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
||||
@@ -32,8 +32,12 @@ fun sanitize(input: String) : String {
|
||||
}
|
||||
|
||||
fun getIndexVersion(name: String) : String {
|
||||
return URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}")
|
||||
.readText()
|
||||
return try {
|
||||
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}")
|
||||
.readText()
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
//search.js
|
||||
@@ -64,14 +68,14 @@ fun getGalleryIDsForQuery(query: String) : List<Int> {
|
||||
val key = hashTerm(it)
|
||||
val field = "galleries"
|
||||
|
||||
val node = getNodeAtAddress(field, 0) ?: return listOf()
|
||||
val node = getNodeAtAddress(field, 0) ?: return emptyList()
|
||||
|
||||
val data = bSearch(field, key, node)
|
||||
|
||||
if (data != null)
|
||||
return getGalleryIDsFromData(data)
|
||||
|
||||
return arrayListOf()
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,24 +91,27 @@ fun getSuggestionsForQuery(query: String) : List<Suggestion> {
|
||||
}
|
||||
|
||||
val key = hashTerm(term)
|
||||
val node = getNodeAtAddress(field, 0) ?: return listOf()
|
||||
val node = getNodeAtAddress(field, 0) ?: return emptyList()
|
||||
val data = bSearch(field, key, node)
|
||||
|
||||
if (data != null)
|
||||
return getSuggestionsFromData(field, data)
|
||||
|
||||
return listOf()
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
|
||||
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
|
||||
if (tag_index_version.isEmpty())
|
||||
tag_index_version = getIndexVersion("tagindex")
|
||||
|
||||
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
|
||||
val (offset, length) = data
|
||||
if (length > 10000 || length <= 0)
|
||||
throw Exception("length $length is too long")
|
||||
|
||||
val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return listOf()
|
||||
val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return emptyList()
|
||||
|
||||
val suggestions = ArrayList<Suggestion>()
|
||||
|
||||
@@ -166,17 +173,20 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
|
||||
return nozomi
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return listOf()
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
||||
if (galleries_index_version.isEmpty())
|
||||
galleries_index_version = getIndexVersion("galleriesindex")
|
||||
|
||||
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
|
||||
val (offset, length) = data
|
||||
if (length > 100000000 || length <= 0)
|
||||
throw Exception("length $length is too long")
|
||||
|
||||
val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return listOf()
|
||||
val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return emptyList()
|
||||
|
||||
val galleryIDs = ArrayList<Int>()
|
||||
|
||||
@@ -200,6 +210,11 @@ fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
||||
}
|
||||
|
||||
fun getNodeAtAddress(field: String, address: Long) : Node? {
|
||||
if (tag_index_version.isEmpty())
|
||||
tag_index_version = getIndexVersion("tagindex")
|
||||
if (galleries_index_version.isEmpty())
|
||||
galleries_index_version = getIndexVersion("galleriesindex")
|
||||
|
||||
val url =
|
||||
when(field) {
|
||||
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package xyz.quaver.hiyobi
|
||||
|
||||
import kotlinx.io.IOException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.json.content
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.ReaderItem
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
@@ -15,11 +17,15 @@ var cookie: String = ""
|
||||
fun renewCookie() : String {
|
||||
val url = "https://$hiyobi/"
|
||||
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
connectTimeout = 2000
|
||||
connect()
|
||||
return headerFields["Set-Cookie"]!![0]
|
||||
try {
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
connectTimeout = 2000
|
||||
connect()
|
||||
return headerFields["Set-Cookie"]!![0]
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,19 +35,23 @@ fun getReader(galleryId: Int) : Reader {
|
||||
if (cookie.isEmpty())
|
||||
cookie = renewCookie()
|
||||
|
||||
val json = Json(JsonConfiguration.Stable).parseJson(
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
connectTimeout = 2000
|
||||
connect()
|
||||
try {
|
||||
val json = Json(JsonConfiguration.Stable).parseJson(
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
connectTimeout = 2000
|
||||
connect()
|
||||
|
||||
inputStream.bufferedReader().use { it.readText() }
|
||||
inputStream.bufferedReader().use { it.readText() }
|
||||
}
|
||||
)
|
||||
|
||||
return json.jsonArray.map {
|
||||
val name = it.jsonObject["name"]!!.content
|
||||
ReaderItem("https://$hiyobi/data/$galleryId/$name", null)
|
||||
}
|
||||
)
|
||||
|
||||
return json.jsonArray.map {
|
||||
val name = it.jsonObject["name"]!!.content
|
||||
Pair(URL("https://$hiyobi/data/$galleryId/$name"), null)
|
||||
} catch (e: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
package xyz.quaver.hitomi
|
||||
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
class UnitTest {
|
||||
@Test
|
||||
fun test() {
|
||||
val url = URL("https://ltn.hitomi.la/galleries/1411672.js")
|
||||
|
||||
print(url.path.substring(url.path.lastIndexOf('/')+1))
|
||||
print(File("C:\\asdf").list()?.size ?: 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -63,6 +62,6 @@ class UnitTest {
|
||||
|
||||
@Test
|
||||
fun test_hiyobi() {
|
||||
xyz.quaver.hiyobi.getReader(1414061)
|
||||
print(xyz.quaver.hiyobi.getReader(1415416).size)
|
||||
}
|
||||
}
|
||||