compose: dispenser: Migrate dispensers logic to compose
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.aurora.store.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display dispenser URL in a list
|
||||||
|
* @param modifier The modifier to be applied to the composable
|
||||||
|
* @param url URL of the dispenser
|
||||||
|
* @param onClick Callback when this URL is clicked
|
||||||
|
* @param onClear Callback when the clear button is clicked
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DispenserListItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
url: String,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
onClear: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(dimensionResource(R.dimen.padding_small)),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1F),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_server),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = url,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(onClick = onClear) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.remove),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun DispenserListItemPreview() {
|
||||||
|
DispenserListItem(url = "https://auroraoss.com/api/auth")
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import com.aurora.store.compose.ui.blacklist.BlacklistScreen
|
|||||||
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
|
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
|
||||||
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
||||||
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
||||||
|
import com.aurora.store.compose.ui.dispenser.DispenserScreen
|
||||||
import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
||||||
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
||||||
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
||||||
@@ -135,6 +136,10 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<Screen.Dispenser> {
|
||||||
|
DispenserScreen(onNavigateUp = ::onNavigateUp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,7 @@ sealed class Screen : NavKey, Parcelable {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Spoof : Screen()
|
data object Spoof : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Dispenser : Screen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.dispenser
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
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.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.aurora.extensions.copyToClipBoard
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.DispenserListItem
|
||||||
|
import com.aurora.store.compose.composable.Error
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.preview.PreviewTemplate
|
||||||
|
import com.aurora.store.viewmodel.dispenser.DispenserViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hiltViewModel()) {
|
||||||
|
val dispensers by viewModel.dispensers.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
var shouldShowInputDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
if (shouldShowInputDialog) {
|
||||||
|
InputDispenserDialog(
|
||||||
|
onAdd = { url ->
|
||||||
|
viewModel.addDispenser(url)
|
||||||
|
shouldShowInputDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { shouldShowInputDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldRemoveDispenser : String? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
shouldRemoveDispenser?.let { url ->
|
||||||
|
RemoveDispenserDialog(
|
||||||
|
url = url,
|
||||||
|
onRemove = {
|
||||||
|
viewModel.removeDispenser(url)
|
||||||
|
shouldRemoveDispenser = null
|
||||||
|
},
|
||||||
|
onDismiss = { shouldRemoveDispenser = null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenContent(
|
||||||
|
dispensers = dispensers,
|
||||||
|
onNavigateUp = onNavigateUp,
|
||||||
|
onAddDispenser = { shouldShowInputDialog = true },
|
||||||
|
onRemoveDispenser = { url -> shouldRemoveDispenser = url }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(
|
||||||
|
onNavigateUp: () -> Unit = {},
|
||||||
|
dispensers: Set<String> = emptySet(),
|
||||||
|
onAddDispenser: () -> Unit = {},
|
||||||
|
onRemoveDispenser: (url: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.pref_dispenser_title),
|
||||||
|
onNavigateUp = onNavigateUp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (dispensers.isNotEmpty()) {
|
||||||
|
FloatingActionButton(onClick = onAddDispenser) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_add),
|
||||||
|
contentDescription = stringResource(R.string.add_dispenser_title)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
if (dispensers.isEmpty()) {
|
||||||
|
Error(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_server),
|
||||||
|
message = stringResource(R.string.no_dispensers_available),
|
||||||
|
actionMessage = stringResource(R.string.add_dispenser_title),
|
||||||
|
onAction = onAddDispenser
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
||||||
|
) {
|
||||||
|
items(items = dispensers.toList(), key = { url -> url }) { url ->
|
||||||
|
DispenserListItem(
|
||||||
|
url = url,
|
||||||
|
onClick = { context.copyToClipBoard(url) },
|
||||||
|
onClear = { onRemoveDispenser(url) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun DispenserScreenPreview() {
|
||||||
|
val dispensers = List(10) { "https://auroraoss.com/api/auth/$it" }
|
||||||
|
PreviewTemplate {
|
||||||
|
ScreenContent(dispensers = dispensers.toSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun DispenserScreenEmptyPreview() {
|
||||||
|
PreviewTemplate {
|
||||||
|
ScreenContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.dispenser
|
||||||
|
|
||||||
|
import android.util.Patterns
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aurora.store.R
|
||||||
|
import kotlinx.coroutines.android.awaitFrame
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for adding a new token dispenser
|
||||||
|
* @param onAdd Callback on adding a new dispenser
|
||||||
|
* @param onDismiss Callback on dismiss
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun InputDispenserDialog(onAdd: (url: String) -> Unit = {}, onDismiss: () -> Unit = {}) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var url by remember { mutableStateOf(TextFieldValue()) }
|
||||||
|
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
awaitFrame()
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.add_dispenser_title)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.add_dispenser_summary))
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
value = url,
|
||||||
|
placeholder = { Text(text = stringResource(R.string.add_dispenser_hint)) },
|
||||||
|
onValueChange = { url = it },
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onAdd(url.text) },
|
||||||
|
enabled = Patterns.WEB_URL.matcher(url.text).matches()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun InputDispenserDialogPreview() {
|
||||||
|
InputDispenserDialog()
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.dispenser
|
||||||
|
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.aurora.store.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for removing a token dispenser
|
||||||
|
* @param onRemove Callback on removing a dispenser
|
||||||
|
* @param onDismiss Callback on dismiss
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RemoveDispenserDialog(url: String, onRemove: () -> Unit = {}, onDismiss: () -> Unit = {}) {
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.remove_dispenser_title)) },
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.remove_dispenser_summary, url))
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onRemove) {
|
||||||
|
Text(text = stringResource(R.string.remove))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun RemoveDispenserDialogPreview() {
|
||||||
|
RemoveDispenserDialog(url = "https://auroraoss.com/api/auth/")
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.aurora.store.view.epoxy.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import com.airbnb.epoxy.CallbackProp
|
|
||||||
import com.airbnb.epoxy.ModelProp
|
|
||||||
import com.airbnb.epoxy.ModelView
|
|
||||||
import com.aurora.store.databinding.ViewDispenserBinding
|
|
||||||
|
|
||||||
@ModelView(
|
|
||||||
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
|
|
||||||
baseModelClass = BaseModel::class
|
|
||||||
)
|
|
||||||
class DispenserView @JvmOverloads constructor(
|
|
||||||
context: Context?,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : BaseView<ViewDispenserBinding>(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
@ModelProp
|
|
||||||
fun url(url: String) {
|
|
||||||
binding.url.text = url
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallbackProp
|
|
||||||
fun copy(onClickListener: OnClickListener?) {
|
|
||||||
binding.url.setOnClickListener(onClickListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallbackProp
|
|
||||||
fun clear(onClickListener: OnClickListener?) {
|
|
||||||
binding.btnAction.setOnClickListener(onClickListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.aurora.store.view.ui.dispenser
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.aurora.extensions.copyToClipBoard
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.databinding.FragmentDispenserBinding
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
|
|
||||||
import com.aurora.store.view.epoxy.views.DispenserViewModel_
|
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DispenserFragment : BaseFragment<FragmentDispenserBinding>(),
|
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
private val dispensers: Set<String>
|
|
||||||
get() = Preferences.getStringSet(requireContext(), PREFERENCE_DISPENSER_URLS)
|
|
||||||
|
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
sharedPreferences = Preferences.getPrefs(view.context)
|
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
|
|
||||||
binding.toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
|
||||||
binding.addFab.setOnClickListener {
|
|
||||||
findNavController().navigate(R.id.inputDispenserDialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDispensers()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
|
||||||
if (key == PREFERENCE_DISPENSER_URLS) setupDispensers()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupDispensers() {
|
|
||||||
if (dispensers.isNotEmpty()) {
|
|
||||||
binding.noDispensersTextView.visibility = View.GONE
|
|
||||||
|
|
||||||
binding.epoxyRecycler.visibility = View.VISIBLE
|
|
||||||
binding.epoxyRecycler.withModels {
|
|
||||||
setFilterDuplicates(true)
|
|
||||||
dispensers.forEachIndexed { index, url ->
|
|
||||||
add(
|
|
||||||
DispenserViewModel_()
|
|
||||||
.id(index)
|
|
||||||
.url(url)
|
|
||||||
.copy { _ -> requireContext().copyToClipBoard(url) }
|
|
||||||
.clear { _ ->
|
|
||||||
findNavController().navigate(
|
|
||||||
DispenserFragmentDirections
|
|
||||||
.actionDispenserFragmentToRemoveDispenserDialog(url)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.epoxyRecycler.visibility = View.GONE
|
|
||||||
binding.noDispensersTextView.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.aurora.store.view.ui.dispenser
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Patterns
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.aurora.extensions.showKeyboard
|
|
||||||
import com.aurora.extensions.toast
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
|
|
||||||
import com.aurora.store.util.save
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class InputDispenserDialog: DialogFragment() {
|
|
||||||
|
|
||||||
private val textInputLayout: TextInputLayout?
|
|
||||||
get() = dialog?.findViewById(R.id.textInputLayout)
|
|
||||||
|
|
||||||
private val dispensers: Set<String>
|
|
||||||
get() = Preferences.getStringSet(requireContext(), PREFERENCE_DISPENSER_URLS)
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_text_input_edit_text, null)
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.add_dispenser_title)
|
|
||||||
.setMessage(R.string.add_dispenser_summary)
|
|
||||||
.setView(view)
|
|
||||||
.setPositiveButton(getString(R.string.add)) { _, _ -> saveDispenserUrl() }
|
|
||||||
.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dialog?.dismiss()}
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
textInputLayout?.editText?.apply {
|
|
||||||
hint = requireContext().getString(R.string.add_dispenser_hint)
|
|
||||||
showKeyboard()
|
|
||||||
}
|
|
||||||
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveDispenserUrl() {
|
|
||||||
val url = textInputLayout?.editText?.text?.toString()
|
|
||||||
if (!url.isNullOrEmpty() && Patterns.WEB_URL.matcher(url).matches()) {
|
|
||||||
val newSet = dispensers.toMutableSet().apply {
|
|
||||||
add(url)
|
|
||||||
}
|
|
||||||
save(PREFERENCE_DISPENSER_URLS, newSet)
|
|
||||||
} else {
|
|
||||||
toast(R.string.add_dispenser_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.aurora.store.view.ui.dispenser
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
|
|
||||||
import com.aurora.store.util.save
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class RemoveDispenserDialog : DialogFragment() {
|
|
||||||
|
|
||||||
private val args: RemoveDispenserDialogArgs by navArgs()
|
|
||||||
|
|
||||||
private val dispensers: Set<String>
|
|
||||||
get() = Preferences.getStringSet(requireContext(), PREFERENCE_DISPENSER_URLS)
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.remove_dispenser_title)
|
|
||||||
.setMessage(getString(R.string.remove_dispenser_summary, args.url))
|
|
||||||
.setPositiveButton(getString(R.string.remove)) { _, _ -> removeDispenserUrl() }
|
|
||||||
.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dialog?.dismiss() }
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeDispenserUrl() {
|
|
||||||
val newSet = dispensers.toMutableSet().apply {
|
|
||||||
remove(args.url)
|
|
||||||
}
|
|
||||||
save(PREFERENCE_DISPENSER_URLS, newSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,9 +25,11 @@ import androidx.appcompat.widget.Toolbar
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import com.aurora.extensions.navigate
|
||||||
import com.aurora.extensions.runOnUiThread
|
import com.aurora.extensions.runOnUiThread
|
||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.navigation.Screen
|
||||||
import com.aurora.store.util.PackageUtil
|
import com.aurora.store.util.PackageUtil
|
||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH
|
import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH
|
||||||
@@ -45,7 +47,7 @@ class NetworkPreference : BasePreferenceFragment() {
|
|||||||
|
|
||||||
findPreference<Preference>(Preferences.PREFERENCE_DISPENSER_URLS)?.apply {
|
findPreference<Preference>(Preferences.PREFERENCE_DISPENSER_URLS)?.apply {
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
findNavController().navigate(R.id.dispenserFragment)
|
requireContext().navigate(Screen.Dispenser)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.viewmodel.dispenser
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aurora.store.util.Preferences
|
||||||
|
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.toMutableSet
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DispenserViewModel @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val sharedPreferences = Preferences.getPrefs(context)
|
||||||
|
|
||||||
|
private var _dispensers: Set<String>
|
||||||
|
set(value) = sharedPreferences.edit { putStringSet(PREFERENCE_DISPENSER_URLS, value) }
|
||||||
|
get() = sharedPreferences.getStringSet(PREFERENCE_DISPENSER_URLS, emptySet()) ?: emptySet()
|
||||||
|
|
||||||
|
val dispensers: StateFlow<Set<String>>
|
||||||
|
get() {
|
||||||
|
return callbackFlow {
|
||||||
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey ->
|
||||||
|
if (changedKey == PREFERENCE_DISPENSER_URLS) trySend(_dispensers)
|
||||||
|
}
|
||||||
|
|
||||||
|
trySend(_dispensers)
|
||||||
|
|
||||||
|
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||||
|
awaitClose {
|
||||||
|
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||||
|
}
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addDispenser(url: String) {
|
||||||
|
_dispensers = _dispensers.toMutableSet().apply {
|
||||||
|
add(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeDispenser(url: String) {
|
||||||
|
_dispensers = _dispensers.toMutableSet().apply {
|
||||||
|
remove(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".view.ui.dispenser.DispenserFragment">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minHeight="?attr/actionBarSize"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:navigationIcon="@drawable/ic_arrow_back"
|
|
||||||
app:title="@string/pref_dispenser_title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/no_dispensers_text_view"
|
|
||||||
style="@style/AuroraTextStyle.Subtitle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/no_dispensers_available"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
|
||||||
|
|
||||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
|
||||||
android:id="@+id/epoxy_recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:overScrollMode="never"
|
|
||||||
android:padding="@dimen/padding_normal"
|
|
||||||
android:scrollbars="none"
|
|
||||||
app:itemSpacing="@dimen/margin_small"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
|
||||||
tools:listitem="@layout/view_dispenser" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/add_fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="@dimen/margin_large"
|
|
||||||
android:contentDescription="@string/action_search"
|
|
||||||
app:backgroundTint="?colorAccent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_add"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?android:selectableItemBackground"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="@dimen/padding_small">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:id="@+id/url"
|
|
||||||
style="@style/AuroraTextStyle.Subtitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_toStartOf="@id/btn_action"
|
|
||||||
android:drawableStart="@drawable/ic_server"
|
|
||||||
android:drawablePadding="@dimen/padding_small"
|
|
||||||
tools:text="https://auroraoss.com/api/auth" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btn_action"
|
|
||||||
style="@style/Widget.Material3.Button.TextButton.Dialog.Flush"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/margin_small"
|
|
||||||
android:text="@string/remove" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
@@ -189,15 +189,6 @@
|
|||||||
android:name="com.aurora.store.view.ui.preferences.installation.InstallerFragment"
|
android:name="com.aurora.store.view.ui.preferences.installation.InstallerFragment"
|
||||||
android:label="InstallerFragment"
|
android:label="InstallerFragment"
|
||||||
tools:layout="@layout/fragment_installer" />
|
tools:layout="@layout/fragment_installer" />
|
||||||
<fragment
|
|
||||||
android:id="@+id/dispenserFragment"
|
|
||||||
android:name="com.aurora.store.view.ui.dispenser.DispenserFragment"
|
|
||||||
android:label="DispenserFragment"
|
|
||||||
tools:layout="@layout/fragment_dispenser">
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_dispenserFragment_to_removeDispenserDialog"
|
|
||||||
app:destination="@id/removeDispenserDialog" />
|
|
||||||
</fragment>
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/appMenuSheet"
|
android:id="@+id/appMenuSheet"
|
||||||
android:name="com.aurora.store.view.ui.sheets.AppMenuSheet"
|
android:name="com.aurora.store.view.ui.sheets.AppMenuSheet"
|
||||||
@@ -216,18 +207,6 @@
|
|||||||
android:id="@+id/moreDialogFragment"
|
android:id="@+id/moreDialogFragment"
|
||||||
android:name="com.aurora.store.view.ui.commons.MoreDialogFragment"
|
android:name="com.aurora.store.view.ui.commons.MoreDialogFragment"
|
||||||
android:label="MoreDialogFragment" />
|
android:label="MoreDialogFragment" />
|
||||||
<dialog
|
|
||||||
android:id="@+id/inputDispenserDialog"
|
|
||||||
android:name="com.aurora.store.view.ui.dispenser.InputDispenserDialog"
|
|
||||||
android:label="@string/add_dispenser_title" />
|
|
||||||
<dialog
|
|
||||||
android:id="@+id/removeDispenserDialog"
|
|
||||||
android:name="com.aurora.store.view.ui.dispenser.RemoveDispenserDialog"
|
|
||||||
android:label="@string/remove_dispenser_title">
|
|
||||||
<argument
|
|
||||||
android:name="url"
|
|
||||||
app:argType="string" />
|
|
||||||
</dialog>
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/proxyURLDialog"
|
android:id="@+id/proxyURLDialog"
|
||||||
android:name="com.aurora.store.view.ui.preferences.network.ProxyURLDialog"
|
android:name="com.aurora.store.view.ui.preferences.network.ProxyURLDialog"
|
||||||
|
|||||||
@@ -438,7 +438,7 @@
|
|||||||
<string name="add_dispenser_summary">Add a token dispenser to Aurora Store. Token dispensers provide account credentials to Aurora Store for logging in anonymously.</string>
|
<string name="add_dispenser_summary">Add a token dispenser to Aurora Store. Token dispensers provide account credentials to Aurora Store for logging in anonymously.</string>
|
||||||
<string name="add_dispenser_error">Invalid URL</string>
|
<string name="add_dispenser_error">Invalid URL</string>
|
||||||
<string name="add_dispenser_hint">Dispenser URL</string>
|
<string name="add_dispenser_hint">Dispenser URL</string>
|
||||||
<string name="remove_dispenser_title">Remove dispenser</string>
|
<string name="remove_dispenser_title">Remove dispenser?</string>
|
||||||
<string name="remove_dispenser_summary">Do you wish to remove the dispenser \"<xliff:g id="url">%1$s</xliff:g>\"?</string>
|
<string name="remove_dispenser_summary">Do you wish to remove the dispenser \"<xliff:g id="url">%1$s</xliff:g>\"?</string>
|
||||||
<string name="add">Add</string>
|
<string name="add">Add</string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user