Merge branch 'feature/settings' into devel
This commit is contained in:
@@ -6,14 +6,18 @@ import androidx.annotation.CallSuper
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
|
||||
|
||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(),
|
||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
PreferenceFragmentCompat(),
|
||||
WindowInsetsDelegate.WindowInsetsListener,
|
||||
RecyclerViewOwner {
|
||||
|
||||
@@ -39,16 +43,20 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : Pre
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (titleId != 0) {
|
||||
activity?.setTitle(titleId)
|
||||
setTitle(getString(titleId))
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
listView.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
protected fun setTitle(title: CharSequence) {
|
||||
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
|
||||
?: activity?.setTitle(title)
|
||||
}
|
||||
}
|
||||
@@ -253,11 +253,9 @@ class AppSettings(context: Context) {
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
const val KEY_APP_UPDATE_AUTO = "app_update_auto"
|
||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||
const val KEY_APP_GRATITUDES = "about_gratitudes"
|
||||
const val KEY_FEEDBACK_4PDA = "about_feedback_4pda"
|
||||
const val KEY_FEEDBACK_DISCORD = "about_feedback_discord"
|
||||
const val KEY_FEEDBACK_GITHUB = "about_feedback_github"
|
||||
const val KEY_SUPPORT_DEVELOPER = "about_support_developer"
|
||||
|
||||
private const val NETWORK_NEVER = 0
|
||||
private const val NETWORK_ALWAYS = 1
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
@@ -158,7 +159,7 @@ class ReaderActivity :
|
||||
)
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
startActivity(SimpleSettingsActivity.newReaderSettingsIntent(this))
|
||||
startActivity(SettingsActivity.newReaderSettingsIntent(this))
|
||||
}
|
||||
R.id.action_chapters -> {
|
||||
ChaptersBottomSheet.show(
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding
|
||||
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.*
|
||||
|
||||
class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>(), AppBarOwner {
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = binding.appbar
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySettingsSimpleBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportFragmentManager.commit {
|
||||
replace(
|
||||
R.id.container,
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment()
|
||||
ACTION_READER -> ReaderSettingsFragment()
|
||||
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL
|
||||
)
|
||||
else -> MainSettingsFragment()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
with(binding.toolbar) {
|
||||
updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right
|
||||
)
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ACTION_READER =
|
||||
"${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
|
||||
private const val ACTION_SUGGESTIONS =
|
||||
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
||||
private const val ACTION_SOURCE =
|
||||
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newReaderSettingsIntent(context: Context) =
|
||||
Intent(context, SimpleSettingsActivity::class.java)
|
||||
.setAction(ACTION_READER)
|
||||
|
||||
fun newSuggestionsSettingsIntent(context: Context) =
|
||||
Intent(context, SimpleSettingsActivity::class.java)
|
||||
.setAction(ACTION_SUGGESTIONS)
|
||||
|
||||
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
|
||||
Intent(context, SimpleSettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCE)
|
||||
.putExtra(EXTRA_SOURCE, source)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.serializableArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
@@ -34,10 +34,7 @@ class RemoteListFragment : MangaListFragment() {
|
||||
return when (item.itemId) {
|
||||
R.id.action_source_settings -> {
|
||||
startActivity(
|
||||
SimpleSettingsActivity.newSourceSettingsIntent(
|
||||
context ?: return false,
|
||||
source,
|
||||
)
|
||||
SettingsActivity.newSourceSettingsIntent(context ?: return false, source)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
|
||||
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
import java.util.*
|
||||
|
||||
class AppearanceSettingsFragment :
|
||||
BasePreferenceFragment(R.string.appearance),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_appearance)
|
||||
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.run {
|
||||
summary = "%d%%".format(value)
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
preference.summary = "%d%%".format(newValue)
|
||||
true
|
||||
}
|
||||
}
|
||||
preferenceScreen?.findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
|
||||
entryValues = ListMode.values().names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = AppSettings.isDynamicColorAvailable
|
||||
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
|
||||
entryValues = resources.getStringArray(R.array.date_formats)
|
||||
val now = Date().time
|
||||
entries = entryValues.map { value ->
|
||||
val formattedDate = settings.getDateFormat(value.toString()).format(now)
|
||||
if (value == "") {
|
||||
"${context.getString(R.string.system_default)} ($formattedDate)"
|
||||
} else {
|
||||
formattedDate
|
||||
}
|
||||
}.toTypedArray()
|
||||
setDefaultValueCompat("")
|
||||
summary = "%s"
|
||||
}
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
AppSettings.KEY_DYNAMIC_THEME -> {
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_THEME_AMOLED -> {
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_APP_PASSWORD -> {
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
val pref = (preference as? TwoStatePreference ?: return false)
|
||||
if (pref.isChecked) {
|
||||
pref.isChecked = false
|
||||
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
|
||||
} else {
|
||||
settings.appPassword = null
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
class ContentSettingsFragment :
|
||||
BasePreferenceFragment(R.string.content),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
StorageSelectDialog.OnStorageSelectListener {
|
||||
|
||||
private val storageManager by inject<LocalStorageManager>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_content)
|
||||
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
bindRemoteSourcesSummary()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
AppSettings.KEY_SUGGESTIONS -> {
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
AppSettings.KEY_SOURCES_HIDDEN -> {
|
||||
bindRemoteSourcesSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
val ctx = context ?: return false
|
||||
StorageSelectDialog.Builder(ctx, storageManager, this)
|
||||
.setTitle(preference.title ?: "")
|
||||
.setNegativeButton(android.R.string.cancel)
|
||||
.create()
|
||||
.show()
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStorageSelected(file: File) {
|
||||
settings.mangaStorageDir = file
|
||||
}
|
||||
|
||||
private fun Preference.bindStorageName() {
|
||||
viewLifecycleScope.launch {
|
||||
val storage = storageManager.getDefaultWriteableDir()
|
||||
summary = storage?.getStorageName(context) ?: getString(R.string.not_available)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindRemoteSourcesSummary() {
|
||||
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.run {
|
||||
val total = MangaSource.values().size - 1
|
||||
summary = getString(
|
||||
R.string.enabled_d_of_d, total - settings.hiddenSources.size, total
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.TwoStatePreference
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
|
||||
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
|
||||
class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
StorageSelectDialog.OnStorageSelectListener {
|
||||
|
||||
private val storageManager by inject<LocalStorageManager>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_main)
|
||||
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.run {
|
||||
summary = "%d%%".format(value)
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
preference.summary = "%d%%".format(newValue)
|
||||
true
|
||||
}
|
||||
}
|
||||
preferenceScreen?.findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
|
||||
entryValues = ListMode.values().names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
|
||||
AppSettings.isDynamicColorAvailable
|
||||
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
|
||||
entryValues = resources.getStringArray(R.array.date_formats)
|
||||
val now = Date().time
|
||||
entries = entryValues.map { value ->
|
||||
val formattedDate = settings.getDateFormat(value.toString()).format(now)
|
||||
if (value == "") {
|
||||
"${context.getString(R.string.system_default)} ($formattedDate)"
|
||||
} else {
|
||||
formattedDate
|
||||
}
|
||||
}.toTypedArray()
|
||||
setDefaultValueCompat("")
|
||||
summary = "%s"
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
|
||||
!settings.appPassword.isNullOrEmpty()
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_settings, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_leaks -> {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(requireContext(), "leakcanary.internal.activity.LeakActivity")
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
AppSettings.KEY_DYNAMIC_THEME -> {
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_THEME_AMOLED -> {
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
AppSettings.KEY_APP_PASSWORD -> {
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
AppSettings.KEY_SUGGESTIONS -> {
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
findPreference<PreferenceScreen>(AppSettings.KEY_REMOTE_SOURCES)?.run {
|
||||
val total = MangaSource.values().size - 1
|
||||
summary = getString(
|
||||
R.string.enabled_d_of_d, total - settings.hiddenSources.size, total
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
val ctx = context ?: return false
|
||||
StorageSelectDialog.Builder(ctx, storageManager, this)
|
||||
.setTitle(preference.title ?: "")
|
||||
.setNegativeButton(android.R.string.cancel)
|
||||
.create()
|
||||
.show()
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
val pref = (preference as? TwoStatePreference ?: return false)
|
||||
if (pref.isChecked) {
|
||||
pref.isChecked = false
|
||||
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
|
||||
} else {
|
||||
settings.appPassword = null
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStorageSelected(file: File) {
|
||||
settings.mangaStorageDir = file
|
||||
}
|
||||
|
||||
private fun Preference.bindStorageName() {
|
||||
viewLifecycleScope.launch {
|
||||
val storage = storageManager.getDefaultWriteableDir()
|
||||
summary = storage?.getStorageName(context) ?: getString(R.string.not_available)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import android.os.Bundle
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
|
||||
class NetworkSettingsFragment : BasePreferenceFragment(R.string.settings) {
|
||||
class RootSettingsFragment : BasePreferenceFragment(R.string.settings) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
//TODO https://developer.android.com/training/basics/network-ops/managing
|
||||
addPreferencesFromResource(R.xml.pref_root)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
@@ -11,6 +15,7 @@ import androidx.fragment.app.commit
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
@@ -34,9 +39,7 @@ class SettingsActivity :
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, MainSettingsFragment())
|
||||
}
|
||||
openDefaultFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +58,22 @@ class SettingsActivity :
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_settings, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.action_leaks -> {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(this, "leakcanary.internal.activity.LeakActivity")
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onBackStackChanged() {
|
||||
val fragment = supportFragmentManager.findFragmentById(R.id.container) as? RecyclerViewOwner ?: return
|
||||
val recyclerView = fragment.recyclerView
|
||||
@@ -70,32 +89,66 @@ class SettingsActivity :
|
||||
val fm = supportFragmentManager
|
||||
val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false)
|
||||
fragment.arguments = pref.extras
|
||||
fragment.setTargetFragment(caller, 0)
|
||||
// fragment.setTargetFragment(caller, 0)
|
||||
openFragment(fragment)
|
||||
return true
|
||||
}
|
||||
|
||||
fun openMangaSourceSettings(mangaSource: MangaSource) {
|
||||
openFragment(SourceSettingsFragment.newInstance(mangaSource))
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.appbar.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
binding.container.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
fun openNotificationSettingsLegacy() {
|
||||
openFragment(NotificationSettingsLegacyFragment())
|
||||
}
|
||||
|
||||
private fun openFragment(fragment: Fragment) {
|
||||
fun openFragment(fragment: Fragment) {
|
||||
supportFragmentManager.commit {
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||
replace(R.id.container, fragment)
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container, fragment)
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
private fun openDefaultFragment() {
|
||||
val fragment = when (intent?.action) {
|
||||
ACTION_READER -> ReaderSettingsFragment()
|
||||
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL
|
||||
)
|
||||
else -> SettingsHeadersFragment()
|
||||
}
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
|
||||
private const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
||||
private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
|
||||
|
||||
fun newReaderSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_READER)
|
||||
|
||||
fun newSuggestionsSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SUGGESTIONS)
|
||||
|
||||
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCE)
|
||||
.putExtra(EXTRA_SOURCE, source)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceHeaderFragmentCompat
|
||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLayout.PanelSlideListener {
|
||||
|
||||
private var currentTitle: CharSequence? = null
|
||||
|
||||
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = RootSettingsFragment()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
slidingPaneLayout.addPanelSlideListener(this)
|
||||
}
|
||||
|
||||
override fun onPanelSlide(panel: View, slideOffset: Float) = Unit
|
||||
|
||||
override fun onPanelOpened(panel: View) {
|
||||
activity?.title = currentTitle ?: getString(R.string.settings)
|
||||
}
|
||||
|
||||
override fun onPanelClosed(panel: View) {
|
||||
activity?.setTitle(R.string.settings)
|
||||
}
|
||||
|
||||
fun setTitle(title: CharSequence?) {
|
||||
currentTitle = title
|
||||
if (slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) {
|
||||
activity?.title = title
|
||||
}
|
||||
}
|
||||
|
||||
fun openFragment(fragment: Fragment) {
|
||||
childFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(androidx.preference.R.id.preferences_detail, fragment)
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.title = source.title
|
||||
setTitle(source.title)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
||||
@@ -31,7 +31,6 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch
|
||||
append(getString(R.string.read_more))
|
||||
}
|
||||
}
|
||||
warningPreference
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +42,10 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
.putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID)
|
||||
startActivity(intent)
|
||||
true
|
||||
} else {
|
||||
(activity as? SettingsActivity)?.openNotificationSettingsLegacy()
|
||||
super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.koitharu.kotatsu.settings.about
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.Preference
|
||||
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.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
@@ -16,20 +16,16 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_about)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val isUpdateSupported = AppUpdateChecker.isUpdateSupported(requireContext())
|
||||
findPreference<Preference>(AppSettings.KEY_APP_UPDATE_AUTO)?.run {
|
||||
isVisible = AppUpdateChecker.isUpdateSupported(context)
|
||||
isVisible = isUpdateSupported
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
|
||||
title = getString(R.string.app_version, BuildConfig.VERSION_NAME)
|
||||
isEnabled = AppUpdateChecker.isUpdateSupported(context)
|
||||
isEnabled = isUpdateSupported
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_APP_VERSION -> {
|
||||
@@ -37,39 +33,19 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_APP_TRANSLATION -> {
|
||||
startActivity(context?.let { BrowserActivity.newIntent(it,
|
||||
"https://hosted.weblate.org/engage/kotatsu",
|
||||
resources.getString(R.string.about_app_translation)) })
|
||||
openLink(getString(R.string.url_weblate), preference.title)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_FEEDBACK_4PDA -> {
|
||||
startActivity(context?.let { BrowserActivity.newIntent(it,
|
||||
"https://4pda.to/forum/index.php?showtopic=697669",
|
||||
resources.getString(R.string.about_feedback_4pda)) })
|
||||
openLink(getString(R.string.url_forpda), preference.title)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_FEEDBACK_DISCORD -> {
|
||||
startActivity(context?.let { BrowserActivity.newIntent(it,
|
||||
"https://discord.gg/NNJ5RgVBC5",
|
||||
"Discord") })
|
||||
openLink(getString(R.string.url_discord), preference.title)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_FEEDBACK_GITHUB -> {
|
||||
startActivity(context?.let { BrowserActivity.newIntent(it,
|
||||
"https://github.com/nv95/Kotatsu/issues",
|
||||
"GitHub") })
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_SUPPORT_DEVELOPER -> {
|
||||
startActivity(context?.let { BrowserActivity.newIntent(it,
|
||||
"https://yoomoney.ru/to/410012543938752",
|
||||
resources.getString(R.string.about_support_developer)) })
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_APP_GRATITUDES -> {
|
||||
startActivity(context?.let { BrowserActivity.newIntent(it,
|
||||
"https://github.com/nv95/Kotatsu/graphs/contributors",
|
||||
resources.getString(R.string.about_gratitudes)) })
|
||||
openLink(getString(R.string.url_github_issues), preference.title)
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
@@ -95,4 +71,16 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLink(url: String, title: CharSequence?) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url.toUri()
|
||||
startActivity(
|
||||
if (title != null) {
|
||||
Intent.createChooser(intent, title)
|
||||
} else {
|
||||
intent
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.databinding.FragmentCopyrightBinding
|
||||
|
||||
class LicenseFragment : BaseFragment<FragmentCopyrightBinding>() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.textView.apply {
|
||||
text =
|
||||
SpannableStringBuilder(resources.openRawResource(R.raw.copyright).bufferedReader()
|
||||
.readText()
|
||||
.parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST))
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentCopyrightBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(R.string.about_license)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
||||
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
|
||||
import org.koitharu.kotatsu.settings.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
@@ -87,7 +89,9 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
||||
}
|
||||
|
||||
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) {
|
||||
(activity as? SettingsActivity)?.openMangaSourceSettings(item.source)
|
||||
val fragment = SourceSettingsFragment.newInstance(item.source)
|
||||
(parentFragment as? SettingsHeadersFragment)?.openFragment(fragment)
|
||||
?: (activity as? SettingsActivity)?.openFragment(fragment)
|
||||
}
|
||||
|
||||
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||
|
||||
@@ -30,18 +30,22 @@ class SliderPreference @JvmOverloads constructor(
|
||||
set(value) = setValueInternal(value, notifyChanged = true)
|
||||
|
||||
private val sliderListener = Slider.OnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
syncValueInternal(value.toInt())
|
||||
}
|
||||
if (fromUser) {
|
||||
syncValueInternal(value.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs,
|
||||
context.withStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.SliderPreference,
|
||||
defStyleAttr,
|
||||
defStyleRes) {
|
||||
valueFrom = getFloat(R.styleable.SliderPreference_android_valueFrom,
|
||||
valueFrom.toFloat()).toInt()
|
||||
defStyleRes
|
||||
) {
|
||||
valueFrom = getFloat(
|
||||
R.styleable.SliderPreference_android_valueFrom,
|
||||
valueFrom.toFloat()
|
||||
).toInt()
|
||||
valueTo =
|
||||
getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt()
|
||||
stepSize =
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
class SuggestionsFragment : MangaListFragment() {
|
||||
|
||||
@@ -37,7 +37,7 @@ class SuggestionsFragment : MangaListFragment() {
|
||||
true
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
startActivity(SimpleSettingsActivity.newSuggestionsSettingsIntent(requireContext()))
|
||||
startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext()))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
|
||||
Reference in New Issue
Block a user