Compare commits

...

14 Commits

Author SHA1 Message Date
tom5079
998433479b Merge branch 'dev' into master 2020-09-15 23:20:01 +09:00
tom5079
c7e75aacf0 Layout fix
History fix
2020-09-15 23:19:26 +09:00
tom5079
690338273a Merge branch 'dev' into master 2020-09-15 02:42:33 +09:00
tom5079
4207ea494d Bug fix 2020-09-15 02:42:18 +09:00
tom5079
265473a15a Merge branch 'dev' into master
# Conflicts:
#	app/release/app-release.apk
#	app/release/output-metadata.json
2020-09-15 02:13:53 +09:00
tom5079
b907d36770 Bug fix 2020-09-15 02:13:25 +09:00
tom5079
fee280341a Blink Recognition 2020-09-15 01:12:29 +09:00
tom5079
0f1ef70752 Bug fix 2020-09-14 22:34:51 +09:00
tom5079
0f8c68b22e Fixed to work on old Androids 2020-09-13 21:42:02 +09:00
tom5079
701017d2ca Merge branch 'face-recog' into dev 2020-09-13 21:10:29 +09:00
tom5079
be6903ca12 App built 2020-09-13 16:24:23 +09:00
tom5079
1521bc1223 Downloader Bug fix
UI Optimized
Scroller autohide, track disable
2020-09-13 16:19:32 +09:00
tom5079
7ed66b827f Implemented eye recognition
TODO: Move pages according to eye blinking
2020-09-12 20:25:55 +09:00
tom5079
df3a478ef3 Bug fix 2020-09-12 20:22:34 +09:00
41 changed files with 815 additions and 957 deletions

1
.idea/vcs.xml generated
View File

@@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/gh-pages" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -21,7 +21,7 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
versionCode 59 versionCode 59
versionName "5.0.1-hotfix1" versionName "5.0.3-hotfix2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -66,11 +66,12 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" //implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
implementation 'androidx.appcompat:appcompat:1.2.0' 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.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.biometric:biometric:1.0.1" 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.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.3.0-alpha02' implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation 'com.google.firebase:firebase-core:17.5.0' 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-crashlytics:17.2.1'
implementation 'com.google.firebase:firebase-perf:19.0.8' 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-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.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
//implementation 'com.quiph.ui:recyclerviewfastscroller:0.2.1' //implementation 'com.quiph.ui:recyclerviewfastscroller:0.2.1'
@@ -94,6 +96,7 @@ dependencies {
transitive = false transitive = false
} }
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2' implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
implementation 'com.gu:option:1.3'
implementation 'net.rdrei.android.dirchooser:library:3.2@aar' implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"properties": [], "properties": [],
"versionCode": 59, "versionCode": 59,
"versionName": "5.0.1-hotfix1", "versionName": "5.0.3-hotfix2",
"enabled": true, "enabled": true,
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }

View File

@@ -6,10 +6,13 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -24,6 +27,10 @@
tools:replace="android:theme" tools:replace="android:theme"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="face" />
<provider <provider
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

@@ -82,9 +82,12 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
when (searchSuggestion) { when (searchSuggestion) {
is TagSuggestion -> { is TagSuggestion -> {
val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}"
with(searchInputView.text) { with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length) delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length)
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
if (!this.contains(tag))
append("$tag ")
} }
} }
is Suggestion -> { is Suggestion -> {

View File

@@ -71,11 +71,9 @@ val client: OkHttpClient
class Pupil : Application() { class Pupil : Application() {
init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
override fun onCreate() { override fun onCreate() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)
val userID = Preferences["user_id", ""].let { userID -> val userID = Preferences["user_id", ""].let { userID ->

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.adapters
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -79,8 +80,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (cache.metadata.reader == null || Preferences["cache_disable"]) { if (cache.metadata.reader == null || Preferences["cache_disable"]) {
view.galleryblock_progressbar.visibility = View.GONE view.galleryblock_progressbar_layout.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE view.galleryblock_progress_complete.visibility = View.INVISIBLE
return@launch return@launch
} }
@@ -90,8 +91,10 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
progress = imageList.filterNotNull().size progress = imageList.filterNotNull().size
max = imageList.size max = imageList.size
with(view.galleryblock_progressbar_layout) {
if (visibility == View.GONE) if (visibility == View.GONE)
visibility = View.VISIBLE visibility = View.VISIBLE
}
if (progress == max) { if (progress == max) {
val downloadManager = DownloadManager.getInstance(context) val downloadManager = DownloadManager.getInstance(context)
@@ -219,13 +222,15 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
} }
galleryblock_tag_group.removeAllViews() galleryblock_tag_group.removeAllViews()
galleryBlock.relatedTags.forEach { CoroutineScope(Dispatchers.Default).launch {
galleryblock_tag_group.addView(TagChip(context, Tag.parse(it)).apply { galleryBlock.relatedTags.map {
TagChip(context, Tag.parse(it)).apply {
setOnClickListener { view -> setOnClickListener { view ->
for (callback in onChipClickedHandler) for (callback in onChipClickedHandler)
callback.invoke((view as TagChip).tag) callback.invoke((view as TagChip).tag)
} }
}) }
}.let { launch(Dispatchers.Main) { it.forEach { galleryblock_tag_group.addView(it) } } }
} }
galleryblock_id.text = galleryBlock.id.toString() galleryblock_id.text = galleryBlock.id.toString()

View File

@@ -41,7 +41,6 @@ import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.io.util.readBytes
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
@@ -143,7 +142,7 @@ class ReaderAdapter(private val activity: ReaderActivity,
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
glide glide
.load(image.readBytes()) .load(image.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true) .skipMemoryCache(true)
.apply { .apply {

View File

@@ -28,6 +28,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -42,6 +43,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.ui.ReaderActivity 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.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.ellipsize import xyz.quaver.pupil.util.ellipsize
@@ -309,11 +311,37 @@ class DownloadService : Service() {
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F }) progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F })
cache.metadata.imageList?.forEachIndexed { index, image -> 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 ->
cache.findFile(file)?.delete()
}
cache.metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
return@let
}
it.forEachIndexed { index, image ->
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F) progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
} }
}
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }
return@launch return@launch

View File

@@ -26,6 +26,7 @@ import android.text.InputType
import android.view.* import android.view.*
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
@@ -97,7 +98,9 @@ class MainActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url -> intent.dataString?.let { url ->
@@ -111,8 +114,6 @@ class MainActivity :
} }
} }
setContentView(R.layout.activity_main)
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")

View File

@@ -18,15 +18,23 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.Manifest
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.view.* 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 android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -38,12 +46,15 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.vision.face.Face
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.* import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -52,11 +63,13 @@ import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences 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.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.startCamera
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.timer
class ReaderActivity : BaseActivity() { class ReaderActivity : BaseActivity() {
@@ -69,11 +82,6 @@ class ReaderActivity : BaseActivity() {
field = value field = value
(reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value (reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value
reader_progressbar.visibility = when {
value -> View.VISIBLE
else -> View.GONE
}
} }
private lateinit var cache: Cache private lateinit var cache: Cache
@@ -89,12 +97,30 @@ class ReaderActivity : BaseActivity() {
} }
private val timer = Timer() private val timer = Timer()
private var autoTimer: Timer? = null
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
private var menu: Menu? = null 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
private var eyeTime: Long = 0L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader) setContentView(R.layout.activity_reader)
@@ -219,6 +245,18 @@ class ReaderActivity : BaseActivity() {
return true return true
} }
override fun onResume() {
super.onResume()
if (cameraEnabled)
startCamera(this, cameraCallback)
}
override fun onPause() {
super.onPause()
closeCamera()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
@@ -286,7 +324,6 @@ class ReaderActivity : BaseActivity() {
runOnUiThread { runOnUiThread {
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0 reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
val reader = cache.metadata.reader val reader = cache.metadata.reader
@@ -349,7 +386,7 @@ class ReaderActivity : BaseActivity() {
return return
currentPage = layoutManager.findFirstVisibleItemPosition()+1 currentPage = layoutManager.findFirstVisibleItemPosition()+1
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}" menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
this@ReaderActivity.reader_progressbar.progress = currentPage
} }
}) })
} }
@@ -368,6 +405,7 @@ class ReaderActivity : BaseActivity() {
animateDownloadFAB(false) animateDownloadFAB(false)
} else { } else {
downloadManager.addDownloadFolder(galleryID) downloadManager.addDownloadFolder(galleryID)
DownloadService.download(context, galleryID, true)
animateDownloadFAB(true) animateDownloadFAB(true)
} }
} }
@@ -377,31 +415,26 @@ class ReaderActivity : BaseActivity() {
with(reader_fab_retry) { with(reader_fab_retry) {
setImageResource(R.drawable.refresh) setImageResource(R.drawable.refresh)
setOnClickListener { setOnClickListener {
downloader?.cancel(galleryID) DownloadService.download(context, galleryID)
downloader?.download(galleryID)
} }
} }
with(reader_fab_auto) { with(reader_fab_auto) {
setImageResource(R.drawable.clock_start) setImageResource(R.drawable.eye_white)
setOnClickListener { setOnClickListener {
if (autoTimer == null) { when {
autoTimer = timer(initialDelay = 10000L, period = 10000L) { ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
CoroutineScope(Dispatchers.Main).launch { toggleCamera()
with(this@ReaderActivity.reader_recyclerview) {
val lastItem =
(layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
if (lastItem < adapter!!.itemCount - 1)
(layoutManager as LinearLayoutManager).scrollToPosition(lastItem + 1)
} }
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 ->
setImageResource(R.drawable.clock_end) requestPermissionLauncher.launch(Manifest.permission.CAMERA)
} else {
autoTimer?.cancel()
autoTimer = null
setImageResource(R.drawable.clock_start)
} }
} }
} }
@@ -488,6 +521,129 @@ class ReaderActivity : BaseActivity() {
} }
} }
val cameraCallback: (List<Face>) -> 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
eyeTime = 0L
}
!left -> {
if (eyeType != Eye.LEFT) {
eyeType = Eye.LEFT
eyeCount = 0
eyeTime = System.currentTimeMillis()
}
eyeCount++
}
!right -> {
if (eyeType != Eye.RIGHT) {
eyeType = Eye.RIGHT
eyeCount = 0
eyeTime = System.currentTimeMillis()
}
eyeCount++
}
}
if (eyeCount > 3 && System.currentTimeMillis() - eyeTime > 500) {
(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
eyeTime = 0L
}
}
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() { override fun onLowMemory() {
super.onLowMemory() super.onLowMemory()
Glide.get(this).onLowMemory() Glide.get(this).onLowMemory()

View File

@@ -186,7 +186,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
if (key == null) entries[key]!!.location_available.text = downloadFolder if (key == null) entries[key]!!.location_available.text = downloadFolder
} }
else else
Preferences["download_folder"] = File(directory).canonicalPath Preferences["download_folder"] = File(directory).toURI().toString()
} }
} }
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)

View File

@@ -18,13 +18,13 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.downloader.Cache
import xyz.quaver.pupil.util.wordCapitalize 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))>() val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()

View File

@@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_proxy.view.* import kotlinx.android.synthetic.main.dialog_proxy.view.*
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -40,7 +41,7 @@ import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.proxyInfo import xyz.quaver.pupil.util.proxyInfo
import java.net.Proxy import java.net.Proxy
class ProxyDialog(context: Context) : Dialog(context) { class ProxyDialog(context: Context) : AlertDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setContentView(build()) setContentView(build())

View File

@@ -59,6 +59,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
override fun add(element: T): Boolean { override fun add(element: T): Boolean {
load() load()
set.remove(element)
return set.add(element).also { return set.add(element).also {
save() save()
} }
@@ -67,6 +69,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
override fun addAll(elements: Collection<T>): Boolean { override fun addAll(elements: Collection<T>): Boolean {
load() load()
set.removeAll(elements)
return set.addAll(elements).also { return set.addAll(elements).also {
save() save()
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
@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<Camera?, Int> {
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<List<Face>>? = null
fun startCamera(context: Context, callback: (List<Face>) -> 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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Int>()
private val readers = SparseArray<Reader?>()
}
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<File?>? {
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))
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Int, Int>) ?: 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<Int>()
/*
* 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<MutableList<Float>?>()
val notification = SparseArray<NotificationCompat.Builder?>()
private val loop = loop()
private val worker = SparseArray<Job?>()
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()
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
)
}

View File

@@ -200,24 +200,42 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch { fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
val downloadFolder = downloadFolder ?: return@launch val downloadFolder = downloadFolder ?: return@launch
if (downloadFolder.getChild(".metadata").exists())
return@launch
metadata.imageList?.forEach { imageName -> metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach imageName ?: return@forEach
val target = downloadFolder.getChild(imageName) val target = downloadFolder.getChild(imageName)
val source = cacheFolder.getChild(imageName) val source = cacheFolder.getChild(imageName)
if (!source.exists()) if (!source.exists() || target.exists())
return@forEach return@forEach
kotlin.runCatching { kotlin.runCatching {
target.createNewFile() target.createNewFile()
source.readBytes()?.let { target.writeBytes(it) } target.outputStream()?.use { target -> source.inputStream()?.use { source ->
source.copyTo(target)
} }
}
}
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
kotlin.runCatching {
downloadThumbnail.createNewFile()
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
source.copyTo(target)
} }
cacheThumbnail.delete()
} }
} }
val cacheMetadata = cacheFolder.getChild(".metadata") val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder.getChild(".metadata") val downloadMetadata = downloadFolder.getChild(".metadata")
if (cacheMetadata.exists()) { if (cacheMetadata.exists() && !downloadMetadata.exists()) {
kotlin.runCatching { kotlin.runCatching {
downloadMetadata.createNewFile() downloadMetadata.createNewFile()
downloadMetadata.writeText(Json.encodeToString(metadata)) downloadMetadata.writeText(Json.encodeToString(metadata))

View File

@@ -104,7 +104,9 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val folder = downloadFolder.getChild(name) val folder = downloadFolder.getChild(name)
if (!folder.exists()) if (folder.exists())
return
folder.mkdir() folder.mkdir()
downloadFolderMap[galleryID] = folder.name downloadFolderMap[galleryID] = folder.name

View File

@@ -93,14 +93,14 @@ fun GalleryBlock.formatDownloadFolder(): String =
formatMap.entries.fold(it) { str, (k, v) -> formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true) str.replace(k, v.invoke(this), true)
} }
}.replace("/", "") }.replace(Regex("""[*\\|"?><:/]"""), "")
fun GalleryBlock.formatDownloadFolderTest(format: String): String = fun GalleryBlock.formatDownloadFolderTest(format: String): String =
format.let { format.let {
formatMap.entries.fold(it) { str, (k, v) -> formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true) str.replace(k, v.invoke(this), true)
} }
}.replace("/", "") }.replace(Regex("""[*\\|"?><:/]"""), "")
val Reader.requestBuilders: List<Request.Builder> val Reader.requestBuilders: List<Request.Builder>
get() { get() {

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <http://www.gnu.org/licenses/>.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/colorAccent"/>
<size
android:width="24dp"
android:height="24dp"/>
</shape>

View File

@@ -0,0 +1,8 @@
<!-- drawable/eye.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?attr/colorControlNormal" android:pathData="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />
</vector>

View File

@@ -0,0 +1,44 @@
<!--
~ 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 <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="15dp"
android:viewportWidth="22"
android:viewportHeight="15">
<path
android:pathData="M21.61,5.4C14.21,13.39 7.16,13.37 0.43,5.32"
android:strokeWidth="1"
android:strokeColor="?attr/colorControlNormal"/>
<path
android:pathData="M1.32,9.8L3.03,7.8"
android:strokeWidth="1"
android:strokeColor="?attr/colorControlNormal"/>
<path
android:pathData="M5.14,12.37L6.16,10.37"
android:strokeWidth="1"
android:strokeColor="?attr/colorControlNormal"/>
<path
android:pathData="M16.27,12.37L15.25,10.37"
android:strokeWidth="1"
android:strokeColor="?attr/colorControlNormal"/>
<path
android:pathData="M18.78,7.8L20.49,9.8"
android:strokeWidth="1"
android:strokeColor="?attr/colorControlNormal"/>
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/eye_off.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?attr/colorControlNormal" android:pathData="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/eye_off.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/eye.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />
</vector>

View File

@@ -0,0 +1,30 @@
<!--
~ 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 <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="640">
<path
android:pathData="M640,320C640,496.61 496.61,640 320,640C143.39,640 0,496.61 0,320C0,143.38 143.39,0 320,0C496.61,0 640,143.38 640,320Z"
android:fillColor="#4ec1f5"/>
<path
android:pathData="M420,320C420,375.19 375.19,420 320,420C264.81,420 220,375.19 220,320C220,264.81 264.81,220 320,220C375.19,220 420,264.81 420,320Z"
android:fillColor="#1d1d1d"/>
</vector>

View File

@@ -0,0 +1,30 @@
<!--
~ 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 <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="640">
<path
android:pathData="M640,320C640,496.61 496.61,640 320,640C143.39,640 0,496.61 0,320C0,143.38 143.39,0 320,0C496.61,0 640,143.38 640,320Z"
android:fillColor="@color/colorAccent"/>
<path
android:pathData="M420,320C420,375.19 375.19,420 320,420C264.81,420 220,375.19 220,320C220,264.81 264.81,220 320,220C375.19,220 420,264.81 420,320Z"
android:fillColor="#1d1d1d"/>
</vector>

View File

@@ -1,4 +1,4 @@
<!--drawable/menu.xml--> <!--drawable/menu.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2z"/> <path android:fillColor="?attr/colorControlNormal" android:pathData="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2z"/>
</vector> </vector>

View File

@@ -70,6 +70,8 @@
app:handleHasFixedSize="true" app:handleHasFixedSize="true"
app:handleHeight="72dp" app:handleHeight="72dp"
app:handleWidth="24dp" app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:trackMarginStart="64dp" app:trackMarginStart="64dp"
app:addLastItemPadding="true" app:addLastItemPadding="true"
app:popupDrawable="@color/transparent"> app:popupDrawable="@color/transparent">

View File

@@ -70,6 +70,8 @@
app:handleHasFixedSize="true" app:handleHasFixedSize="true"
app:handleHeight="72dp" app:handleHeight="72dp"
app:handleWidth="24dp" app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:trackMarginStart="64dp" app:trackMarginStart="64dp"
app:addLastItemPadding="true" app:addLastItemPadding="true"
app:popupDrawable="@color/transparent"> app:popupDrawable="@color/transparent">

View File

@@ -33,6 +33,8 @@
app:handleDrawable="@drawable/thumb" app:handleDrawable="@drawable/thumb"
app:handleHeight="72dp" app:handleHeight="72dp"
app:handleWidth="24dp" app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:handleHasFixedSize="true" app:handleHasFixedSize="true"
app:addLastItemPadding="true" app:addLastItemPadding="true"
app:popupDrawable="@color/transparent"> app:popupDrawable="@color/transparent">
@@ -45,28 +47,19 @@
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller> </com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<LinearLayout <include layout="@layout/reader_eye_card"
android:layout_width="match_parent" android:id="@+id/eye_card"
android:visibility="gone"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:layout_width="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"/>
<ProgressBar <ProgressBar
android:id="@+id/reader_download_progressbar" android:id="@+id/reader_download_progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="4dp"/>
android:layout_gravity="center"/>
<ProgressBar
android:id="@+id/reader_progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:progressTint="@color/material_green_a700"
tools:ignore="UnusedAttribute"
android:visibility="gone"/>
</LinearLayout>
<com.github.clans.fab.FloatingActionMenu <com.github.clans.fab.FloatingActionMenu
android:id="@+id/reader_fab" android:id="@+id/reader_fab"
@@ -80,6 +73,7 @@
android:id="@+id/reader_fab_download" android:id="@+id/reader_fab_download"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_download"
app:fab_label="@string/reader_fab_download" app:fab_label="@string/reader_fab_download"
app:fab_size="mini"/> app:fab_size="mini"/>
@@ -87,6 +81,7 @@
android:id="@+id/reader_fab_retry" android:id="@+id/reader_fab_retry"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/refresh"
app:fab_label="@string/reader_fab_retry" app:fab_label="@string/reader_fab_retry"
app:fab_size="mini"/> app:fab_size="mini"/>
@@ -94,6 +89,7 @@
android:id="@+id/reader_fab_auto" android:id="@+id/reader_fab_auto"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/eye_white"
app:fab_label="@string/reader_fab_auto" app:fab_label="@string/reader_fab_auto"
app:fab_size="mini"/> app:fab_size="mini"/>
@@ -101,6 +97,7 @@
android:id="@+id/reader_fab_fullscreen" android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen" app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/> app:fab_size="mini"/>

View File

@@ -25,7 +25,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
android:clipChildren="true"> android:clipChildren="true"
tools:ignore="RtlHardcoded">
<com.daimajia.swipe.SwipeLayout <com.daimajia.swipe.SwipeLayout
android:id="@+id/galleryblock_swipe_layout" android:id="@+id/galleryblock_swipe_layout"
@@ -48,7 +49,7 @@
android:background="@android:color/holo_blue_dark" android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:text="@string/main_download" android:text="@string/main_download"
android:foreground="?attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true" android:clickable="true"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute" />
@@ -63,34 +64,37 @@
android:background="@android:color/holo_red_dark" android:background="@android:color/holo_red_dark"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:text="@string/main_delete" android:text="@string/main_delete"
android:foreground="?attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true" android:clickable="true"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute" />
</LinearLayout> </LinearLayout>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/galleryblock_primary" android:id="@+id/galleryblock_primary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:background="?android:attr/selectableItemBackground"
android:foreground="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true" android:clickable="true">
tools:ignore="UnusedAttribute">
<androidx.constraintlayout.widget.ConstraintLayout <FrameLayout
android:layout_width="match_parent" android:id="@+id/galleryblock_progressbar_layout"
android:layout_height="wrap_content">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="4dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-4dp"
android:layout_marginTop="-4dp"
android:progress="50"
android:layout_gravity="center_vertical"/>
<ImageView <ImageView
android:id="@+id/galleryblock_progress_complete" android:id="@+id/galleryblock_progress_complete"
@@ -101,6 +105,8 @@
android:contentDescription="@string/reader_imageview_description" android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
</FrameLayout>
<ImageView <ImageView
android:id="@+id/galleryblock_thumbnail" android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp" android:layout_width="150dp"
@@ -108,8 +114,8 @@
android:contentDescription="@string/galleryblock_thumbnail_description" android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar" app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="@id/barrier"/>
<TextView <TextView
style="@style/TextAppearance.AppCompat.Headline" style="@style/TextAppearance.AppCompat.Headline"
@@ -117,18 +123,16 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"/>
<TextView <TextView
style="@style/TextAppearance.AppCompat.Medium" style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist" android:id="@+id/galleryblock_artist"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
@@ -138,101 +142,98 @@
android:id="@+id/galleryblock_series" android:id="@+id/galleryblock_series"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist" app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintRight_toRightOf="parent"/>
<TextView <TextView
android:id="@+id/galleryblock_type" android:id="@+id/galleryblock_type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series" app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" /> app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<TextView <TextView
android:id="@+id/galleryblock_language" android:id="@+id/galleryblock_language"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type" app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<View <androidx.constraintlayout.widget.Barrier
android:id="@+id/galleryblock_padding" android:id="@+id/padding"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" app:barrierDirection="bottom"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language" app:constraint_referenced_ids="galleryblock_language"/>
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/galleryblock_tag_group" android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:chipSpacing="4dp" app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding" app:layout_constraintTop_toBottomOf="@id/padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"/>
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> <androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="galleryblock_thumbnail,galleryblock_tag_group"/>
<View <View
android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_margin="8dp" android:background="@color/light_gray"
android:background="@android:color/darker_gray"/> app:layout_constraintTop_toBottomOf="@id/barrier"
android:layout_margin="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView <TextView
android:id="@+id/galleryblock_id" android:id="@+id/galleryblock_id"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
<View android:layout_marginTop="8dp"
android:layout_width="0dp" android:layout_marginBottom="8dp"
android:layout_height="1dp" app:layout_constraintTop_toBottomOf="@id/divider"
android:layout_weight="1"/> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView <TextView
android:id="@+id/galleryblock_pagecount" android:id="@+id/galleryblock_pagecount"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"
android:layout_marginTop="8dp"
<View android:layout_marginBottom="8dp"
android:layout_width="0dp" app:layout_constraintTop_toBottomOf="@id/divider"
android:layout_height="1dp" app:layout_constraintBottom_toBottomOf="parent"
android:layout_weight="1"/> app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent" />
<ImageView <ImageView
android:id="@+id/galleryblock_favorite" android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name" android:contentDescription="@string/app_name"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
app:srcCompat="@drawable/ic_star_empty"/> android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:srcCompat="@drawable/ic_star_empty"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</com.daimajia.swipe.SwipeLayout> </com.daimajia.swipe.SwipeLayout>

View File

@@ -42,7 +42,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/menu" app:srcCompat="@drawable/menu"
app:tint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <http://www.gnu.org/licenses/>.
-->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/left_eye"
android:layout_width="8dp"
android:layout_height="8dp"
app:srcCompat="@drawable/eye_off"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_margin="4dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/right_eye"
android:layout_width="8dp"
android:layout_height="8dp"
app:srcCompat="@drawable/eye_off"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/left_eye"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="4dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/dot"
android:layout_width="4dp"
android:layout_height="4dp"
android:visibility="invisible"
app:srcCompat="@drawable/dot"
app:layout_constraintLeft_toLeftOf="@id/left_eye"
app:layout_constraintRight_toRightOf="@id/right_eye"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@@ -132,7 +132,7 @@
<string name="settings_user_id">ユーザーID</string> <string name="settings_user_id">ユーザーID</string>
<string name="settings_user_id_toast">ユーザーIDをクリップボードにコピーしました</string> <string name="settings_user_id_toast">ユーザーIDをクリップボードにコピーしました</string>
<string name="reader_fab_retry">リトライ</string> <string name="reader_fab_retry">リトライ</string>
<string name="reader_fab_auto">自動スクロール</string> <string name="reader_fab_auto">まばたき検知スクロール</string>
<string name="search_all">全てのギャラリーを対象に検索</string> <string name="search_all">全てのギャラリーを対象に検索</string>
<string name="settings_rtl">綴じ方向を左にする</string> <string name="settings_rtl">綴じ方向を左にする</string>
<string name="settings_manage_favorites">ブックマーク管理</string> <string name="settings_manage_favorites">ブックマーク管理</string>
@@ -148,4 +148,8 @@
<string name="settings_oss">オープンソースライセンス</string> <string name="settings_oss">オープンソースライセンス</string>
<string name="search_show_tags">お気に入りのタグを見る</string> <string name="search_show_tags">お気に入りのタグを見る</string>
<string name="search_show_histories">履歴を見る</string> <string name="search_show_histories">履歴を見る</string>
<string name="reader_fab_auto_cancel">まばたき検知を中止</string>
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
<string name="no_camera">この機器には前面カメラが装着されていません</string>
<string name="error">エラー</string>
</resources> </resources>

View File

@@ -132,7 +132,7 @@
<string name="settings_user_id">유저 ID</string> <string name="settings_user_id">유저 ID</string>
<string name="settings_user_id_toast">유저 ID를 클립보드에 복사했습니다</string> <string name="settings_user_id_toast">유저 ID를 클립보드에 복사했습니다</string>
<string name="reader_fab_retry">재시도</string> <string name="reader_fab_retry">재시도</string>
<string name="reader_fab_auto">자동 스크롤</string> <string name="reader_fab_auto">눈 깜빡임 감지 스크롤</string>
<string name="search_all">모든 갤러리 검색</string> <string name="search_all">모든 갤러리 검색</string>
<string name="settings_rtl">좌측으로 페이지 넘기기</string> <string name="settings_rtl">좌측으로 페이지 넘기기</string>
<string name="settings_manage_favorites">즐겨찾기 관리</string> <string name="settings_manage_favorites">즐겨찾기 관리</string>
@@ -148,4 +148,8 @@
<string name="settings_oss">오픈 소스 라이선스</string> <string name="settings_oss">오픈 소스 라이선스</string>
<string name="search_show_histories">검색 기록 보기</string> <string name="search_show_histories">검색 기록 보기</string>
<string name="search_show_tags">즐겨찾기 태그 보기</string> <string name="search_show_tags">즐겨찾기 태그 보기</string>
<string name="reader_fab_auto_cancel">눈 깜빡임 감지 중지</string>
<string name="camera_denied">카메라 권한이 거부되었기 때문에 눈 깜빡임 감지가 불가능합니다</string>
<string name="no_camera">이 장치에는 전면 카메라가 없습니다</string>
<string name="error">오류</string>
</resources> </resources>

View File

@@ -20,6 +20,7 @@
<!-- Translate needed down here --> <!-- Translate needed down here -->
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="error">Error</string>
<string name="ignore_update">Ignore</string> <string name="ignore_update">Ignore</string>
@@ -99,12 +100,16 @@
<string name="reader_go_to_page">Go to page</string> <string name="reader_go_to_page">Go to page</string>
<string name="reader_fab_fullscreen">Fullscreen</string>> <string name="reader_fab_fullscreen">Fullscreen</string>>
<string name="reader_fab_retry">Retry</string> <string name="reader_fab_retry">Retry</string>
<string name="reader_fab_auto">Automatic scroll</string> <string name="reader_fab_auto">Scroll with eye blink</string>
<string name="reader_fab_auto_cancel">Stop scroll with eye blink</string>
<string name="reader_fab_download">Background download</string> <string name="reader_fab_download">Background download</string>
<string name="reader_fab_download_cancel">Cancel background download</string> <string name="reader_fab_download_cancel">Cancel background download</string>
<string name="reader_notification_text">Downloading&#8230;</string> <string name="reader_notification_text">Downloading&#8230;</string>
<string name="reader_notification_complete">Download complete</string> <string name="reader_notification_complete">Download complete</string>
<string name="camera_denied">Eye blink detection cannot be used without a permission</string>
<string name="no_camera">There is no front facing camera in this device</string>
<!-- DOWNLOADER --> <!-- DOWNLOADER -->
<string name="downloader_running">Downloader running…</string> <string name="downloader_running">Downloader running…</string>