Compare commits

...

14 Commits

Author SHA1 Message Date
tom5079
acc85da80f Bug fix 2020-09-10 22:47:57 +09:00
tom5079
b53de8624d Bug fix 2020-09-10 22:45:27 +09:00
tom5079
6e2eeb29cc Bug fix 2020-09-10 21:41:57 +09:00
tom5079
62eb28ac01 Bug fix 2020-09-10 19:44:08 +09:00
tom5079
fd298529bf Memory usage optimization 2020-09-10 19:16:42 +09:00
tom5079
297ce506b1 Bug fix 2020-09-10 17:03:17 +09:00
tom5079
18c6954be3 Improved Suggestions
resolves #100
2020-09-10 16:50:26 +09:00
tom5079
cea3fb1e65 Bug fix 2020-09-09 16:58:01 +09:00
tom5079
7f274fd238 Added OSS Notice 2020-09-09 13:06:32 +09:00
tom5079
439a8e93ec App built 2020-09-09 11:25:53 +09:00
tom5079
83801feee9 Bug fix 2020-09-09 11:23:22 +09:00
tom5079
8a6860c96e Import cleanup 2020-09-09 09:29:33 +09:00
tom5079
5c959f2987 Bug fix 2020-09-09 09:29:33 +09:00
tom5079
4e4397287a Implemented fast scroll 2020-09-09 09:22:17 +09:00
47 changed files with 738 additions and 584 deletions

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<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

@@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) { if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
logger.lifecycle("Firebase Enabled") logger.lifecycle("Firebase Enabled")
@@ -19,8 +20,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
versionCode 57 versionCode 58
versionName "5.0-beta6" versionName "5.0-hotfix3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -76,8 +77,10 @@ dependencies {
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.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
//implementation 'com.quiph.ui:recyclerviewfastscroller:0.2.1'
//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'
@@ -96,10 +99,10 @@ dependencies {
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0' //implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
implementation "ru.noties.markwon:core:3.1.0" implementation "ru.noties.markwon:core:3.1.0"
implementation ("xyz.quaver:libpupil:1.5") { implementation ("xyz.quaver:libpupil:1.6") {
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm' exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
} }
implementation "xyz.quaver:documentfilex:0.2.14" implementation "xyz.quaver:documentfilex:0.2.15"
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0'

Binary file not shown.

View File

@@ -21,7 +21,6 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontobfuscate -dontobfuscate
-dontoptimize
-keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule { -keep class * extends com.bumptech.glide.module.AppGlideModule {

View File

@@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"properties": [], "properties": [],
"versionCode": 57, "versionCode": 58,
"versionName": "5.0-beta6", "versionName": "5.0-hotfix3",
"enabled": true, "enabled": true,
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }

View File

@@ -20,26 +20,10 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import java.io.InputStreamReader
import java.net.URL
import javax.net.ssl.HttpsURLConnection
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
@@ -54,77 +38,4 @@ class ExampleInstrumentedTest {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
} }
@Test
fun checkCacheDir() {
val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Runtime.getRuntime().exec("du -hs " + getDownloadDirectory(appContext)).let {
InputStreamReader(it.inputStream).readLines().forEach { res ->
Log.i("PUPILD", res)
}
}
}
@Test
fun test_nozomi() {
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
Log.i("PUPILD", nozomi.size.toString())
}
@Test
fun test_doSearch() {
val reader = getReader( 1426382)
val data: ByteArray
with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
data = inputStream.readBytes()
}
Log.d("Pupil", data.size.toString())
}
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 515515
val worker = DownloadWorker.getInstance(context)
worker.queue.add(galleryID)
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { it.isInfinite() } == true)
break
}
Log.i("PUPILD", "DONE!!")
}
@Test
fun test_getReaderOrNull() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
}
@Test
fun test_suggestion() {
getSuggestionsForQuery("female:l")
}
} }

View File

@@ -19,13 +19,199 @@
package com.arlib.floatingsearchview package com.arlib.floatingsearchview
import android.content.Context import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Animatable
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.types.*
import java.util.*
class FloatingSearchViewDayNight @JvmOverloads constructor( class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
context: Context, FloatingSearchView(context, attrs),
attrs: AttributeSet? = null) FloatingSearchView.OnSearchListener,
: FloatingSearchView(context, attrs) { SearchSuggestionsAdapter.OnBindSuggestionCallback,
TextWatcher
{
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
init {
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
searchInputView.addTextChangedListener(this)
setOnSearchListener(this)
setOnBindSuggestionCallback(this)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
}
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
when (searchSuggestion) {
is TagSuggestion -> {
with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
}
}
is Suggestion -> {
with(searchInputView.text) {
clear()
append(searchSuggestion.str)
}
}
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
}
}
override fun onSearchAction(currentQuery: String?) {}
override fun onBindSuggestion(
suggestionView: View?,
leftIcon: ImageView?,
textView: TextView?,
item: SearchSuggestion?,
itemPosition: Int
) {
when(item) {
is TagSuggestion -> {
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
leftIcon?.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
when(item.n) {
"female" -> R.drawable.gender_female
"male" -> R.drawable.gender_male
"language" -> R.drawable.translate
"group" -> R.drawable.account_group
"character" -> R.drawable.account_star
"series" -> R.drawable.book_open
"artist" -> R.drawable.brush
else -> R.drawable.tag
},
context.theme)
)
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
this ?: return@with
if (favoriteTags.contains(Tag.parse(tag)))
setImageResource(R.drawable.ic_star_filled)
else
setImageResource(R.drawable.ic_star_empty)
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
isClickable = true
setOnClickListener {
val tag = Tag.parse(tag)
if (favoriteTags.contains(tag)) {
setImageResource(R.drawable.ic_star_empty)
favoriteTags.remove(tag)
}
else {
setImageDrawable(
AnimatedVectorDrawableCompat.create(context,
R.drawable.avd_star
))
(drawable as Animatable).start()
favoriteTags.add(tag)
}
}
}
if (item.t == -1) {
textView?.text = item.s
} else {
(suggestionView as? LinearLayout)?.let {
val count = it.findViewById<TextView>(R.id.count)
if (count == null)
it.addView(
LayoutInflater.from(context).inflate(R.layout.suggestion_count, suggestionView, false)
.apply {
this as TextView
text = item.t.toString()
}, 2
)
else
count.text = item.t.toString()
}
}
}
is FavoriteHistorySwitch -> {
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.swap_horizontal, context.theme))
}
is Suggestion -> {
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.history, context.theme))
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
this ?: return@with
setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.delete, context.theme))
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
isClickable = true
setOnClickListener {
onHistoryDeleteClickedListener?.invoke(item.str)
}
}
}
is LoadingSuggestion -> {
leftIcon?.setImageDrawable(CircularProgressDrawable(context).also {
it.setStyle(CircularProgressDrawable.DEFAULT)
it.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
it.start()
})
}
is NoResultSuggestion -> {
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.close, context.theme))
}
}
}
// hack to remove color attributes which should not be reused // hack to remove color attributes which should not be reused
override fun onSaveInstanceState(): Parcelable? { override fun onSaveInstanceState(): Parcelable? {

View File

@@ -18,10 +18,7 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.app.Application import android.app.*
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -41,7 +38,9 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.setClient import xyz.quaver.setClient
import java.io.File import java.io.File
import java.util.* import java.util.*
@@ -50,9 +49,13 @@ import kotlin.reflect.KClass
typealias PupilInterceptor = (Interceptor.Chain) -> Response typealias PupilInterceptor = (Interceptor.Chain) -> Response
lateinit var histories: GalleryList lateinit var histories: SavedSet<Int>
private set private set
lateinit var favorites: GalleryList lateinit var favorites: SavedSet<Int>
private set
lateinit var favoriteTags: SavedSet<Tag>
private set
lateinit var searchHistory: SavedSet<String>
private set private set
val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>() val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>()
@@ -105,13 +108,17 @@ class Pupil : Application() {
if (!FileX(this, it).canWrite()) if (!FileX(this, it).canWrite())
throw Exception() throw Exception()
DownloadManager.getInstance(this).migrate()
} }
} catch (e: Exception) { } catch (e: Exception) {
Preferences.remove("download_folder") Preferences.remove("download_folder")
} }
histories = GalleryList(File(ContextCompat.getDataDir(this), "histories.json")) histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
favorites = GalleryList(File(ContextCompat.getDataDir(this), "favorites.json")) favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
if (Preferences["new_history"]) { if (Preferences["new_history"]) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {

View File

@@ -19,9 +19,6 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -29,7 +26,6 @@ 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.content.ContextCompat
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
@@ -43,7 +39,6 @@ import com.bumptech.glide.request.target.Target
import com.daimajia.swipe.SwipeLayout import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.item_galleryblock.view.* import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View File

@@ -42,6 +42,7 @@ 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.io.util.readBytes
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
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
@@ -123,6 +124,10 @@ class ReaderAdapter(private val activity: ReaderActivity,
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false) .skipMemoryCache(false)
.fitCenter() .fitCenter()
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.into(holder.view.image) .into(holder.view.image)
} }
@@ -139,6 +144,10 @@ class ReaderAdapter(private val activity: ReaderActivity,
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true) .skipMemoryCache(true)
.fitCenter() .fitCenter()
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.listener(object: RequestListener<Drawable> { .listener(object: RequestListener<Drawable> {
override fun onLoadFailed( override fun onLoadFailed(

View File

@@ -27,6 +27,7 @@ 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -46,7 +47,6 @@ import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.ellipsize 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 xyz.quaver.pupil.util.startForegroundServiceCompat
import java.io.IOException import java.io.IOException
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
@@ -215,7 +215,7 @@ class DownloadService : Service() {
val ext = call.request().url().encodedPath().split('.').last() val ext = call.request().url().encodedPath().split('.').last()
kotlin.runCatching { kotlin.runCatching {
val image = response.body()?.use { it.bytes() } ?: throw Exception() val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching { kotlin.runCatching {
@@ -354,7 +354,7 @@ class DownloadService : Service() {
const val COMMAND_DELETE = "DELETE" const val COMMAND_DELETE = "DELETE"
private fun command(context: Context, extras: Intent.() -> Unit) { private fun command(context: Context, extras: Intent.() -> Unit) {
context.startForegroundServiceCompat(Intent(context, DownloadService::class.java).apply(extras)) ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
} }
fun download(context: Context, galleryID: Int, priority: Boolean = false) { fun download(context: Context, galleryID: Int, priority: Boolean = false) {
@@ -381,6 +381,8 @@ class DownloadService : Service() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(R.id.downloader_notification_id, serviceNotification.build())
when (intent?.getStringExtra(KEY_COMMAND)) { when (intent?.getStringExtra(KEY_COMMAND)) {
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId) download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
@@ -389,8 +391,6 @@ class DownloadService : Service() {
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) } COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) }
} }
startForeground(R.id.downloader_notification_id, serviceNotification.build())
return START_NOT_STICKY return START_NOT_STICKY
} }
@@ -402,6 +402,7 @@ class DownloadService : Service() {
override fun onBind(p0: Intent?) = binder override fun onBind(p0: Intent?) = binder
override fun onCreate() { override fun onCreate() {
startForeground(R.id.downloader_notification_id, serviceNotification.build())
interceptors[Tag::class] = interceptor interceptors[Tag::class] = interceptor
} }

View File

@@ -29,4 +29,25 @@ data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String
override fun getBody(): String { override fun getBody(): String {
return s return s
} }
}
@Parcelize
class Suggestion(val str: String) : SearchSuggestion {
override fun getBody() = str
}
@Parcelize
class NoResultSuggestion(val str: String) : SearchSuggestion {
override fun getBody() = str
}
@Parcelize
class LoadingSuggestion(val str: String) : SearchSuggestion {
override fun getBody() = str
}
@Parcelize
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
class FavoriteHistorySwitch(private val body: String) : SearchSuggestion {
override fun getBody() = body
} }

View File

@@ -19,13 +19,12 @@
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable @Serializable
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) { data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
companion object { companion object {
fun parse(tag: String) : Tag { fun parse(tag: String) : Tag {
if (tag.first() == '-') { if (tag.firstOrNull() == '-') {
tag.substring(1).split(Regex(":"), 2).let { tag.substring(1).split(Regex(":"), 2).let {
return when(it.size) { return when(it.size) {
2 -> Tag(it[0], it[1], true) 2 -> Tag(it[0], it[1], true)
@@ -63,9 +62,7 @@ data class Tag(val area: String?, val tag: String, val isNegative: Boolean = fal
return false return false
} }
override fun hashCode(): Int { override fun hashCode() = toString().hashCode()
return super.hashCode()
}
} }
class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags { class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags {
@@ -111,7 +108,4 @@ class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags
override fun toString(): String { override fun toString(): String {
return tags.joinToString(" ") { it.toString() } return tags.joinToString(" ") { it.toString() }
} }
} }

View File

@@ -0,0 +1,72 @@
/*
* 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.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import android.view.WindowManager
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.normalizeID
open class BaseActivity : AppCompatActivity() {
private var locked: Boolean = true
@CallSuper
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
locked = !LockManager(this).locks.isNullOrEmpty()
}
@CallSuper
override fun onResume() {
super.onResume()
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked)
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
}
@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

@@ -39,6 +39,7 @@ import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
private var lastUnlocked = 0L
class LockActivity : AppCompatActivity() { class LockActivity : AppCompatActivity() {
private lateinit var lockManager: LockManager private lateinit var lockManager: LockManager
@@ -52,6 +53,7 @@ class LockActivity : AppCompatActivity() {
val result = lockManager.check(it) val result = lockManager.check(it)
if (result == true) { if (result == true) {
lastUnlocked = System.currentTimeMillis()
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()
} else } else
@@ -86,6 +88,7 @@ class LockActivity : AppCompatActivity() {
val result = lockManager.check(it) val result = lockManager.check(it)
if (result == true) { if (result == true) {
lastUnlocked = System.currentTimeMillis()
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()
} else { } else {
@@ -157,6 +160,7 @@ class LockActivity : AppCompatActivity() {
override fun onAuthenticationSucceeded( override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult) { result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result) super.onAuthenticationSucceeded(result)
lastUnlocked = System.currentTimeMillis()
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
return return
@@ -185,6 +189,7 @@ class LockActivity : AppCompatActivity() {
} }
mode = intent.getStringExtra("mode") mode = intent.getStringExtra("mode")
val force = intent.getBooleanExtra("force", false)
when(mode) { when(mode) {
null -> { null -> {
@@ -194,6 +199,13 @@ class LockActivity : AppCompatActivity() {
return return
} }
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
lastUnlocked = System.currentTimeMillis()
setResult(RESULT_OK)
finish()
return
}
if ( if (
Preferences["lock_fingerprint"] Preferences["lock_fingerprint"]
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS && BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS

View File

@@ -19,65 +19,49 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.* import android.text.InputType
import android.text.style.AlignmentSpan import android.view.*
import android.util.TypedValue
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.FloatingSearchViewDayNight import com.arlib.floatingsearchview.FloatingSearchViewDayNight
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView 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.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics 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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
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
import xyz.quaver.pupil.R import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.TagSuggestion import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.types.Tags
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.*
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 java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MainActivity : AppCompatActivity() { class MainActivity :
BaseActivity(),
FloatingSearchView.OnMenuItemClickListener,
NavigationView.OnNavigationItemSelectedListener
{
enum class Mode { enum class Mode {
SEARCH, SEARCH,
@@ -115,26 +99,9 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val lockManager = try {
LockManager(this)
} catch (e: Exception) {
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
}.show()
return
}
if (lockManager.isNotEmpty())
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url -> intent.dataString?.let { url ->
restore(favorites, url, restore(url,
onFailure = { onFailure = {
Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = { }, onSuccess = {
@@ -176,17 +143,6 @@ class MainActivity : AppCompatActivity() {
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel() (main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
} }
override fun onResume() {
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
super.onResume()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
@@ -234,10 +190,6 @@ class MainActivity : AppCompatActivity() {
loadBlocks() loadBlocks()
} }
} }
R.id.request_lock.normalizeID() -> {
if (resultCode != Activity.RESULT_OK)
finish()
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
@@ -261,71 +213,7 @@ class MainActivity : AppCompatActivity() {
) )
//NavigationView //NavigationView
main_nav_view.setNavigationItemSelectedListener { main_nav_view.setNavigationItemSelectedListener(this)
runOnUiThread {
main_drawer_layout.closeDrawers()
when(it.itemId) {
R.id.main_drawer_home -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.SEARCH
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.HISTORY
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.FAVORITE
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
}
R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
}
R.id.main_drawer_homepage -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
}
R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
}
R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
}
}
}
true
}
with(main_fab_cancel) { with(main_fab_cancel) {
setImageResource(R.drawable.cancel) setImageResource(R.drawable.cancel)
@@ -720,36 +608,24 @@ class MainActivity : AppCompatActivity() {
} }
} }
private var isFavorite = false
private val defaultSuggestions: List<SearchSuggestion>
get() = when {
isFavorite -> {
favoriteTags.map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
} + FavoriteHistorySwitch(getString(R.string.search_show_histories))
}
else -> {
searchHistory.map {
Suggestion(it)
}.takeLast(20) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
}
}.reversed()
private var suggestionJob : Job? = null private var suggestionJob : Job? = null
private fun setupSearchBar() { private fun setupSearchBar() {
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
//Change upper case letters to lower case
searchInputView.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
}
})
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
with(main_searchview as FloatingSearchViewDayNight) { with(main_searchview as FloatingSearchViewDayNight) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
if (!favoritesFile.exists()) {
favoritesFile.createNewFile()
favoritesFile.writeText("[]")
}
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener { setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
override fun onMenuOpened() { override fun onMenuOpened() {
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems() (this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
@@ -760,62 +636,30 @@ class MainActivity : AppCompatActivity() {
} }
}) })
setOnMenuItemClickListener { onHistoryDeleteClickedListener = {
when(it.itemId) { searchHistory.remove(it)
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), R.id.request_settings.normalizeID()) swapSuggestions(defaultSuggestions)
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
it.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
it.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}
} }
onFavoriteHistorySwitchClickListener = {
isFavorite = !isFavorite
swapSuggestions(defaultSuggestions)
}
setOnMenuItemClickListener(this@MainActivity)
setOnQueryChangeListener { _, query -> setOnQueryChangeListener { _, query ->
this@MainActivity.query = query this@MainActivity.query = query
suggestionJob?.cancel() suggestionJob?.cancel()
clearSuggestions()
if (query.isEmpty() or query.endsWith(' ')) { if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(Tags(Json.decodeFromString(favoritesFile.readText())).map { swapSuggestions(defaultSuggestions)
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
return@setOnQueryChangeListener return@setOnQueryChangeListener
} }
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
val currentQuery = query.split(" ").last().replace('_', ' ') val currentQuery = query.split(" ").last().replace('_', ' ')
suggestionJob = CoroutineScope(Dispatchers.IO).launch { suggestionJob = CoroutineScope(Dispatchers.IO).launch {
@@ -825,113 +669,22 @@ class MainActivity : AppCompatActivity() {
suggestions.filter { suggestions.filter {
val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}" val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}"
Tags(Json.decodeFromString(favoritesFile.readText())).contains(tag) favoriteTags.contains(Tag.parse(tag))
}.reversed().forEach { }.reversed().forEach {
suggestions.remove(it) suggestions.remove(it)
suggestions.add(0, it) suggestions.add(0, it)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
swapSuggestions(suggestions) swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
} }
} }
} }
setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ ->
item as TagSuggestion
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
val color = TypedValue()
theme.resolveAttribute(R.attr.colorControlNormal, color, true)
leftIcon.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
when(item.n) {
"female" -> R.drawable.gender_female
"male" -> R.drawable.gender_male
"language" -> R.drawable.translate
"group" -> R.drawable.account_group
"character" -> R.drawable.account_star
"series" -> R.drawable.book_open
"artist" -> R.drawable.brush
else -> R.drawable.tag
},
context.theme)
)
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
if (Tags(Json.decodeFromString(favoritesFile.readText())).contains(tag))
setImageResource(R.drawable.ic_star_filled)
else
setImageResource(R.drawable.ic_star_empty)
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
isClickable = true
setOnClickListener {
val favorites = Tags(Json.decodeFromString(favoritesFile.readText()))
if (favorites.contains(tag)) {
setImageResource(R.drawable.ic_star_empty)
favorites.remove(tag)
}
else {
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
R.drawable.avd_star
))
(drawable as Animatable).start()
favorites.add(tag)
}
favoritesFile.writeText(Json.encodeToString(favorites.tags))
}
}
if (item.t == -1) {
textView.text = item.s
} else {
val text = "${item.s}\n ${item.t}"
val len = text.length
val left = item.s.length
textView.text = SpannableString(text).apply {
val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)
setSpan(s, left, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(SetLineOverlap(true), 1, len-2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(SetLineOverlap(false), len-1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
setOnSearchListener(object : FloatingSearchView.OnSearchListener {
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
if (searchSuggestion !is TagSuggestion)
return
with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
}
}
override fun onSearchAction(currentQuery: String?) {
//Do search on onFocusCleared()
}
})
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener { setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() { override fun onFocus() {
if (query.isEmpty() or query.endsWith(' ')) if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(Tags(Json.decodeFromString(favoritesFile.readText())).map { swapSuggestions(defaultSuggestions)
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
} }
override fun onFocusCleared() { override fun onFocusCleared() {
@@ -951,6 +704,113 @@ class MainActivity : AppCompatActivity() {
} }
} }
override fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), R.id.request_settings.normalizeID())
R.id.main_menu_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
item.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
item.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
runOnUiThread {
main_drawer_layout.closeDrawers()
when(item.itemId) {
R.id.main_drawer_home -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.SEARCH
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.HISTORY
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.FAVORITE
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
}
R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
}
R.id.main_drawer_homepage -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
}
R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
}
R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
}
}
}
return true
}
private fun cancelFetch() { private fun cancelFetch() {
galleryIDs?.cancel() galleryIDs?.cancel()
loadingJob?.cancel() loadingJob?.cancel()
@@ -974,6 +834,9 @@ class MainActivity : AppCompatActivity() {
private fun fetchGalleries(query: String, sortMode: SortMode) { private fun fetchGalleries(query: String, sortMode: SortMode) {
val defaultQuery: String = Preferences["default_query"] val defaultQuery: String = Preferences["default_query"]
if (query.isNotBlank())
searchHistory.add(query)
if (query != queryStack.lastOrNull()) { if (query != queryStack.lastOrNull()) {
queryStack.remove(query) queryStack.remove(query)
queryStack.add(query) queryStack.add(query)
@@ -1039,7 +902,7 @@ class MainActivity : AppCompatActivity() {
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
downloads.filter { result.binarySearch(it) >= 0 }.also { downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
} }
@@ -1052,7 +915,7 @@ class MainActivity : AppCompatActivity() {
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query).sorted()
favorites.filter { result.binarySearch(it) >= 0 }.also { favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
} }
@@ -1105,4 +968,14 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).onLowMemory()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).onTrimMemory(level)
}
} }

View File

@@ -28,7 +28,6 @@ import android.os.IBinder
import android.view.* import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -36,6 +35,7 @@ import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
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 kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
@@ -57,7 +57,7 @@ import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.timer import kotlin.concurrent.timer
class ReaderActivity : AppCompatActivity() { class ReaderActivity : BaseActivity() {
private var galleryID = 0 private var galleryID = 0
private var currentPage = 0 private var currentPage = 0
@@ -101,10 +101,6 @@ class ReaderActivity : AppCompatActivity() {
title = getString(R.string.reader_loading) title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
handleIntent(intent) handleIntent(intent)
cache = Cache.getInstance(this, galleryID) cache = Cache.getInstance(this, galleryID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID) FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
@@ -113,6 +109,7 @@ class ReaderActivity : AppCompatActivity() {
onBackPressed() onBackPressed()
return return
} }
if (Preferences["cache_disable"]) { if (Preferences["cache_disable"]) {
reader_download_progressbar.visibility = View.GONE reader_download_progressbar.visibility = View.GONE
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -171,17 +168,6 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
override fun onResume() {
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
super.onResume()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu) menuInflater.inflate(R.menu.reader, menu)
@@ -488,4 +474,14 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
} }
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).onLowMemory()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).onTrimMemory(level)
}
} }

View File

@@ -22,34 +22,25 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.io.FileX
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment 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.* import xyz.quaver.pupil.util.Preferences
import java.io.File import xyz.quaver.pupil.util.normalizeID
import java.nio.charset.Charset import java.nio.charset.Charset
class SettingsActivity : AppCompatActivity() { class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.settings_activity) setContentView(R.layout.settings_activity)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
@@ -58,16 +49,6 @@ class SettingsActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
override fun onResume() {
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
super.onResume()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> onBackPressed() android.R.id.home -> onBackPressed()
@@ -76,48 +57,6 @@ class SettingsActivity : AppCompatActivity() {
return true return true
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
R.id.request_lock.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, LockSettingsFragment())
.addToBackStack("Lock")
.commitAllowingStateLoss()
}
}
R.id.request_restore.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) {
val uri = data?.data ?: return
try {
val str = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!!
inputStream.readBytes().toString(Charset.defaultCharset())
}
favorites.addAll(Json.decodeFromString<List<Int>>(str).also {
Snackbar.make(
window.decorView,
getString(R.string.settings_restore_success, it.size),
Snackbar.LENGTH_LONG
).show()
})
} catch (e: Exception) {
Snackbar.make(
window.decorView,
R.string.settings_restore_failed,
Snackbar.LENGTH_LONG
).show()
}
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) { when (requestCode) {

View File

@@ -123,7 +123,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
.setView(build()) .setView(build())
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ -> .setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.canonicalPath ?: "" Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
DownloadManager.getInstance(requireContext()).migrate() DownloadManager.getInstance(requireContext()).migrate()
} }

View File

@@ -62,6 +62,11 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
if (response.code() != 200) {
response.close()
return
}
Intent(Intent.ACTION_SEND).apply { Intent(Intent.ACTION_SEND).apply {
type = "text/plain" type = "text/plain"
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", "")) putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
@@ -82,7 +87,7 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
.setTitle(R.string.settings_restore_title) .setTitle(R.string.settings_restore_title)
.setView(editText) .setView(editText)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
restore(favorites, editText.text.toString(), restore(editText.text.toString(),
onFailure = onFailure@{ onFailure = onFailure@{
val view = view ?: return@onFailure val view = view ?: return@onFailure
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
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
@@ -26,14 +27,20 @@ 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.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(),
@@ -68,7 +75,7 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true) checkUpdate(activity as SettingsActivity, true)
} }
"download_folder" -> { "download_folder" -> {
DownloadLocationDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
} }
"default_query" -> { "default_query" -> {
DefaultQueryDialog(requireContext()).apply { DefaultQueryDialog(requireContext()).apply {
@@ -79,8 +86,10 @@ class SettingsFragment :
}.show() }.show()
} }
"app_lock" -> { "app_lock" -> {
val intent = Intent(requireContext(), LockActivity::class.java) val intent = Intent(requireContext(), LockActivity::class.java).apply {
activity?.startActivityForResult(intent, R.id.request_lock.normalizeID()) putExtra("force", true)
}
startActivityForResult(intent, R.id.request_lock.normalizeID())
} }
"mirrors" -> { "mirrors" -> {
MirrorDialog(requireContext()) MirrorDialog(requireContext())
@@ -246,10 +255,31 @@ class SettingsFragment :
summary = Preferences.get<String>("user_id") summary = Preferences.get<String>("user_id")
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"oss" -> {
setOnPreferenceClickListener {
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
true
}
}
} }
} }
} }
} }
} }
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

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.ui.view package xyz.quaver.pupil.ui.view
import android.annotation.SuppressLint
import android.content.Context 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
@@ -25,7 +26,16 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
class TagChip(context: Context, val tag: Tag) : Chip(context) { @SuppressLint("ViewConstructor")
class TagChip(context: Context, tag: Tag) : Chip(context) {
val tag: Tag =
tag.let {
when {
it.area != null -> it
else -> Tag("tag", tag.tag)
}
}
private val languages = context.resources.getStringArray(R.array.languages).map { private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split -> it.split("|").let { split ->
@@ -34,13 +44,6 @@ class TagChip(context: Context, val tag: Tag) : Chip(context) {
}.toMap() }.toMap()
init { init {
val tag = tag.let {
when {
it.area != null -> it
else -> Tag("tag", tag.tag)
}
}
chipIcon = when(tag.area) { chipIcon = when(tag.area) {
"male" -> { "male" -> {
setChipBackgroundColorResource(R.color.material_blue_700) setChipBackgroundColorResource(R.color.material_blue_700)

View File

@@ -1,6 +1,6 @@
/* /*
* 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
@@ -18,12 +18,17 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.decodeFromString import kotlinx.serialization.*
import kotlinx.serialization.encodeToString import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
class GalleryList(private val file: File, private val list: MutableSet<Int> = mutableSetOf()) : MutableSet<Int> by list { class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
val serializer: KSerializer<List<T>>
get() = ListSerializer(serializer(any::class.java) as KSerializer<T>)
init { init {
if (!file.exists()) { if (!file.exists()) {
@@ -35,47 +40,48 @@ class GalleryList(private val file: File, private val list: MutableSet<Int> = mu
fun load() { fun load() {
synchronized(this) { synchronized(this) {
list.clear() set.clear()
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString<List<Int>>(file.bufferedReader().use { it.readText() }) Json.decodeFromString(serializer, file.readText())
}.onSuccess { }.onSuccess {
list.addAll(it) set.addAll(it)
} }
} }
} }
@OptIn(ExperimentalSerializationApi::class)
fun save() { fun save() {
synchronized(this) { synchronized(this) {
file.writeText(Json.encodeToString(list.toList())) file.writeText(Json.encodeToString(serializer, set.toList()))
} }
} }
override fun add(element: Int): Boolean { override fun add(element: T): Boolean {
load() load()
return list.add(element).also { return set.add(element).also {
save() save()
} }
} }
override fun addAll(elements: Collection<Int>): Boolean { override fun addAll(elements: Collection<T>): Boolean {
load() load()
return list.addAll(elements).also { return set.addAll(elements).also {
save() save()
} }
} }
override fun remove(element: Int): Boolean { override fun remove(element: T): Boolean {
load() load()
return list.remove(element).also { return set.remove(element).also {
save() save()
} }
} }
override fun clear() { override fun clear() {
list.clear() set.clear()
save() save()
} }

View File

@@ -277,6 +277,12 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
override fun onResponse(call: Call, response: Response) { 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() val ext = call.request().url().encodedPath().split('.').last()
try { try {

View File

@@ -34,12 +34,10 @@ import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.*
import xyz.quaver.io.util.readBytes
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
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
@Serializable @Serializable
data class Metadata( data class Metadata(
@@ -141,7 +139,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
.url(it) .url(it)
.build() .build()
client.newCall(request).execute().body()?.use { it.bytes() } client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
}.getOrNull()?.also { kotlin.run { }.getOrNull()?.also { kotlin.run {
cacheFolder.getChild(".thumbnail").writeBytes(it) cacheFolder.getChild(".thumbnail").writeBytes(it)
} } } }

View File

@@ -75,7 +75,6 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
data ?: { data ?: {
file.createNewFile() file.createNewFile()
file.writeText("{}")
mutableMapOf<Int, String>() mutableMapOf<Int, String>()
}.invoke() }.invoke()
}.invoke() }.invoke()
@@ -99,9 +98,6 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
@Synchronized @Synchronized
fun addDownloadFolder(galleryID: Int) { fun addDownloadFolder(galleryID: Int) {
if (downloadFolderMap.containsKey(galleryID))
return
val name = runBlocking { val name = runBlocking {
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock() Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
}?.formatDownloadFolder() ?: return }?.formatDownloadFolder() ?: return
@@ -119,12 +115,9 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
@Synchronized @Synchronized
fun deleteDownloadFolder(galleryID: Int) { fun deleteDownloadFolder(galleryID: Int) {
if (!downloadFolderMap.containsKey(galleryID))
return
downloadFolderMap[galleryID]?.let { downloadFolderMap[galleryID]?.let {
kotlin.runCatching { kotlin.runCatching {
downloadFolder.getChild(it).delete() downloadFolder.getChild(it).deleteRecursively()
downloadFolderMap.remove(galleryID) downloadFolderMap.remove(galleryID)
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() } downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }

View File

@@ -22,6 +22,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import xyz.quaver.Code import xyz.quaver.Code
@@ -92,21 +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("/", "")
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("/", "")
fun Context.startForegroundServiceCompat(service: Intent) {
if (Build.VERSION.SDK_INT >= 26)
startForegroundService(service)
else
startService(service)
}
val Reader.requestBuilders: List<Request.Builder> val Reader.requestBuilders: List<Request.Builder>
get() { get() {

View File

@@ -56,6 +56,7 @@ import xyz.quaver.io.util.*
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
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata import xyz.quaver.pupil.util.downloader.Metadata
@@ -195,7 +196,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
fun restore(favorites: GalleryList, url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) { fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) { if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException()) onFailure?.invoke(IllegalArgumentException())
return return
@@ -213,7 +214,7 @@ fun restore(favorites: GalleryList, url: String, onFailure: ((Throwable) -> Unit
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString<List<Int>>(response.body().use { it?.string() } ?: "[]").let { Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
favorites.addAll(it) favorites.addAll(it)
onSuccess?.invoke(it) onSuccess?.invoke(it)
} }
@@ -257,7 +258,12 @@ fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
job?.cancel() job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch { job = CoroutineScope(Dispatchers.IO).launch {
val downloadFolders = downloadFolder.listFiles { folder -> val downloadFolders = downloadFolder.listFiles { folder ->
(folder as? FileX)?.isDirectory == true && !downloadFolderMap.values.contains(folder.name) folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
}?.map {
if (it !is FileX)
FileX(this@migrate, it)
else
it
} }
if (downloadFolders.isNullOrEmpty()) return@launch if (downloadFolders.isNullOrEmpty()) return@launch
@@ -269,8 +275,6 @@ fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
notificationManager.notify(R.id.notification_id_import, notification.build()) notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching { kotlin.runCatching {
if (folder !is FileX) return@runCatching
val metadata = kotlin.runCatching { val metadata = kotlin.runCatching {
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject } folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
}.getOrNull() }.getOrNull()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,8 @@
<!-- drawable/close.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="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/delete.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="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/history.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="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/swap_horizontal.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="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z" />
</vector>

View File

@@ -63,18 +63,22 @@
android:text="@string/main_no_result" android:text="@string/main_no_result"
android:visibility="invisible"/> android:visibility="invisible"/>
<androidx.recyclerview.widget.RecyclerView <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/main_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingTop="64dp" app:handleHeight="100dp"
android:clipToPadding="false" app:addLastItemPadding="true"
app:fastScrollEnabled="true" app:popupDrawable="@color/transparent">
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" <androidx.recyclerview.widget.RecyclerView
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable" android:id="@+id/main_recyclerview"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable" android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> 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 <com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab" android:id="@+id/main_fab"

View File

@@ -63,18 +63,22 @@
android:text="@string/main_no_result" android:text="@string/main_no_result"
android:visibility="invisible"/> android:visibility="invisible"/>
<androidx.recyclerview.widget.RecyclerView <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/main_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingTop="64dp" app:handleHeight="100dp"
android:clipToPadding="false" app:addLastItemPadding="true"
app:fastScrollEnabled="true" app:popupDrawable="@color/transparent">
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" <androidx.recyclerview.widget.RecyclerView
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable" android:id="@+id/main_recyclerview"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable" android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> 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 <com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab" android:id="@+id/main_fab"

View File

@@ -26,16 +26,20 @@
android:background="@color/dark_gray" android:background="@color/dark_gray"
tools:context=".ui.ReaderActivity"> tools:context=".ui.ReaderActivity">
<androidx.recyclerview.widget.RecyclerView <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/reader_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:fastScrollEnabled="true" app:handleHeight="100dp"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable" app:addLastItemPadding="true"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" app:popupDrawable="@color/transparent">
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable" <androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> android:id="@+id/reader_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,32 @@
<?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/>.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center"
android:layout_marginStart="@dimen/search_bar_search_input_left_margin"
android:gravity="end|center"
android:paddingBottom="4dp"
android:paddingTop="4dp"
android:textSize="@dimen/suggestion_body_text_size"
tools:text="body"
android:layout_marginLeft="@dimen/search_bar_search_input_left_margin" />

View File

@@ -27,7 +27,7 @@
<item android:id="@+id/main_drawer_history" <item android:id="@+id/main_drawer_history"
android:title="@string/main_drawer_history" android:title="@string/main_drawer_history"
android:icon="@drawable/ic_history"/> android:icon="@drawable/history"/>
<item android:id="@+id/main_drawer_downloads" <item android:id="@+id/main_drawer_downloads"
android:title="@string/main_drawer_downloads" android:title="@string/main_drawer_downloads"

View File

@@ -145,4 +145,7 @@
<string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string> <string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string>
<string name="settings_download_folder_name_message">%sに含まれている文字列を対応する変数に置換します\n\n%s</string> <string name="settings_download_folder_name_message">%sに含まれている文字列を対応する変数に置換します\n\n%s</string>
<string name="settings_manage_storage">ストレージ管理</string> <string name="settings_manage_storage">ストレージ管理</string>
<string name="settings_oss">オープンソースライセンス</string>
<string name="search_show_tags">お気に入りのタグを見る</string>
<string name="search_show_histories">履歴を見る</string>
</resources> </resources>

View File

@@ -145,4 +145,7 @@
<string name="settings_invalid_download_folder_name">폴더 패턴에 사용할 수 없는 문자가 포함되어 있습니다</string> <string name="settings_invalid_download_folder_name">폴더 패턴에 사용할 수 없는 문자가 포함되어 있습니다</string>
<string name="settings_download_folder_name_message">지원되는 변수는 %s 입니다\n\n%s</string> <string name="settings_download_folder_name_message">지원되는 변수는 %s 입니다\n\n%s</string>
<string name="settings_manage_storage">저장소 관리</string> <string name="settings_manage_storage">저장소 관리</string>
<string name="settings_oss">오픈 소스 라이선스</string>
<string name="search_show_histories">검색 기록 보기</string>
<string name="search_show_tags">즐겨찾기 태그 보기</string>
</resources> </resources>

View File

@@ -75,6 +75,8 @@
<string name="search_hint">Search galleries</string> <string name="search_hint">Search galleries</string>
<string name="search_all">Search all galleries</string> <string name="search_all">Search all galleries</string>
<string name="search_show_histories">Show histories</string>
<string name="search_show_tags">Show favorite tags</string>
<string name="gallery_details">Details</string> <string name="gallery_details">Details</string>
<string name="gallery_thumbnails">Thumbnails</string> <string name="gallery_thumbnails">Thumbnails</string>
@@ -172,6 +174,7 @@
<string name="settings_import_old_galleries">Import old galleries</string> <string name="settings_import_old_galleries">Import old galleries</string>
<string name="settings_user_id">User ID</string> <string name="settings_user_id">User ID</string>
<string name="settings_user_id_toast">User ID is copied to clipboard</string> <string name="settings_user_id_toast">User ID is copied to clipboard</string>
<string name="settings_oss">Open Source Notice</string>
<!-- MANAGE FAVORITES --> <!-- MANAGE FAVORITES -->

View File

@@ -104,6 +104,10 @@
app:key="user_id" app:key="user_id"
app:title="@string/settings_user_id"/> app:title="@string/settings_user_id"/>
<Preference
app:key="oss"
app:title="@string/settings_oss"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -26,14 +26,21 @@ package xyz.quaver.pupil
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
import kotlinx.serialization.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.typeOf
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
val a = mutableSetOf<Int>()
print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType)
} }
} }

View File

@@ -15,6 +15,7 @@ buildscript {
// 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.2.1'
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'
} }
} }
@@ -22,7 +23,6 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
mavenLocal()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url 'https://guardian.github.com/maven/repo-releases' } maven { url 'https://guardian.github.com/maven/repo-releases' }
} }