diff --git a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt index f6cbca02..79db10d2 100644 --- a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt @@ -7,7 +7,12 @@ import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith +import xyz.quaver.hiyobi.cookie +import xyz.quaver.hiyobi.getReader +import xyz.quaver.hiyobi.user_agent import java.io.File +import java.net.URL +import javax.net.ssl.HttpsURLConnection /** * Instrumented test, which will execute on an Android device. @@ -35,6 +40,17 @@ class ExampleInstrumentedTest { @Test fun test_doSearch() { + val reader = getReader(1426382) + val data: ByteArray + + with(URL(reader[0].url).openConnection() as HttpsURLConnection) { + setRequestProperty("User-Agent", user_agent) + setRequestProperty("Cookie", cookie) + + data = inputStream.readBytes() + } + + Log.d("Pupil", data.size.toString()) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 766a3ca7..e8cbecd4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="xyz.quaver.pupil"> + - - - - diff --git a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt index d35b14b4..28079aaf 100644 --- a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt @@ -1,12 +1,14 @@ package xyz.quaver.pupil +import android.Manifest import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri 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.* import android.widget.EditText import android.widget.ImageView @@ -15,16 +17,20 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.GravityCompat import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.util.view.SearchInputView import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.snackbar.Snackbar 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.io.IOException import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonObject @@ -35,9 +41,12 @@ import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.types.TagSuggestion import xyz.quaver.pupil.util.* import java.io.File +import java.io.FileInputStream import java.io.FileOutputStream import java.net.URL import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import javax.net.ssl.HttpsURLConnection import kotlin.collections.ArrayList import kotlin.math.roundToInt @@ -68,6 +77,8 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + checkPermissions() + val preference = PreferenceManager.getDefaultSharedPreferences(this) if (Locale.getDefault().language == "ko") { @@ -238,6 +249,11 @@ class MainActivity : AppCompatActivity() { } } + private fun checkPermissions() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489) + } + private fun initView() { var prevP1 = 0 main_appbar_layout.addOnOffsetChangedListener( @@ -280,12 +296,7 @@ class MainActivity : AppCompatActivity() { loadBlocks() } 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() + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) } R.id.main_drawer_github -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) @@ -359,10 +370,9 @@ class MainActivity : AppCompatActivity() { isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(galleryBlock.id, false) setOnClickListener { val downloader = GalleryDownloader.get(galleryBlock.id) - if (downloader == null) { + if (downloader == null) GalleryDownloader(context, galleryBlock, true).start() - downloads.add(galleryBlock.id) - } else { + else { downloader.cancel() downloader.clearNotification() } @@ -377,11 +387,91 @@ class MainActivity : AppCompatActivity() { this?.cancelAndJoin() this?.clearNotification() } - val cache = File(cacheDir, "imageCache/${galleryBlock.id}/images/") + val cache = File(cacheDir, "imageCache/${galleryBlock.id}") + val data = File(ContextCompat.getDataDir(this@MainActivity), "images/${galleryBlock.id}") cache.deleteRecursively() + data.deleteRecursively() + + downloads.remove(galleryBlock.id) + + if (mode == Mode.DOWNLOAD) { + runOnUiThread { + cancelFetch() + clearGalleries() + fetchGalleries(query) + loadBlocks() + } + } + + (adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false) + } + dialog.dismiss() + } + + with(view.main_dialog_export) { + val images = File(ContextCompat.getDataDir(this@MainActivity), "images/${galleryBlock.id}/images").let { + when { + it.exists() -> it + else -> File(cacheDir, "imageCache/${galleryBlock.id}/images") + } + } + isEnabled = images.exists() + + setOnClickListener { + CoroutineScope(Dispatchers.Default).launch { + val preference = PreferenceManager.getDefaultSharedPreferences(context) + val zip = preference.getBoolean("export_zip", false) + + if (zip) { + var target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id} ${galleryBlock.title}.zip") + + try { + target.createNewFile() + } catch (e: IOException) { + target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id}.zip") + + try { + target.createNewFile() + } catch (e: IOException) { + Snackbar.make(main_layout, getString(R.string.main_export_error), Snackbar.LENGTH_LONG).show() + return@launch + } + } + + FileOutputStream(target).use { targetStream -> + ZipOutputStream(targetStream).use {zipStream -> + images.listFiles().forEach { + zipStream.putNextEntry(ZipEntry(it.name)) + + FileInputStream(it).use { fileStream -> + fileStream.copyTo(zipStream) + } + } + } + } + } else { + var target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id} ${galleryBlock.title}") + + try { + target.canonicalPath + } catch (e: IOException) { + target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id}") + + try { + target.canonicalPath + } catch (e: IOException) { + Snackbar.make(main_layout, getString(R.string.main_export_error), Snackbar.LENGTH_LONG).show() + return@launch + } + } + + images.copyRecursively(target, true) + } + + Snackbar.make(main_layout, getString(R.string.main_export_complete), Snackbar.LENGTH_LONG).show() + } dialog.dismiss() - (adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false) } } diff --git a/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt index c92e65b0..57cc02cd 100644 --- a/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ReaderActivity.kt @@ -2,6 +2,7 @@ package xyz.quaver.pupil import android.graphics.drawable.Drawable import android.os.Bundle +import android.os.Environment import android.view.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -26,6 +27,11 @@ 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.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream class ReaderActivity : AppCompatActivity() { @@ -163,6 +169,7 @@ class ReaderActivity : AppCompatActivity() { onProgressHandler = { CoroutineScope(Dispatchers.Main).launch { reader_progressbar.progress = it + menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = true } } onDownloadedHandler = { @@ -274,6 +281,14 @@ class ReaderActivity : AppCompatActivity() { if (!downloader.download) downloader.clearNotification() } + + reader_fab_export.setOnClickListener { + downloader.export( { + Snackbar.make(reader_layout, getString(R.string.main_export_complete), Snackbar.LENGTH_LONG).show() + }, { + Snackbar.make(reader_layout, getString(R.string.main_export_error), Snackbar.LENGTH_LONG).show() + }) + } } private fun fullscreen(isFullscreen: Boolean) { diff --git a/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt index 7f941162..9f7b30be 100644 --- a/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt @@ -12,6 +12,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import kotlinx.android.synthetic.main.dialog_default_query.view.* @@ -95,6 +96,33 @@ class SettingsActivity : AppCompatActivity() { true } } + with(findPreference("delete_downloads")) { + this ?: return@with + + val dir = File(ContextCompat.getDataDir(context), "images") + + summary = getCacheSize(dir) + + setOnPreferenceClickListener { + AlertDialog.Builder(context).apply { + setTitle(R.string.warning) + setMessage(R.string.settings_clear_downloads_alert_message) + setPositiveButton(android.R.string.yes) { _, _ -> + if (dir.exists()) + dir.deleteRecursively() + + val downloads = (activity!!.application as Pupil).downloads + + downloads.clear() + + summary = getCacheSize(dir) + } + setNegativeButton(android.R.string.no) { _, _ -> } + }.show() + + true + } + } with(findPreference("clear_history")) { this ?: return@with 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 c75529b7..ed8aa83d 100644 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt +++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt @@ -68,16 +68,30 @@ class GalleryBlockAdapter(private val galleries: List it + else -> File(context.cacheDir, "imageCache/${gallery.id}/reader.json") + } + } + } + val imageCache = { + File(ContextCompat.getDataDir(context), "images/${gallery.id}/images").let { + when { + it.exists() -> it + else -> File(context.cacheDir, "imageCache/${gallery.id}/images") + } + } + } - if (readerCache.exists()) { + if (readerCache.invoke().exists()) { val reader = Json(JsonConfiguration.Stable) - .parse(ReaderItem.serializer().list, readerCache.readText()) + .parse(ReaderItem.serializer().list, readerCache.invoke().readText()) with(galleryblock_progressbar) { max = reader.size - progress = imageCache.list()?.size ?: 0 + progress = imageCache.invoke().list()?.size ?: 0 visibility = View.VISIBLE } @@ -89,9 +103,9 @@ class GalleryBlockAdapter(private val galleries: List it + else -> File(cacheDir, "imageCache/${galleryBlock.id}/reader.json") + } + } if (cache.exists()) { val cached = json.parse(serializer, cache.readText()) @@ -90,7 +115,10 @@ class GalleryDownloader( useHiyobi -> { xyz.quaver.hiyobi.getReader(galleryBlock.id).let { when { - it.isEmpty() -> getReader(galleryBlock.id) + it.isEmpty() -> { + useHiyobi = false + getReader(galleryBlock.id) + } else -> it } } @@ -148,12 +176,21 @@ class GalleryDownloader( val name = "$index".padStart(4, '0') val ext = url.split('.').last() - val cache = File(cacheDir, "/imageCache/${galleryBlock.id}/images/$name.$ext") + val cache = File(ContextCompat.getDataDir(this@GalleryDownloader), "images/${galleryBlock.id}/images/$name.$ext").let { + when { + it.exists() -> it + else -> File(cacheDir, "/imageCache/${galleryBlock.id}/images/$name.$ext") + } + } if (!cache.exists()) try { with(URL(url).openConnection() as HttpsURLConnection) { - setRequestProperty("Referer", getReferer(galleryBlock.id)) + if (useHiyobi) { + setRequestProperty("User-Agent", user_agent) + setRequestProperty("Cookie", cookie) + } else + setRequestProperty("Referer", getReferer(galleryBlock.id)) if (!cache.parentFile.exists()) cache.parentFile.mkdirs() @@ -189,8 +226,35 @@ class GalleryDownloader( .setContentText(getString(R.string.reader_notification_complete)) .setProgress(0, 0, false) - if (download) - notificationManager.notify(galleryBlock.id, notificationBuilder.build()) + if (download) { + File(cacheDir, "imageCache/${galleryBlock.id}").let { + if (it.exists()) { + it.copyRecursively( + File(ContextCompat.getDataDir(this@GalleryDownloader), "images/${galleryBlock.id}"), + true + ) + it.deleteRecursively() + } + } + + val preference = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader) + val autoExport = preference.getBoolean("auto_export", false) + + if (autoExport) { + export({ + notificationManager.notify(galleryBlock.id, notificationBuilder.build()) + }, { + notificationBuilder + .setContentTitle(galleryBlock.title) + .setContentText(getString(R.string.main_export_error)) + .setProgress(0, 0, false) + + notificationManager.notify(galleryBlock.id, notificationBuilder.build()) + }) + } else { + notificationManager.notify(galleryBlock.id, notificationBuilder.build()) + } + } download = false } @@ -207,6 +271,8 @@ class GalleryDownloader( suspend fun cancelAndJoin() { downloadJob?.cancelAndJoin() + + remove(galleryBlock.id) } fun invokeOnReaderLoaded() { @@ -243,4 +309,69 @@ class GalleryDownloader( notificationManager = NotificationManagerCompat.from(this) } + fun export(onSuccess: (() -> Unit)? = null, onError: (() -> Unit)? = null) { + val images = File(ContextCompat.getDataDir(this), "images/${galleryBlock.id}/images").let { + when { + it.exists() -> it + else -> File(cacheDir, "imageCache/${galleryBlock.id}/images") + } + } + + if (!images.exists()) + return + + CoroutineScope(Dispatchers.Default).launch { + val preference = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader) + val zip = preference.getBoolean("export_zip", false) + + if (zip) { + var target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id} ${galleryBlock.title}.zip") + + try { + target.createNewFile() + } catch (e: IOException) { + target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id}.zip") + + try { + target.createNewFile() + } catch (e: IOException) { + onError?.invoke() + return@launch + } + } + + FileOutputStream(target).use { targetStream -> + ZipOutputStream(targetStream).use { zipStream -> + images.listFiles().forEach { + zipStream.putNextEntry(ZipEntry(it.name)) + + FileInputStream(it).use { fileStream -> + fileStream.copyTo(zipStream) + } + } + } + } + } else { + var target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id} ${galleryBlock.title}") + + try { + target.canonicalPath + } catch (e: IOException) { + target = File(Environment.getExternalStorageDirectory(), "Pupil/${galleryBlock.id}") + + try { + target.canonicalPath + } catch (e: IOException) { + onError?.invoke() + return@launch + } + } + + images.copyRecursively(target, true) + } + + onSuccess?.invoke() + } + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_export.xml b/app/src/main/res/drawable/ic_export.xml new file mode 100644 index 00000000..32d0ca69 --- /dev/null +++ b/app/src/main/res/drawable/ic_export.xml @@ -0,0 +1,8 @@ + + + + \ 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 9e49df68..e9827e8d 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -51,6 +51,14 @@ app:fab_label="@string/reader_fab_download" app:fab_size="mini"/> + + +