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…