diff --git a/app/build.gradle b/app/build.gradle index 396af997..64729574 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "xyz.quaver.pupil" minSdkVersion 16 targetSdkVersion 28 - versionCode 3 - versionName "1.2" + versionCode 4 + versionName "1.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -40,6 +40,7 @@ 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' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt index 5a6f14f7..f6cbca02 100644 --- a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt @@ -4,16 +4,10 @@ import android.graphics.BitmapFactory import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import xyz.quaver.hiyobi.getReader import java.io.File -import java.util.* /** * Instrumented test, which will execute on an Android device. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1bdffc64..0e380367 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,8 +3,6 @@ package="xyz.quaver.pupil"> - - - () - private var galleryID = 0 - private lateinit var reader: Deferred - private var loadJob: Job? = null - private var screenMode = 0 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE) - - setContentView(R.layout.activity_gallery) - - supportActionBar?.title = intent.getStringExtra("GALLERY_TITLE") - - galleryID = intent.getIntExtra("GALLERY_ID", 0) - CoroutineScope(Dispatchers.Unconfined).launch { - reader = async(Dispatchers.IO) { - val preference = PreferenceManager.getDefaultSharedPreferences(this@GalleryActivity) - if (preference.getBoolean("use_hiyobi", false)) { - try { - xyz.quaver.hiyobi.getReader(galleryID) - Log.d("Pupil", "Using Hiyobi.me") - } catch (e: Exception) { - getReader(galleryID) - } - } - getReader(galleryID) - } - } - - initView() - loadImages() - } - - override fun onResume() { - val preferences = android.preference.PreferenceManager.getDefaultSharedPreferences(this) - - if (preferences.getBoolean("security_mode", false)) - window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE) - else - window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - super.onResume() - } - - override fun onDestroy() { - super.onDestroy() - loadJob?.cancel() - } - - private fun initView() { - gallery_recyclerview.adapter = GalleryAdapter(images).apply { - setOnClick { - val attrs = window.attributes - - screenMode = (screenMode+1)%2 - - when(screenMode) { - 0 -> { - attrs.flags = attrs.flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv() - supportActionBar?.show() - } - 1 -> { - attrs.flags = attrs.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN - supportActionBar?.hide() - } - } - window.attributes = attrs - } - } - } - - 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(gallery_progressbar) { - max = reader.size - progress = 0 - - 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) - gallery_recyclerview.adapter?.notifyItemInserted(images.size - 1) - gallery_progressbar.progress++ - } - } - } - - launch(Dispatchers.Main) { - gallery_progressbar.visibility = View.GONE - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt index 65ea4cec..194bfb15 100644 --- a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt @@ -1,31 +1,15 @@ package xyz.quaver.pupil -import android.Manifest -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri -import android.os.Build import android.os.Bundle -import android.os.Environment import android.preference.PreferenceManager import android.text.* import android.text.style.AlignmentSpan -import android.util.Log import android.view.View import android.view.WindowManager -import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider import androidx.core.content.res.ResourcesCompat import androidx.core.view.GravityCompat import androidx.recyclerview.widget.LinearLayoutManager @@ -44,19 +28,18 @@ 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.getApkUrl import java.io.File -import java.lang.StringBuilder +import java.io.FileOutputStream import java.util.* import javax.net.ssl.HttpsURLConnection import kotlin.collections.ArrayList class MainActivity : AppCompatActivity() { - private val permissionRequestCode = 4585 - private val galleries = ArrayList>() + private val galleries = ArrayList>>() private var query = "" @@ -67,9 +50,12 @@ class MainActivity : AppCompatActivity() { Histories.default = Histories(File(cacheDir, "histories.json")) super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + window.setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + ) - checkPermission() + setContentView(R.layout.activity_main) checkUpdate() @@ -81,7 +67,11 @@ class MainActivity : AppCompatActivity() { ) with(main_swipe_layout) { - setProgressViewOffset(false, 0, resources.getDimensionPixelSize(R.dimen.progress_view_offset)) + setProgressViewOffset( + false, + resources.getDimensionPixelSize(R.dimen.progress_view_start), + resources.getDimensionPixelSize(R.dimen.progress_view_offset) + ) setOnRefreshListener { CoroutineScope(Dispatchers.Main).launch { @@ -97,17 +87,36 @@ class MainActivity : AppCompatActivity() { CoroutineScope(Dispatchers.Main).launch { main_drawer_layout.closeDrawers() - cancelFetch() - clearGalleries() when(it.itemId) { R.id.main_drawer_home -> { + cancelFetch() + clearGalleries() query = query.replace("HISTORY", "") fetchGalleries(query) } R.id.main_drawer_history -> { + cancelFetch() + clearGalleries() query += "HISTORY" fetchGalleries(query) } + R.id.main_drawer_help -> { + AlertDialog.Builder(this@MainActivity).apply { + title = getString(R.string.help_dialog_title) + setMessage(R.string.help_dialog_message) + + setPositiveButton(android.R.string.ok) { _, _ -> } + }.show() + } + R.id.main_drawer_github -> { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) + } + R.id.main_drawer_homepage -> { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page)))) + } + R.id.main_drawer_email -> { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) + } } loadBlocks() } @@ -140,23 +149,6 @@ class MainActivity : AppCompatActivity() { super.onResume() } - private fun checkPermission() { - val permissions = arrayOf( - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - - if (permissions.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }) { - if (permissions.any { ActivityCompat.shouldShowRequestPermissionRationale(this, it) }) - AlertDialog.Builder(this).apply { - setTitle(R.string.warning) - setMessage(R.string.permission_explain) - setPositiveButton(android.R.string.ok) { _, _ -> } - }.show() - else - ActivityCompat.requestPermissions(this, permissions, permissionRequestCode) - } - } - private fun checkUpdate() { fun extractReleaseNote(update: JsonObject, locale: String) : String { @@ -177,7 +169,7 @@ class MainActivity : AppCompatActivity() { val result = StringBuilder() - for(line in markdown.split('\n')) { + for(line in markdown.lines()) { if (releaseNote.matches(line)) { releaseNoteFlag = true continue @@ -201,57 +193,16 @@ class MainActivity : AppCompatActivity() { return getString(R.string.update_release_note, update["tag_name"]?.content, result.toString()) } - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) - return - CoroutineScope(Dispatchers.Default).launch { val update = checkUpdate(getString(R.string.release_url), BuildConfig.VERSION_NAME) ?: return@launch - val (url, fileName) = getApkUrl(update, getString(R.string.release_name)) ?: return@launch - val dialog = AlertDialog.Builder(this@MainActivity).apply { setTitle(R.string.update_title) val msg = extractReleaseNote(update, Locale.getDefault().language) setMessage(Markwon.create(context).toMarkdown(msg)) setPositiveButton(android.R.string.yes) { _, _ -> - Toast.makeText( - context, getString(R.string.update_download_started), Toast.LENGTH_SHORT - ).show() - - val dest = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName) - val desturi = - FileProvider.getUriForFile( - applicationContext, - "xyz.quaver.pupil.provider", - dest - ) - - if (dest.exists()) - dest.delete() - - val request = DownloadManager.Request(Uri.parse(url)).apply { - setDescription(getString(R.string.update_notification_description)) - setTitle(getString(R.string.app_name)) - setDestinationUri(Uri.fromFile(dest)) - setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) - } - - val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val id = manager.enqueue(request) - - registerReceiver(object: BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val install = Intent(Intent.ACTION_VIEW).apply { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION - setDataAndType(desturi, manager.getMimeTypeForDownloadedFile(id)) - } - - startActivity(install) - unregisterReceiver(this) - finish() - } - }, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page)))) } setNegativeButton(android.R.string.no) { _, _ ->} } @@ -264,18 +215,7 @@ class MainActivity : AppCompatActivity() { private fun setupRecyclerView() { with(main_recyclerview) { - adapter = GalleryBlockAdapter(galleries).apply { - setClickListener { galleryID, title -> - val intent = Intent(this@MainActivity, GalleryActivity::class.java) - intent.putExtra("GALLERY_ID", galleryID) - intent.putExtra("GALLERY_TITLE", title) - - //TODO: Maybe sprinke some transitions will be nice :D - startActivity(intent) - - Histories.default.add(galleryID) - } - } + adapter = GalleryBlockAdapter(galleries) addOnScrollListener( object: RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -289,6 +229,17 @@ 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) + + //TODO: Maybe sprinke some transitions will be nice :D + startActivity(intent) + + Histories.default.add(gallery.id) + } } } @@ -402,9 +353,11 @@ class MainActivity : AppCompatActivity() { if (query != this@MainActivity.query) { this@MainActivity.query = query - cancelFetch() - clearGalleries() - fetchGalleries(query) + CoroutineScope(Dispatchers.Main).launch { + cancelFetch() + clearGalleries() + fetchGalleries(query) + } } } }) @@ -481,29 +434,43 @@ class MainActivity : AppCompatActivity() { galleryIDs else -> galleryIDs.slice(galleries.size until Math.min(galleries.size+perPage, galleryIDs.size)) - }.chunked(4).forEach { chunked -> - chunked.map { - async { - val galleryBlock = getGalleryBlock(it) - val thumbnail: Bitmap + }.chunked(4).let { chunks -> + for (chunk in chunks) + chunk.map { + async { + try { + val galleryBlock = getGalleryBlock(it) - with(galleryBlock.thumbnails[0].openConnection() as HttpsURLConnection) { - thumbnail = BitmapFactory.decodeStream(inputStream) + val thumbnail = async { + val cache = File(cacheDir, "imageCache/$it/thumbnail.${galleryBlock.thumbnails[0].path.split('.').last()}") + + if (!cache.exists()) + with(galleryBlock.thumbnails[0].openConnection() as HttpsURLConnection) { + if (!cache.parentFile.exists()) + cache.parentFile.mkdirs() + + inputStream.copyTo(FileOutputStream(cache)) + } + + cache.absolutePath + } + + Pair(galleryBlock, thumbnail) + } catch (e: Exception) { + null + } } + }.forEach { + val galleryBlock = it.await() ?: return@forEach - Pair(galleryBlock, thumbnail) - } - }.forEach { - val galleryBlock = it.await() + withContext(Dispatchers.Main) { + main_progressbar.hide() - withContext(Dispatchers.Main) { - main_progressbar.hide() - - galleries.add(galleryBlock) - main_recyclerview.adapter?.notifyItemInserted(galleries.size - 1) + galleries.add(galleryBlock) + main_recyclerview.adapter?.notifyItemInserted(galleries.size - 1) + } } } - } } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt new file mode 100644 index 00000000..80471c11 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt @@ -0,0 +1,254 @@ +package xyz.quaver.pupil + +import android.os.Bundle +import android.util.Log +import android.view.* +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.PagerSnapHelper +import androidx.recyclerview.widget.RecyclerView +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 xyz.quaver.pupil.adapters.ReaderAdapter +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 snapHelper: PagerSnapHelper + + private var menu: Menu? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE) + + setContentView(R.layout.activity_reader) + + supportActionBar?.title = intent.getStringExtra("GALLERY_TITLE") + + 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) + } + } + + 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) + } + + initView() + loadImages() + } + + override fun onResume() { + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + + if (preferences.getBoolean("security_mode", false)) + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE) + else + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + + super.onResume() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.reader, menu) + this.menu = menu + return true + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when(item?.itemId) { + R.id.reader_menu_page_indicator -> { + val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false) + with(view.reader_dialog_number_picker) { + minValue=1 + maxValue=gallerySize + value=currentPage + } + val dialog = AlertDialog.Builder(this).apply { + setView(view) + }.create() + view.reader_dialog_ok.setOnClickListener { + (reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.reader_dialog_number_picker.value-1, 0) + dialog.dismiss() + } + + dialog.show() + } + } + + return true + } + + override fun onDestroy() { + super.onDestroy() + loadJob?.cancel() + } + + private fun initView() { + with(reader_recyclerview) { + adapter = ReaderAdapter(images) + + addOnScrollListener(object: RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + + if (layoutManager.findFirstVisibleItemPosition() == -1) + return + currentPage = layoutManager.findFirstVisibleItemPosition()+1 + menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize" + } + }) + + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + ItemClickSupport.addTo(this) + .setOnItemClickListener { _, _, _ -> + val attrs = window.attributes + val fullscreen = preferences.getBoolean("reader_fullscreen", false) + + if (fullscreen) { + attrs.flags = attrs.flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv() + supportActionBar?.show() + } else { + attrs.flags = attrs.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN + supportActionBar?.hide() + } + + 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 + } + } + } + + 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" + } + + 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 + } + } + } +} \ 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 d5c301ce..1640e5d5 100644 --- a/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt @@ -2,12 +2,22 @@ package xyz.quaver.pupil import android.os.Bundle import android.preference.PreferenceManager +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.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 import java.io.File @@ -109,6 +119,120 @@ class SettingsActivity : AppCompatActivity() { true } } + + with(findPreference("default_query")) { + this ?: return@with + + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + summary = preferences.getString("default_query", "") ?: "" + + val languages = resources.getStringArray(R.array.languages).map { + it.split("|").let { split -> + Pair(split[0], split[1]) + } + }.toMap() + val reverseLanguages = languages.entries.associate { (k, v) -> v to k } + + val excludeBL = "-male:yaoi" + val excludeGuro = listOf("-female:guro", "-male:guro") + + setOnPreferenceClickListener { + val dialogView = LayoutInflater.from(context).inflate( + R.layout.dialog_default_query, + null + ) + + val tags = Tags.parse( + preferences.getString("default_query", "") ?: "" + ) + + summary = tags.toString() + + with(dialogView.default_query_dialog_language_selector) { + adapter = + ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, + arrayListOf( + getString(R.string.default_query_dialog_language_selector_none) + ).apply { + addAll(languages.values) + } + ) + if (tags.any { it.area == "language" }) { + val tag = languages[tags.first { it.area == "language" }.tag] + if (tag != null) { + setSelection( + (adapter as ArrayAdapter).getPosition(tag) + ) + tags.removeByArea("language") + } + } + } + + with(dialogView.default_query_dialog_BL_checkbox) { + isChecked = tags.contains(excludeBL) + if (tags.contains(excludeBL)) + tags.remove(excludeBL) + } + + with(dialogView.default_query_dialog_guro_checkbox) { + isChecked = excludeGuro.all { tags.contains(it) } + if (excludeGuro.all { tags.contains(it) }) + excludeGuro.forEach { + tags.remove(it) + } + } + + with(dialogView.default_query_dialog_edittext) { + setText(tags.toString(), TextView.BufferType.EDITABLE) + addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + s ?: return + + if (s.any { it.isUpperCase() }) + s.replace(0, s.length, s.toString().toLowerCase()) + } + }) + } + + val dialog = AlertDialog.Builder(context!!).apply { + setView(dialogView) + }.create() + + dialogView.default_query_dialog_ok.setOnClickListener { + val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString()) + + with(dialogView.default_query_dialog_language_selector) { + if (selectedItemPosition != 0) + newTags.add("language:${reverseLanguages[selectedItem]}") + } + + if (dialogView.default_query_dialog_BL_checkbox.isChecked) + newTags.add(excludeBL) + + if (dialogView.default_query_dialog_guro_checkbox.isChecked) + excludeGuro.forEach { tag -> + newTags.add(tag) + } + + preferenceManager.sharedPreferences.edit().putString("default_query", newTags.toString()).apply() + summary = preferences.getString("default_query", "") ?: "" + tags.clear() + tags.addAll(newTags) + dialog.dismiss() + } + + dialog.show() + + true + } + } } } 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 71851505..e83e4f86 100644 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt +++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt @@ -1,18 +1,21 @@ package xyz.quaver.pupil.adapters -import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.cardview.widget.CardView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.chip.Chip import kotlinx.android.synthetic.main.item_galleryblock.view.* +import kotlinx.coroutines.* import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.toTag import xyz.quaver.pupil.R +import java.io.File -class GalleryBlockAdapter(private val galleries: List>) : RecyclerView.Adapter() { +class GalleryBlockAdapter(private val galleries: List>>) : RecyclerView.Adapter() { private enum class ViewType { VIEW_ITEM, @@ -33,11 +36,6 @@ class GalleryBlockAdapter(private val galleries: List Unit)? = null - fun setClickListener(callback: ((Int, String) -> Unit)?) { - this.callback = callback - } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { when(viewType) { ViewType.VIEW_ITEM.ordinal -> { @@ -70,21 +68,43 @@ class GalleryBlockAdapter(private val galleries: List View.VISIBLE + else -> View.GONE + } + } + with(galleryblock_series) { + text = + resources.getString( + R.string.galleryblock_series, + series.joinToString(", ") { it.wordCapitalize() }) + visibility = when { + series.isNotEmpty() -> View.VISIBLE + else -> View.GONE + } + } galleryblock_type.text = resources.getString(R.string.galleryblock_type, gallery.type).wordCapitalize() - galleryblock_language.text = - resources.getString(R.string.galleryblock_language, languages[gallery.language]) + with(galleryblock_language) { + text = + resources.getString(R.string.galleryblock_language, languages[gallery.language]) + visibility = when { + gallery.language.isNotEmpty() -> View.VISIBLE + else -> View.GONE + } + } galleryblock_tag_group.removeAllViews() gallery.relatedTags.forEach { diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt similarity index 70% rename from app/src/main/java/xyz/quaver/pupil/adapters/GalleryAdapter.kt rename to app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt index 5d1febdc..8e84e6a6 100644 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryAdapter.kt +++ b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt @@ -1,39 +1,31 @@ package xyz.quaver.pupil.adapters import android.graphics.BitmapFactory -import android.util.Log import android.view.LayoutInflater +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 import xyz.quaver.pupil.R -class GalleryAdapter(private val images: List) : RecyclerView.Adapter() { +class ReaderAdapter(private val images: List) : RecyclerView.Adapter() { - class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view) - - private var onClick: (() -> Unit)? = null - fun setOnClick(callback: (() -> Unit)?) { - this.onClick = callback - } + class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { LayoutInflater.from(parent.context).inflate( - R.layout.item_gallery, parent, false + R.layout.item_reader, parent, false ).let { - return ViewHolder(it as ImageView) + return ViewHolder(it) } } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - with(holder.view) { - setOnClickListener { - onClick?.invoke() - } - + with(holder.view as ImageView) { CoroutineScope(Dispatchers.Default).launch { val options = BitmapFactory.Options() diff --git a/app/src/main/java/xyz/quaver/pupil/types/Tags.kt b/app/src/main/java/xyz/quaver/pupil/types/Tags.kt new file mode 100644 index 00000000..7abc6c97 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/types/Tags.kt @@ -0,0 +1,96 @@ +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 { + if (tag.first() == '-') { + tag.substring(1).split(Regex(":"), 2).let { + return when(it.size) { + 2 -> Tag(it[0], it[1], true) + else -> Tag(null, tag, true) + } + } + } + tag.split(Regex(":"), 2).let { + return when(it.size) { + 2 -> Tag(it[0], it[1]) + else -> Tag(null, tag) + } + } + } + } + + override fun toString(): String { + return (if (isNegative) "-" else "") + when(area) { + null -> tag + else -> "$area:$tag" + } + } + + override fun equals(other: Any?): Boolean { + if (other !is Tag) + return false + + if (other.area == area && other.tag == tag) + return true + + return false + } + + override fun hashCode(): Int { + return super.hashCode() + } +} + +class Tags(tag: List?) : ArrayList() { + + companion object { + fun parse(tags: String) : Tags { + return Tags( + tags.split(' ').map { + if (it.isNotEmpty()) + Tag.parseTag(it) + else + null + } + ) + } + } + + init { + tag?.forEach { + if (it != null) + add(it) + } + } + + fun contains(element: String): Boolean { + forEach { + if (it.toString() == element) + return true + } + + return false + } + + fun add(element: String): Boolean { + return super.add(Tag.parseTag(element)) + } + + fun remove(element: String) { + filter { it.toString() == element }.forEach { + remove(it) + } + } + + fun removeByArea(area: String) { + filter { it.area == area }.forEach { + remove(it) + } + } + + override fun toString(): String { + return joinToString(" ") { it.toString() } + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/ItemClickSupport.java b/app/src/main/java/xyz/quaver/pupil/util/ItemClickSupport.java new file mode 100644 index 00000000..765ace41 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/util/ItemClickSupport.java @@ -0,0 +1,107 @@ +package xyz.quaver.pupil.util; + +import android.view.View; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import xyz.quaver.pupil.R; + +/* + Source: http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/ + USAGE: + + ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() { + @Override + public void onItemClicked(RecyclerView recyclerView, int position, View v) { + // do it + } + }); + +*/ +public class ItemClickSupport { + private final RecyclerView mRecyclerView; + private OnItemClickListener mOnItemClickListener; + private OnItemLongClickListener mOnItemLongClickListener; + private View.OnClickListener mOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mOnItemClickListener != null) { + RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); + mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v); + } + } + }; + private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (mOnItemLongClickListener != null) { + RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); + return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v); + } + return false; + } + }; + private RecyclerView.OnChildAttachStateChangeListener mAttachListener + = new RecyclerView.OnChildAttachStateChangeListener() { + @Override + public void onChildViewAttachedToWindow(@NonNull View view) { + if (mOnItemClickListener != null) { + view.setOnClickListener(mOnClickListener); + } + if (mOnItemLongClickListener != null) { + view.setOnLongClickListener(mOnLongClickListener); + } + } + + @Override + public void onChildViewDetachedFromWindow(@NonNull View view) { + + } + }; + + private ItemClickSupport(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + mRecyclerView.setTag(R.id.item_click_support, this); + mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener); + } + + public static ItemClickSupport addTo(RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support == null) { + support = new ItemClickSupport(view); + } + return support; + } + + public static ItemClickSupport removeFrom(RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support != null) { + support.detach(view); + } + return support; + } + + public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) { + mOnItemClickListener = listener; + return this; + } + + public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) { + mOnItemLongClickListener = listener; + return this; + } + + private void detach(RecyclerView view) { + view.removeOnChildAttachStateChangeListener(mAttachListener); + view.setTag(R.id.item_click_support, null); + } + + public interface OnItemClickListener { + + void onItemClicked(RecyclerView recyclerView, int position, View v); + } + + public interface OnItemLongClickListener { + + boolean onItemLongClicked(RecyclerView recyclerView, int position, View v); + } +} \ No newline at end of file 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 e5037094..b25e03e0 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -18,14 +18,5 @@ fun checkUpdate(url: String, currentVersion: String) : JsonObject? { if (currentVersion != releases[0].jsonObject["tag_name"]?.content) return releases[0].jsonObject - return null -} - -fun getApkUrl(releases: JsonObject, releaseName: String) : Pair? { - releases["assets"]?.jsonArray?.forEach { - if (Regex(releaseName).matches(it.jsonObject["name"]?.content ?: "")) - return Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content) - } - return null } \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_email.xml b/app/src/main/res/drawable-anydpi/ic_email.xml new file mode 100644 index 00000000..4163eb94 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_email.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_help.xml b/app/src/main/res/drawable-anydpi/ic_help.xml new file mode 100644 index 00000000..785b2478 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_help.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_email.png b/app/src/main/res/drawable-hdpi/ic_email.png new file mode 100644 index 00000000..6cf75e2a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_email.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_help.png b/app/src/main/res/drawable-hdpi/ic_help.png new file mode 100644 index 00000000..64941638 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_help.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_email.png b/app/src/main/res/drawable-mdpi/ic_email.png new file mode 100644 index 00000000..d305e584 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_email.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_help.png b/app/src/main/res/drawable-mdpi/ic_help.png new file mode 100644 index 00000000..b2b8c636 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_help.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_email.png b/app/src/main/res/drawable-xhdpi/ic_email.png new file mode 100644 index 00000000..6854119d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_email.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_help.png b/app/src/main/res/drawable-xhdpi/ic_help.png new file mode 100644 index 00000000..6ef7a89d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_help.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_email.png b/app/src/main/res/drawable-xxhdpi/ic_email.png new file mode 100644 index 00000000..ed17daf2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_email.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_help.png b/app/src/main/res/drawable-xxhdpi/ic_help.png new file mode 100644 index 00000000..760e70d4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_help.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3d702355..0aec4ddd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,7 +6,6 @@ android:id="@+id/main_drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" - android:fitsSystemWindows="true" tools:openDrawer="start"> diff --git a/app/src/main/res/layout/activity_main_content.xml b/app/src/main/res/layout/activity_main_content.xml index f0d84786..ffebabe9 100644 --- a/app/src/main/res/layout/activity_main_content.xml +++ b/app/src/main/res/layout/activity_main_content.xml @@ -22,7 +22,7 @@ + android:layout_marginBottom="-80dp"> @@ -71,7 +71,7 @@ android:layout_height="match_parent" app:floatingSearch_searchBarMarginLeft="8dp" app:floatingSearch_searchBarMarginRight="8dp" - app:floatingSearch_searchBarMarginTop="8dp" + app:floatingSearch_searchBarMarginTop="24dp" app:floatingSearch_searchHint="@string/search_hint" app:floatingSearch_suggestionsListAnimDuration="250" app:floatingSearch_showSearchKey="true" diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_reader.xml similarity index 63% rename from app/src/main/res/layout/activity_gallery.xml rename to app/src/main/res/layout/activity_reader.xml index 34130f3e..a77f48e5 100644 --- a/app/src/main/res/layout/activity_gallery.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -1,18 +1,25 @@ - + tools:context=".ReaderActivity"> + + + android:layout_height="wrap_content"> - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_default_query.xml b/app/src/main/res/layout/dialog_default_query.xml new file mode 100644 index 00000000..ca1bd29d --- /dev/null +++ b/app/src/main/res/layout/dialog_default_query.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +