diff --git a/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt b/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt index f6d1c5c..e4df701 100644 --- a/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt +++ b/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt @@ -134,7 +134,7 @@ fun FileBrowser( // Parent directory item if (canNavigateUp) { item { - FileItem( + FileItemCard( name = "..", size = "", isDirectory = true, @@ -145,7 +145,7 @@ fun FileBrowser( } items(files) { file -> - FileItem( + FileItemCard( name = file.name, size = file.formattedSize, isIso = file.name.lowercase().endsWith(".iso"), @@ -164,7 +164,7 @@ fun FileBrowser( @OptIn(ExperimentalFoundationApi::class) @Composable -private fun FileItem( +fun FileItemCard( name: String, size: String, isDirectory: Boolean = false, diff --git a/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt b/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt index 048e1c4..95c5baa 100644 --- a/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt +++ b/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt @@ -5,18 +5,28 @@ package sh.sar.isodroid.ui.screens +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Album import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Eject -import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Settings +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton @@ -42,10 +52,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import sh.sar.isodroid.data.IsoFile -import sh.sar.isodroid.data.MountOptions import sh.sar.isodroid.ui.components.CreateImgDialog import sh.sar.isodroid.ui.components.FileContextMenu -import sh.sar.isodroid.ui.components.FileBrowser +import sh.sar.isodroid.ui.components.FileItemCard import sh.sar.isodroid.ui.components.MountDialog import sh.sar.isodroid.ui.components.StatusCard import sh.sar.isodroid.viewmodel.MainViewModel @@ -66,6 +75,22 @@ fun MainScreen( var showCreateImgDialog by remember { mutableStateOf(false) } var contextMenuFile by remember { mutableStateOf(null) } + val pullToRefreshState = rememberPullToRefreshState() + + // Handle pull-to-refresh + if (pullToRefreshState.isRefreshing) { + LaunchedEffect(true) { + viewModel.refresh() + } + } + + // Stop refreshing when loading completes + LaunchedEffect(uiState.isLoading) { + if (!uiState.isLoading) { + pullToRefreshState.endRefresh() + } + } + // Show error messages LaunchedEffect(uiState.errorMessage) { uiState.errorMessage?.let { message -> @@ -104,12 +129,6 @@ fun MainScreen( contentDescription = "Create IMG" ) } - IconButton(onClick = { viewModel.refresh() }) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Refresh" - ) - } IconButton(onClick = onNavigateToDownloads) { Icon( imageVector = Icons.Default.Download, @@ -147,45 +166,118 @@ fun MainScreen( } } ) { paddingValues -> - Column( + val hapticFeedback = LocalHapticFeedback.current + + Box( modifier = Modifier .fillMaxSize() .padding(paddingValues) - .padding(16.dp) + .nestedScroll(pullToRefreshState.nestedScrollConnection) ) { - StatusCard( - mountStatus = uiState.mountStatus, - rootAvailable = uiState.hasRoot, - deviceSupported = uiState.isSupported, - rootDenied = uiState.rootDenied, - onRequestRoot = { viewModel.requestRootAccess() } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - if (uiState.isLoading) { + if (uiState.isLoading && !pullToRefreshState.isRefreshing) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } - } else if (uiState.hasRoot == true && uiState.isSupported == true) { - FileBrowser( - files = uiState.isoFiles, - currentPath = uiState.currentPath, - onFileClick = { file -> - selectedFile = file - showMountDialog = true - }, - onFileLongClick = { file -> - contextMenuFile = file - }, - onNavigateUp = { viewModel.navigateUp() }, - canNavigateUp = viewModel.canNavigateUp(), - modifier = Modifier.weight(1f) - ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Status card + item { + StatusCard( + mountStatus = uiState.mountStatus, + rootAvailable = uiState.hasRoot, + deviceSupported = uiState.isSupported, + rootDenied = uiState.rootDenied, + onRequestRoot = { viewModel.requestRootAccess() } + ) + } + + // File browser content + if (uiState.hasRoot == true && uiState.isSupported == true) { + if (uiState.isoFiles.isEmpty()) { + // Empty state + item { + Column( + modifier = Modifier + .fillParentMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.Album, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "No ISO/IMG files found", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Place ISO or IMG files in:\n${uiState.currentPath}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Tap + to create an empty IMG file\nChange directory in Settings", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } + } else { + // Parent directory navigation + if (viewModel.canNavigateUp()) { + item { + FileItemCard( + name = "..", + size = "", + isDirectory = true, + onClick = { viewModel.navigateUp() }, + onLongClick = null + ) + } + } + + // File list + items(uiState.isoFiles) { file -> + FileItemCard( + name = file.name, + size = file.formattedSize, + isIso = file.name.lowercase().endsWith(".iso"), + isImg = file.name.lowercase().endsWith(".img"), + onClick = { + selectedFile = file + showMountDialog = true + }, + onLongClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + contextMenuFile = file + } + ) + } + } + } + } } + + PullToRefreshContainer( + state = pullToRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } }