@ -3,6 +3,9 @@
package com.tailscale.ipn.ui.view
package com.tailscale.ipn.ui.view
import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement
@ -36,8 +39,10 @@ import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clip
@ -49,6 +54,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import com.tailscale.ipn.R
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.IpnLocal
@ -60,6 +69,7 @@ import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.flag
import com.tailscale.ipn.ui.util.flag
import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.MainViewModel
import com.tailscale.ipn.ui.viewModel.MainViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
// Navigation actions for the MainView
// Navigation actions for the MainView
@ -69,6 +79,7 @@ data class MainViewNavigation(
val onNavigateToExitNodes : ( ) -> Unit
val onNavigateToExitNodes : ( ) -> Unit
)
)
@OptIn ( ExperimentalPermissionsApi :: class )
@Composable
@Composable
fun MainView ( navigation : MainViewNavigation , viewModel : MainViewModel = viewModel ( ) ) {
fun MainView ( navigation : MainViewNavigation , viewModel : MainViewModel = viewModel ( ) ) {
LoadingIndicator . Wrap {
LoadingIndicator . Wrap {
@ -108,6 +119,8 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
when ( state . value ) {
when ( state . value ) {
Ipn . State . Running -> {
Ipn . State . Running -> {
PromptWriteStoragePermissionsIfNecessary ( )
val selfPeerId = viewModel . selfPeerId . collectAsState ( initial = " " )
val selfPeerId = viewModel . selfPeerId . collectAsState ( initial = " " )
Row (
Row (
modifier =
modifier =
@ -395,3 +408,34 @@ fun PeerList(
}
}
}
}
}
}
@OptIn ( ExperimentalPermissionsApi :: class )
@Composable
fun PromptWriteStoragePermissionsIfNecessary ( ) {
val writeStoragePermissionState =
rememberPermissionState ( Manifest . permission . WRITE _EXTERNAL _STORAGE )
val showDialog = remember { MutableStateFlow ( false ) }
val requestPermissionLauncher =
rememberLauncherForActivityResult ( ActivityResultContracts . RequestPermission ( ) ) { granted ->
if ( ! granted ) {
showDialog . value = true
}
}
LaunchedEffect ( writeStoragePermissionState ) {
if ( ! writeStoragePermissionState . status . isGranted &&
writeStoragePermissionState . status . shouldShowRationale ) {
showDialog . value = true
} else {
requestPermissionLauncher . launch ( Manifest . permission . WRITE _EXTERNAL _STORAGE )
}
}
if ( showDialog . collectAsState ( ) . value ) {
ErrorDialog ( title = R . string . permission _required , message = R . string . taildrop _requires _write ) {
showDialog . value = false
}
}
}