Merge pull request #63 from tom5079/development

Version 4.3-hotfix1
This commit is contained in:
Pupil
2020-01-31 10:39:06 +09:00
committed by GitHub
25 changed files with 622 additions and 499 deletions

View File

@@ -20,7 +20,7 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 32 versionCode 32
versionName "4.3" versionName "4.3-hotfix1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -50,34 +50,34 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.1" implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
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.2.0-alpha03' implementation 'com.google.android.material:material:1.2.0-alpha04'
implementation 'com.google.firebase:firebase-core:17.2.1' implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.google.firebase:firebase-perf:19.0.4' implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.9.0' implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.9.0") { implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") {
transitive = false transitive = false
} }
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.9.0' kapt 'com.github.bumptech.glide:compiler:4.10.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":31,"versionName":"4.2","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":32,"versionName":"4.3-hotfix1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -220,6 +220,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
else -> null else -> null
} }
text = tag.tag.wordCapitalize() text = tag.tag.wordCapitalize()
setEnsureMinTouchTargetSize(false)
setOnClickListener { setOnClickListener {
for (callback in onChipClickedHandler) for (callback in onChipClickedHandler)
callback.invoke(tag) callback.invoke(tag)

View File

@@ -27,6 +27,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_lock.* import kotlinx.android.synthetic.main.activity_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
import xyz.quaver.pupil.util.Lock import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.LockManager

View File

@@ -29,6 +29,7 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -46,7 +47,6 @@ 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.snackbar.Snackbar
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.*
@@ -365,21 +365,14 @@ class MainActivity : AppCompatActivity() {
setTitle(R.string.main_open_gallery_by_id) setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
CoroutineScope(Dispatchers.Default).launch { val galleryID = editText.text.toString().toInt()
try { val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
val intent = Intent(this@MainActivity, ReaderActivity::class.java) putExtra("galleryID", galleryID)
val gallery =
getGalleryBlock(editText.text.toString().toInt()) ?: throw Exception()
intent.putExtra("galleryID", gallery.id)
startActivity(intent)
histories.add(gallery.id)
} catch (e: Exception) {
Snackbar.make(main_layout,
R.string.main_open_gallery_by_id_error, Snackbar.LENGTH_LONG).show()
} }
}
startActivity(intent)
histories.add(galleryID)
} }
}.show() }.show()
} }
@@ -729,6 +722,7 @@ class MainActivity : AppCompatActivity() {
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault())) s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
} }
}) })
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
with(main_searchview as FloatingSearchView) { with(main_searchview as FloatingSearchView) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json") val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")

View File

@@ -21,34 +21,19 @@ package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_default_query.view.*
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList import kotlinx.serialization.parseList
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.ui.fragment.LockFragment
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.util.*
import java.io.File
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
@@ -82,380 +67,6 @@ class SettingsActivity : AppCompatActivity() {
super.onResume() super.onResume()
} }
class SettingsFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when(it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
}
private fun getDirSize(dir: File) : String {
val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size))
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
with(findPreference<Preference>("app_version")) {
this!!
val manager = context.packageManager
val info = manager.getPackageInfo(context.packageName, 0)
summary = context.getString(R.string.settings_app_version_description, info.versionName)
setOnPreferenceClickListener {
checkUpdate(activity as SettingsActivity, true)
true
}
}
with(findPreference<Preference>("delete_cache")) {
this!!
val dir = File(context.cacheDir, "imageCache")
summary = getDirSize(dir)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
true
}
}
with(findPreference<Preference>("delete_downloads")) {
this!!
val dir = getDownloadDirectory(context)
summary = getDirSize(dir)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_downloads_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
val downloads = (activity!!.application as Pupil).downloads
downloads.clear()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
true
}
}
with(findPreference<Preference>("clear_history")) {
this!!
val histories = (activity!!.application as Pupil).histories
summary = getString(R.string.settings_clear_history_summary, histories.size)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
histories.clear()
summary = getString(R.string.settings_clear_history_summary, histories.size)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
true
}
}
with(findPreference<Preference>("dl_location")) {
this!!
summary = getDownloadDirectory(context).absolutePath
onPreferenceClickListener = Preference.OnPreferenceClickListener {
DownloadLocationDialog(context).apply {
onDownloadLocationChangedListener = { value ->
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(key, value)
.apply()
summary = getDownloadDirectory(context).absolutePath
}
}.show()
true
}
}
with(findPreference<Preference>("default_query")) {
this!!
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
summary = preferences.getString("default_query", "") ?: ""
val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
val excludeBL = "-male:yaoi"
val excludeGuro = listOf("-female:guro", "-male:guro")
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val dialogView = LayoutInflater.from(context).inflate(
R.layout.dialog_default_query,
LinearLayout(context),
false
)
val tags = Tags.parse(
preferences.getString("default_query", "") ?: ""
)
summary = tags.toString()
with(dialogView.default_query_dialog_language_selector) {
adapter =
ArrayAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
arrayListOf(
getString(R.string.default_query_dialog_language_selector_none)
).apply {
addAll(languages.values)
}
)
if (tags.any { it.area == "language" && !it.isNegative }) {
val tag = languages[tags.first { it.area == "language" }.tag]
if (tag != null) {
setSelection(
@Suppress("UNCHECKED_CAST")
(adapter as ArrayAdapter<String>).getPosition(tag)
)
tags.removeByArea("language", false)
}
}
}
with(dialogView.default_query_dialog_BL_checkbox) {
isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL))
tags.remove(excludeBL)
}
with(dialogView.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach {
tags.remove(it)
}
}
with(dialogView.default_query_dialog_edittext) {
setText(tags.toString(), TextView.BufferType.EDITABLE)
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()))
}
})
}
val dialog = AlertDialog.Builder(context!!).apply {
setView(dialogView)
}.create()
dialogView.default_query_dialog_ok.setOnClickListener {
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
with(dialogView.default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
preferenceManager.sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = preferences.getString("default_query", "") ?: ""
tags.clear()
tags.addAll(newTags)
dialog.dismiss()
}
dialog.show()
true
}
}
with(findPreference<Preference>("app_lock")) {
this!!
val lockManager = LockManager(context)
summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when(it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(context, LockActivity::class.java)
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
true
}
}
with(findPreference<Preference>("dark_mode")) {
this!!
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
true
}
}
with(findPreference<Preference>("backup")) {
this!!
onPreferenceClickListener = Preference.OnPreferenceClickListener {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"),
true
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
.show()
true
}
}
with(findPreference<Preference>("restore")) {
this!!
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_RESTORE)
true
}
}
}
}
class LockFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("lock_pattern")?.summary =
if (lockManager.contains(Lock.Type.PATTERN))
getString(R.string.settings_lock_enabled)
else
""
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
with(findPreference<Preference>("lock_pattern")) {
this!!
if (LockManager(context!!).contains(Lock.Type.PATTERN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(context!!)
if (lockManager.contains(Lock.Type.PATTERN)) {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(context, LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pattern")
}
startActivity(intent)
}
true
}
}
}
}
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()

View File

@@ -0,0 +1,141 @@
/*
* 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.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.dialog_default_query.view.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro")
private lateinit var dialogView : View
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
initView()
setContentView(dialogView)
}
private fun initView() {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val tags = Tags.parse(
preferences.getString("default_query", "") ?: ""
)
with(dialogView.default_query_dialog_language_selector) {
adapter =
ArrayAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
arrayListOf(
context.getString(R.string.default_query_dialog_language_selector_none)
).apply {
addAll(languages.values)
}
)
if (tags.any { it.area == "language" && !it.isNegative }) {
val tag = languages[tags.first { it.area == "language" }.tag]
if (tag != null) {
setSelection(
@Suppress("UNCHECKED_CAST")
(adapter as ArrayAdapter<String>).getPosition(tag)
)
tags.removeByArea("language", false)
}
}
}
with(dialogView.default_query_dialog_BL_checkbox) {
isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL))
tags.remove(excludeBL)
}
with(dialogView.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach {
tags.remove(it)
}
}
with(dialogView.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
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(java.util.Locale.getDefault()))
}
})
}
dialogView.default_query_dialog_ok.setOnClickListener {
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
with(dialogView.default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.fragment
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager
class LockFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("lock_pattern")?.summary =
if (lockManager.contains(Lock.Type.PATTERN))
getString(R.string.settings_lock_enabled)
else
""
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
with(findPreference<Preference>("lock_pattern")) {
this!!
if (LockManager(context!!).contains(Lock.Type.PATTERN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(context!!)
if (lockManager.contains(Lock.Type.PATTERN)) {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(context, LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pattern")
}
startActivity(intent)
}
true
}
}
}
}

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 xyz.quaver.pupil.ui package xyz.quaver.pupil.ui.fragment
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View File

@@ -0,0 +1,277 @@
/*
* 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.fragment
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.util.*
import java.io.File
class SettingsFragment :
PreferenceFragmentCompat(),
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when(it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
}
private fun getDirSize(dir: File) : String {
val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size))
}
override fun onPreferenceClick(preference: Preference?): Boolean {
with (preference) {
this ?: return false
when (key) {
"app_version" -> {
checkUpdate(activity as SettingsActivity, true)
}
"delete_cache" -> {
val dir = File(context.cacheDir, "imageCache")
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_downloads_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
val downloads = (activity!!.application as Pupil).downloads
downloads.clear()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"clear_history" -> {
val histories = (context.applicationContext as Pupil).histories
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
histories.clear()
summary = getString(R.string.settings_clear_history_summary, histories.size)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"dl_location" -> {
DownloadLocationDialog(context).apply {
onDownloadLocationChangedListener = { value ->
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(key, value)
.apply()
summary = getDownloadDirectory(context).absolutePath
}
}.show()
}
"default_query" -> {
DefaultQueryDialog(context).apply {
onPositiveButtonClickListener = { newTags ->
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = newTags.toString()
dismiss() //This sucks
// TODO: make dialog dissmiss itself :P
}
}.show()
}
"app_lock" -> {
val intent = Intent(context, LockActivity::class.java)
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"),
true
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
.show()
}
"restore" -> {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_RESTORE)
}
else -> return false
}
}
return true
}
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
with (preference) {
this ?: return false
when (key) {
"dark_mode" -> {
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
}
else -> return false
}
}
return true
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
initPreferences()
}
private fun initPreferences() {
for (i in 0 until preferenceScreen.preferenceCount) {
preferenceScreen.getPreference(i).run {
if (this is PreferenceCategory)
(0 until preferenceCount).map { getPreference(it) }
else
listOf(this)
}.forEach { preference ->
with (preference) {
when (key) {
"app_version" -> {
val manager = context.packageManager
val info = manager.getPackageInfo(context.packageName, 0)
summary = context.getString(R.string.settings_app_version_description, info.versionName)
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
val dir = File(context.cacheDir, "imageCache")
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"clear_history" -> {
val histories = (activity!!.application as Pupil).histories
summary = getString(R.string.settings_clear_history_summary, histories.size)
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
summary = getDownloadDirectory(context).absolutePath
onPreferenceClickListener = this@SettingsFragment
}
"default_query" -> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
summary = preferences.getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment
}
"app_lock" -> {
val lockManager = LockManager(context)
summary =
if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when (it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment
}
"backup" -> {
onPreferenceClickListener = this@SettingsFragment
}
"restore" -> {
onPreferenceClickListener = this@SettingsFragment
}
}
}
}
}
}
}

View File

@@ -166,6 +166,8 @@ class GalleryDownloader(
fun start() { fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch { downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await() ?: return@launch val reader = reader!!.await() ?: return@launch
val lowQuality = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader)
.getBoolean("low_quality", false)
notificationBuilder.setContentTitle(reader.title) notificationBuilder.setContentTitle(reader.title)
@@ -177,15 +179,15 @@ class GalleryDownloader(
.setProgress(reader.galleryInfo.size, 0, false) .setProgress(reader.galleryInfo.size, 0, false)
.setContentText("0/${reader.galleryInfo.size}") .setContentText("0/${reader.galleryInfo.size}")
reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunked -> reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunk ->
chunked.mapIndexed { i, galleryInfo -> chunk.mapIndexed { i, galleryInfo ->
val index = chunkIndex*4+i val index = chunkIndex*4+i
async(Dispatchers.IO) { async(Dispatchers.IO) {
val url = when(useHiyobi) { val url = when(useHiyobi) {
true -> createImgList(galleryID, reader)[index].path true -> createImgList(galleryID, reader, lowQuality)[index].path
false -> when { false -> when {
(!galleryInfo.hash.isNullOrBlank()) and (galleryInfo.haswebp == 1) -> (!galleryInfo.hash.isNullOrBlank()) && (galleryInfo.haswebp == 1) && lowQuality ->
urlFromUrlFromHash(galleryID, galleryInfo, "webp") urlFromUrlFromHash(galleryID, galleryInfo, "webp")
else -> else ->
urlFromUrlFromHash(galleryID, galleryInfo) urlFromUrlFromHash(galleryID, galleryInfo)

View File

@@ -18,28 +18,21 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.round
@UseExperimental(ExperimentalStdlibApi::class) @UseExperimental(ExperimentalStdlibApi::class)
fun String.wordCapitalize() : String { fun String.wordCapitalize() : String {
val result = ArrayList<String>() val result = ArrayList<String>()
@SuppressLint("DefaultLocale")
for (word in this.split(" ")) for (word in this.split(" "))
result.add(word.capitalize(Locale.US)) result.add(word.capitalize(Locale.US))
return result.joinToString(" ") return result.joinToString(" ")
} }
//https://discuss.kotlinlang.org/t/how-do-you-round-a-number-to-n-decimal-places/8843(fvasco)
fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return round(this * multiplier) / multiplier
}
fun byteToString(byte: Long, precision : Int = 1) : String { fun byteToString(byte: Long, precision : Int = 1) : String {
val suffix = listOf( val suffix = listOf(
@@ -49,14 +42,13 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
"GB", "GB",
"TB" //really? "TB" //really?
) )
var size = byte.toDouble() var size = byte.toDouble(); var suffixIndex = 0
var suffixIndex = 0
while (size >= 1024) { while (size >= 1024) {
size /= 1024 size /= 1024
suffixIndex++ suffixIndex++
} }
return "${size.round(precision)} ${suffix[suffixIndex]}" return "%.${precision}f ${suffix[suffixIndex]}".format(size)
} }

View File

@@ -21,6 +21,7 @@ package xyz.quaver.pupil.util
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -93,9 +94,9 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
fun extractReleaseNote(update: JsonObject, locale: Locale) : String { fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
val markdown = update["body"]!!.content val markdown = update["body"]!!.content
val target = when(locale) { val target = when(locale.language) {
Locale.KOREAN -> "한국어" "ko" -> "한국어"
Locale.JAPANESE -> "日本語" "ja" -> "日本語"
else -> "English" else -> "English"
} }
@@ -151,12 +152,25 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch io@{
val target = File(getDownloadDirectory(context), "Pupil.apk") val target = File(getDownloadDirectory(context), "Pupil.apk")
URL(url).download(target) { progress, fileSize -> try {
builder.setProgress(fileSize.toInt(), progress.toInt(), false) URL(url).download(target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
} catch (e: Exception) {
builder.apply {
setContentText(context.getString(R.string.update_failed))
setMessage(context.getString(R.string.update_failed_message))
setSmallIcon(android.R.drawable.stat_sys_download_done)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build()) notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
return@io
} }
val install = Intent(Intent.ACTION_VIEW).apply { val install = Intent(Intent.ACTION_VIEW).apply {
@@ -165,7 +179,11 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
context, context,
context.applicationContext.packageName + ".fileprovider", context.applicationContext.packageName + ".fileprovider",
target target
), MimeTypeMap.getSingleton().getExtensionFromMimeType(".apk")) ), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
if (resolveActivity(context.packageManager) == null)
setDataAndType(Uri.fromFile(target),
MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
} }
builder.apply { builder.apply {

View File

@@ -22,7 +22,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.PatternLockFragment"> tools:context=".ui.fragment.PatternLockFragment">
<com.andrognito.patternlockview.PatternLockView <com.andrognito.patternlockview.PatternLockView
android:id="@+id/lock_pattern_view" android:id="@+id/lock_pattern_view"

View File

@@ -17,7 +17,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/>.
--> -->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android" <com.github.chrisbanes.photoview.PhotoView xmlns:android="http://schemas.android.com/apk/res/android"
android:contentDescription="@string/reader_imageview_description" android:contentDescription="@string/reader_imageview_description"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -19,7 +19,6 @@
<ImageView <ImageView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height" android:layout_height="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar" android:background="@drawable/side_nav_bar"

View File

@@ -84,7 +84,7 @@
<string name="main_menu_sort_newest">投稿日時順</string> <string name="main_menu_sort_newest">投稿日時順</string>
<string name="main_menu_sort_popular">人気順</string> <string name="main_menu_sort_popular">人気順</string>
<string name="update_failed">アップデートに失敗しました</string> <string name="update_failed">アップデートに失敗しました</string>
<string name="update_failed_message">マニュアルインストールが必要です。APKファイルは</string> <string name="update_failed_message">アップデート中エラーが発生しました</string>
<string name="ignore_update">無視</string> <string name="ignore_update">無視</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string> <string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
<string name="update_no_permission">権限がないため自動アップデートを行えません。ホームページで直接ダウンロードしてください。</string> <string name="update_no_permission">権限がないため自動アップデートを行えません。ホームページで直接ダウンロードしてください。</string>
@@ -118,4 +118,6 @@
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string> <string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string> <string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="settings_app_version_description">v%s</string> <string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
</resources> </resources>

View File

@@ -83,8 +83,8 @@
<string name="main_menu_sort">정렬</string> <string name="main_menu_sort">정렬</string>
<string name="main_menu_sort_popular">인기순</string> <string name="main_menu_sort_popular">인기순</string>
<string name="main_menu_sort_newest">시간순</string> <string name="main_menu_sort_newest">시간순</string>
<string name="update_failed">"업데이트 "</string> <string name="update_failed">"업데이트 에러</string>
<string name="update_failed_message">수동 업데이트가 필요합니다. APK 파일은 다운로드 폴더에 있습니다.</string> <string name="update_failed_message">업데이트 중 에러가 발생했습니다</string>
<string name="ignore_update">무시</string> <string name="ignore_update">무시</string>
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string> <string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
<string name="update_no_permission">권한이 부여되어 있지 않아 자동 업데이트를 진행할 수 없습니다. 홈페이지에서 직접 다운로드 받으시기 바랍니다.</string> <string name="update_no_permission">권한이 부여되어 있지 않아 자동 업데이트를 진행할 수 없습니다. 홈페이지에서 직접 다운로드 받으시기 바랍니다.</string>
@@ -118,4 +118,6 @@
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string> <string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
<string name="settings_beta">베타 채널에서 업데이트</string> <string name="settings_beta">베타 채널에서 업데이트</string>
<string name="settings_app_version_description">v%s</string> <string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">저해상도 이미지</string>
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
</resources> </resources>

View File

@@ -28,7 +28,7 @@
<string name="https_block_alert">(Korean only)</string> <string name="https_block_alert">(Korean only)</string>
<string name="update_failed">Update failed</string> <string name="update_failed">Update failed</string>
<string name="update_failed_message">Please install manually. APK file is in the Downloads folder.</string> <string name="update_failed_message">Please install manually by visiting github release page :{ (or try again!)</string>
<string name="update_no_permission">Cannot auto update because permission is denied. Please download manually from the webpage.</string> <string name="update_no_permission">Cannot auto update because permission is denied. Please download manually from the webpage.</string>
<string name="ignore_update">Ignore</string> <string name="ignore_update">Ignore</string>
@@ -140,6 +140,8 @@
<string name="settings_dl_location_removable">Removable Storage</string> <string name="settings_dl_location_removable">Removable Storage</string>
<string name="settings_dl_location_internal">Internal Storage</string> <string name="settings_dl_location_internal">Internal Storage</string>
<string name="settings_dl_location_available">%s available</string> <string name="settings_dl_location_available">%s available</string>
<string name="settings_low_quality">Low quality images</string>
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
<!-- SETTINGS/APP LOCK --> <!-- SETTINGS/APP LOCK -->

View File

@@ -3,12 +3,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference <Preference
app:title="@string/settings_app_version_title" app:key="app_version"
app:key="app_version"/> app:title="@string/settings_app_version_title"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:title="@string/settings_beta" app:key="beta"
app:key="beta"/> app:title="@string/settings_beta"/>
<PreferenceCategory <PreferenceCategory
app:title="@string/settings_search_title"> app:title="@string/settings_search_title">
@@ -32,20 +32,25 @@
app:title="@string/settings_storage"> app:title="@string/settings_storage">
<Preference <Preference
app:title="@string/settings_clear_cache" app:key="delete_cache"
app:key="delete_cache"/> app:title="@string/settings_clear_cache"/>
<Preference <Preference
app:title="@string/settings_clear_downloads" app:key="delete_downloads"
app:key="delete_downloads"/> app:title="@string/settings_clear_downloads"/>
<Preference <Preference
app:title="@string/settings_clear_history" app:key="clear_history"
app:key="clear_history"/> app:title="@string/settings_clear_history"/>
<Preference <Preference
app:title="@string/settings_dl_location" app:key="dl_location"
app:key="dl_location"/> app:title="@string/settings_dl_location"/>
<SwitchPreferenceCompat
app:key="dl_low_quality"
app:title="@string/settings_low_quality"
app:summary="@string/settings_low_quality_summary"/>
</PreferenceCategory> </PreferenceCategory>
@@ -53,8 +58,8 @@
app:title="@string/settings_app_lock"> app:title="@string/settings_app_lock">
<Preference <Preference
app:title="@string/settings_app_lock_type" app:key="app_lock"
app:key="app_lock"/> app:title="@string/settings_app_lock_type"/>
</PreferenceCategory> </PreferenceCategory>

View File

@@ -17,20 +17,19 @@
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list import kotlinx.serialization.list
import java.net.URL import java.net.URL
const val protocol = "https:" const val protocol = "https:"
fun getGalleryInfo(galleryID: Int): List<GalleryInfo> { @Suppress("EXPERIMENTAL_API_USAGE")
return Json(JsonConfiguration.Stable).parse( fun getGalleryInfo(galleryID: Int) =
Json.nonstrict.parse(
GalleryInfo.serializer().list, GalleryInfo.serializer().list,
Regex("""\[.+]""").find( Regex("""\[.+]""").find(
URL("$protocol//$domain/galleries/$galleryID.js").readText() URL("$protocol//$domain/galleries/$galleryID.js").readText()
)?.value ?: "[]" )?.value ?: "[]"
) )
}
//common.js //common.js
var adapose = false var adapose = false
@@ -54,17 +53,9 @@ fun subdomainFromURL(url: String, base: String? = null) : String {
if (!base.isNullOrBlank()) if (!base.isNullOrBlank())
retval = base retval = base
val r = Regex("""/galleries/\d*(\d)/""") val b = 16
var m = r.find(url) val r = Regex("""/[0-9a-f]/([0-9a-f]{2})/""")
var b = 10 val m = r.find(url) ?: return retval
if (m == null) {
b = 16
val r2 = Regex("""/[0-9a-f]/([0-9a-f]{2})/""")
m = r2.find(url)
if (m == null)
return retval
}
val g = m.groupValues[1].toIntOrNull(b) ?: return retval val g = m.groupValues[1].toIntOrNull(b) ?: return retval
@@ -84,15 +75,12 @@ fun fullPathFromHash(hash: String?) : String? {
} }
} }
fun urlFromHash(galleryID: Int, image: GalleryInfo, webp: String? = null) : String { @Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
val ext = webp ?: image.name.split('.').last() fun urlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null) : String {
return when { val ext = ext ?: dir ?: image.name.split('.').last()
image.hash.isNullOrBlank() -> val dir = dir ?: "images"
"$protocol//a.hitomi.la/galleries/$galleryID/${image.name}" return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
else ->
"$protocol//a.hitomi.la/${webp?:"images"}/${fullPathFromHash(image.hash)}.$ext"
}
} }
fun urlFromUrlFromHash(galleryID: Int, image: GalleryInfo, webp: String? = null) = fun urlFromUrlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null, base: String? = null) =
urlFromURL(urlFromHash(galleryID, image, webp)) urlFromURL(urlFromHash(galleryID, image, dir, ext), base)

View File

@@ -68,8 +68,8 @@ fun getGallery(galleryID: Int) : Gallery {
href.slice(5 until href.indexOf('-')) href.slice(5 until href.indexOf('-'))
} }
val thumbnails = getGalleryInfo(galleryID).map { val thumbnails = getGalleryInfo(galleryID).map { galleryInfo ->
"$protocol//tn.hitomi.la/smalltn/$galleryID/${it.name}.jpg" urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
} }
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails) return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)

View File

@@ -20,7 +20,6 @@ import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html" fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
@Serializable @Serializable
data class GalleryInfo( data class GalleryInfo(

View File

@@ -17,7 +17,6 @@
package xyz.quaver.hiyobi package xyz.quaver.hiyobi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list import kotlinx.serialization.list
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.hitomi.GalleryInfo import xyz.quaver.hitomi.GalleryInfo
@@ -64,7 +63,8 @@ fun getReader(galleryID: Int) : Reader {
val title = Jsoup.connect(reader).get().title() val title = Jsoup.connect(reader).get().title()
val galleryInfo = Json(JsonConfiguration.Stable).parse( @Suppress("EXPERIMENTAL_API_USAGE")
val galleryInfo = Json.parse(
GalleryInfo.serializer().list, GalleryInfo.serializer().list,
with(URL(url).openConnection() as HttpsURLConnection) { with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
@@ -79,5 +79,11 @@ fun getReader(galleryID: Int) : Reader {
return Reader(Reader.Code.HIYOBI, title, galleryInfo) return Reader(Reader.Code.HIYOBI, title, galleryInfo)
} }
fun createImgList(galleryID: Int, reader: Reader) = fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) } if (lowQuality)
reader.galleryInfo.map {
val name = it.name.replace(Regex("/.[^/.]+$"), "") + ".jpg"
Images("$protocol//$hiyobi/data/$galleryID/$name.jpg", galleryID, it.name)
}
else
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }

View File

@@ -68,7 +68,7 @@ class UnitTest {
@Test @Test
fun test_getGallery() { fun test_getGallery() {
val gallery = getGallery(1510566) val gallery = getGallery(1552751)
print(gallery) print(gallery)
} }
@@ -82,7 +82,9 @@ class UnitTest {
@Test @Test
fun test_hiyobi() { fun test_hiyobi() {
xyz.quaver.hiyobi.getReader(1510567) val reader = xyz.quaver.hiyobi.getReader(10000062)
print(reader)
} }
@Test @Test