Migrate to MVVM
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.github.AppVersion
|
||||
import org.koitharu.kotatsu.core.github.GithubRepository
|
||||
import org.koitharu.kotatsu.core.github.VersionId
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
|
||||
private val settings by activity.inject<AppSettings>()
|
||||
private val repo by activity.inject<GithubRepository>()
|
||||
|
||||
fun launchIfNeeded(): Job? {
|
||||
return if (settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) {
|
||||
launch()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(): Job? {
|
||||
return if (isUpdateSupported(activity)) {
|
||||
launchInternal()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkNow() = runCatching {
|
||||
val version = repo.getLatestVersion()
|
||||
val newVersionId = VersionId.parse(version.name)
|
||||
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
|
||||
val result = newVersionId > currentVersionId
|
||||
if (result) {
|
||||
showUpdateDialog(version)
|
||||
}
|
||||
settings.appUpdate = System.currentTimeMillis()
|
||||
result
|
||||
}.getOrNull()
|
||||
|
||||
private fun launchInternal() = activity.lifecycleScope.launch(Dispatchers.Main) {
|
||||
checkNow()
|
||||
}
|
||||
|
||||
private fun showUpdateDialog(version: AppVersion) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.app_update_available)
|
||||
.setMessage(buildString {
|
||||
append(activity.getString(R.string.new_version_s, version.name))
|
||||
appendLine()
|
||||
append(
|
||||
activity.getString(
|
||||
R.string.size_s,
|
||||
FileSizeUtils.formatBytes(activity, version.apkSize)
|
||||
)
|
||||
)
|
||||
appendLine()
|
||||
appendLine()
|
||||
append(version.description)
|
||||
})
|
||||
.setPositiveButton(R.string.download) { _, _ ->
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
|
||||
}
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
|
||||
private val PERIOD = TimeUnit.HOURS.toMillis(6)
|
||||
|
||||
fun isUpdateSupported(context: Context): Boolean {
|
||||
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getCertificateSHA1Fingerprint(context: Context): String? {
|
||||
val packageInfo = try {
|
||||
context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.GET_SIGNATURES
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
val signatures = packageInfo?.signatures
|
||||
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
|
||||
val input: InputStream = ByteArrayInputStream(cert)
|
||||
val c = try {
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
cf.generateCertificate(input) as X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val md: MessageDigest = MessageDigest.getInstance("SHA1")
|
||||
val publicKey: ByteArray = md.digest(c.encoded)
|
||||
publicKey.byte2HexFormatted()
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
} catch (e: CertificateEncodingException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.Cache
|
||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.CacheUtils
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
|
||||
|
||||
private val trackerRepo by inject<TrackingRepository>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_history)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.let { pref ->
|
||||
viewLifecycleScope.launchWhenResumed {
|
||||
val size = withContext(Dispatchers.IO) {
|
||||
CacheUtils.computeCacheSize(pref.context, Cache.PAGES.dir)
|
||||
}
|
||||
pref.summary = FileSizeUtils.formatBytes(pref.context, size)
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.let { pref ->
|
||||
viewLifecycleScope.launchWhenResumed {
|
||||
val size = withContext(Dispatchers.IO) {
|
||||
CacheUtils.computeCacheSize(pref.context, Cache.THUMBS.dir)
|
||||
}
|
||||
pref.summary = FileSizeUtils.formatBytes(pref.context, size)
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||
viewLifecycleScope.launchWhenResumed {
|
||||
val items = MangaSuggestionsProvider.getItemsCount(pref.context)
|
||||
pref.summary =
|
||||
pref.context.resources.getQuantityString(R.plurals.items, items, items)
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref ->
|
||||
viewLifecycleScope.launchWhenResumed {
|
||||
val items = trackerRepo.count()
|
||||
pref.summary =
|
||||
pref.context.resources.getQuantityString(R.plurals.items, items, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
|
||||
clearCache(preference, Cache.PAGES)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||
clearCache(preference, Cache.THUMBS)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
|
||||
viewLifecycleScope.launch {
|
||||
MangaSuggestionsProvider.clearHistory(preference.context)
|
||||
preference.summary = preference.context.resources
|
||||
.getQuantityString(R.plurals.items, 0, 0)
|
||||
Snackbar.make(
|
||||
view ?: return@launch,
|
||||
R.string.search_history_cleared,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||
viewLifecycleScope.launch {
|
||||
trackerRepo.clearLogs()
|
||||
preference.summary = preference.context.resources
|
||||
.getQuantityString(R.plurals.items, 0, 0)
|
||||
Snackbar.make(
|
||||
view ?: return@launch,
|
||||
R.string.updates_feed_cleared,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCache(preference: Preference, cache: Cache) {
|
||||
val ctx = preference.context.applicationContext
|
||||
viewLifecycleScope.launch {
|
||||
try {
|
||||
preference.isEnabled = false
|
||||
val size = withContext(Dispatchers.IO) {
|
||||
CacheUtils.clearCache(ctx, cache.dir)
|
||||
CacheUtils.computeCacheSize(ctx, cache.dir)
|
||||
}
|
||||
preference.summary = FileSizeUtils.formatBytes(ctx, size)
|
||||
} catch (e: Exception) {
|
||||
preference.summary = e.getDisplayMessage(ctx.resources)
|
||||
} finally {
|
||||
preference.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.arrayMapOf
|
||||
import androidx.preference.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
|
||||
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.list.ui.ListModeSelectDialog
|
||||
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.md5
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
|
||||
|
||||
class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
StorageSelectDialog.OnStorageSelectListener {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_main)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_LIST_MODE)?.summary =
|
||||
LIST_MODES[settings.listMode]?.let(::getString)
|
||||
findPreference<SeekBarPreference>(AppSettings.KEY_GRID_SIZE)?.run {
|
||||
summary = "%d%%".format(value)
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
preference.summary = "%d%%".format(newValue)
|
||||
true
|
||||
}
|
||||
}
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.summaryProvider =
|
||||
MultiSummaryProvider(R.string.gestures_only)
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_TRACK_SOURCES)?.summaryProvider =
|
||||
MultiSummaryProvider(R.string.dont_check)
|
||||
findPreference<Preference>(AppSettings.KEY_APP_UPDATE_AUTO)?.run {
|
||||
isVisible = AppUpdateChecker.isUpdateSupported(context)
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.run {
|
||||
summary = settings.getStorageDir(context)?.getStorageName(context)
|
||||
?: getString(R.string.not_available)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.let {
|
||||
it.entryValues = ZoomMode.values().names()
|
||||
it.setDefaultValue(ZoomMode.FIT_CENTER.name)
|
||||
}
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
|
||||
!settings.appPassword.isNullOrEmpty()
|
||||
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
|
||||
title = getString(R.string.app_version, BuildConfig.VERSION_NAME)
|
||||
isEnabled = AppUpdateChecker.isUpdateSupported(context)
|
||||
}
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_LIST_MODE -> findPreference<Preference>(key)?.summary =
|
||||
LIST_MODES[settings.listMode]?.let(::getString)
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
AppSettings.KEY_THEME_AMOLED -> {
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.run {
|
||||
summary = settings.getStorageDir(context)?.getStorageName(context)
|
||||
?: getString(R.string.not_available)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
findPreference<PreferenceScreen>(AppSettings.KEY_REMOTE_SOURCES)?.run {
|
||||
val total = MangaSource.values().size - 1
|
||||
summary = getString(
|
||||
R.string.enabled_d_from_d, total - settings.hiddenSources.size, total
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
return when (preference?.key) {
|
||||
AppSettings.KEY_LIST_MODE -> {
|
||||
ListModeSelectDialog.show(childFragmentManager)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_NOTIFICATIONS_SETTINGS -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
.putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
(activity as? SettingsActivity)?.openNotificationSettingsLegacy()
|
||||
}
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
val ctx = context ?: return false
|
||||
StorageSelectDialog.Builder(ctx, settings.getStorageDir(ctx), this)
|
||||
.setTitle(preference.title)
|
||||
.setNegativeButton(android.R.string.cancel)
|
||||
.create()
|
||||
.show()
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
if ((preference as? SwitchPreference ?: return false).isChecked) {
|
||||
enableAppProtection(preference)
|
||||
} else {
|
||||
settings.appPassword = null
|
||||
}
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_APP_VERSION -> {
|
||||
checkForUpdates()
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStorageSelected(file: File) {
|
||||
settings.setStorageDir(context ?: return, file)
|
||||
}
|
||||
|
||||
private fun enableAppProtection(preference: SwitchPreference) {
|
||||
val ctx = preference.context ?: return
|
||||
val cancelListener =
|
||||
object : DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
|
||||
|
||||
override fun onCancel(dialog: DialogInterface?) {
|
||||
settings.appPassword = null
|
||||
preference.isChecked = false
|
||||
preference.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) = onCancel(dialog)
|
||||
}
|
||||
preference.isEnabled = false
|
||||
TextInputDialog.Builder(ctx)
|
||||
.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
||||
.setHint(R.string.enter_password)
|
||||
.setNegativeButton(android.R.string.cancel, cancelListener)
|
||||
.setOnCancelListener(cancelListener)
|
||||
.setPositiveButton(android.R.string.ok) { d, password ->
|
||||
if (password.isBlank()) {
|
||||
cancelListener.onCancel(d)
|
||||
return@setPositiveButton
|
||||
}
|
||||
TextInputDialog.Builder(ctx)
|
||||
.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
||||
.setHint(R.string.repeat_password)
|
||||
.setNegativeButton(android.R.string.cancel, cancelListener)
|
||||
.setOnCancelListener(cancelListener)
|
||||
.setPositiveButton(android.R.string.ok) { d2, password2 ->
|
||||
if (password == password2) {
|
||||
settings.appPassword = password.md5()
|
||||
preference.isChecked = true
|
||||
preference.isEnabled = true
|
||||
} else {
|
||||
cancelListener.onCancel(d2)
|
||||
Snackbar.make(
|
||||
listView,
|
||||
R.string.passwords_mismatch,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}.setTitle(preference.title)
|
||||
.create()
|
||||
.show()
|
||||
}.setTitle(preference.title)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun checkForUpdates() {
|
||||
viewLifecycleScope.launch {
|
||||
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
|
||||
setSummary(R.string.checking_for_updates)
|
||||
isSelectable = false
|
||||
}
|
||||
val result = AppUpdateChecker(activity ?: return@launch).checkNow()
|
||||
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
|
||||
setSummary(
|
||||
when (result) {
|
||||
true -> R.string.check_for_updates
|
||||
false -> R.string.no_update_available
|
||||
null -> R.string.update_check_failed
|
||||
}
|
||||
)
|
||||
isSelectable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val LIST_MODES = arrayMapOf(
|
||||
ListMode.DETAILED_LIST to R.string.detailed_list,
|
||||
ListMode.GRID to R.string.grid,
|
||||
ListMode.LIST to R.string.list
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
|
||||
class NetworkSettingsFragment : BasePreferenceFragment(R.string.settings) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
//TODO https://developer.android.com/training/basics/network-ops/managing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.utils.RingtonePickContract
|
||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||
|
||||
class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notifications) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_notifications)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_NOTIFICATIONS_SOUND)?.run {
|
||||
val uri = settings.notificationSound.toUriOrNull()
|
||||
summary = RingtoneManager.getRingtone(context, uri).getTitle(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
return when (preference?.key) {
|
||||
AppSettings.KEY_NOTIFICATIONS_SOUND -> {
|
||||
registerForActivityResult(RingtonePickContract(preference.title.toString())) { uri ->
|
||||
settings.notificationSound = uri?.toString().orEmpty()
|
||||
findPreference<Preference>(AppSettings.KEY_NOTIFICATIONS_SOUND)?.run {
|
||||
summary = RingtoneManager.getRingtone(context, uri).getTitle(context)
|
||||
}
|
||||
}.launch(settings.notificationSound.toUriOrNull())
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
|
||||
class ReaderSettingsFragment : BasePreferenceFragment(R.string.reader_settings) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_reader)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.let {
|
||||
it.summaryProvider = MultiSummaryProvider(R.string.gestures_only)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.let {
|
||||
it.entryValues = ZoomMode.values().names()
|
||||
it.setDefaultValue(ZoomMode.FIT_CENTER.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
|
||||
class SettingsActivity : BaseActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, MainSettingsFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onPreferenceStartFragment(
|
||||
caller: PreferenceFragmentCompat,
|
||||
pref: Preference
|
||||
): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment)
|
||||
fragment.arguments = pref.extras
|
||||
fragment.setTargetFragment(caller, 0)
|
||||
openFragment(fragment)
|
||||
return true
|
||||
}
|
||||
|
||||
fun openMangaSourceSettings(mangaSource: MangaSource) {
|
||||
openFragment(SourceSettingsFragment.newInstance(mangaSource))
|
||||
}
|
||||
|
||||
fun openNotificationSettingsLegacy() {
|
||||
openFragment(NotificationSettingsLegacyFragment())
|
||||
}
|
||||
|
||||
private fun openFragment(fragment: Fragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, fragment)
|
||||
setReorderingAllowed(true)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.net.Uri
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.RestoreRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.backup.BackupViewModel
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
|
||||
|
||||
val settingsModule
|
||||
get() = module {
|
||||
|
||||
single { BackupRepository(get()) }
|
||||
single { RestoreRepository(get()) }
|
||||
single { AppSettings(androidContext()) }
|
||||
|
||||
viewModel { BackupViewModel(get(), androidContext()) }
|
||||
viewModel { (uri: Uri?) -> RestoreViewModel(uri, get(), androidContext()) }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.settings.utils.EditTextSummaryProvider
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class SourceSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val source by lazy(LazyThreadSafetyMode.NONE) {
|
||||
requireArguments().getParcelable<MangaSource>(EXTRA_SOURCE)!!
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.title = source.title
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.sharedPreferencesName = source.name
|
||||
val repo = source.repository as? RemoteMangaRepository ?: return
|
||||
val keys = repo.onCreatePreferences()
|
||||
addPreferencesFromResource(R.xml.pref_source)
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
val pref = preferenceScreen.getPreference(i)
|
||||
pref.isVisible = pref.key in keys
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<EditTextPreference>(SourceSettings.KEY_DOMAIN)?.summaryProvider =
|
||||
EditTextSummaryProvider(R.string._default)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) {
|
||||
putParcelable(EXTRA_SOURCE, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.dialog_progress.*
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
|
||||
class BackupDialogFragment : AlertDialogFragment(R.layout.dialog_progress) {
|
||||
|
||||
private val viewModel by viewModel<BackupViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
textView_title.setText(R.string.create_backup)
|
||||
textView_subtitle.setText(R.string.processing_)
|
||||
|
||||
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
|
||||
viewModel.onBackupDone.observe(viewLifecycleOwner, this::onBackupDone)
|
||||
viewModel.onError.observe(viewLifecycleOwner, this::onError)
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||
builder.setCancelable(false)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
AlertDialog.Builder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(e.getDisplayMessage(resources))
|
||||
.show()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun onProgressChanged(progress: Progress?) {
|
||||
with(progressBar) {
|
||||
isVisible = true
|
||||
isIndeterminate = progress == null
|
||||
if (progress != null) {
|
||||
this.max = progress.total
|
||||
this.progress = progress.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBackupDone(file: File) {
|
||||
ShareHelper.shareBackup(context ?: return, file)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "BackupDialogFragment"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.fragment_list.*
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
|
||||
ActivityResultCallback<Uri> {
|
||||
|
||||
private val backupSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument(),
|
||||
this
|
||||
)
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_backup)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_BACKUP -> {
|
||||
BackupDialogFragment().show(childFragmentManager, BackupDialogFragment.TAG)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_RESTORE -> {
|
||||
try {
|
||||
backupSelectCall.launch(arrayOf("*/*"))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
Snackbar.make(
|
||||
recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
RestoreDialogFragment.newInstance(result ?: return)
|
||||
.show(childFragmentManager, BackupDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.backup.BackupArchive
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
|
||||
class BackupViewModel(
|
||||
private val repository: BackupRepository,
|
||||
private val context: Context
|
||||
) : BaseViewModel() {
|
||||
|
||||
val progress = MutableLiveData<Progress?>(null)
|
||||
val onBackupDone = SingleLiveEvent<File>()
|
||||
|
||||
init {
|
||||
launchLoadingJob {
|
||||
val backup = BackupArchive.createNew(context)
|
||||
backup.put(repository.createIndex())
|
||||
|
||||
progress.value = Progress(0, 3)
|
||||
backup.put(repository.dumpHistory())
|
||||
|
||||
progress.value = Progress(1, 3)
|
||||
backup.put(repository.dumpCategories())
|
||||
|
||||
progress.value = Progress(2, 3)
|
||||
backup.put(repository.dumpFavourites())
|
||||
|
||||
progress.value = Progress(3, 3)
|
||||
backup.flush()
|
||||
progress.value = null
|
||||
backup.cleanup()
|
||||
onBackupDone.call(backup.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.dialog_progress.*
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
|
||||
class RestoreDialogFragment : AlertDialogFragment(R.layout.dialog_progress) {
|
||||
|
||||
private val viewModel by viewModel<RestoreViewModel> {
|
||||
parametersOf(arguments?.getString(ARG_FILE)?.toUriOrNull())
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
textView_title.setText(R.string.restore_backup)
|
||||
textView_subtitle.setText(R.string.preparing_)
|
||||
|
||||
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
|
||||
viewModel.onRestoreDone.observe(viewLifecycleOwner, this::onRestoreDone)
|
||||
viewModel.onError.observe(viewLifecycleOwner, this::onError)
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
AlertDialog.Builder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(e.getDisplayMessage(resources))
|
||||
.show()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun onProgressChanged(progress: Progress?) {
|
||||
with(progressBar) {
|
||||
isVisible = true
|
||||
isIndeterminate = progress == null
|
||||
if (progress != null) {
|
||||
this.max = progress.total
|
||||
this.progress = progress.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRestoreDone(result: CompositeResult) {
|
||||
val builder = AlertDialog.Builder(context ?: return)
|
||||
when {
|
||||
result.isAllSuccess -> builder.setTitle(R.string.data_restored)
|
||||
.setMessage(R.string.data_restored_success)
|
||||
result.isAllFailed -> builder.setTitle(R.string.error)
|
||||
.setMessage(
|
||||
result.failures.map {
|
||||
it.getDisplayMessage(resources)
|
||||
}.distinct().joinToString("\n")
|
||||
)
|
||||
else -> builder.setTitle(R.string.data_restored)
|
||||
.setMessage(R.string.data_restored_with_errors)
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val ARG_FILE = "file"
|
||||
const val TAG = "RestoreDialogFragment"
|
||||
|
||||
fun newInstance(uri: Uri) = RestoreDialogFragment().withArgs(1) {
|
||||
putString(ARG_FILE, uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.backup.BackupArchive
|
||||
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.core.backup.RestoreRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class RestoreViewModel(
|
||||
uri: Uri?,
|
||||
private val repository: RestoreRepository,
|
||||
private val context: Context
|
||||
) : BaseViewModel() {
|
||||
|
||||
val progress = MutableLiveData<Progress?>(null)
|
||||
val onRestoreDone = SingleLiveEvent<CompositeResult>()
|
||||
|
||||
init {
|
||||
launchLoadingJob {
|
||||
if (uri == null) {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val backup = withContext(Dispatchers.IO) {
|
||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||
(contentResolver.openInputStream(uri)
|
||||
?: throw FileNotFoundException()).use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
BackupArchive(tempFile)
|
||||
}
|
||||
try {
|
||||
backup.unpack()
|
||||
val result = CompositeResult()
|
||||
|
||||
progress.value = Progress(0, 3)
|
||||
result += repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
|
||||
progress.value = Progress(1, 3)
|
||||
result += repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
|
||||
progress.value = Progress(2, 3)
|
||||
result += repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
|
||||
progress.value = Progress(3, 3)
|
||||
onRestoreDone.call(result)
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
backup.cleanup()
|
||||
backup.file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.item_source_config.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
|
||||
class SourceViewHolder(parent: ViewGroup) :
|
||||
BaseViewHolder<MangaSource, Boolean>(parent, R.layout.item_source_config) {
|
||||
|
||||
override fun onBind(data: MangaSource, extra: Boolean) {
|
||||
textView_title.text = data.title
|
||||
imageView_hidden.isChecked = extra
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.item_source_config.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
import org.koitharu.kotatsu.utils.ext.safe
|
||||
|
||||
class SourcesAdapter(private val onItemClickListener: OnRecyclerItemClickListener<MangaSource>) :
|
||||
RecyclerView.Adapter<SourceViewHolder>(), KoinComponent {
|
||||
|
||||
private val dataSet = MangaProviderFactory.getSources(includeHidden = true).toMutableList()
|
||||
private val settings by inject<AppSettings>()
|
||||
private val hiddenItems = settings.hiddenSources.mapNotNull {
|
||||
safe {
|
||||
MangaSource.valueOf(it)
|
||||
}
|
||||
}.toMutableSet()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = SourceViewHolder(parent).also(::onViewHolderCreated)
|
||||
|
||||
override fun getItemCount() = dataSet.size
|
||||
|
||||
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
|
||||
val item = dataSet[position]
|
||||
holder.bind(item, !hiddenItems.contains(item))
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun onViewHolderCreated(holder: SourceViewHolder) {
|
||||
holder.imageView_hidden.setOnCheckedChangeListener {
|
||||
if (it) {
|
||||
hiddenItems.remove(holder.requireData())
|
||||
} else {
|
||||
hiddenItems.add(holder.requireData())
|
||||
}
|
||||
settings.hiddenSources = hiddenItems.mapToSet { x -> x.name }
|
||||
}
|
||||
holder.imageView_config.setOnClickListener { v ->
|
||||
onItemClickListener.onItemClick(holder.requireData(), holder.bindingAdapterPosition, v)
|
||||
}
|
||||
holder.imageView_handle.setOnTouchListener { v, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
onItemClickListener.onItemLongClick(
|
||||
holder.requireData(),
|
||||
holder.bindingAdapterPosition,
|
||||
v
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveItem(oldPos: Int, newPos: Int) {
|
||||
val item = dataSet.removeAt(oldPos)
|
||||
dataSet.add(newPos, item)
|
||||
notifyItemMoved(oldPos, newPos)
|
||||
settings.sourcesOrder = dataSet.map { it.ordinal }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class SourcesReorderCallback :
|
||||
ItemTouchHelper.SimpleCallback(ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0) {
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val adapter = recyclerView.adapter as? SourcesAdapter ?: return false
|
||||
val oldPos = viewHolder.bindingAdapterPosition
|
||||
val newPos = target.bindingAdapterPosition
|
||||
adapter.moveItem(oldPos, newPos)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
|
||||
|
||||
override fun isLongPressDragEnabled() = false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.fragment_settings_sources.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
class SourcesSettingsFragment : BaseFragment(R.layout.fragment_settings_sources),
|
||||
OnRecyclerItemClickListener<MangaSource> {
|
||||
|
||||
private lateinit var reorderHelper: ItemTouchHelper
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
reorderHelper = ItemTouchHelper(SourcesReorderCallback())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(R.string.remote_sources)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
|
||||
recyclerView.adapter = SourcesAdapter(this)
|
||||
reorderHelper.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
reorderHelper.attachToRecyclerView(null)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaSource, position: Int, view: View) {
|
||||
(activity as? SettingsActivity)?.openMangaSourceSettings(item)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MangaSource, position: Int, view: View): Boolean {
|
||||
reorderHelper.startDrag(
|
||||
recyclerView.findViewHolderForAdapterPosition(position) ?: return false
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
|
||||
class EditTextSummaryProvider(@StringRes private val emptySummaryId: Int) :
|
||||
Preference.SummaryProvider<EditTextPreference> {
|
||||
|
||||
override fun provideSummary(preference: EditTextPreference): CharSequence {
|
||||
return if (preference.text.isNullOrEmpty()) {
|
||||
preference.context.getString(emptySummaryId)
|
||||
} else {
|
||||
preference.text
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
|
||||
class MultiSummaryProvider(@StringRes private val emptySummaryId: Int) :
|
||||
Preference.SummaryProvider<MultiSelectListPreference> {
|
||||
|
||||
override fun provideSummary(preference: MultiSelectListPreference): CharSequence {
|
||||
val values = preference.values
|
||||
return if (values.isEmpty()) {
|
||||
return preference.context.getString(emptySummaryId)
|
||||
} else {
|
||||
values.joinToString(", ") {
|
||||
preference.entries[preference.findIndexOfValue(it)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class RingtonePickContract(private val title: String?) : ActivityResultContract<Uri?, Uri?>() {
|
||||
|
||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
|
||||
intent.putExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_TYPE,
|
||||
RingtoneManager.TYPE_NOTIFICATION
|
||||
)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
|
||||
intent.putExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
|
||||
Settings.System.DEFAULT_NOTIFICATION_URI
|
||||
)
|
||||
if (title != null) {
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, title)
|
||||
}
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, input)
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
|
||||
return intent?.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user