diff --git a/Cable/Loads/LoadsView.swift b/Cable/Loads/LoadsView.swift index af7daf0..ff013e8 100644 --- a/Cable/Loads/LoadsView.swift +++ b/Cable/Loads/LoadsView.swift @@ -29,7 +29,8 @@ struct LoadsView: View { @State private var overviewExportRequested = false @State private var diagramExportRequested = false @State private var isExportingOverview = false - @State private var overviewShareItem: OverviewShareItem? + @State private var previewURL: URL? + @State private var previewTempURL: URL? @State private var overviewExportError: OverviewExportError? let system: ElectricalSystem @@ -219,11 +220,12 @@ struct LoadsView: View { } ) } - .sheet(item: $overviewShareItem, onDismiss: { - cleanupOverviewShareItem() - ReviewPrompt.registerSuccessfulExport() - }) { item in - ShareSheet(items: item.shareItems) + .quickLookPreview($previewURL) + .onChange(of: previewURL) { _, newValue in + if newValue == nil { + cleanupPreview() + ReviewPrompt.registerSuccessfulExport() + } } .alert( String(localized: "overview.share.error.title", defaultValue: "Export Failed"), @@ -1106,12 +1108,6 @@ struct LoadsView: View { // MARK: - PDF Export - private struct OverviewShareItem: Identifiable { - let id = UUID() - let shareItems: [Any] - let tempURL: URL? - } - private struct OverviewExportError: Identifiable { let message: String var id: String { message } @@ -1134,7 +1130,8 @@ struct LoadsView: View { "system": snapshot.systemName, ]) await MainActor.run { - overviewShareItem = OverviewShareItem(shareItems: [url], tempURL: url) + previewURL = url + previewTempURL = url isExportingOverview = false } } catch { @@ -1164,7 +1161,8 @@ struct LoadsView: View { AnalyticsTracker.log("Diagram Image Shared", properties: [ "system": snapshot.systemName, ]) - overviewShareItem = OverviewShareItem(shareItems: [url], tempURL: url) + previewURL = url + previewTempURL = url } else { overviewExportError = OverviewExportError( message: String(localized: "overview.share.diagram.error", defaultValue: "Could not generate diagram. Check your internet connection.") @@ -1284,11 +1282,10 @@ struct LoadsView: View { ) } - private func cleanupOverviewShareItem() { - guard let item = overviewShareItem else { return } - overviewShareItem = nil - if let url = item.tempURL { + private func cleanupPreview() { + if let url = previewTempURL { try? FileManager.default.removeItem(at: url) + previewTempURL = nil } } } diff --git a/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt b/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt index 52c538b..43b81d7 100644 --- a/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt +++ b/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt @@ -87,16 +87,20 @@ object SystemDiagram { onError: () -> Unit, ) { val bitmap = fetchOrFallback(context, state, unit) + share(context, bitmap, state.system?.name ?: "System") + } + + suspend fun share(context: Context, bitmap: Bitmap, systemName: String) { val file = withContext(Dispatchers.IO) { val opaque = flattenOnWhite(bitmap) - val name = state.system?.name?.takeIf { it.isNotBlank() } ?: "System" + val name = systemName.takeIf { it.isNotBlank() } ?: "System" val dir = File(context.cacheDir, "exports").apply { mkdirs() } val out = File(dir, "${name.replace(Regex("[^A-Za-z0-9-_]"), "_")}-Diagram.png") out.outputStream().use { opaque.compress(Bitmap.CompressFormat.PNG, 100, it) } out } withContext(Dispatchers.Main) { - Analytics.log("Diagram Image Shared", mapOf("system" to (state.system?.name ?: ""))) + Analytics.log("Diagram Image Shared", mapOf("system" to systemName)) PdfShare.shareFile(context, file, "image/png") } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/system/DiagramPreviewDialog.kt b/android/app/src/main/java/app/voltplan/cable/ui/system/DiagramPreviewDialog.kt new file mode 100644 index 0000000..9ed5236 --- /dev/null +++ b/android/app/src/main/java/app/voltplan/cable/ui/system/DiagramPreviewDialog.kt @@ -0,0 +1,103 @@ +package app.voltplan.cable.ui.system + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.transformable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.IosShare +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import app.voltplan.cable.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DiagramPreviewDialog( + bitmap: Bitmap, + onShare: () -> Unit, + onDismiss: () -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.overview_share_diagram)) }, + navigationIcon = { + IconButton(onClick = onDismiss) { + Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.action_back)) + } + }, + actions = { + IconButton(onClick = onShare) { + Icon(Icons.Outlined.IosShare, contentDescription = stringResource(R.string.overview_share_diagram)) + } + }, + ) + }, + ) { padding -> + var scale by remember { mutableFloatStateOf(1f) } + var offset by remember { mutableStateOf(Offset.Zero) } + val transformState = rememberTransformableState { zoomChange, panChange, _ -> + scale = (scale * zoomChange).coerceIn(1f, 8f) + offset = if (scale == 1f) Offset.Zero else offset + panChange + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .transformable(transformState), + contentAlignment = Alignment.Center, + ) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = stringResource(R.string.overview_share_diagram), + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .graphicsLayer( + scaleX = scale, + scaleY = scale, + translationX = offset.x, + translationY = offset.y, + ), + ) + } + } + } + } +} diff --git a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt index adebffe..7540cc5 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt @@ -54,6 +54,7 @@ import app.voltplan.cable.ui.theme.componentColor import app.voltplan.cable.data.ReviewPrompt import app.voltplan.cable.pdf.SystemDiagram import app.voltplan.cable.pdf.SystemOverviewPdf +import android.graphics.Bitmap import android.widget.Toast import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu @@ -106,6 +107,7 @@ fun SystemDetailScreen( var showSystemEditor by remember { mutableStateOf(false) } var showOverviewMenu by remember { mutableStateOf(false) } var exporting by remember { mutableStateOf(false) } + var diagramBitmapPreview by remember { mutableStateOf(null) } val system = state.system // Switch to the matching tab before opening an editor, so returning from the @@ -167,13 +169,9 @@ fun SystemDetailScreen( showOverviewMenu = false scope.launch { exporting = true - var failed = false - SystemDiagram.exportAndShare(context, state, unitSystem) { - failed = true - Toast.makeText(context, R.string.overview_share_diagram_error, Toast.LENGTH_LONG).show() - } + val bitmap = SystemDiagram.fetchOrFallback(context, state, unitSystem) exporting = false - if (!failed) ReviewPrompt.registerSuccessfulExport(context) + diagramBitmapPreview = bitmap } }, ) @@ -256,6 +254,20 @@ fun SystemDetailScreen( } } + diagramBitmapPreview?.let { bmp -> + DiagramPreviewDialog( + bitmap = bmp, + onShare = { + scope.launch { + SystemDiagram.share(context, bmp, state.system?.name ?: "System") + diagramBitmapPreview = null + ReviewPrompt.registerSuccessfulExport(context) + } + }, + onDismiss = { diagramBitmapPreview = null }, + ) + } + if (showSystemEditor && system != null) { var location by remember { mutableStateOf(system.location) } AppearanceEditorSheet(