Use Voyager on Source Preference screen (#8651)
This commit is contained in:
parent
75a687138d
commit
5b189a909b
@ -53,6 +53,9 @@ interface ThemingDelegate {
|
|||||||
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
|
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For source preference theme
|
||||||
|
resIds += R.style.PreferenceThemeOverlay_Tachiyomi
|
||||||
|
|
||||||
return resIds
|
return resIds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.browse.ExtensionDetailsScreen
|
import eu.kanade.presentation.browse.ExtensionDetailsScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.util.LocalRouter
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
data class ExtensionDetailsScreen(
|
data class ExtensionDetailsScreen(
|
||||||
@ -32,13 +30,12 @@ data class ExtensionDetailsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val router = LocalRouter.currentOrThrow
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
ExtensionDetailsScreen(
|
ExtensionDetailsScreen(
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
state = state,
|
state = state,
|
||||||
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
|
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
|
||||||
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
|
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
|
||||||
onClickReadme = { uriHandler.openUri(screenModel.getReadmeUrl()) },
|
onClickReadme = { uriHandler.openUri(screenModel.getReadmeUrl()) },
|
||||||
onClickEnableAll = { screenModel.toggleSources(true) },
|
onClickEnableAll = { screenModel.toggleSources(true) },
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension.details
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.preference.DialogPreference
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.EditTextPreferenceDialogController
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.ListPreferenceDialogController
|
|
||||||
import androidx.preference.MultiSelectListPreference
|
|
||||||
import androidx.preference.MultiSelectListPreferenceDialogController
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceGroupAdapter
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.get
|
|
||||||
import androidx.preference.getOnBindEditTextListener
|
|
||||||
import androidx.preference.isNotEmpty
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
|
||||||
import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
|
||||||
import logcat.LogPriority
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
class SourcePreferencesController(bundle: Bundle? = null) :
|
|
||||||
NucleusController<SourcePreferencesControllerBinding, SourcePreferencesPresenter>(bundle),
|
|
||||||
PreferenceManager.OnDisplayPreferenceDialogListener,
|
|
||||||
DialogPreference.TargetFragment {
|
|
||||||
|
|
||||||
private var lastOpenPreferencePosition: Int? = null
|
|
||||||
|
|
||||||
private var preferenceScreen: PreferenceScreen? = null
|
|
||||||
|
|
||||||
constructor(sourceId: Long) : this(
|
|
||||||
bundleOf(SOURCE_ID to sourceId),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater): SourcePreferencesControllerBinding {
|
|
||||||
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
|
||||||
return SourcePreferencesControllerBinding.inflate(themedInflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createPresenter(): SourcePreferencesPresenter {
|
|
||||||
return SourcePreferencesPresenter(args.getLong(SOURCE_ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
|
||||||
return presenter.source?.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
|
|
||||||
val source = presenter.source ?: return
|
|
||||||
val context = view.context
|
|
||||||
|
|
||||||
val themedContext by lazy { getPreferenceThemeContext() }
|
|
||||||
val manager = PreferenceManager(themedContext)
|
|
||||||
val dataStore = SharedPreferencesDataStore(
|
|
||||||
context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE),
|
|
||||||
)
|
|
||||||
manager.preferenceDataStore = dataStore
|
|
||||||
manager.onDisplayPreferenceDialogListener = this
|
|
||||||
val screen = manager.createPreferenceScreen(themedContext)
|
|
||||||
preferenceScreen = screen
|
|
||||||
|
|
||||||
try {
|
|
||||||
addPreferencesForSource(screen, source)
|
|
||||||
} catch (e: AbstractMethodError) {
|
|
||||||
logcat(LogPriority.ERROR) { "Source did not implement [addPreferencesForSource]: ${source.name}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.setPreferences(screen)
|
|
||||||
|
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(context)
|
|
||||||
binding.recycler.adapter = PreferenceGroupAdapter(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
preferenceScreen = null
|
|
||||||
super.onDestroyView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
|
||||||
lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
|
|
||||||
val context = screen.context
|
|
||||||
|
|
||||||
if (source is ConfigurableSource) {
|
|
||||||
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
|
||||||
source.setupPreferenceScreen(newScreen)
|
|
||||||
|
|
||||||
// Reparent the preferences
|
|
||||||
while (newScreen.isNotEmpty()) {
|
|
||||||
val pref = newScreen[0]
|
|
||||||
pref.isIconSpaceReserved = false
|
|
||||||
pref.order = Int.MAX_VALUE // reset to default order
|
|
||||||
|
|
||||||
// Apply incognito IME for EditTextPreference
|
|
||||||
if (pref is EditTextPreference) {
|
|
||||||
val setListener = pref.getOnBindEditTextListener()
|
|
||||||
pref.setOnBindEditTextListener {
|
|
||||||
setListener?.onBindEditText(it)
|
|
||||||
it.setIncognito(viewScope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newScreen.removePreference(pref)
|
|
||||||
screen.addPreference(pref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPreferenceThemeContext(): Context {
|
|
||||||
val tv = TypedValue()
|
|
||||||
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
|
|
||||||
return ContextThemeWrapper(activity, tv.resourceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
|
||||||
if (!isAttached) return
|
|
||||||
|
|
||||||
val screen = preference.parent!!
|
|
||||||
|
|
||||||
lastOpenPreferencePosition = (0 until screen.preferenceCount).indexOfFirst {
|
|
||||||
screen[it] === preference
|
|
||||||
}
|
|
||||||
|
|
||||||
val f = when (preference) {
|
|
||||||
is EditTextPreference ->
|
|
||||||
EditTextPreferenceDialogController
|
|
||||||
.newInstance(preference.getKey())
|
|
||||||
is ListPreference ->
|
|
||||||
ListPreferenceDialogController
|
|
||||||
.newInstance(preference.getKey())
|
|
||||||
is MultiSelectListPreference ->
|
|
||||||
MultiSelectListPreferenceDialogController
|
|
||||||
.newInstance(preference.getKey())
|
|
||||||
else -> throw IllegalArgumentException(
|
|
||||||
"Tried to display dialog for unknown " +
|
|
||||||
"preference type. Did you forget to override onDisplayPreferenceDialog()?",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
f.targetController = this
|
|
||||||
f.showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : Preference> findPreference(key: CharSequence): T? {
|
|
||||||
// We track [lastOpenPreferencePosition] when displaying the dialog
|
|
||||||
// [key] isn't useful since there may be duplicates
|
|
||||||
return preferenceScreen!![lastOpenPreferencePosition!!] as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val SOURCE_ID = "source_id"
|
|
||||||
private const val LASTOPENPREFERENCE_KEY = "last_open_preference"
|
|
@ -1,14 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension.details
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class SourcePreferencesPresenter(
|
|
||||||
val sourceId: Long,
|
|
||||||
sourceManager: SourceManager = Injekt.get(),
|
|
||||||
) : BasePresenter<SourcePreferencesController>() {
|
|
||||||
|
|
||||||
val source = sourceManager.get(sourceId)
|
|
||||||
}
|
|
@ -0,0 +1,174 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.FragmentContainerView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.DialogPreference
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.forEach
|
||||||
|
import androidx.preference.getOnBindEditTextListener
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.Scaffold
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||||
|
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class SourcePreferencesScreen(val sourceId: Long) : Screen {
|
||||||
|
|
||||||
|
override val key = uniqueScreenKey
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = Injekt.get<SourceManager>().get(sourceId)!!.toString()) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigator::pop) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
FragmentContainer(
|
||||||
|
fragmentManager = (context as FragmentActivity).supportFragmentManager,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(contentPadding),
|
||||||
|
) {
|
||||||
|
val fragment = SourcePreferencesFragment.getInstance(sourceId)
|
||||||
|
add(it, fragment, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From https://stackoverflow.com/questions/60520145/fragment-container-in-jetpack-compose/70817794#70817794
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun FragmentContainer(
|
||||||
|
fragmentManager: FragmentManager,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
commit: FragmentTransaction.(containerId: Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val containerId by rememberSaveable {
|
||||||
|
mutableStateOf(View.generateViewId())
|
||||||
|
}
|
||||||
|
var initialized by rememberSaveable { mutableStateOf(false) }
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = { context ->
|
||||||
|
FragmentContainerView(context)
|
||||||
|
.apply { id = containerId }
|
||||||
|
},
|
||||||
|
update = { view ->
|
||||||
|
if (!initialized) {
|
||||||
|
fragmentManager.commit { commit(view.id) }
|
||||||
|
initialized = true
|
||||||
|
} else {
|
||||||
|
fragmentManager.onContainerAvailable(view)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Access to package-private method in FragmentManager through reflection */
|
||||||
|
private fun FragmentManager.onContainerAvailable(view: FragmentContainerView) {
|
||||||
|
val method = FragmentManager::class.java.getDeclaredMethod(
|
||||||
|
"onContainerAvailable",
|
||||||
|
FragmentContainerView::class.java,
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(this, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
preferenceScreen = populateScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateScreen(): PreferenceScreen {
|
||||||
|
val sourceId = requireArguments().getLong(SOURCE_ID)
|
||||||
|
val source = Injekt.get<SourceManager>().get(sourceId)!!
|
||||||
|
|
||||||
|
check(source is ConfigurableSource)
|
||||||
|
|
||||||
|
val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||||
|
val dataStore = SharedPreferencesDataStore(sharedPreferences)
|
||||||
|
preferenceManager.preferenceDataStore = dataStore
|
||||||
|
|
||||||
|
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||||
|
source.setupPreferenceScreen(sourceScreen)
|
||||||
|
sourceScreen.forEach { pref ->
|
||||||
|
pref.isIconSpaceReserved = false
|
||||||
|
if (pref is DialogPreference) {
|
||||||
|
pref.dialogTitle = pref.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply incognito IME for EditTextPreference
|
||||||
|
if (pref is EditTextPreference) {
|
||||||
|
val setListener = pref.getOnBindEditTextListener()
|
||||||
|
pref.setOnBindEditTextListener {
|
||||||
|
setListener?.onBindEditText(it)
|
||||||
|
it.setIncognito(lifecycleScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SOURCE_ID = "source_id"
|
||||||
|
|
||||||
|
fun getInstance(sourceId: Long): SourcePreferencesFragment {
|
||||||
|
val fragment = SourcePreferencesFragment()
|
||||||
|
fragment.arguments = bundleOf(SOURCE_ID to sourceId)
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,9 @@
|
|||||||
<style name="ThemeOverlay.Tachiyomi.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
<style name="ThemeOverlay.Tachiyomi.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
||||||
<item name="android:textColorPrimary">?attr/colorOnSurface</item>
|
<item name="android:textColorPrimary">?attr/colorOnSurface</item>
|
||||||
<item name="android:textColor">?attr/colorOnSurface</item>
|
<item name="android:textColor">?attr/colorOnSurface</item>
|
||||||
|
<item name="android:colorBackground">?attr/colorSurface</item>
|
||||||
|
<item name="android:layout">@layout/m3_alert_dialog</item>
|
||||||
|
<item name="dialogCornerRadius">@dimen/m3_alert_dialog_corner_size</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
<item name="android:enforceStatusBarContrast" tools:targetApi="Q">false</item>
|
<item name="android:enforceStatusBarContrast" tools:targetApi="Q">false</item>
|
||||||
<item name="android:itemTextAppearance">@style/TextAppearance.Widget.Menu</item>
|
<item name="android:itemTextAppearance">@style/TextAppearance.Widget.Menu</item>
|
||||||
<item name="materialAlertDialogTheme">@style/ThemeOverlay.Tachiyomi.MaterialAlertDialog</item>
|
<item name="materialAlertDialogTheme">@style/ThemeOverlay.Tachiyomi.MaterialAlertDialog</item>
|
||||||
|
<item name="alertDialogTheme">@style/ThemeOverlay.Tachiyomi.MaterialAlertDialog</item>
|
||||||
<item name="textAppearanceButton">@style/TextAppearance.Widget.Button</item>
|
<item name="textAppearanceButton">@style/TextAppearance.Widget.Button</item>
|
||||||
<item name="android:buttonStyle">?attr/borderlessButtonStyle</item>
|
<item name="android:buttonStyle">?attr/borderlessButtonStyle</item>
|
||||||
<item name="android:backgroundDimAmount">0.32</item>
|
<item name="android:backgroundDimAmount">0.32</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user