Merge pull request #4 from tom5079/development

Version 2.0
This commit is contained in:
tom5079
2019-05-18 23:24:05 +09:00
committed by GitHub
46 changed files with 1180 additions and 296 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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" />

View File

@@ -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)
}
}
}
}
}
}
}
}

View 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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View 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)
}
}

View File

@@ -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)

View File

@@ -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())
}
}

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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

View 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"/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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&#8230;</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>

View File

@@ -12,7 +12,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
@ImplicitReflectionSerializer
fun test() {
}

View File

@@ -20,6 +20,13 @@ allprojects {
repositories {
google()
jcenter()
maven {
url "s3://finotescore-android/release"
credentials(AwsCredentials) {
accessKey = "AKIAJ7TPIN63PV5SWK3A"
secretKey = "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"
}
}
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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"

View File

@@ -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()
}
}

View File

@@ -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)
}
}