mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-21 07:18:05 -04:00
Hoist IPFS state to IpfsManager
and observe that state in UI. Also move all action code into the manager.
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
package org.fdroid.ui.ipfs
|
||||
|
||||
/**
|
||||
* Interface for accessing IPFS related preferences.
|
||||
*
|
||||
* Splitting this into a separate interfaces allows for easily injecting data into Compose UI
|
||||
* previews. (and maybe unit-tests too)
|
||||
*/
|
||||
interface IPreferencesIpfs {
|
||||
var isIpfsEnabled: Boolean
|
||||
var ipfsGwDisabledDefaults: List<String>
|
||||
var ipfsGwUserList: List<String>
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.ipfs.IpfsManager.Companion.DEFAULT_GATEWAYS
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -21,18 +21,8 @@ class IpfsGatewaySettingsActivity : AppCompatActivity() {
|
||||
setContent {
|
||||
FDroidContent {
|
||||
SettingsScreen(
|
||||
prefs = manager,
|
||||
onAddUserGateway = { url ->
|
||||
// don't allow adding default gateways to the user gateways list
|
||||
if (!DEFAULT_GATEWAYS.contains(url)) {
|
||||
val updatedUserGwList = manager.ipfsGwUserList.toMutableList()
|
||||
// don't allow double adding gateways
|
||||
if (!updatedUserGwList.contains(url)) {
|
||||
updatedUserGwList.add(url)
|
||||
manager.ipfsGwUserList = updatedUserGwList
|
||||
}
|
||||
}
|
||||
},
|
||||
prefs = manager.preferences.collectAsStateWithLifecycle().value,
|
||||
actions = manager,
|
||||
onBackClicked = { onBackPressedDispatcher.onBackPressed() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.fdroid.ui.ipfs
|
||||
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.fdroid.settings.SettingsManager
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@@ -10,7 +13,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class IpfsManager @Inject constructor(
|
||||
settingsManager: SettingsManager,
|
||||
) : IPreferencesIpfs {
|
||||
) : IpfsActions {
|
||||
|
||||
companion object {
|
||||
val DEFAULT_GATEWAYS = listOf(
|
||||
@@ -23,14 +26,17 @@ class IpfsManager @Inject constructor(
|
||||
|
||||
private val prefs = settingsManager.prefs
|
||||
|
||||
override var isIpfsEnabled: Boolean
|
||||
private val _preferences = MutableStateFlow(loadPreferences())
|
||||
val preferences = _preferences.asStateFlow()
|
||||
|
||||
private var enabled: Boolean
|
||||
get() = prefs.getBoolean(PREF_USE_IPFS_GATEWAYS, false)
|
||||
set(value) {
|
||||
prefs.edit {
|
||||
putBoolean(PREF_USE_IPFS_GATEWAYS, value)
|
||||
}
|
||||
}
|
||||
override var ipfsGwDisabledDefaults: List<String>
|
||||
private var disabledDefaultGateways: List<String>
|
||||
get() {
|
||||
val list = prefs.getString(PREF_IPFSGW_DISABLED_DEFAULTS_LIST, "[]") ?: "[]"
|
||||
return parseJsonStringArray(list)
|
||||
@@ -40,7 +46,7 @@ class IpfsManager @Inject constructor(
|
||||
putString(PREF_IPFSGW_DISABLED_DEFAULTS_LIST, toJsonStringArray(value))
|
||||
}
|
||||
}
|
||||
override var ipfsGwUserList: List<String>
|
||||
private var userGateways: List<String>
|
||||
get() = parseJsonStringArray(prefs.getString(PREF_IPFSGW_USER_LIST, "[]") ?: "[]")
|
||||
set(value) {
|
||||
prefs.edit {
|
||||
@@ -48,6 +54,55 @@ class IpfsManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPreferences() = IpfsPreferences(
|
||||
isIpfsEnabled = prefs.getBoolean(PREF_USE_IPFS_GATEWAYS, false),
|
||||
disabledDefaultGateways = parseJsonStringArray(
|
||||
prefs.getString(PREF_IPFSGW_DISABLED_DEFAULTS_LIST, "[]") ?: "[]"
|
||||
),
|
||||
userGateways = parseJsonStringArray(prefs.getString(PREF_IPFSGW_USER_LIST, "[]") ?: "[]"),
|
||||
)
|
||||
|
||||
override fun setIpfsEnabled(enabled: Boolean) {
|
||||
_preferences.update {
|
||||
it.copy(isIpfsEnabled = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDefaultGatewayEnabled(url: String, enabled: Boolean) {
|
||||
_preferences.update {
|
||||
val newList = it.disabledDefaultGateways.toMutableList()
|
||||
if (!enabled) {
|
||||
newList.add(url)
|
||||
} else {
|
||||
newList.remove(url)
|
||||
}
|
||||
disabledDefaultGateways = newList
|
||||
it.copy(disabledDefaultGateways = newList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addUserGateway(url: String) {
|
||||
// don't allow adding default gateways to the user gateways list
|
||||
if (!DEFAULT_GATEWAYS.contains(url)) _preferences.update {
|
||||
if (it.userGateways.contains(url)) {
|
||||
it // already has URL in list
|
||||
} else {
|
||||
val newList = it.userGateways.toMutableList().apply {
|
||||
add(url)
|
||||
}
|
||||
userGateways = newList
|
||||
it.copy(userGateways = newList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeUserGateway(url: String) = _preferences.update {
|
||||
val newGateways = it.userGateways.toMutableList()
|
||||
newGateways.remove(url)
|
||||
userGateways = newGateways
|
||||
it.copy(userGateways = newGateways)
|
||||
}
|
||||
|
||||
private fun parseJsonStringArray(json: String): List<String> {
|
||||
try {
|
||||
val jsonArray = JSONArray(json)
|
||||
|
||||
14
app/src/full/kotlin/org/fdroid/ui/ipfs/IpfsPreferences.kt
Normal file
14
app/src/full/kotlin/org/fdroid/ui/ipfs/IpfsPreferences.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.fdroid.ui.ipfs
|
||||
|
||||
interface IpfsActions {
|
||||
fun setIpfsEnabled(enabled: Boolean)
|
||||
fun setDefaultGatewayEnabled(url: String, enabled: Boolean)
|
||||
fun addUserGateway(url: String)
|
||||
fun removeUserGateway(url: String)
|
||||
}
|
||||
|
||||
data class IpfsPreferences(
|
||||
val isIpfsEnabled: Boolean,
|
||||
val disabledDefaultGateways: List<String>,
|
||||
val userGateways: List<String>,
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.fdroid.ui.ipfs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -24,23 +23,16 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import org.fdroid.R
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.ipfs.IpfsManager.Companion.DEFAULT_GATEWAYS
|
||||
@@ -48,14 +40,11 @@ import org.fdroid.ui.ipfs.IpfsManager.Companion.DEFAULT_GATEWAYS
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun SettingsScreen(
|
||||
onAddUserGateway: (url: String) -> Unit,
|
||||
prefs: IpfsPreferences,
|
||||
actions: IpfsActions,
|
||||
onBackClicked: () -> Unit,
|
||||
prefs: IPreferencesIpfs,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
var ipfsEnabled by remember { mutableStateOf(prefs.isIpfsEnabled) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -73,7 +62,7 @@ fun SettingsScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
// it doesn't seam to be supported to disable FABs, so just hide it for now.
|
||||
if (ipfsEnabled) {
|
||||
if (prefs.isIpfsEnabled) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
showAddDialog = true
|
||||
@@ -100,32 +89,25 @@ fun SettingsScreen(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(checked = ipfsEnabled, onCheckedChange = { checked ->
|
||||
ipfsEnabled = checked
|
||||
prefs.isIpfsEnabled = checked
|
||||
})
|
||||
Switch(checked = prefs.isIpfsEnabled, onCheckedChange = actions::setIpfsEnabled)
|
||||
}
|
||||
DefaultGatewaysSettings(prefs = prefs, ipfsEnabled = ipfsEnabled)
|
||||
UserGatewaysSettings(prefs = prefs, ipfsEnabled = ipfsEnabled)
|
||||
DefaultGatewaysSettings(prefs, actions)
|
||||
UserGatewaysSettings(prefs, actions)
|
||||
// make sure FAB doesn't occlude the delete button of the last user gateway
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
}
|
||||
if (showAddDialog) AddGatewaysDialog(onAddUserGateway) { showAddDialog = false }
|
||||
if (showAddDialog) AddGatewaysDialog(actions::addUserGateway) { showAddDialog = false }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MutableCollectionMutableState")
|
||||
@Composable
|
||||
fun DefaultGatewaysSettings(
|
||||
prefs: IPreferencesIpfs,
|
||||
ipfsEnabled: Boolean,
|
||||
) {
|
||||
var disabledDefaultGateways by remember { mutableStateOf(prefs.ipfsGwDisabledDefaults) }
|
||||
|
||||
fun DefaultGatewaysSettings(prefs: IpfsPreferences, actions: IpfsActions) {
|
||||
Column {
|
||||
CaptionText(
|
||||
Text(
|
||||
text = stringResource(id = R.string.ipfsgw_caption_official_gateways),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(0.dp, 16.dp, 0.dp, 4.dp)
|
||||
)
|
||||
for (gatewayUrl in DEFAULT_GATEWAYS) {
|
||||
Row(
|
||||
@@ -139,21 +121,14 @@ fun DefaultGatewaysSettings(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.alpha(if (ipfsEnabled) 1f else 0.5f)
|
||||
.alpha(if (prefs.isIpfsEnabled) 1f else 0.5f)
|
||||
)
|
||||
Switch(
|
||||
checked = !disabledDefaultGateways.contains(gatewayUrl),
|
||||
checked = !prefs.disabledDefaultGateways.contains(gatewayUrl),
|
||||
onCheckedChange = { checked ->
|
||||
val newList = disabledDefaultGateways.toMutableList()
|
||||
if (!checked) {
|
||||
newList.add(gatewayUrl)
|
||||
} else {
|
||||
newList.remove(gatewayUrl)
|
||||
}
|
||||
disabledDefaultGateways = newList
|
||||
prefs.ipfsGwDisabledDefaults = newList
|
||||
actions.setDefaultGatewayEnabled(gatewayUrl, checked)
|
||||
},
|
||||
enabled = ipfsEnabled,
|
||||
enabled = prefs.isIpfsEnabled,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
@@ -161,31 +136,21 @@ fun DefaultGatewaysSettings(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MutableCollectionMutableState")
|
||||
@Composable
|
||||
fun UserGatewaysSettings(
|
||||
prefs: IPreferencesIpfs,
|
||||
ipfsEnabled: Boolean,
|
||||
) {
|
||||
var userGateways by remember { mutableStateOf(prefs.ipfsGwUserList) }
|
||||
|
||||
// Make sure list get updated when user returns from IpfsGatewayAddActivity
|
||||
LifecycleEventListener { _, event ->
|
||||
if (Lifecycle.Event.ON_RESUME == event) {
|
||||
userGateways = prefs.ipfsGwUserList
|
||||
}
|
||||
}
|
||||
|
||||
fun UserGatewaysSettings(prefs: IpfsPreferences, actions: IpfsActions) {
|
||||
Column {
|
||||
if (userGateways.isNotEmpty()) {
|
||||
CaptionText(text = stringResource(id = R.string.ipfsgw_caption_custom_gateways))
|
||||
if (prefs.userGateways.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ipfsgw_caption_custom_gateways),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(0.dp, 16.dp, 0.dp, 4.dp)
|
||||
)
|
||||
}
|
||||
for (gatewayUrl in userGateways) {
|
||||
for (gatewayUrl in prefs.userGateways) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(48.dp, 4.dp, 0.dp, 4.dp)
|
||||
|
||||
) {
|
||||
Text(
|
||||
text = gatewayUrl,
|
||||
@@ -193,17 +158,11 @@ fun UserGatewaysSettings(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.alpha(if (ipfsEnabled) 1f else 0.5f)
|
||||
.alpha(if (prefs.isIpfsEnabled) 1f else 0.5f)
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
val newGateways = userGateways.toMutableList()
|
||||
newGateways.remove(gatewayUrl)
|
||||
|
||||
userGateways = newGateways
|
||||
prefs.ipfsGwUserList = newGateways
|
||||
},
|
||||
enabled = ipfsEnabled,
|
||||
onClick = { actions.removeUserGateway(gatewayUrl) },
|
||||
enabled = prefs.isIpfsEnabled,
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
) {
|
||||
Icon(
|
||||
@@ -216,64 +175,22 @@ fun UserGatewaysSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that mimics MDC TextView with `@style/CaptionText`
|
||||
*/
|
||||
@Composable
|
||||
private fun CaptionText(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(0.dp, 16.dp, 0.dp, 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A tiny helper for consuming Activity lifecycle events.
|
||||
*
|
||||
* copied from https://stackoverflow.com/a/66807899
|
||||
*
|
||||
* There is also an official API for consuming lifecycle events. However, at the time of writing
|
||||
* it's not stable and I also couldn't find any actually working code snippets demonstrating
|
||||
* its use. "androidx.lifecycle:lifecycle-runtime-compose"
|
||||
*/
|
||||
@Composable
|
||||
fun LifecycleEventListener(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) {
|
||||
val eventHandler = rememberUpdatedState(onEvent)
|
||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
DisposableEffect(lifecycleOwner.value) {
|
||||
val lifecycle = lifecycleOwner.value.lifecycle
|
||||
val observer = LifecycleEventObserver { owner, event ->
|
||||
eventHandler.value(owner, event)
|
||||
}
|
||||
|
||||
lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SettingsScreenPreview() {
|
||||
val prefs = object : IPreferencesIpfs {
|
||||
override var isIpfsEnabled: Boolean
|
||||
get() = true
|
||||
set(_) {}
|
||||
override var ipfsGwDisabledDefaults: List<String>
|
||||
get() = listOf("https://4everland.io/ipfs/")
|
||||
set(_) {}
|
||||
override var ipfsGwUserList: List<String>
|
||||
get() = listOf("https://my.imaginary.gateway/ifps/")
|
||||
set(_) {}
|
||||
}
|
||||
|
||||
FDroidContent {
|
||||
SettingsScreen(
|
||||
prefs = prefs,
|
||||
onAddUserGateway = {},
|
||||
prefs = IpfsPreferences(
|
||||
isIpfsEnabled = true,
|
||||
disabledDefaultGateways = listOf("https://4everland.io/ipfs/"),
|
||||
userGateways = listOf("https://my.imaginary.gateway/ifps/")
|
||||
),
|
||||
actions = object : IpfsActions {
|
||||
override fun setIpfsEnabled(enabled: Boolean) {}
|
||||
override fun setDefaultGatewayEnabled(url: String, enabled: Boolean) {}
|
||||
override fun addUserGateway(url: String) {}
|
||||
override fun removeUserGateway(url: String) {}
|
||||
},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user