diff --git a/app/build.gradle b/app/build.gradle index ea00135a..3fef32d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..efb4fd24 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,11 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile +-keep class com.finotes.android.finotescore.* { *; } + +-keepclassmembers class * { + @com.finotes.android.finotescore.annotation.Observe *; +} + +-keepattributes SourceFile,LineNumberTable \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e380367..766a3ca7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> + android:parentActivityName=".MainActivity" + android:configChanges="keyboardHidden|orientation|screenSize"/> diff --git a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt index 0d97193c..a355b261 100644 --- a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt @@ -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>? = 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(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(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) + } } } - } + } } } } diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt new file mode 100644 index 00000000..8e1184dd --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt index 80471c11..b2022c2b 100644 --- a/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt @@ -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() - private var galleryID = 0 - private var gallerySize: Int = 0 - private var currentPage: Int = 0 - private lateinit var reader: Deferred - 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) + } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt index 1640e5d5..9a3c92b0 100644 --- a/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt @@ -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).getPosition(tag) ) tags.removeByArea("language") diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt index e83e4f86..08f73716 100644 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt +++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt @@ -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>>) : RecyclerView.Adapter() { @@ -32,9 +48,13 @@ class GalleryBlockAdapter(private val galleries: List() + 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 { + 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 { diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt index 8e84e6a6..ea653cf4 100644 --- a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt +++ b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt @@ -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 diff --git a/app/src/main/java/xyz/quaver/pupil/types/Tags.kt b/app/src/main/java/xyz/quaver/pupil/types/Tags.kt index 7abc6c97..b7e8039b 100644 --- a/app/src/main/java/xyz/quaver/pupil/types/Tags.kt +++ b/app/src/main/java/xyz/quaver/pupil/types/Tags.kt @@ -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?) : ArrayList() { return Tags( tags.split(' ').map { if (it.isNotEmpty()) - Tag.parseTag(it) + Tag.parse(it) else null } @@ -74,7 +78,7 @@ class Tags(tag: List?) : ArrayList() { } fun add(element: String): Boolean { - return super.add(Tag.parseTag(element)) + return super.add(Tag.parse(element)) } fun remove(element: String) { diff --git a/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt b/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt new file mode 100644 index 00000000..c7c57ab6 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt @@ -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 + 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) -> Unit)? = null + var onErrorHandler: (() -> Unit)? = null + var onCompleteHandler: (() -> Unit)? = null + var onNotifyChangedHandler: ((Boolean) -> Unit)? = null + + companion object : SparseArray() + + 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() + + 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/history.kt b/app/src/main/java/xyz/quaver/pupil/util/history.kt index 8ebfaad1..a7fdce46 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/history.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/history.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.parseList import kotlinx.serialization.stringify import java.io.File - class Histories(private val file: File) : ArrayList() { init { @@ -23,10 +22,6 @@ class Histories(private val file: File) : ArrayList() { companion object { lateinit var default: Histories - - fun load(file: File) : Histories { - return Histories(file).load() - } } @UseExperimental(ImplicitReflectionSerializer::class) diff --git a/app/src/main/java/xyz/quaver/pupil/util/update.kt b/app/src/main/java/xyz/quaver/pupil/util/update.kt index b25e03e0..198c82a2 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -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()) } } diff --git a/app/src/main/res/drawable-anydpi/ic_downloading.xml b/app/src/main/res/drawable-anydpi/ic_downloading.xml new file mode 100644 index 00000000..ac96e761 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_downloading.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_fullscreen.xml b/app/src/main/res/drawable-anydpi/ic_fullscreen.xml new file mode 100644 index 00000000..f9831623 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_fullscreen.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_placeholder.xml b/app/src/main/res/drawable-anydpi/ic_placeholder.xml new file mode 100644 index 00000000..7b74d4b6 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_placeholder.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_progressbar_complete.xml b/app/src/main/res/drawable-anydpi/ic_progressbar_complete.xml new file mode 100644 index 00000000..85cae265 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_progressbar_complete.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_fullscreen.png b/app/src/main/res/drawable-hdpi/ic_fullscreen.png new file mode 100644 index 00000000..c72a9b9e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_fullscreen.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_placeholder.png b/app/src/main/res/drawable-hdpi/ic_placeholder.png new file mode 100644 index 00000000..1604365e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_placeholder.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen.png b/app/src/main/res/drawable-mdpi/ic_fullscreen.png new file mode 100644 index 00000000..f19ccbfa Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_fullscreen.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_placeholder.png b/app/src/main/res/drawable-mdpi/ic_placeholder.png new file mode 100644 index 00000000..cc9eb1eb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_placeholder.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen.png new file mode 100644 index 00000000..6caba05d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fullscreen.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_placeholder.png b/app/src/main/res/drawable-xhdpi/ic_placeholder.png new file mode 100644 index 00000000..2a7d65cb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_placeholder.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen.png new file mode 100644 index 00000000..4712c6c1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fullscreen.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_placeholder.png b/app/src/main/res/drawable-xxhdpi/ic_placeholder.png new file mode 100644 index 00000000..e40691db Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_placeholder.png differ diff --git a/app/src/main/res/drawable/github_circle.xml b/app/src/main/res/drawable/github_circle.xml new file mode 100644 index 00000000..7df41959 --- /dev/null +++ b/app/src/main/res/drawable/github_circle.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 00000000..c95797c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_gender_female_white.xml b/app/src/main/res/drawable/ic_gender_female_white.xml new file mode 100644 index 00000000..6620153b --- /dev/null +++ b/app/src/main/res/drawable/ic_gender_female_white.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_gender_male_white.xml b/app/src/main/res/drawable/ic_gender_male_white.xml new file mode 100644 index 00000000..a6e1a428 --- /dev/null +++ b/app/src/main/res/drawable/ic_gender_male_white.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_progressbar.xml b/app/src/main/res/drawable/ic_progressbar.xml new file mode 100644 index 00000000..3899286b --- /dev/null +++ b/app/src/main/res/drawable/ic_progressbar.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml index a77f48e5..59ff93c1 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -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"> - + android:layout_height="wrap_content" + android:layout_gravity="center_vertical"> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_galleryblock.xml b/app/src/main/res/layout/dialog_galleryblock.xml new file mode 100644 index 00000000..3c9bc82a --- /dev/null +++ b/app/src/main/res/layout/dialog_galleryblock.xml @@ -0,0 +1,23 @@ + + + +