Compare commits

...

22 Commits

Author SHA1 Message Date
tom5079
608c6e6a1d App built 2020-09-26 21:01:36 +09:00
tom5079
bb2c91145f Dependency update 2020-09-26 20:58:46 +09:00
tom5079
db074df0f7 Fixed Download Concurrency issue
Fixed image not showing up after reader is paused and resumed
2020-09-26 11:07:35 +09:00
tom5079
f7c45df9a6 Tag favorite bug fix 2020-09-26 09:36:20 +09:00
tom5079
44e3d16cd6 Merge branch 'dev' into master 2020-09-26 09:08:29 +09:00
tom5079
a973cdfe0b Download Bug fix
Added favorite to TagChip
Improved eyeblink recognition
2020-09-26 09:07:52 +09:00
tom5079
fca42c79a8 Updated startActivityForResult to launchers 2020-09-25 15:39:07 +09:00
tom5079
f236775599 Bug fix
Remember thin mode preference
TagChip favorites
2020-09-25 15:17:05 +09:00
tom5079
360decd37c FloatingSearchView migration 2020-09-16 14:31:45 +09:00
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
56 changed files with 1143 additions and 1430 deletions

View File

@@ -61,5 +61,10 @@
<option name="name" value="MavenLocal" /> <option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository" /> <option name="url" value="file:/$USER_HOME$/.m2/repository" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://dl.bintray.com/tom5079/maven" />
</remote-repository>
</component> </component>
</project> </project>

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

@@ -20,8 +20,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
versionCode 59 versionCode 60
versionName "5.0.1-hotfix2" versionName "5.1-hotfix3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -60,54 +60,58 @@ android {
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
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-json:1.0.0-RC2"
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.preference:preference:1.1.1' implementation "androidx.fragment:fragment-ktx:1.3.0-alpha08"
implementation 'androidx.gridlayout:gridlayout:1.0.0' 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.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"
implementation 'com.google.firebase:firebase-analytics:17.5.0' implementation "com.google.firebase:firebase-analytics:17.5.0"
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.github.arimorty:floatingsearchview:2.1.1' implementation "com.google.android.gms:play-services-mlkit-face-detection:16.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"
//noinspection GradleDependency //noinspection GradleDependency
implementation 'com.squareup.okhttp3:okhttp:3.12.12' implementation "com.squareup.okhttp3:okhttp:3.12.12"
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation "com.github.bumptech.glide:glide:4.11.0"
implementation ("com.github.bumptech.glide:okhttp3-integration:4.11.0") { implementation ("com.github.bumptech.glide:okhttp3-integration:4.11.0") {
transitive = false transitive = false
} }
implementation 'com.github.bumptech.glide:annotations:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = false transitive = false
} }
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2' implementation "com.github.bumptech.glide:annotations:4.11.0"
implementation 'net.rdrei.android.dirchooser:library:3.2@aar' annotationProcessor "com.github.bumptech.glide:compiler:4.11.0"
implementation 'com.github.chrisbanes:PhotoView:2.3.0' kapt "com.github.bumptech.glide:compiler:4.11.0"
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0' transitive = false
implementation "ru.noties.markwon:core:3.1.0"
implementation ("xyz.quaver:libpupil:1.6") {
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
} }
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"
//implementation "com.andrognito.pinlockview:pinlockview:2.1.0"
implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:1.7.2"
implementation "xyz.quaver:documentfilex:0.2.15" implementation "xyz.quaver:documentfilex:0.2.15"
testImplementation 'junit:junit:4.13' implementation "xyz.quaver:floatingsearchview:1.0.5"
androidTestImplementation 'androidx.test.ext:junit:1.1.2' testImplementation "junit:junit:4.13"
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation "androidx.test:rules:1.3.0"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
} }
androidExtensions { androidExtensions {

View File

@@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"properties": [], "properties": [],
"versionCode": 59, "versionCode": 60,
"versionName": "5.0.1-hotfix2", "versionName": "5.1-hotfix3",
"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"
@@ -22,8 +25,13 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:theme" tools:replace="android:theme"
android:largeHeap="true"
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

@@ -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

@@ -26,6 +26,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
@@ -48,6 +49,7 @@ import xyz.quaver.hitomi.getReader
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.view.TagChip import xyz.quaver.pupil.ui.view.TagChip
@@ -69,7 +71,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
val timer = Timer() val timer = Timer()
var isThin = false var thin: Boolean = Preferences["thin"]
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) { inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var timerTask: TimerTask? = null var timerTask: TimerTask? = null
@@ -79,21 +81,23 @@ 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
} }
with(view.galleryblock_progressbar) { with(view.galleryblock_progressbar) {
val imageList = cache.metadata.imageList!! val imageList = cache.metadata.imageList!!
progress = imageList.filterNotNull().size progress = imageList.count { it != null }
max = imageList.size max = imageList.size
if (visibility == View.GONE) with(view.galleryblock_progressbar_layout) {
visibility = View.VISIBLE if (visibility == View.GONE)
visibility = View.VISIBLE
}
if (progress == max) { if (!imageList.contains(null)) {
val downloadManager = DownloadManager.getInstance(context) val downloadManager = DownloadManager.getInstance(context)
if (completeFlag.get(galleryID, false)) { if (completeFlag.get(galleryID, false)) {
@@ -140,7 +144,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
val artists = galleryBlock.artists val artists = galleryBlock.artists
val series = galleryBlock.series val series = galleryBlock.series
if (isThin) if (thin)
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize( galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
R.dimen.galleryblock_thumbnail_thin R.dimen.galleryblock_thumbnail_thin
) )
@@ -219,13 +223,26 @@ 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.sortedBy {
setOnClickListener { view -> val tag = Tag.parse(it)
for (callback in onChipClickedHandler)
callback.invoke((view as TagChip).tag) if (favoriteTags.contains(tag))
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
TagChip(context, Tag.parse(it)).apply {
setOnClickListener { view ->
for (callback in onChipClickedHandler)
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()
@@ -268,7 +285,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
// Make some views invisible to make it thinner // Make some views invisible to make it thinner
if (isThin) { if (thin) {
galleryblock_language.visibility = View.GONE galleryblock_language.visibility = View.GONE
galleryblock_type.visibility = View.GONE galleryblock_type.visibility = View.GONE
galleryblock_tag_group.visibility = View.GONE galleryblock_tag_group.visibility = View.GONE

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

@@ -23,11 +23,11 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.SparseArray
import androidx.core.app.NotificationCompat 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
@@ -48,6 +48,9 @@ import xyz.quaver.pupil.util.ellipsize
import xyz.quaver.pupil.util.normalizeID import xyz.quaver.pupil.util.normalizeID
import xyz.quaver.pupil.util.requestBuilders import xyz.quaver.pupil.util.requestBuilders
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.ceil
import kotlin.math.log10
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
class DownloadService : Service() { class DownloadService : Service() {
@@ -66,7 +69,7 @@ class DownloadService : Service() {
.setOngoing(true) .setOngoing(true)
} }
private val notification = SparseArray<NotificationCompat.Builder?>() private val notification = ConcurrentHashMap<Int, NotificationCompat.Builder?>()
private fun initNotification(galleryID: Int) { private fun initNotification(galleryID: Int) {
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
@@ -194,7 +197,7 @@ class DownloadService : Service() {
* 0 <= value < 100 -> Download in progress * 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed * Float.POSITIVE_INFINITY -> Download completed
*/ */
val progress = SparseArray<MutableList<Float>?>() val progress = ConcurrentHashMap<Int, MutableList<Float>>()
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
@@ -216,10 +219,11 @@ class DownloadService : Service() {
kotlin.runCatching { kotlin.runCatching {
val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception() val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching { kotlin.runCatching {
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "$index.$ext", image) Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
}.onSuccess { }.onSuccess {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY) progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID) notify(galleryID)
@@ -291,7 +295,7 @@ class DownloadService : Service() {
} }
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch { fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
if (progress.indexOfKey(galleryID) >= 0) if (progress.containsKey(galleryID))
cancel(galleryID) cancel(galleryID)
val cache = Cache.getInstance(this@DownloadService, galleryID) val cache = Cache.getInstance(this@DownloadService, galleryID)
@@ -303,17 +307,23 @@ class DownloadService : Service() {
// Gallery doesn't exist // Gallery doesn't exist
if (reader == null) { if (reader == null) {
delete(galleryID) delete(galleryID)
progress.put(galleryID, null) progress[galleryID] = mutableListOf()
return@launch return@launch
} }
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F }) progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F }
cache.metadata.imageList?.forEachIndexed { index, image -> cache.metadata.imageList?.let {
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F) it.forEachIndexed { index, image ->
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
@@ -334,7 +344,7 @@ class DownloadService : Service() {
} }
reader.requestBuilders.forEachIndexed { index, it -> reader.requestBuilders.forEachIndexed { index, it ->
if (progress[galleryID]?.get(index)?.isInfinite() != true) { if (progress[galleryID]?.get(index)?.isInfinite() == false) {
val request = it.tag(Tag(galleryID, index, startId)).build() val request = it.tag(Tag(galleryID, index, startId)).build()
client.newCall(request).enqueue(callback) client.newCall(request).enqueue(callback)
} }

View File

@@ -18,36 +18,28 @@
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.hitomi.Suggestion import xyz.quaver.hitomi.Suggestion
@Parcelize @Parcelize
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
override fun getBody(): String { @IgnoredOnParcel
return s override val body = s
}
} }
@Parcelize @Parcelize
class Suggestion(val str: String) : SearchSuggestion { class Suggestion(override val body: String) : SearchSuggestion
override fun getBody() = str
}
@Parcelize @Parcelize
class NoResultSuggestion(val str: String) : SearchSuggestion { class NoResultSuggestion(override val body: String) : SearchSuggestion
override fun getBody() = str
}
@Parcelize @Parcelize
class LoadingSuggestion(val str: String) : SearchSuggestion { class LoadingSuggestion(override val body: String) : SearchSuggestion
override fun getBody() = str
}
@Parcelize @Parcelize
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY") @Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
class FavoriteHistorySwitch(private val body: String) : SearchSuggestion { class FavoriteHistorySwitch(override val body: String) : SearchSuggestion
override fun getBody() = body
}

View File

@@ -23,6 +23,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -34,6 +35,13 @@ open class BaseActivity : AppCompatActivity() {
private var locked: Boolean = true private var locked: Boolean = true
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK)
locked = false
else
finish()
}
@CallSuper @CallSuper
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState) super.onCreate(savedInstanceState, persistentState)
@@ -53,20 +61,7 @@ open class BaseActivity : AppCompatActivity() {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked) if (locked)
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID()) lockLauncher.launch(Intent(this, LockActivity::class.java))
}
@CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
R.id.request_lock.normalizeID() -> {
if (resultCode == Activity.RESULT_OK)
locked = false
else
finish()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
} }
} }

View File

@@ -23,15 +23,15 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.* import android.view.KeyEvent
import android.view.MenuItem
import android.view.MotionEvent
import android.view.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.FloatingSearchViewDayNight
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@@ -40,6 +40,10 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.hitomi.doSearch import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery import xyz.quaver.hitomi.getSuggestionsForQuery
@@ -49,9 +53,12 @@ import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.checkUpdate
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.restore
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
@@ -59,7 +66,6 @@ import kotlin.math.roundToInt
class MainActivity : class MainActivity :
BaseActivity(), BaseActivity(),
FloatingSearchView.OnMenuItemClickListener,
NavigationView.OnNavigationItemSelectedListener NavigationView.OnNavigationItemSelectedListener
{ {
@@ -95,9 +101,10 @@ class MainActivity :
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var currentPage = 0 private var currentPage = 0
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 +118,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")
@@ -137,6 +142,17 @@ class MainActivity :
} }
} }
override fun onResume() {
super.onResume()
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
@@ -180,20 +196,6 @@ class MainActivity :
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
R.id.request_settings.normalizeID() -> {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
private fun initView() { private fun initView() {
var prevP1 = 0 var prevP1 = 0
main_appbar_layout.addOnOffsetChangedListener( main_appbar_layout.addOnOffsetChangedListener(
@@ -619,14 +621,14 @@ class MainActivity :
else -> { else -> {
searchHistory.map { searchHistory.map {
Suggestion(it) Suggestion(it)
}.takeLast(20) + FavoriteHistorySwitch(getString(R.string.search_show_tags)) }.takeLast(10) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
} }
}.reversed() }.reversed()
private var suggestionJob : Job? = null private var suggestionJob : Job? = null
private fun setupSearchBar() { private fun setupSearchBar() {
with(main_searchview as FloatingSearchViewDayNight) { with(main_searchview as xyz.quaver.pupil.ui.view.FloatingSearchView) {
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener { onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() { override fun onMenuOpened() {
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems() (this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
} }
@@ -634,7 +636,15 @@ class MainActivity :
override fun onMenuClosed() { override fun onMenuClosed() {
//Do Nothing //Do Nothing
} }
}) }
post {
findViewById<MenuView>(R.id.menu_view).menuItems.firstOrNull {
(it as MenuItem).itemId == R.id.main_menu_thin
}?.let {
(it as MenuItem).isChecked = Preferences["thin"]
}
}
onHistoryDeleteClickedListener = { onHistoryDeleteClickedListener = {
searchHistory.remove(it) searchHistory.remove(it)
@@ -645,9 +655,11 @@ class MainActivity :
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
} }
setOnMenuItemClickListener(this@MainActivity) onMenuItemClickListener = {
onActionMenuItemSelected(it)
}
setOnQueryChangeListener { _, query -> onQueryChangeListener = lambda@{ _, query ->
this@MainActivity.query = query this@MainActivity.query = query
suggestionJob?.cancel() suggestionJob?.cancel()
@@ -655,12 +667,14 @@ class MainActivity :
if (query.isEmpty() or query.endsWith(' ')) { if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
return@setOnQueryChangeListener return@lambda
} }
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString()))) swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
val currentQuery = query.split(" ").last().replace('_', ' ') val currentQuery = query.split(" ").last()
.replace(Regex("^-"), "")
.replace('_', ' ')
suggestionJob = CoroutineScope(Dispatchers.IO).launch { suggestionJob = CoroutineScope(Dispatchers.IO).launch {
val suggestions = kotlin.runCatching { val suggestions = kotlin.runCatching {
@@ -681,7 +695,7 @@ class MainActivity :
} }
} }
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener { onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() { override fun onFocus() {
if (query.isEmpty() or query.endsWith(' ')) if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
@@ -698,19 +712,24 @@ class MainActivity :
loadBlocks() loadBlocks()
} }
} }
}) }
attachNavigationDrawerToMenuButton(main_drawer_layout) attachNavigationDrawerToMenuButton(main_drawer_layout)
} }
} }
override fun onActionMenuItemSelected(item: MenuItem?) { fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) { when(item?.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), R.id.request_settings.normalizeID()) R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.main_menu_thin -> { R.id.main_menu_thin -> {
val thin = !item.isChecked
item.isChecked = thin
main_recyclerview.apply { main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply { (adapter as GalleryBlockAdapter).apply {
isThin = !isThin this.thin = thin
Preferences["thin"] = thin
} }
adapter = adapter // Force to redraw adapter = adapter // Force to redraw

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,18 +82,16 @@ 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
var downloader: DownloadService? = null var downloader: DownloadService? = null
private val conn = object: ServiceConnection { private val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
downloader = (service as DownloadService.Binder).service downloader = (service as DownloadService.Binder).service.also {
if (!it.progress.containsKey(galleryID))
DownloadService.download(this@ReaderActivity, galleryID, true)
}
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
@@ -89,12 +100,29 @@ 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 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)
@@ -141,7 +169,7 @@ class ReaderActivity : BaseActivity() {
} }
} }
} else } else
initDownloader() initDownloadListener()
initView() initView()
} }
@@ -219,17 +247,31 @@ class ReaderActivity : BaseActivity() {
return true return true
} }
override fun onResume() {
super.onResume()
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
if (cameraEnabled)
startCamera(this, cameraCallback)
}
override fun onPause() {
super.onPause()
closeCamera()
if (downloader != null)
unbindService(conn)
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
DownloadService.cancel(this, galleryID)
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
timer.cancel() timer.cancel()
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel() (reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
DownloadService.cancel(this, galleryID)
if (downloader != null)
unbindService(conn)
} }
override fun onBackPressed() { override fun onBackPressed() {
@@ -264,17 +306,14 @@ class ReaderActivity : BaseActivity() {
} }
} }
private fun initDownloader() { private fun initDownloadListener() {
DownloadService.download(this, galleryID, true)
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
timer.schedule(1000, 1000) { timer.schedule(1000, 1000) {
val downloader = downloader ?: return@schedule val downloader = downloader ?: return@schedule
if (downloader.progress.indexOfKey(galleryID) < 0) //loading if (!downloader.progress.containsKey(galleryID)) //loading
return@schedule return@schedule
if (downloader.progress[galleryID] == null) { //Gallery not found if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
timer.cancel() timer.cancel()
Snackbar Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE) .make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
@@ -286,7 +325,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 +387,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 +406,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 +416,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)
}
}
} }
setImageResource(R.drawable.clock_end) Build.VERSION.SDK_INT >= 23 && shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
} else { AlertDialog.Builder(this@ReaderActivity)
autoTimer?.cancel() .setTitle(R.string.warning)
autoTimer = null .setMessage(R.string.camera_denied)
setImageResource(R.drawable.clock_start) .setPositiveButton(android.R.string.ok) { _, _ ->}
.show()
}
else ->
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
} }
} }
} }
@@ -488,6 +522,122 @@ 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
eyeTime = 0L
}
!left -> {
if (eyeType != Eye.LEFT) {
eyeType = Eye.LEFT
eyeTime = System.currentTimeMillis()
}
}
!right -> {
if (eyeType != Eye.RIGHT) {
eyeType = Eye.RIGHT
eyeTime = System.currentTimeMillis()
}
}
}
if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
(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)
}
eyeTime = System.currentTimeMillis() + 500
}
}
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

@@ -18,24 +18,10 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
import xyz.quaver.pupil.ui.fragment.SettingsFragment import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.normalizeID
import java.nio.charset.Charset
class SettingsActivity : BaseActivity() { class SettingsActivity : BaseActivity() {
@@ -56,19 +42,4 @@ class SettingsActivity : BaseActivity() {
return true return true
} }
@SuppressLint("InlinedApi")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
R.id.request_write_permission_and_saf.normalizeID() -> {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
}
}
}
}
} }

View File

@@ -39,7 +39,7 @@ class DownloadFolderNameDialogFragment : DialogFragment() {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun build(): View { private fun build(): View {
val galleryID = Cache.instances.let { if (it.size() == 0) 1199708 else it.keyAt((0 until it.size()).random()) } val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
val galleryBlock = runBlocking { val galleryBlock = runBlocking {
Cache.getInstance(requireContext(), galleryID).getGalleryBlock() Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
} }

View File

@@ -26,26 +26,87 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
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.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
import kotlinx.android.synthetic.main.item_download_folder.view.* import kotlinx.android.synthetic.main.item_download_folder.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.toFile
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.migrate import xyz.quaver.pupil.util.migrate
import xyz.quaver.pupil.util.normalizeID
import java.io.File import java.io.File
class DownloadLocationDialogFragment : DialogFragment() { class DownloadLocationDialogFragment : DialogFragment() {
private val entries = mutableMapOf<File?, View>() private val entries = mutableMapOf<File?, View>()
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val activity = activity ?: return@registerForActivityResult
val context = context ?: return@registerForActivityResult
val dialog = dialog ?: return@registerForActivityResult
it.data?.data?.also { uri ->
val takeFlags: Int =
activity.intent.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) {
entries[null]?.location_available?.text = uri.toFile(context)?.canonicalPath
Preferences["download_folder"] = uri.toString()
} else {
Snackbar.make(
dialog.window!!.decorView.rootView,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder
}
}
}
}
private val requestDownloadFolderOldLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val context = context ?: return@registerForActivityResult
val dialog = dialog ?: return@registerForActivityResult
if (it.resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = it.data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite()) {
Snackbar.make(
dialog.window!!.decorView.rootView,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder
}
else {
entries[null]?.location_available?.text = directory
Preferences["download_folder"] = File(directory).toURI().toString()
}
}
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun build() : View? { private fun build() : View? {
val context = context ?: return null val context = context ?: return null
@@ -90,7 +151,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
putExtra("android.content.extra.SHOW_ADVANCED", true) putExtra("android.content.extra.SHOW_ADVANCED", true)
} }
startActivityForResult(intent, R.id.request_download_folder.normalizeID()) requestDownloadFolderLauncher.launch(intent)
} else { // Can't use SAF on old Androids! } else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder() val config = DirectoryChooserConfig.builder()
.newDirectoryName("Pupil") .newDirectoryName("Pupil")
@@ -101,7 +162,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config) putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
} }
startActivityForResult(intent, R.id.request_download_folder_old.normalizeID()) requestDownloadFolderOldLauncher.launch(intent)
} }
} }
entries[null] = this entries[null] = this
@@ -132,65 +193,4 @@ class DownloadLocationDialogFragment : DialogFragment() {
return builder.create() return builder.create()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
R.id.request_download_folder.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) {
val activity = activity ?: return
val context = context ?: return
val dialog = dialog ?: return
data?.data?.also { uri ->
val takeFlags: Int =
activity.intent.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false))
Preferences["download_folder"] = uri.toString()
else {
Snackbar.make(
dialog.window!!.decorView.rootView,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder
}
}
}
}
R.id.request_download_folder_old.normalizeID() -> {
val context = context ?: return
val dialog = dialog ?: return
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite()) {
Snackbar.make(
dialog.window!!.decorView.rootView,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder
}
else
Preferences["download_folder"] = File(directory).canonicalPath
}
}
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
@@ -45,6 +45,7 @@ import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
@@ -53,7 +54,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))>()
@@ -141,7 +142,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
listOf(gallery.language).map { Tag("language", it) }, listOf(gallery.language).map { Tag("language", it) },
gallery.series.map { Tag("series", it) }, gallery.series.map { Tag("series", it) },
gallery.characters.map { Tag("character", it) }, gallery.characters.map { Tag("character", it) },
gallery.tags.map { gallery.tags.sortedBy {
val tag = Tag.parse(it)
if (favoriteTags.contains(tag))
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it).let { tag -> Tag.parse(it).let { tag ->
when { when {
tag.area != null -> tag tag.area != null -> tag

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

@@ -22,25 +22,21 @@ import android.app.Activity
import android.content.* import android.content.*
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.android.material.snackbar.Snackbar
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.* import xyz.quaver.pupil.ui.dialog.*
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import java.nio.charset.Charset
class SettingsFragment : class SettingsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
@@ -48,6 +44,16 @@ class SettingsFragment :
Preference.OnPreferenceChangeListener, Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
parentFragmentManager
.beginTransaction()
.replace(R.id.settings, LockSettingsFragment())
.addToBackStack("Lock")
.commitAllowingStateLoss()
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@@ -89,7 +95,7 @@ class SettingsFragment :
val intent = Intent(requireContext(), LockActivity::class.java).apply { val intent = Intent(requireContext(), LockActivity::class.java).apply {
putExtra("force", true) putExtra("force", true)
} }
startActivityForResult(intent, R.id.request_lock.normalizeID()) lockLauncher.launch(intent)
} }
"mirrors" -> { "mirrors" -> {
MirrorDialog(requireContext()) MirrorDialog(requireContext())
@@ -267,19 +273,4 @@ class SettingsFragment :
} }
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
R.id.request_lock.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) {
parentFragmentManager
.beginTransaction()
.replace(R.id.settings, LockSettingsFragment())
.addToBackStack("Lock")
.commitAllowingStateLoss()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
} }

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package com.arlib.floatingsearchview package xyz.quaver.pupil.ui.view
import android.content.Context import android.content.Context
import android.graphics.PorterDuff import android.graphics.PorterDuff
@@ -36,21 +36,21 @@ import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter import xyz.quaver.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.MenuPopupHelper
import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.*
import java.util.* import java.util.*
class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FloatingSearchView(context, attrs), FloatingSearchView(context, attrs),
FloatingSearchView.OnSearchListener, FloatingSearchView.OnSearchListener,
SearchSuggestionsAdapter.OnBindSuggestionCallback,
TextWatcher TextWatcher
{ {
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text) private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
@@ -60,8 +60,10 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
searchInputView.addTextChangedListener(this) searchInputView.addTextChangedListener(this)
setOnSearchListener(this) onSearchListener = this
setOnBindSuggestionCallback(this) onBindSuggestionCallback = { a, b, c, d, e ->
onBindSuggestion(a, b, c, d, e)
}
} }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
@@ -82,15 +84,18 @@ 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 -> {
with(searchInputView.text) { val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}"
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length) with(searchInputView.text!!) {
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ") delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length)
if (!this.contains(tag))
append("$tag ")
} }
} }
is Suggestion -> { is Suggestion -> {
with(searchInputView.text) { with(searchInputView.text!!) {
clear() clear()
append(searchSuggestion.str) append(searchSuggestion.body)
} }
} }
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke() is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
@@ -99,14 +104,14 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
override fun onSearchAction(currentQuery: String?) {} override fun onSearchAction(currentQuery: String?) {}
override fun onBindSuggestion( fun onBindSuggestion(
suggestionView: View?, suggestionView: View?,
leftIcon: ImageView?, leftIcon: ImageView?,
textView: TextView?, textView: TextView?,
item: SearchSuggestion?, item: SearchSuggestion?,
itemPosition: Int itemPosition: Int
) { ) {
when(item) { when(item) {
is TagSuggestion -> { is TagSuggestion -> {
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}" val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
@@ -196,7 +201,7 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
isClickable = true isClickable = true
setOnClickListener { setOnClickListener {
onHistoryDeleteClickedListener?.invoke(item.str) onHistoryDeleteClickedListener?.invoke(item.body)
} }
} }
} }
@@ -212,10 +217,4 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
} }
} }
} }
// hack to remove color attributes which should not be reused
override fun onSaveInstanceState(): Parcelable? {
super.onSaveInstanceState()
return null
}
} }

View File

@@ -23,17 +23,18 @@ import android.content.Context
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class TagChip(context: Context, tag: Tag) : Chip(context) { class TagChip(context: Context, _tag: Tag) : Chip(context) {
val tag: Tag = val tag: Tag =
tag.let { _tag.let {
when { when {
it.area != null -> it it.area != null -> it
else -> Tag("tag", tag.tag) else -> Tag("tag", _tag.tag)
} }
} }
@@ -44,18 +45,47 @@ class TagChip(context: Context, tag: Tag) : Chip(context) {
}.toMap() }.toMap()
init { init {
chipIcon = when(tag.area) { when(tag.area) {
"male" -> { "male" -> {
setChipBackgroundColorResource(R.color.material_blue_700) setChipBackgroundColorResource(R.color.material_blue_700)
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.gender_male_white) setCloseIconTintResource(android.R.color.white)
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_male_white)
} }
"female" -> { "female" -> {
setChipBackgroundColorResource(R.color.material_pink_600) setChipBackgroundColorResource(R.color.material_pink_600)
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.gender_female_white) setCloseIconTintResource(android.R.color.white)
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_female_white)
}
}
if (favoriteTags.contains(tag))
setChipBackgroundColorResource(R.color.material_orange_500)
isCloseIconVisible = true
closeIcon = ContextCompat.getDrawable(context,
if (favoriteTags.contains(tag))
R.drawable.ic_star_filled
else
R.drawable.ic_star_empty
)
setOnCloseIconClickListener {
if (favoriteTags.contains(tag)) {
favoriteTags.remove(tag)
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty)
when(tag.area) {
"male" -> setChipBackgroundColorResource(R.color.material_blue_700)
"female" -> setChipBackgroundColorResource(R.color.material_pink_600)
else -> chipBackgroundColor = null
}
} else {
favoriteTags.add(tag)
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled)
setChipBackgroundColorResource(R.color.material_orange_500)
} }
else -> null
} }
text = when (tag.area) { text = when (tag.area) {

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

@@ -20,7 +20,6 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.SparseArray
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,6 +37,7 @@ import xyz.quaver.io.util.*
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
@Serializable @Serializable
data class Metadata( data class Metadata(
@@ -51,7 +51,7 @@ data class Metadata(
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) { class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
companion object { companion object {
val instances = SparseArray<Cache>() val instances = ConcurrentHashMap<Int, Cache>()
fun getInstance(context: Context, galleryID: Int) = fun getInstance(context: Context, galleryID: Int) =
instances[galleryID] ?: synchronized(this) { instances[galleryID] ?: synchronized(this) {
@@ -61,7 +61,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
@Synchronized @Synchronized
fun delete(galleryID: Int) { fun delete(galleryID: Int) {
instances[galleryID]?.cacheFolder?.deleteRecursively() instances[galleryID]?.cacheFolder?.deleteRecursively()
instances.delete(galleryID) instances.remove(galleryID)
} }
} }
@@ -200,31 +200,52 @@ 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
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder.getChild(imageName)
val source = cacheFolder.getChild(imageName)
if (!source.exists())
return@forEach
kotlin.runCatching {
target.createNewFile()
source.readBytes()?.let { target.writeBytes(it) }
}
}
val cacheMetadata = cacheFolder.getChild(".metadata") val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder.getChild(".metadata") val downloadMetadata = downloadFolder.getChild(".metadata")
if (downloadMetadata.exists() || !cacheMetadata.exists())
return@launch
if (cacheMetadata.exists()) { if (cacheMetadata.exists()) {
kotlin.runCatching { kotlin.runCatching {
downloadMetadata.createNewFile() downloadMetadata.createNewFile()
downloadMetadata.writeText(Json.encodeToString(metadata)) downloadMetadata.writeText(Json.encodeToString(metadata))
cacheMetadata.delete() cacheMetadata.delete()
} }
} }
cacheFolder.delete() val cacheThumbnail = cacheFolder.getChild(".thumbnail")
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
kotlin.runCatching {
if (!downloadThumbnail.exists())
downloadThumbnail.createNewFile()
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
source.copyTo(target)
} }
cacheThumbnail.delete()
}
}
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder.getChild(imageName)
val source = cacheFolder.getChild(imageName)
if (!source.exists() || target.exists())
return@forEach
kotlin.runCatching {
if (!target.exists())
target.createNewFile()
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
source.copyTo(target)
} }
}
}
} }
} }

View File

@@ -104,8 +104,10 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val folder = downloadFolder.getChild(name) val folder = downloadFolder.getChild(name)
if (!folder.exists()) if (folder.exists())
folder.mkdir() return
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

@@ -27,13 +27,11 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
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
@@ -52,7 +50,9 @@ import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader import xyz.quaver.hitomi.getReader
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.* import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
@@ -160,7 +160,6 @@ fun checkUpdate(context: Context, force: Boolean = false) {
val msg = extractReleaseNote(update, Locale.getDefault()) val msg = extractReleaseNote(update, Locale.getDefault())
setMessage(Markwon.create(context).toMarkdown(msg)) setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
//Cancel any download queued before //Cancel any download queued before

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

@@ -4,7 +4,7 @@
<item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp"> <item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<stroke android:width="1dp" android:color="#555555"/> <stroke android:width="1dp" android:color="#555555"/>
<solid android:color="@color/transparent"/> <solid android:color="@android:color/transparent"/>
</shape> </shape>
</item> </item>
</layer-list> </layer-list>

View File

@@ -0,0 +1,8 @@
<!-- drawable/sort_variant.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="#000" android:pathData="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
</vector>

View File

@@ -1,150 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 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/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="64dp"
android:visibility="invisible"
android:background="@color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"
android:id="@+id/main_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<TextView
android:id="@+id/main_noresult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/main_no_result"
android:visibility="invisible"/>
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent"
android:layout_height="match_parent"
app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp"
app:handleWidth="24dp"
app:trackMarginStart="64dp"
app:addLastItemPadding="true"
app:popupDrawable="@color/transparent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/main_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_random"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.arlib.floatingsearchview.FloatingSearchViewDayNight
android:id="@+id/main_searchview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:floatingSearch_backgroundColor="?android:attr/colorBackgroundFloating"
app:floatingSearch_leftActionColor="?attr/colorControlNormal"
app:floatingSearch_menuItemIconColor="?attr/colorControlNormal"
app:floatingSearch_actionMenuOverflowColor="?attr/colorControlNormal"
app:floatingSearch_clearBtnColor="?attr/colorControlNormal"
app:floatingSearch_viewTextColor="?android:attr/textColorPrimary"
app:floatingSearch_suggestionRightIconColor="@color/material_orange_500"
app:floatingSearch_searchBarMarginLeft="8dp"
app:floatingSearch_searchBarMarginRight="8dp"
app:floatingSearch_searchBarMarginTop="8dp"
app:floatingSearch_searchHint="@string/search_hint"
app:floatingSearch_suggestionsListAnimDuration="250"
app:floatingSearch_showSearchKey="true"
app:floatingSearch_leftActionMode="showHamburger"
app:floatingSearch_menu="@menu/main"
app:floatingSearch_dismissOnOutsideTouch="true"
app:floatingSearch_close_search_on_keyboard_dismiss="true"
tools:ignore="NewApi" />
</RelativeLayout>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
~ Pupil, Hitomi.la viewer for Android ~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079 ~ Copyright (C) 2020 tom5079
~ ~
~ This program is free software: you can redistribute it and/or modify ~ 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 ~ it under the terms of the GNU General Public License as published by
@@ -32,7 +32,7 @@
android:id="@+id/main_appbar_layout" android:id="@+id/main_appbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/transparent" android:background="@android:color/transparent"
android:visibility="invisible" android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"> app:layout_constraintLeft_toLeftOf="parent">
@@ -41,7 +41,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp" android:layout_height="64dp"
android:visibility="invisible" android:visibility="invisible"
android:background="@color/transparent" android:background="@android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways" app:layout_scrollFlags="scroll|enterAlways"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/> app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
@@ -70,9 +70,11 @@
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="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/main_recyclerview" android:id="@+id/main_recyclerview"
@@ -124,21 +126,20 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.arlib.floatingsearchview.FloatingSearchViewDayNight <xyz.quaver.pupil.ui.view.FloatingSearchView
android:id="@+id/main_searchview" android:id="@+id/main_searchview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:floatingSearch_suggestionRightIconColor="@color/material_orange_500" app:searchBarMarginLeft="6dp"
app:floatingSearch_searchBarMarginLeft="8dp" app:searchBarMarginRight="6dp"
app:floatingSearch_searchBarMarginRight="8dp" app:searchBarMarginTop="6dp"
app:floatingSearch_searchBarMarginTop="8dp" app:searchHint="@string/search_hint"
app:floatingSearch_searchHint="@string/search_hint" app:suggestionAnimDuration="250"
app:floatingSearch_suggestionsListAnimDuration="250" app:showSearchKey="true"
app:floatingSearch_showSearchKey="true" app:leftActionMode="showHamburger"
app:floatingSearch_leftActionMode="showHamburger" app:menu="@menu/main"
app:floatingSearch_menu="@menu/main" app:dismissOnOutsideTouch="true"
app:floatingSearch_dismissOnOutsideTouch="true" app:close_search_on_keyboard_dismiss="false"
app:floatingSearch_close_search_on_keyboard_dismiss="true"
tools:ignore="NewApi" /> tools:ignore="NewApi" />
</RelativeLayout> </RelativeLayout>

View File

@@ -23,7 +23,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/dark_gray" android:background="@android:color/darker_gray"
tools:context=".ui.ReaderActivity"> tools:context=".ui.ReaderActivity">
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
@@ -33,9 +33,11 @@
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="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/reader_recyclerview" android:id="@+id/reader_recyclerview"
@@ -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:id="@+id/galleryblock_progressbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar <androidx.core.widget.ContentLoadingProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="?android:attr/progressBarStyleHorizontal"
android:id="@+id/galleryblock_progressbar" android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="wrap_content"
android:visibility="gone" android:layout_marginBottom="-4dp"
app:layout_constraintTop_toTopOf="parent"/> 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,138 +105,135 @@
android:contentDescription="@string/reader_imageview_description" android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<ImageView </FrameLayout>
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView <ImageView
style="@style/TextAppearance.AppCompat.Headline" android:id="@+id/galleryblock_thumbnail"
android:id="@+id/galleryblock_title" android:layout_width="150dp"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:contentDescription="@string/galleryblock_thumbnail_description"
android:layout_marginTop="8dp" android:adjustViewBounds="true"
android:layout_marginStart="8dp" app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginLeft="8dp" app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintBottom_toBottomOf="@id/barrier"/>
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView <TextView
style="@style/TextAppearance.AppCompat.Medium" style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_artist" android:id="@+id/galleryblock_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginTop="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_toBottomOf="@id/galleryblock_title"/> app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"/>
<TextView <TextView
android:id="@+id/galleryblock_series" style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="0dp" android:id="@+id/galleryblock_artist"
android:layout_height="wrap_content" android:layout_width="0dp"
android:layout_marginStart="8dp" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
<TextView <TextView
android:id="@+id/galleryblock_type" android:id="@+id/galleryblock_series"
android:layout_width="wrap_content" 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_series" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" /> app:layout_constraintRight_toRightOf="parent"/>
<TextView <TextView
android:id="@+id/galleryblock_language" 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"
android:layout_marginBottom="8dp" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<View <TextView
android:id="@+id/galleryblock_padding" android:id="@+id/galleryblock_language"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/> app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<com.google.android.material.chip.ChipGroup <androidx.constraintlayout.widget.Barrier
android:id="@+id/galleryblock_tag_group" android:id="@+id/padding"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" app:barrierDirection="bottom"
android:layout_marginStart="8dp" app:constraint_referenced_ids="galleryblock_language"/>
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.chip.ChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<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="?android:attr/listDivider"
android:background="@android:color/darker_gray"/> app:layout_constraintTop_toBottomOf="@id/barrier"
android:layout_margin="8dp"/>
<LinearLayout <TextView
android:layout_width="match_parent" android:id="@+id/galleryblock_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="8dp" android:layout_marginLeft="8dp"
android:paddingRight="8dp" android:layout_marginTop="8dp"
android:paddingBottom="8dp" android:layout_marginBottom="8dp"
android:orientation="horizontal" app:layout_constraintTop_toBottomOf="@id/divider"
android:gravity="center_vertical"> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView <TextView
android:id="@+id/galleryblock_id" 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"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent" />
<View <ImageView
android:layout_width="0dp" android:id="@+id/galleryblock_favorite"
android:layout_height="1dp" android:contentDescription="@string/app_name"
android:layout_weight="1"/> android:layout_width="32dp"
android:layout_height="32dp"
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" />
<TextView </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/galleryblock_pagecount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<ImageView
android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name"
android:layout_width="32dp"
android:layout_height="32dp"
app:srcCompat="@drawable/ic_star_empty"/>
</LinearLayout>
</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

@@ -20,10 +20,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/main_menu_thin" <item android:id="@+id/sort"
android:title="@string/main_menu_thin"/> android:title="@string/main_menu_sort"
android:icon="@drawable/sort_variant"
<item android:title="@string/main_menu_sort"> app:showAsAction="ifRoom">
<menu> <menu>
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest" <item android:id="@+id/main_menu_sort_newest"
@@ -41,4 +41,9 @@
android:title="@string/main_settings" android:title="@string/main_settings"
app:showAsAction="always"/> app:showAsAction="always"/>
<item android:id="@+id/main_menu_thin"
android:title="@string/main_menu_thin"
app:showAsAction="never"
android:checkable="true"/>
</menu> </menu>

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

@@ -114,7 +114,7 @@
<string name="proxy_dialog_error">잘못된 값</string> <string name="proxy_dialog_error">잘못된 값</string>
<string name="proxy_dialog_addr_hint">서버 주소</string> <string name="proxy_dialog_addr_hint">서버 주소</string>
<string name="proxy_dialog_server">서버</string> <string name="proxy_dialog_server">서버</string>
<string name="main_menu_thin">간단히 보기 모드</string> <string name="main_menu_thin">간단히 보기</string>
<string name="main_fab_cancel">다운로드 모두 취소</string> <string name="main_fab_cancel">다운로드 모두 취소</string>
<string name="channel_update">업데이트</string> <string name="channel_update">업데이트</string>
<string name="channel_update_description">업데이트 진행상황 표시</string> <string name="channel_update_description">업데이트 진행상황 표시</string>
@@ -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

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="galleryblock_thumbnail_thin">100dp</dimen> <dimen name="galleryblock_thumbnail_thin">100dp</dimen>
<dimen name="reader_max_height">2000dp</dimen> <dimen name="reader_max_height" tools:ignore="PxUsage">2000px</dimen>
<dimen name="thumb_width">24dp</dimen> <dimen name="thumb_width">24dp</dimen>
<dimen name="thumb_height">72dp</dimen> <dimen name="thumb_height">72dp</dimen>

View File

@@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item name="item_click_support" type="id" /> <item name="item_click_support" type="id" />
<item name="request_settings" type="id" />
<item name="request_lock" type="id" />
<item name="request_restore" type="id" />
<item name="request_download_folder" type="id" />
<item name="request_download_folder_old" type="id" />
<item name="request_write_permission_and_saf" type="id" />
<item name="notification_id_update" type="id" /> <item name="notification_id_update" type="id" />
<item name="notification_id_import" type="id" /> <item name="notification_id_import" type="id" />

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>
@@ -49,7 +50,7 @@
<string name="main_drawer_group_contact_email">Email me!</string> <string name="main_drawer_group_contact_email">Email me!</string>
<string name="main_drawer_grouop_contact_discord">Discord</string> <string name="main_drawer_grouop_contact_discord">Discord</string>
<string name="main_menu_thin">Toggle Thin Mode</string> <string name="main_menu_thin">Thin Mode</string>
<string name="main_menu_sort">Sort</string> <string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string> <string name="main_menu_sort_newest">Newest</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>

View File

@@ -13,7 +13,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.3.3' classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.1' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
classpath 'com.google.firebase:perf-plugin:1.3.1' classpath 'com.google.firebase:perf-plugin:1.3.1'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2'
} }