android: Add settings screen (#196)
updates tailscale/corp#18202 updates ENG-2854 Adds a basic settings screen. This isn't correctly localized, but that's on the way. Adds the required hooks to edit prefs via localAPI. Adds basic but incomplete login/logout flow. Fixes the sorting of nodes on the main screen and fixes the proper display of your current node details. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>pull/197/head
parent
3926cf4b56
commit
bf0e56469f
@ -0,0 +1,76 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.service
|
||||
|
||||
import com.tailscale.ipn.ui.localapi.APIErrorVals
|
||||
import com.tailscale.ipn.ui.localapi.Result
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
|
||||
|
||||
// Handles all types of preference modifications typically invoked by the UI.
|
||||
// Callers generally shouldn't care about the returned prefs value - the source of
|
||||
// truth is the IPNModel, who's prefs flow will change in value to reflect the true
|
||||
// value of the pref setting in the back end (and will match the value returned here).
|
||||
// Generally, you will want to inspect the returned value in the callback for errors
|
||||
// to indicate why a particular setting did not change in the interface.
|
||||
//
|
||||
// Usage:
|
||||
// - User/Interface changed to new value. Render the new value.
|
||||
// - Submit the new value to the PrefsEditor
|
||||
// - Observe the prefs on the IpnModel and update the UI when/if the value changes.
|
||||
// For a typical flow, the changed value should reflect the value already shown.
|
||||
// - Inform the user of any error which may have occurred
|
||||
//
|
||||
// The "toggle' functions here will attempt to set the pref value to the inverse of
|
||||
// what is currently known in the IpnModel.prefs. If IpnModel.prefs is not available,
|
||||
// the callback will be called with a NO_PREFS error
|
||||
|
||||
fun IpnModel.setWantRunning(wantRunning: Boolean, callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
Ipn.MaskedPrefs().WantRunning = wantRunning
|
||||
apiClient.editPrefs(Ipn.MaskedPrefs(), callback)
|
||||
}
|
||||
|
||||
fun IpnModel.toggleCorpDNS(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = prefs.value ?: run {
|
||||
callback(Result(Error(APIErrorVals.NO_PREFS.rawValue)))
|
||||
return@toggleCorpDNS
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.CorpDNS = !prefs.CorpDNS
|
||||
apiClient.editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun IpnModel.toggleShieldsUp(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = prefs.value ?: run {
|
||||
callback(Result(Error(APIErrorVals.NO_PREFS.rawValue)))
|
||||
return@toggleShieldsUp
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.ShieldsUp = !prefs.ShieldsUp
|
||||
apiClient.editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun IpnModel.setExitNodeId(id: String, callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.ExitNodeId = id
|
||||
apiClient.editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun IpnModel.toggleRouteAll(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = prefs.value ?: run {
|
||||
callback(Result(Error(APIErrorVals.NO_PREFS.rawValue)))
|
||||
return@toggleRouteAll
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.RouteAll = !prefs.RouteAll
|
||||
apiClient.editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.util
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun settingsRowModifier(): Modifier {
|
||||
return Modifier
|
||||
.clip(shape = RoundedCornerShape(8.dp))
|
||||
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun defaultPaddingModifier(): Modifier {
|
||||
return Modifier.padding(8.dp)
|
||||
}
|
@ -1,12 +1,97 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
package com.tailscale.ipn.ui.viewModel
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tailscale.ipn.ui.service.IpnActions
|
||||
import com.tailscale.ipn.ui.service.IpnModel
|
||||
import com.tailscale.ipn.ui.service.toggleCorpDNS
|
||||
import com.tailscale.ipn.ui.view.SettingsNav
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class SettingType { NAV, SWITCH, NAV_WITH_TEXT }
|
||||
|
||||
// Represents a UI setting.
|
||||
// title: The title of the setting
|
||||
// type: The type of setting
|
||||
// enabled: Whether the setting is enabled
|
||||
// value: The value of the setting for textual settings
|
||||
// isOn: The value of the setting for switch settings
|
||||
// onClick: The action to take when the setting is clicked (typicall for navigation)
|
||||
// onToggle: The action to take when the setting is toggled (typically for switches)
|
||||
//
|
||||
// Behavior is undefined if you mix the types here. Switch settings should supply an
|
||||
// isOn and onToggle, while navigation settings should supply an onClick and an optional
|
||||
// value
|
||||
data class Setting(
|
||||
val title: String,
|
||||
val type: SettingType,
|
||||
val enabled: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val value: MutableStateFlow<String?>? = null,
|
||||
val isOn: MutableStateFlow<Boolean?>? = null,
|
||||
val onClick: () -> Unit = {},
|
||||
val onToggle: (Boolean) -> Unit = {})
|
||||
|
||||
data class SettingBundle(val title: String? = null, val settings: List<Setting>)
|
||||
|
||||
class SettingsViewModel(val model: IpnModel, val ipnActions: IpnActions, val navigation: SettingsNav) : ViewModel() {
|
||||
// The logged in user
|
||||
val user = model.loggedInUser.value
|
||||
|
||||
// Display name for the logged in user
|
||||
val userName = user?.UserProfile?.DisplayName ?: ""
|
||||
val tailnetName = user?.Name ?: ""
|
||||
val isAdmin = model.netmap.value?.SelfNode?.isAdmin ?: false
|
||||
|
||||
val useDNSSetting = Setting(
|
||||
"Use Tailscale DNS",
|
||||
SettingType.SWITCH,
|
||||
isOn = MutableStateFlow(model.prefs.value?.CorpDNS),
|
||||
onToggle = {
|
||||
model.toggleCorpDNS {
|
||||
// (jonathan) TODO: Error handling
|
||||
}
|
||||
})
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
// Monitor our prefs for changes and update the displayed values accordingly
|
||||
model.prefs.collect { prefs ->
|
||||
useDNSSetting.isOn?.value = prefs?.CorpDNS
|
||||
useDNSSetting.enabled?.value = prefs != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val settings: List<SettingBundle> = listOf(
|
||||
SettingBundle(settings = listOf(
|
||||
useDNSSetting,
|
||||
)),
|
||||
// General settings, always enabled
|
||||
SettingBundle(settings = listOf(
|
||||
Setting("About", SettingType.NAV, onClick = { navigation.onNavigateToAbout() }, enabled = MutableStateFlow(true)),
|
||||
Setting("Bug Report", SettingType.NAV, onClick = { navigation.onNavigateToBugReport() }, enabled = MutableStateFlow(true))
|
||||
))
|
||||
)
|
||||
|
||||
class SettingsViewModel(val model: IpnModel) : ViewModel() {
|
||||
fun adminText(): AnnotatedString {
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append("You can manage your account from the admin console. ")
|
||||
|
||||
pushStringAnnotation(tag = "policy", annotation = "https://google.com/policy")
|
||||
withStyle(style = SpanStyle(color = Color.Blue)) {
|
||||
append("View admin console...")
|
||||
}
|
||||
pop()
|
||||
}
|
||||
return annotatedString
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue