Folder import\sync rework (#18)
* feat: rework Folder Sync architecture to separate Managed vs Linked books - Introduced "Managed" (imported to app storage) vs "Linked" (SAF URI) book logic. - Disabled Google Drive/Metadata synchronization for Folder-Linked books to respect privacy and storage. - Updated deletion logic: Folder books are now marked as deleted in the DB to blacklist them from future auto-scans, while Managed books are permanently purged from local and cloud storage. - Enhanced folder sync discoverability by adding a direct "Sync Folder" navigation button in Home screen. - Implemented reactive pager navigation in MainScreen and LibraryScreen via LaunchedEffect to allow programmatic tab switching from the ViewModel. * Implemented a local folder synchronization system to track and update book metadata across devices. Key changes: - Added `FolderBookMetadata` and `LocalSyncUtils` for serializing and managing metadata files in a `.episteme` directory. - Implemented bidirectional sync in `FolderSyncWorker` to reconcile local database state with folder-based metadata using timestamps and Syncthing conflict resolution. - Updated `MainViewModel` to trigger folder metadata sync when closing a book and verify for remote updates upon opening. - Enhanced folder management with manual scan support via `WorkManager` and automatic cleanup of database entries when a folder is disconnected. - Added `RecentFileDao` methods to support bulk deletion and prefix-based file lookups. * Removed API 34 restriction for chapter processing and updated local folder sync directory name * Increased TTS timeouts, added edge-to-edge support in MainScreen, and improved TtsChunk mapping safety * Improved folder synchronization and metadata management. - Added metadata-only sync option and triggered it on app start. - Implemented hidden metadata filenames (prefixed with `.`) to reduce file clutter. - Added automatic creation of `.nomedia` files in the sync directory. - Enhanced sync conflict resolution by picking the latest metadata and cleaning up obsolete or orphaned files. - Added "Sync Metadata" button to the Library screen. - Updated UI labels for local folder sync to clarify Google Drive integration. - Refactored `FolderSyncWorker` to support targeted metadata-only synchronization. * Implement folder sync migration and refactoring - Added a migration dialog to inform users about the folder sync refactor, where books are now read directly from external storage. - Updated `MainViewModel` to handle migration state and trigger a full scan upon completion or dismissal of the dialog. - Modified `FolderSyncWorker` to support legacy book matching during migration, updating file URIs and cleaning up internal app storage for migrated books. - Improved synchronization logic between local and remote metadata, including cleanup of orphaned metadata files. - Refined `RecentFileItem` updates during sync to preserve progress and bookmarks based on last modified timestamps. * Implemented pull-to-refresh for library sync and enhanced folder file deletion. - Added `isRefreshing` state to `MainViewModel` and integrated `PullToRefreshBox` in `HomeScreen`. - Implemented `refreshLibrary` to trigger cloud and folder metadata synchronization. - Updated deletion logic to physically remove files and metadata from synced local folders. - Added a warning to the delete confirmation dialog when removing folder-synced items. - Improved folder sync reliability by performing lazy cleanup of missing files during interaction and sync. - Fixed a bug where sync loading indicators would not retract on failure or cancellation. * Improved bookmark navigation and scroll synchronization in EPUB reader - Refined bookmark navigation logic for vertical scroll mode to handle chunk injection more reliably. - Added `isNavigatingToBookmark` state to show a loading overlay during long jumps. - Implemented `onScrollFinished` callback in `CfiJsBridge` to synchronize UI state with WebView scroll completion. - Updated `ChapterWebView` to use `rememberUpdatedState` for JavaScript bridge callbacks to ensure data consistency. - Improved `getCurrentCfi` and `scrollToCfi` in `epub_reader.js` with better visibility probing and retry logic for detached nodes. * Implemented background metadata extraction for folder sync. Key changes: - Added `MetadataExtractionWorker` to handle heavy metadata and cover extraction for files in the background. - Refactored `FolderSyncWorker` to perform fast file discovery using placeholders, enqueuing the metadata worker upon completion. - Added `getFolderBooksWithoutCovers` query to `RecentFileDao`. - Updated UI components (`EmptyState`, `MainViewModel`) to improve user feedback during sync and provide clear setup actions. - Enhanced `EmptyState` composable to support secondary actions and custom button text. * Updated Library Screen UI
This commit is contained in:
parent
60f6566b31
commit
a81df6921d
21 changed files with 3378 additions and 2478 deletions
|
|
@ -17,13 +17,17 @@
|
|||
*
|
||||
* mail: epistemereader@gmail.com
|
||||
*/
|
||||
// LibraryScreen.kt
|
||||
package com.aryan.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -49,14 +53,20 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.FolderSpecial
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -67,6 +77,7 @@ import androidx.compose.material3.Tab
|
|||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -79,6 +90,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -88,6 +100,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
|
|
@ -96,20 +110,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FolderSpecial
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import androidx.core.net.toUri
|
||||
|
||||
private fun getBookCountString(count: Int): String {
|
||||
return if (count == 1) "1 book" else "$count books"
|
||||
|
|
@ -130,6 +133,17 @@ fun LibraryScreen(
|
|||
initialPage = uiState.libraryScreenStartPage,
|
||||
pageCount = { 3 }
|
||||
)
|
||||
|
||||
val containsFolderItems = remember(selectedItems) {
|
||||
selectedItems.any { it.sourceFolderUri != null }
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.libraryScreenStartPage) {
|
||||
if (pagerState.currentPage != uiState.libraryScreenStartPage) {
|
||||
pagerState.animateScrollToPage(uiState.libraryScreenStartPage)
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
|
|
@ -160,8 +174,11 @@ fun LibraryScreen(
|
|||
pickFileLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
viewModel.setLibraryScreenPage(pagerState.currentPage)
|
||||
LaunchedEffect(pagerState) {
|
||||
androidx.compose.runtime.snapshotFlow { pagerState.settledPage }
|
||||
.collect { page ->
|
||||
viewModel.setLibraryScreenPage(page)
|
||||
}
|
||||
}
|
||||
|
||||
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -217,6 +234,7 @@ fun LibraryScreen(
|
|||
onNewShelfClick = viewModel::showCreateShelfDialog,
|
||||
onSelectFileClick = onSelectFileClick,
|
||||
onScanNowClick = viewModel::scanSyncedFolder,
|
||||
onSyncMetadataClick = viewModel::syncFolderMetadata,
|
||||
onSelectSyncFolderClick = onSelectSyncFolderClick,
|
||||
onDisconnectSyncFolderClick = viewModel::disconnectSyncedFolder,
|
||||
downloadingBookIds = uiState.downloadingBookIds,
|
||||
|
|
@ -241,9 +259,11 @@ fun LibraryScreen(
|
|||
showDeleteConfirmDialog = false
|
||||
},
|
||||
onDismiss = { showDeleteConfirmDialog = false },
|
||||
isPermanentDelete = true
|
||||
isPermanentDelete = true,
|
||||
containsFolderItems = containsFolderItems
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteShelvesDialog) {
|
||||
DeleteShelvesConfirmationDialog(
|
||||
count = selectedShelves.size,
|
||||
|
|
@ -409,6 +429,7 @@ fun LibraryScreenContent(
|
|||
onNewShelfClick: () -> Unit,
|
||||
onSelectFileClick: () -> Unit,
|
||||
onScanNowClick: () -> Unit,
|
||||
onSyncMetadataClick: () -> Unit,
|
||||
onSelectSyncFolderClick: () -> Unit,
|
||||
onDisconnectSyncFolderClick: () -> Unit,
|
||||
downloadingBookIds: Set<String>,
|
||||
|
|
@ -621,6 +642,7 @@ fun LibraryScreenContent(
|
|||
lastScanTime = lastFolderScanTime,
|
||||
onSelectFolderClick = onSelectSyncFolderClick,
|
||||
onScanNowClick = onScanNowClick,
|
||||
onSyncMetadataClick = onSyncMetadataClick,
|
||||
onChangeFolderClick = onSelectSyncFolderClick,
|
||||
onDisconnectClick = onDisconnectSyncFolderClick,
|
||||
isLoading = isLoading
|
||||
|
|
@ -1147,6 +1169,16 @@ private fun LibraryListItem(
|
|||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (item.sourceFolderUri != null) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = item.title ?: item.displayName,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
|
|
@ -1326,6 +1358,7 @@ private fun FolderSyncScreen(
|
|||
lastScanTime: Long?,
|
||||
onSelectFolderClick: () -> Unit,
|
||||
onScanNowClick: () -> Unit,
|
||||
onSyncMetadataClick: () -> Unit,
|
||||
onChangeFolderClick: () -> Unit,
|
||||
onDisconnectClick: () -> Unit,
|
||||
isLoading: Boolean
|
||||
|
|
@ -1334,9 +1367,10 @@ private fun FolderSyncScreen(
|
|||
|
||||
if (syncedFolderUri == null) {
|
||||
EmptyState(
|
||||
title = "Import files from a Folder",
|
||||
message = "Select a folder on your device. Episteme will automatically find and import any new books you add to it.",
|
||||
title = "Sync Local Folder",
|
||||
message = "Connect a folder to create a live library. Episteme will automatically monitor your files for new additions and keep your reading progress and other book metadata in sync with your folder.",
|
||||
onSelectFileClick = onSelectFolderClick,
|
||||
primaryButtonText = "Select Folder",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
|
|
@ -1344,86 +1378,224 @@ private fun FolderSyncScreen(
|
|||
getDisplayPathFromUri(context, syncedFolderUri)
|
||||
}
|
||||
|
||||
// Calculate times
|
||||
val dateFormat = remember { SimpleDateFormat("MMM d, h:mm a", Locale.getDefault()) }
|
||||
|
||||
val lastScanText = remember(lastScanTime) {
|
||||
if (lastScanTime == null || lastScanTime == 0L) {
|
||||
"Never scanned"
|
||||
} else {
|
||||
"Last scan: ${
|
||||
SimpleDateFormat("MMM d, yyyy h:mm a", Locale.getDefault()).format(
|
||||
Date(lastScanTime)
|
||||
)
|
||||
}"
|
||||
if (lastScanTime == null || lastScanTime == 0L) "Never"
|
||||
else dateFormat.format(Date(lastScanTime))
|
||||
}
|
||||
|
||||
val nextScanText = remember(lastScanTime) {
|
||||
if (lastScanTime == null || lastScanTime == 0L) "Pending first scan..."
|
||||
else {
|
||||
// Adding 4 hours (4 * 60 * 60 * 1000) to match the Worker interval
|
||||
val nextTime = lastScanTime + (4 * 60 * 60 * 1000)
|
||||
dateFormat.format(Date(nextTime))
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderSpecial, // Using a standard icon
|
||||
contentDescription = "Synced Folder",
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Folder Sync is Active",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth() // Add this modifier
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Monitoring folder:",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = folderPath,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (isLoading) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Scanning...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = lastScanText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth() // Add this modifier
|
||||
// 1. Status Dashboard Card
|
||||
androidx.compose.material3.ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = androidx.compose.material3.CardDefaults.elevatedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Header Row with Status
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderSpecial,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Active Sync",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Button(onClick = onScanNowClick, enabled = !isLoading) {
|
||||
Text("Scan for New Books")
|
||||
// Status Indicator
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
shape = androidx.compose.foundation.shape.CircleShape
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(12.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
"Scanning...",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(
|
||||
Color(0xFF4CAF50), // Green for active
|
||||
androidx.compose.foundation.shape.CircleShape
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
"Monitoring",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
|
||||
// Folder Path
|
||||
Column {
|
||||
Text(
|
||||
text = "LOCATION",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = folderPath,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
// Times Grid
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "LAST CHECK",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = lastScanText, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "NEXT AUTO SYNC",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = nextScanText, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(onClick = onChangeFolderClick, enabled = !isLoading) {
|
||||
Text("Change Folder")
|
||||
|
||||
// 2. Main Actions
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = "Actions",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
androidx.compose.material3.FilledTonalButton(
|
||||
onClick = onScanNowClick,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Scan Files")
|
||||
}
|
||||
|
||||
androidx.compose.material3.OutlinedButton(
|
||||
onClick = onSyncMetadataClick,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.sync),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Sync Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TextButton(onClick = onDisconnectClick, enabled = !isLoading) {
|
||||
Text("Disconnect Folder")
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Column {
|
||||
HorizontalDivider(modifier = Modifier.padding(bottom = 16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = onChangeFolderClick, enabled = !isLoading) {
|
||||
Text("Change Folder")
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onDisconnectClick,
|
||||
enabled = !isLoading,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Disconnect")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue