diff --git a/app/src/main/java/com/aurora/store/compose/composable/DispenserListItem.kt b/app/src/main/java/com/aurora/store/compose/composable/DispenserListItem.kt new file mode 100644 index 000000000..db2654916 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/DispenserListItem.kt @@ -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") +} diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index df528f12a..1ac5fd84c 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -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.details.AppDetailsScreen 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.favourite.FavouriteScreen import com.aurora.store.compose.ui.onboarding.OnboardingScreen @@ -135,6 +136,10 @@ fun NavDisplay(startDestination: NavKey) { onNavigateToSplash = { activity?.startActivity(splashIntent) } ) } + + entry { + DispenserScreen(onNavigateUp = ::onNavigateUp) + } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index e64a88362..b7e41759b 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -54,4 +54,7 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object Spoof : Screen() + + @Serializable + data object Dispenser : Screen() } diff --git a/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt new file mode 100644 index 000000000..9ee02c1c7 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt @@ -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 = 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() + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/dispenser/InputDispenserDialog.kt b/app/src/main/java/com/aurora/store/compose/ui/dispenser/InputDispenserDialog.kt new file mode 100644 index 000000000..2201e5a3f --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/dispenser/InputDispenserDialog.kt @@ -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() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/dispenser/RemoveDispenserDialog.kt b/app/src/main/java/com/aurora/store/compose/ui/dispenser/RemoveDispenserDialog.kt new file mode 100644 index 000000000..b0ecabce6 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/dispenser/RemoveDispenserDialog.kt @@ -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/") +} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/DispenserView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/DispenserView.kt deleted file mode 100644 index a0735ad69..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/DispenserView.kt +++ /dev/null @@ -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(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) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/dispenser/DispenserFragment.kt b/app/src/main/java/com/aurora/store/view/ui/dispenser/DispenserFragment.kt deleted file mode 100644 index 8860351a6..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/dispenser/DispenserFragment.kt +++ /dev/null @@ -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(), - SharedPreferences.OnSharedPreferenceChangeListener { - - private val dispensers: Set - 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 - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/dispenser/InputDispenserDialog.kt b/app/src/main/java/com/aurora/store/view/ui/dispenser/InputDispenserDialog.kt deleted file mode 100644 index 2f774a043..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/dispenser/InputDispenserDialog.kt +++ /dev/null @@ -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 - 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) - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/dispenser/RemoveDispenserDialog.kt b/app/src/main/java/com/aurora/store/view/ui/dispenser/RemoveDispenserDialog.kt deleted file mode 100644 index 82b0f6e35..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/dispenser/RemoveDispenserDialog.kt +++ /dev/null @@ -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 - 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) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/preferences/network/NetworkPreference.kt b/app/src/main/java/com/aurora/store/view/ui/preferences/network/NetworkPreference.kt index 30cba12fa..39dd1d7e5 100644 --- a/app/src/main/java/com/aurora/store/view/ui/preferences/network/NetworkPreference.kt +++ b/app/src/main/java/com/aurora/store/view/ui/preferences/network/NetworkPreference.kt @@ -25,9 +25,11 @@ import androidx.appcompat.widget.Toolbar import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.SwitchPreferenceCompat +import com.aurora.extensions.navigate import com.aurora.extensions.runOnUiThread import com.aurora.extensions.toast import com.aurora.store.R +import com.aurora.store.compose.navigation.Screen import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH @@ -45,7 +47,7 @@ class NetworkPreference : BasePreferenceFragment() { findPreference(Preferences.PREFERENCE_DISPENSER_URLS)?.apply { setOnPreferenceClickListener { - findNavController().navigate(R.id.dispenserFragment) + requireContext().navigate(Screen.Dispenser) true } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/dispenser/DispenserViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/dispenser/DispenserViewModel.kt new file mode 100644 index 000000000..842528ffe --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/dispenser/DispenserViewModel.kt @@ -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 + set(value) = sharedPreferences.edit { putStringSet(PREFERENCE_DISPENSER_URLS, value) } + get() = sharedPreferences.getStringSet(PREFERENCE_DISPENSER_URLS, emptySet()) ?: emptySet() + + val dispensers: StateFlow> + 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) + } + } +} diff --git a/app/src/main/res/layout/fragment_dispenser.xml b/app/src/main/res/layout/fragment_dispenser.xml deleted file mode 100644 index 7ea222f94..000000000 --- a/app/src/main/res/layout/fragment_dispenser.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/view_dispenser.xml b/app/src/main/res/layout/view_dispenser.xml deleted file mode 100644 index f36622a2d..000000000 --- a/app/src/main/res/layout/view_dispenser.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 6a5d5ead3..690685184 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -189,15 +189,6 @@ android:name="com.aurora.store.view.ui.preferences.installation.InstallerFragment" android:label="InstallerFragment" tools:layout="@layout/fragment_installer" /> - - - - - - - Add a token dispenser to Aurora Store. Token dispensers provide account credentials to Aurora Store for logging in anonymously. Invalid URL Dispenser URL - Remove dispenser + Remove dispenser? Do you wish to remove the dispenser \"%1$s\"? Add Remove