diff --git a/app/build.gradle b/app/build.gradle
index 16bf58e5..d61a98a0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,7 +21,7 @@ android {
minSdkVersion 16
targetSdkVersion 30
versionCode 59
- versionName "5.0.2"
+ versionName "5.0.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
@@ -66,11 +66,12 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
+ implementation "androidx.activity:activity-ktx:1.2.0-alpha08"
+ implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08'
implementation 'androidx.preference:preference:1.1.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.biometric:biometric:1.0.1"
- implementation 'androidx.fragment:fragment-ktx:1.2.5'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation 'com.google.firebase:firebase-core:17.5.0'
@@ -78,6 +79,7 @@ dependencies {
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
implementation 'com.google.firebase:firebase-perf:19.0.8'
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
+ implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
//implementation 'com.quiph.ui:recyclerviewfastscroller:0.2.1'
@@ -94,6 +96,7 @@ dependencies {
transitive = false
}
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
+ implementation 'com.gu:option:1.3'
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
diff --git a/app/libs/recyclerviewfastscroller-release.aar b/app/libs/recyclerviewfastscroller-release.aar
index 9b121127..d0f8ce00 100644
Binary files a/app/libs/recyclerviewfastscroller-release.aar and b/app/libs/recyclerviewfastscroller-release.aar differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
index 94b2c235..bcad787b 100644
--- a/app/release/output-metadata.json
+++ b/app/release/output-metadata.json
@@ -12,7 +12,7 @@
"filters": [],
"properties": [],
"versionCode": 59,
- "versionName": "5.0.2",
+ "versionName": "5.0.3",
"enabled": true,
"outputFile": "app-release.apk"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47df453c..d96312dc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,10 +6,13 @@
-
-
+
+
+
+
+
+
+
for (callback in onChipClickedHandler)
callback.invoke((view as TagChip).tag)
}
- }.let { launch(Dispatchers.Main) { galleryblock_tag_group.addView(it) } }
- }
+ }
+ }.let { launch(Dispatchers.Main) { it.forEach { galleryblock_tag_group.addView(it) } } }
}
galleryblock_id.text = galleryBlock.id.toString()
@@ -279,7 +279,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
galleryblock_tag_group.visibility = View.GONE
}
}
- Log.i("PUPILD", "${System.currentTimeMillis() - time}")
}
}
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
index 92d6a2ad..d3055a8e 100644
--- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
+++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
@@ -28,6 +28,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -42,6 +43,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.ui.ReaderActivity
+import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.ellipsize
@@ -309,6 +311,18 @@ class DownloadService : Service() {
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F })
+ FirebaseCrashlytics.getInstance().log(
+ """
+ GALLERYID: $galleryID
+ CACHE: ${cache.findFile(".metadata")}
+ PATTERN: ${Preferences["download_folder_name", ""]}
+ READER ID: ${reader.galleryInfo.id}
+ READER SIZE: ${reader.galleryInfo.files.size}
+ CACHE READER ID: ${cache.metadata.reader?.galleryInfo?.id}}
+ CACHE READER SIZE: ${cache.metadata.reader?.galleryInfo?.files?.size}
+ """.trimIndent()
+ )
+
cache.metadata.imageList?.let {
if (progress[galleryID]?.size != it.size) {
cache.metadata.imageList?.filterNotNull()?.forEach { file ->
@@ -325,7 +339,7 @@ class DownloadService : Service() {
if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService)
- .getDownloadFolder(galleryID) != null)
+ .getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID)
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
index 92a9bc24..f9810946 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
@@ -18,15 +18,23 @@
package xyz.quaver.pupil.ui
+import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
+import android.content.pm.PackageManager
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
+import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.view.*
+import android.view.animation.Animation
+import android.view.animation.AnticipateInterpolator
+import android.view.animation.OvershootInterpolator
+import android.view.animation.TranslateAnimation
import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
@@ -38,12 +46,15 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.google.mlkit.vision.face.Face
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
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.android.synthetic.main.reader_eye_card.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import xyz.quaver.Code
import xyz.quaver.pupil.R
@@ -52,11 +63,13 @@ import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences
+import xyz.quaver.pupil.util.camera
+import xyz.quaver.pupil.util.closeCamera
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
+import xyz.quaver.pupil.util.startCamera
import java.util.*
import kotlin.concurrent.schedule
-import kotlin.concurrent.timer
class ReaderActivity : BaseActivity() {
@@ -89,12 +102,29 @@ class ReaderActivity : BaseActivity() {
}
private val timer = Timer()
- private var autoTimer: Timer? = null
-
private val snapHelper = PagerSnapHelper()
-
private var menu: Menu? = null
+ private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ if (isGranted)
+ toggleCamera()
+ else
+ AlertDialog.Builder(this)
+ .setTitle(R.string.error)
+ .setMessage(R.string.camera_denied)
+ .setPositiveButton(android.R.string.ok) { _, _ ->}
+ .show()
+ }
+
+ enum class Eye {
+ LEFT,
+ RIGHT
+ }
+
+ private var cameraEnabled = false
+ private var eyeType: Eye? = null
+ private var eyeCount: Int = 0
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader)
@@ -219,6 +249,18 @@ class ReaderActivity : BaseActivity() {
return true
}
+ override fun onResume() {
+ super.onResume()
+
+ if (cameraEnabled)
+ startCamera(this, cameraCallback)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ closeCamera()
+ }
+
override fun onDestroy() {
super.onDestroy()
@@ -368,6 +410,7 @@ class ReaderActivity : BaseActivity() {
animateDownloadFAB(false)
} else {
downloadManager.addDownloadFolder(galleryID)
+ DownloadService.download(context, galleryID, true)
animateDownloadFAB(true)
}
}
@@ -377,31 +420,26 @@ class ReaderActivity : BaseActivity() {
with(reader_fab_retry) {
setImageResource(R.drawable.refresh)
setOnClickListener {
- downloader?.cancel(galleryID)
- downloader?.download(galleryID)
+ DownloadService.download(context, galleryID)
}
}
with(reader_fab_auto) {
- setImageResource(R.drawable.clock_start)
+ setImageResource(R.drawable.eye_white)
setOnClickListener {
- if (autoTimer == null) {
- autoTimer = timer(initialDelay = 10000L, period = 10000L) {
- CoroutineScope(Dispatchers.Main).launch {
- with(this@ReaderActivity.reader_recyclerview) {
- val lastItem =
- (layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
-
- if (lastItem < adapter!!.itemCount - 1)
- (layoutManager as LinearLayoutManager).scrollToPosition(lastItem + 1)
- }
- }
+ when {
+ ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
+ toggleCamera()
}
- setImageResource(R.drawable.clock_end)
- } else {
- autoTimer?.cancel()
- autoTimer = null
- setImageResource(R.drawable.clock_start)
+ Build.VERSION.SDK_INT >= 23 && shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
+ AlertDialog.Builder(this@ReaderActivity)
+ .setTitle(R.string.warning)
+ .setMessage(R.string.camera_denied)
+ .setPositiveButton(android.R.string.ok) { _, _ ->}
+ .show()
+ }
+ else ->
+ requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
@@ -454,6 +492,17 @@ class ReaderActivity : BaseActivity() {
} else {
snapHelper.attachToRecyclerView(reader_recyclerview)
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false])
+
+ if (Preferences["rtl", false])
+ with(reader_progressbar) {
+ scaleX = -1F
+ translationX = 1F
+ }
+ else
+ with(reader_progressbar) {
+ scaleX = 0F
+ translationX = 0F
+ }
}
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
@@ -488,6 +537,125 @@ class ReaderActivity : BaseActivity() {
}
}
+ val cameraCallback: (List) -> Unit = callback@{ faces ->
+ eye_card.dot.let {
+ it.visibility = View.VISIBLE
+ CoroutineScope(Dispatchers.Main).launch {
+ delay(50)
+ it.visibility = View.INVISIBLE
+ }
+ }
+
+ if (faces.size != 1)
+ ContextCompat.getDrawable(this, R.drawable.eye_off).let {
+ with(eye_card) {
+ left_eye.setImageDrawable(it)
+ right_eye.setImageDrawable(it)
+ }
+
+ return@callback
+ }
+
+ val (left, right) = Pair(
+ faces[0].rightEyeOpenProbability?.let { it > 0.4 } == true,
+ faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
+ )
+
+ with(eye_card) {
+ left_eye.setImageDrawable(
+ ContextCompat.getDrawable(
+ context,
+ if (left) R.drawable.eye else R.drawable.eye_closed
+ )
+ )
+ right_eye.setImageDrawable(
+ ContextCompat.getDrawable(
+ context,
+ if (right) R.drawable.eye else R.drawable.eye_closed
+ )
+ )
+ }
+
+ when {
+ // Both closed / opened
+ !left.xor(right) -> {
+ eyeType = null
+ eyeCount = 0
+ }
+ !left -> {
+ if (eyeType != Eye.LEFT) {
+ eyeType = Eye.LEFT
+ eyeCount = 0
+ }
+ eyeCount++
+ }
+ !right -> {
+ if (eyeType != Eye.RIGHT) {
+ eyeType = Eye.RIGHT
+ eyeCount = 0
+ }
+ eyeCount++
+ }
+ }
+
+ if (eyeCount > 3) {
+ (this@ReaderActivity.reader_recyclerview.layoutManager as LinearLayoutManager).let {
+ it.scrollToPositionWithOffset(when(eyeType!!) {
+ Eye.RIGHT -> {
+ if (it.reverseLayout) currentPage - 2 else currentPage
+ }
+ Eye.LEFT -> {
+ if (it.reverseLayout) currentPage else currentPage - 2
+ }
+ }, 0)
+ }
+
+ eyeType = null
+ eyeCount = 0
+ }
+ }
+
+ private fun toggleCamera() {
+ val eyes = this@ReaderActivity.eye_card
+ when (camera) {
+ null -> {
+ reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel)
+ reader_fab_auto.setImageResource(R.drawable.eye_off_white)
+ eyes.apply {
+ visibility = View.VISIBLE
+ TranslateAnimation(0F, 0F, -100F, 0F).apply {
+ duration = 500
+ fillAfter = false
+ interpolator = OvershootInterpolator()
+ }.let { startAnimation(it) }
+ }
+ startCamera(this, cameraCallback)
+ cameraEnabled = true
+ }
+ else -> {
+ reader_fab_auto.labelText = getString(R.string.reader_fab_auto)
+ reader_fab_auto.setImageResource(R.drawable.eye_white)
+ eyes.apply {
+ TranslateAnimation(0F, 0F, 0F, -100F).apply {
+ duration = 500
+ fillAfter = false
+ interpolator = AnticipateInterpolator()
+ setAnimationListener(object: Animation.AnimationListener {
+ override fun onAnimationStart(p0: Animation?) {}
+ override fun onAnimationRepeat(p0: Animation?) {}
+
+ override fun onAnimationEnd(p0: Animation?) {
+ eyes.visibility = View.GONE
+ }
+ })
+ }.let { startAnimation(it) }
+ }
+ closeCamera()
+ cameraEnabled = false
+ }
+ }
+ }
+
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).onLowMemory()
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
index c235eac3..042854e2 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
@@ -186,7 +186,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
if (key == null) entries[key]!!.location_available.text = downloadFolder
}
else
- Preferences["download_folder"] = File(directory).canonicalPath
+ Preferences["download_folder"] = File(directory).toURI().toString()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt
index e54ace31..6fe1b42a 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt
@@ -18,13 +18,13 @@
package xyz.quaver.pupil.ui.dialog
-import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout.LayoutParams
+import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -53,7 +53,7 @@ import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.wordCapitalize
-class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
+class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : AlertDialog(context) {
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
index cd79be1d..ef387396 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
@@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
+import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_proxy.view.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -40,7 +41,7 @@ import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.proxyInfo
import java.net.Proxy
-class ProxyDialog(context: Context) : Dialog(context) {
+class ProxyDialog(context: Context) : AlertDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(build())
diff --git a/app/src/main/java/xyz/quaver/pupil/util/camera.kt b/app/src/main/java/xyz/quaver/pupil/util/camera.kt
new file mode 100644
index 00000000..1b2e5652
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/camera.kt
@@ -0,0 +1,119 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2020 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+@file:Suppress("DEPRECATION", "Recycle")
+
+package xyz.quaver.pupil.util
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
+import android.hardware.Camera
+import android.view.Surface
+import android.view.WindowManager
+import com.google.android.gms.tasks.Task
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.face.Face
+import com.google.mlkit.vision.face.FaceDetection
+import com.google.mlkit.vision.face.FaceDetectorOptions
+
+/** Check if this device has a camera */
+private fun Context.checkCameraHardware() =
+ this.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
+
+private fun openFrontCamera() : Pair {
+ var camera: Camera? = null
+ var cameraID: Int = -1
+
+ val cameraInfo = Camera.CameraInfo()
+
+ for (i in 0 until Camera.getNumberOfCameras()) {
+ Camera.getCameraInfo(i, cameraInfo)
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
+ runCatching { Camera.open(i) }.getOrNull()?.let { camera = it; cameraID = i }
+
+ if (camera != null) break
+ }
+
+ return Pair(camera, cameraID)
+}
+
+val orientations = mapOf(
+ Surface.ROTATION_0 to 0,
+ Surface.ROTATION_90 to 90,
+ Surface.ROTATION_180 to 180,
+ Surface.ROTATION_270 to 270,
+)
+
+private fun getRotation(context: Context, cameraID: Int): Int {
+ val cameraRotation = Camera.CameraInfo().also { Camera.getCameraInfo(cameraID, it) }.orientation
+ val rotation = orientations[(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation] ?: error("")
+
+ return (cameraRotation + rotation) % 360
+}
+
+var camera: Camera? = null
+var surfaceTexture: SurfaceTexture? = null
+private val detector = FaceDetection.getClient(
+ FaceDetectorOptions.Builder()
+ .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
+ .build()
+)
+private var process: Task>? = null
+
+fun startCamera(context: Context, callback: (List) -> Unit) {
+ if (camera != null) closeCamera()
+
+ val cameraID = openFrontCamera().let { (cam, cameraID) ->
+ cam ?: return
+ camera = cam
+ cameraID
+ }
+
+ with (camera!!) {
+ parameters = parameters.apply {
+ setPreviewSize(640, 480)
+ previewFormat = ImageFormat.NV21
+ }
+
+ setPreviewTexture(surfaceTexture ?: SurfaceTexture(0).also {
+ surfaceTexture = it
+ })
+ startPreview()
+ setPreviewCallback { bytes, _ ->
+ if (process?.isComplete == false)
+ return@setPreviewCallback
+
+ val rotation = getRotation(context, cameraID)
+
+ val image = InputImage.fromByteArray(bytes, 640, 480, rotation, InputImage.IMAGE_FORMAT_NV21)
+ process = detector.process(image)
+ .addOnSuccessListener(callback)
+ }
+ }
+}
+
+fun closeCamera() {
+ camera?.setPreviewCallback(null)
+ camera?.stopPreview()
+ surfaceTexture?.release()
+ surfaceTexture = null
+ camera?.release()
+ camera = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
deleted file mode 100644
index 46e7b8e7..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.util.download
-
-import android.content.Context
-import android.content.ContextWrapper
-import android.util.Base64
-import android.util.SparseArray
-import androidx.preference.PreferenceManager
-import com.google.firebase.crashlytics.FirebaseCrashlytics
-import kotlinx.coroutines.*
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
-import xyz.quaver.Code
-import xyz.quaver.hitomi.GalleryBlock
-import xyz.quaver.hitomi.Reader
-import xyz.quaver.pupil.util.getCachedGallery
-import xyz.quaver.pupil.util.getDownloadDirectory
-import xyz.quaver.pupil.util.isParentOf
-import xyz.quaver.readBytes
-import java.io.BufferedInputStream
-import java.io.File
-import java.io.FileOutputStream
-import java.io.InputStream
-import java.net.URL
-
-@Suppress("DEPRECATION")
-@Deprecated("Use downloader.Cache instead")
-class Cache(context: Context) : ContextWrapper(context) {
-
- companion object {
- private val moving = mutableListOf()
- private val readers = SparseArray()
- }
-
- private val preference = PreferenceManager.getDefaultSharedPreferences(this)
-
- // Search in this order
- // Download -> Cache
- fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
- if (!it.exists())
- it.mkdirs()
- }
-
- fun getCachedMetadata(galleryID: Int) : Metadata? {
- val file = File(getCachedGallery(galleryID), ".metadata")
-
- if (!file.exists())
- return null
-
- return try {
- Json.decodeFromString(file.readText())
- } catch (e: Exception) {
- //File corrupted
- file.delete()
- null
- }
- }
-
- fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
- if (preference.getBoolean("cache_disable", false))
- return
-
- val file = File(getCachedGallery(galleryID), ".metadata").also {
- if (!it.exists())
- it.createNewFile()
- }
-
- file.writeText(Json.encodeToString(metadata))
- }
-
- suspend fun getThumbnail(galleryID: Int): String? {
- val metadata = Cache(this).getCachedMetadata(galleryID)
-
- @Suppress("BlockingMethodInNonBlockingContext")
- val thumbnail = if (metadata?.thumbnail == null)
- withContext(Dispatchers.IO) {
- val thumbnail = getGalleryBlock(galleryID)?.thumbnails?.firstOrNull() ?: return@withContext null
- try {
- val data = URL(thumbnail).readBytes().apply {
- if (isEmpty()) return@withContext null
- }
- Base64.encodeToString(data, Base64.DEFAULT)
- } catch (e: Exception) {
- null
- }
- }
- else
- metadata.thumbnail
-
- setCachedMetadata(
- galleryID,
- Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
- )
-
- return thumbnail
- }
-
- suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
- val metadata = Cache(this).getCachedMetadata(galleryID)
-
- val sources = listOf(
- { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
- { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
- )
-
- val galleryBlock = if (metadata?.galleryBlock == null) {
- withContext(Dispatchers.IO) {
- var galleryBlock: GalleryBlock? = null
-
- for (source in sources) {
- galleryBlock = try {
- source.invoke()
- } catch (e: Exception) {
- null
- }
-
- if (galleryBlock != null)
- break
- }
-
- galleryBlock
- } ?: return null
- }
- else
- metadata.galleryBlock
-
- setCachedMetadata(
- galleryID,
- Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
- )
-
- return galleryBlock
- }
-
- fun getReaderOrNull(galleryID: Int): Reader? {
- return readers[galleryID] ?: getCachedMetadata(galleryID)?.reader
- }
-
- suspend fun getReader(galleryID: Int): Reader? {
- val metadata = getCachedMetadata(galleryID)
- val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
-
- val sources = mapOf(
- Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
- Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
- ).let {
- if (mirrors.isNotEmpty())
- it.toSortedMap{ o1, o2 ->
- mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
- }
- else
- it
- }
-
- val reader =
- if (readers[galleryID] != null)
- return readers[galleryID]
- else if (metadata?.reader == null) {
- var retval: Reader? = null
-
- for (source in sources) {
- retval = try {
- withContext(Dispatchers.IO) {
- withTimeoutOrNull(1000) {
- source.value.invoke()
- }
- }
- } catch (e: Exception) {
- FirebaseCrashlytics.getInstance().recordException(e)
- null
- }
-
- if (retval != null)
- break
- }
-
- retval
- } else
- metadata.reader
-
- readers.put(galleryID, reader)
-
- setCachedMetadata(
- galleryID,
- Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
- )
-
- return reader
- }
-
- val imageNameRegex = Regex("""^\d+\..+$""")
- fun getImages(galleryID: Int): List? {
- val gallery = getCachedGallery(galleryID)
-
- return gallery.list { _, name ->
- imageNameRegex.matches(name)
- }?.map {
- File(gallery, it)
- }
- }
-
- val imageExtensions = listOf(
- "png",
- "jpg",
- "webp",
- "gif"
- )
- fun getImage(galleryID: Int, index: Int): File? {
- val gallery = getCachedGallery(galleryID)
-
- for (ext in imageExtensions) {
- File(gallery, "%05d.$ext".format(index)).let {
- if (it.exists())
- return it
- }
- }
-
- return null
- }
-
-
- fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
- if (preference.getBoolean("cache_disable", false))
- return
-
- val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
- if (!it.exists())
- it.createNewFile()
- }
-
- try {
- BufferedInputStream(data).use { inputStream ->
- FileOutputStream(cache).use { outputStream ->
- inputStream.copyTo(outputStream)
- }
- }
- } catch (e: Exception) {
- cache.delete()
- }
- }
-
- fun moveToDownload(galleryID: Int) {
- if (preference.getBoolean("cache_disable", false))
- return
-
- if (moving.contains(galleryID))
- return
-
- CoroutineScope(Dispatchers.IO).launch {
- val cache = getCachedGallery(galleryID).also {
- if (!it.exists())
- return@launch
- }
- val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
-
- if (download.isParentOf(cache))
- return@launch
-
- FirebaseCrashlytics.getInstance().log("MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
-
- cache.copyRecursively(download, true) { file, err ->
- FirebaseCrashlytics.getInstance().log("MOVING ERROR ${file.canonicalPath} ${err.message}")
- OnErrorAction.SKIP
- }
- FirebaseCrashlytics.getInstance().log("MOVED ${cache.canonicalPath}")
-
- FirebaseCrashlytics.getInstance().log("DELETING ${cache.canonicalPath}")
- cache.deleteRecursively()
- FirebaseCrashlytics.getInstance().log("DELETED ${cache.canonicalPath}")
- }
- }
-
- fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
-
- fun setDownloading(galleryID: Int, isDownloading: Boolean) {
- setCachedMetadata(galleryID, Metadata(getCachedMetadata(galleryID), isDownloading = isDownloading))
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt b/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt
deleted file mode 100644
index edd22251..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt
+++ /dev/null
@@ -1,389 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.util.download
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.Intent
-import android.content.SharedPreferences
-import android.util.Log
-import android.util.SparseArray
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.app.TaskStackBuilder
-import androidx.preference.PreferenceManager
-import com.google.firebase.crashlytics.FirebaseCrashlytics
-import kotlinx.coroutines.*
-import okhttp3.*
-import okio.*
-import xyz.quaver.Code
-import xyz.quaver.hitomi.Reader
-import xyz.quaver.hitomi.getReferer
-import xyz.quaver.hitomi.imageUrlFromImage
-import xyz.quaver.hiyobi.createImgList
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.client
-import xyz.quaver.pupil.interceptors
-import xyz.quaver.pupil.ui.ReaderActivity
-import java.io.File
-import java.io.IOException
-import java.util.concurrent.LinkedBlockingQueue
-
-@Suppress("DEPRECATION")
-@Deprecated("Use DownloadService instead")
-@OptIn(ExperimentalCoroutinesApi::class)
-class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
-
- private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
-
- //region ProgressListener
- @Suppress("UNCHECKED_CAST")
- private val progressListener = object: ProgressListener {
- override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
- val (galleryID, index) = (tag as? Pair) ?: return
-
- if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
- progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
- }
- }
-
- interface ProgressListener {
- fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
- }
-
- class ProgressResponseBody(
- val tag: Any?,
- val responseBody: ResponseBody,
- val progressListener : ProgressListener
- ) : ResponseBody() {
- private var bufferedSource : BufferedSource? = null
-
- override fun contentLength() = responseBody.contentLength()
- override fun contentType() = responseBody.contentType()
-
- override fun source(): BufferedSource {
- if (bufferedSource == null)
- bufferedSource = Okio.buffer(source(responseBody.source()))
-
- return bufferedSource!!
- }
-
- private fun source(source: Source) = object: ForwardingSource(source) {
- var totalBytesRead = 0L
-
- override fun read(sink: Buffer, byteCount: Long): Long {
- val bytesRead = super.read(sink, byteCount)
-
- totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
- progressListener.update(tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
-
- return bytesRead
- }
-
- }
- }
-
- init {
- interceptors[Pair::class] = { chain ->
- val request = chain.request()
- var response = chain.proceed(request)
-
- var retry = 5
- while (!response.isSuccessful && retry > 0) {
- response = chain.proceed(request)
- retry--
- }
-
- response.newBuilder()
- .body(response.body()?.let {
- ProgressResponseBody(request.tag(), it, progressListener)
- }).build()
- }
- }
- //endregion
-
- //region Singleton
- companion object {
-
- @Volatile private var instance: DownloadWorker? = null
-
- fun getInstance(context: Context) =
- instance ?: synchronized(this) {
- instance ?: DownloadWorker(context).also { instance = it }
- }
- }
- //endregion
-
- val notificationManager = NotificationManagerCompat.from(context)
-
- val queue = LinkedBlockingQueue()
-
- /*
- * KEY
- * primary galleryID
- * secondary index
- * PRIMARY VALUE
- * MutableList -> Download in progress
- * null -> Loading / Gallery doesn't exist
- * SECONDARY VALUE
- * 0 <= value < 100 -> Download in progress
- * Float.POSITIVE_INFINITY -> Download completed
- */
- val progress = SparseArray?>()
- val notification = SparseArray()
-
- private val loop = loop()
- private val worker = SparseArray()
-
- fun stop() {
- queue.clear()
-
- loop.cancel()
- for (i in 0 until worker.size()) {
- val galleryID = worker.keyAt(i)
-
- Cache(this@DownloadWorker).setDownloading(galleryID, false)
- worker[galleryID]?.cancel()
- }
-
- client.dispatcher().queuedCalls().filter {
- it.request().tag() is Pair<*, *>
- }.forEach {
- it.cancel()
- }
- client.dispatcher().runningCalls().filter {
- it.request().tag() is Pair<*, *>
- }.forEach {
- it.cancel()
- }
-
- progress.clear()
- notification.clear()
- notificationManager.cancelAll()
- }
-
- fun cancel(galleryID: Int) {
- queue.remove(galleryID)
- worker[galleryID]?.cancel()
-
- client.dispatcher().queuedCalls().filter {
- ((it.request().tag() as Pair<*, *>).first as Int) == galleryID
- }.forEach {
- it.cancel()
- }
- client.dispatcher().runningCalls().filter {
- ((it.request().tag() as Pair<*, *>).first as Int) == galleryID
- }.forEach {
- it.cancel()
- }
-
- progress.remove(galleryID)
- notification.remove(galleryID)
- notificationManager.cancel(galleryID)
- }
-
- fun isCompleted(galleryID: Int) = progress[galleryID]?.all { it.isInfinite() } == true
-
- private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
- val lowQuality = preferences.getBoolean("low_quality", false)
-
- val request = Request.Builder().apply {
- when (reader.code) {
- Code.HITOMI -> {
- url(
- imageUrlFromImage(
- galleryID,
- reader.galleryInfo.files[index],
- !lowQuality
- )
- )
- addHeader("Referer", getReferer(galleryID))
- }
- Code.HIYOBI -> {
- url(createImgList(galleryID, reader, lowQuality)[index].path)
- }
- else -> {
- //shouldn't be called anyway
- }
- }
- tag(galleryID to index)
- }.build()
-
- client.newCall(request).enqueue(callback)
- }
-
- private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
- val reader = Cache(this@DownloadWorker).getReader(galleryID)
-
- //gallery doesn't exist
- if (reader == null) {
- progress.put(galleryID, null)
-
- Cache(this@DownloadWorker).setDownloading(galleryID, false)
- return@launch
- }
-
- val cache = Cache(this@DownloadWorker).getImages(galleryID)
-
- progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
- if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
- Float.POSITIVE_INFINITY
- else
- 0F
- }.toMutableList())
-
- if (notification[galleryID] == null)
- initNotification(galleryID)
-
- notification[galleryID]?.setContentTitle(reader.galleryInfo.title)
- notify(galleryID)
-
- if (isCompleted(galleryID)) {
- with(Cache(this@DownloadWorker)) {
- if (isDownloading(galleryID)) {
- moveToDownload(galleryID)
- setDownloading(galleryID, false)
- }
- }
-
- return@launch
- }
-
- for (i in reader.galleryInfo.files.indices) {
- val callback = object : Callback {
- override fun onFailure(call: Call, e: IOException) {
- if (e.message?.contains("cancel", true) != false)
- return
-
- cancel(galleryID)
- queue.add(galleryID)
- }
-
- override fun onResponse(call: Call, response: Response) {
- if (response.code() != 200) {
- response.close()
- onFailure(call, IOException())
- return
- }
-
- val ext = call.request().url().encodedPath().split('.').last()
-
- try {
- response.body()!!.use {
- Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
- }
- progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
-
- notify(galleryID)
-
- CoroutineScope(Dispatchers.IO).launch {
- if (isCompleted(galleryID)) {
- with(Cache(this@DownloadWorker)) {
- if (isDownloading(galleryID)) {
- moveToDownload(galleryID)
- setDownloading(galleryID, false)
- }
- }
- }
- }
- } catch (e: Exception) {
- FirebaseCrashlytics.getInstance().apply {
- log("FAIL ON OK ${call.request().tag()} (${e.message})")
- setCustomKey("POS", "FAIL ON OK")
- recordException(e)
- }
-
- File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
-
- cancel(galleryID)
- queue.add(galleryID)
- }
- }
- }
-
- if (progress[galleryID]?.get(i)?.isFinite() == true)
- queueDownload(galleryID, reader, i, callback)
- }
- }
-
- private fun notify(galleryID: Int) {
- val max = progress[galleryID]?.size ?: 0
- val progress = progress[galleryID]?.count { it.isInfinite() } ?: 0
-
- if (isCompleted(galleryID)) {
- notification[galleryID]
- ?.setContentText(getString(R.string.reader_notification_complete))
- ?.setSmallIcon(android.R.drawable.stat_sys_download_done)
- ?.setProgress(0, 0, false)
- ?.setOngoing(false)
-
- notificationManager.cancel(galleryID)
- } else
- notification[galleryID]
- ?.setProgress(max, progress, false)
- ?.setContentText("$progress/$max")
-
- if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
- notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
- else
- notificationManager.cancel(galleryID)
- }
-
- private fun initNotification(galleryID: Int) {
- val intent = Intent(this, ReaderActivity::class.java).apply {
- putExtra("galleryID", galleryID)
- }
- val pendingIntent = TaskStackBuilder.create(this).run {
- addNextIntentWithParentStack(intent)
- getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
- }
-
- notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
- setContentTitle(getString(R.string.reader_loading))
- setContentText(getString(R.string.reader_notification_text))
- setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
- setContentIntent(pendingIntent)
- setProgress(0, 0, true)
- setOngoing(true)
- })
- }
-
- private fun loop() = CoroutineScope(Dispatchers.Default).launch {
- while (true) {
- if (queue.isEmpty())
- continue
-
- val galleryID = queue.peek() ?: continue
-
- if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
- cancel(galleryID)
-
- if (notification[galleryID] == null)
- initNotification(galleryID)
-
- if (Cache(this@DownloadWorker).isDownloading(galleryID))
- notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
-
- worker.put(galleryID, download(galleryID))
- queue.poll()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt b/app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
deleted file mode 100644
index 8dead901..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.util.download
-
-import kotlinx.serialization.Serializable
-import xyz.quaver.hitomi.GalleryBlock
-import xyz.quaver.hitomi.Reader
-
-@Suppress("DEPRECATION")
-@Deprecated("Use downloader.Cache.Metadata instead")
-@Serializable
-data class Metadata(
- var thumbnail: String? = null,
- var galleryBlock: GalleryBlock? = null,
- var reader: Reader? = null,
- var isDownloading: Boolean? = null
-) {
- constructor(
- metadata: Metadata?,
- thumbnail: String? = null,
- galleryBlock: GalleryBlock? = null,
- readers: Reader? = null,
- isDownloading: Boolean? = null
- ) : this(
- thumbnail ?: metadata?.thumbnail,
- galleryBlock ?: metadata?.galleryBlock,
- readers ?: metadata?.reader,
- isDownloading ?: metadata?.isDownloading
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
index 2d8e6c76..d9520c82 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
@@ -104,8 +104,10 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val folder = downloadFolder.getChild(name)
- if (!folder.exists())
- folder.mkdir()
+ if (folder.exists())
+ return
+
+ folder.mkdir()
downloadFolderMap[galleryID] = folder.name
diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
index 9f9a6613..48d84ae8 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
@@ -93,14 +93,14 @@ fun GalleryBlock.formatDownloadFolder(): String =
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
- }.replace("/", "")
+ }.replace(Regex("""[*\\|"?><:/]"""), "")
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
format.let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
- }.replace("/", "")
+ }.replace(Regex("""[*\\|"?><:/]"""), "")
val Reader.requestBuilders: List
get() {
diff --git a/app/src/main/res/drawable/dot.xml b/app/src/main/res/drawable/dot.xml
new file mode 100644
index 00000000..f75e450c
--- /dev/null
+++ b/app/src/main/res/drawable/dot.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/eye.xml b/app/src/main/res/drawable/eye.xml
new file mode 100644
index 00000000..c5853f1e
--- /dev/null
+++ b/app/src/main/res/drawable/eye.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/eye_closed.xml b/app/src/main/res/drawable/eye_closed.xml
new file mode 100644
index 00000000..cb8e83fd
--- /dev/null
+++ b/app/src/main/res/drawable/eye_closed.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/eye_off.xml b/app/src/main/res/drawable/eye_off.xml
new file mode 100644
index 00000000..2f1a1e82
--- /dev/null
+++ b/app/src/main/res/drawable/eye_off.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/eye_off_white.xml b/app/src/main/res/drawable/eye_off_white.xml
new file mode 100644
index 00000000..15afd4cd
--- /dev/null
+++ b/app/src/main/res/drawable/eye_off_white.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/eye_white.xml b/app/src/main/res/drawable/eye_white.xml
new file mode 100644
index 00000000..c1f3d205
--- /dev/null
+++ b/app/src/main/res/drawable/eye_white.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/icon.xml b/app/src/main/res/drawable/icon.xml
new file mode 100644
index 00000000..8bca8ba9
--- /dev/null
+++ b/app/src/main/res/drawable/icon.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/icon_red.xml b/app/src/main/res/drawable/icon_red.xml
new file mode 100644
index 00000000..00fdcd21
--- /dev/null
+++ b/app/src/main/res/drawable/icon_red.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml
index 2356f143..e058761e 100644
--- a/app/src/main/res/drawable/menu.xml
+++ b/app/src/main/res/drawable/menu.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml
index 2e53a643..0c19d226 100644
--- a/app/src/main/res/layout/activity_reader.xml
+++ b/app/src/main/res/layout/activity_reader.xml
@@ -47,6 +47,14 @@
+
+
@@ -89,6 +98,7 @@
android:id="@+id/reader_fab_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ app:srcCompat="@drawable/refresh"
app:fab_label="@string/reader_fab_retry"
app:fab_size="mini"/>
@@ -96,6 +106,7 @@
android:id="@+id/reader_fab_auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ app:srcCompat="@drawable/eye_white"
app:fab_label="@string/reader_fab_auto"
app:fab_size="mini"/>
@@ -103,6 +114,7 @@
android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/>
diff --git a/app/src/main/res/layout/item_galleryblock.xml b/app/src/main/res/layout/item_galleryblock.xml
index 7d6a9f4f..678208f9 100644
--- a/app/src/main/res/layout/item_galleryblock.xml
+++ b/app/src/main/res/layout/item_galleryblock.xml
@@ -49,7 +49,7 @@
android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white"
android:text="@string/main_download"
- android:foreground="?attr/selectableItemBackground"
+ android:foreground="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
tools:ignore="UnusedAttribute" />
@@ -64,7 +64,7 @@
android:background="@android:color/holo_red_dark"
android:textColor="@android:color/white"
android:text="@string/main_delete"
- android:foreground="?attr/selectableItemBackground"
+ android:foreground="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
tools:ignore="UnusedAttribute" />
@@ -74,24 +74,38 @@
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:focusable="true"
+ android:clickable="true">
-
+ app:layout_constraintTop_toTopOf="parent">
-
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"
+ app:layout_constraintBottom_toBottomOf="@id/barrier"/>
+ app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index ffce239f..8fb154ec 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -132,7 +132,7 @@
ユーザーID
ユーザーIDをクリップボードにコピーしました
リトライ
- 自動スクロール
+ まばたき検知スクロール
全てのギャラリーを対象に検索
綴じ方向を左にする
ブックマーク管理
@@ -148,4 +148,8 @@
オープンソースライセンス
お気に入りのタグを見る
履歴を見る
+ まばたき検知を中止
+ カメラ権限が拒否されているため、まばたき検知使用できません
+ この機器には前面カメラが装着されていません
+ エラー
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index f18284ed..74ad60e7 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -132,7 +132,7 @@
유저 ID
유저 ID를 클립보드에 복사했습니다
재시도
- 자동 스크롤
+ 눈 깜빡임 감지 스크롤
모든 갤러리 검색
좌측으로 페이지 넘기기
즐겨찾기 관리
@@ -148,4 +148,8 @@
오픈 소스 라이선스
검색 기록 보기
즐겨찾기 태그 보기
+ 눈 깜빡임 감지 중지
+ 카메라 권한이 거부되었기 때문에 눈 깜빡임 감지가 불가능합니다
+ 이 장치에는 전면 카메라가 없습니다
+ 오류
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7695d3e4..b9c18a0a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,6 +20,7 @@
Warning
+ Error
Ignore
@@ -99,12 +100,16 @@
Go to page
Fullscreen>
Retry
- Automatic scroll
+ Scroll with eye blink
+ Stop scroll with eye blink
Background download
Cancel background download
Downloading…
Download complete
+ Eye blink detection cannot be used without a permission
+ There is no front facing camera in this device
+
Downloader running…