diff --git a/CHANGELOG.md b/CHANGELOG.md index 3286a88..5345260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.5.1] - 2026-09-09 + +### Changed + +- Group the Nearby devices header, device rows, and bottom surface with coordinated corners and tighter spacing. +- Restyle discovery errors with a neutral surface, a small error-colored Wi-Fi badge, centered text, and an outlined retry action. +- Use extra-large default corners for shared surfaces. +- Simplify file-picker actions with text labels, media/document descriptions, and primary/secondary container colors; remove the camera and document illustrations. +- Configure the Desktop Hot Reload `hotRun` JavaExec task to use Java 23. +- Prepare Android, Desktop, and iOS version metadata as `0.5.1`; Android and iOS build numbers are `8`. +- Correct outdated navigation and Android legacy-resolution documentation. Discovery lifecycle and transfer protocol behavior are unchanged by this patch. + +Release preparation only: new package builds and device validation have not been recorded in this task. + +## [0.5.0] + ### Changed - Remove the separate discovery repair command, its Settings screen, and Troubleshoot shortcuts; use Start/Stop or failure retry in Nearby devices. @@ -19,7 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Present nearby devices in compact rows with a header Stop action and a centered Start discovery state when off. - Shorten discovery messages and make lifecycle, callback, and platform-operation names more descriptive. -These changes have not yet been built or validated on devices. +Build and device-validation results were not recorded during the original 0.5.0 preparation. ## [0.4.1] - 2026-08-28 diff --git a/README.md b/README.md index aef3217..2fa5196 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ In an initial Windows 11 Ethernet test, the native Windows DNS-SD backend discov - Cancel a pending send or active file transfer on a best-effort basis. - Show batch-wide byte percentage while files are being sent and received. - Show clear preparation, transfer, success, failure, and cancelled states on the sender, with receiving and received states on the receiver. -- Run the shared Send/Receive UI on Desktop, with an adaptive 50/50 two-pane layout in wider windows. +- Run the shared Send/Receive UI on Desktop, with one screen at a time and a left navigation rail in wider windows. - Discover and advertise Windows devices through the operating system DNS-SD API, with JmDNS retained for macOS and Linux, using the same service as Android. - Select multiple Desktop files with the native file dialog and send them through the same offer and TCP protocol. - Save received Desktop files safely into Downloads through a temporary `.part` file, then move completed files into place without overwriting an existing name. @@ -183,7 +183,7 @@ The project remains Android-first, but Desktop and iOS reuse the shared UI, View - JmDNS 3.6.3 for current macOS/Linux DNS-SD/mDNS - Java `Socket` / `ServerSocket` for file bytes - Android `ContentResolver` and `MediaStore` -- Navigation 3 with a Material-adaptive 50/50 two-pane Scene on wider windows +- Navigation 3 with one visible screen, compact bottom navigation, and a left rail on wider windows - Gradle 9.3.1 wrapper ## Getting started diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a3dcc1e..5d7eb95 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -36,8 +36,8 @@ android { applicationId = "com.liftley.sync360" minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 7 - versionName = "0.5.0" + versionCode = 8 + versionName = "0.5.1" } buildFeatures { diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 921f3b1..4031212 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -37,7 +37,7 @@ compose.desktop { nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Sync360" - packageVersion = "0.5.0" + packageVersion = "0.5.1" appResourcesRootDir.set( project.layout.projectDirectory.dir("packaging/app-resources") ) @@ -58,3 +58,13 @@ compose.desktop { } } } + +// Force the Compose Hot Reload runner to use Java 23 +tasks.matching { it.name == "hotRun" }.configureEach { + if (this is JavaExec) { + val toolchains = project.extensions.getByType() + javaLauncher.set(toolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(23)) + }) + } +} \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2e7289c..c4bdfd9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -60,13 +60,13 @@ shared/src/iosMain/ ### `Sync360Root` -Owns the single app `Scaffold`, compact bottom navigation, and one Navigation 3 `NavDisplay`. A small `TwoPaneSceneStrategy` renders Send and Receive in a fixed 50/50 split when the Material adaptive window size reaches the medium-width breakpoint. Compact windows use Navigation 3's normal single-pane fallback. +Owns the single app `Scaffold` and one Navigation 3 `NavDisplay`. Compact windows use bottom navigation; wider windows use a left navigation rail. Both show one screen at a time. ### ViewModels - `SendScreenViewModel` owns nearby-device state, selected files/text, send operations, results, and cancellation. - `ReceiveScreenViewModel` maps incoming server state and the session receive code to Receive UI, and handles Copy, Clear, and Open Downloads actions. -- `NavigationViewModel` keeps Send and Receive available as top-level entries and selects the active compact destination. +- `NavigationViewModel` keeps Send and Receive available as top-level entries and selects the active destination. ViewModels launch UI-facing work. They do not implement platform APIs or socket protocols. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b81a98f..c671b09 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -54,9 +54,11 @@ Windows: ./gradlew.bat :desktopApp:run ``` +The Desktop `hotRun` task is configured to use a Java 23 toolchain when it is a `JavaExec` task, matching the project JVM requirement. This is development-runner configuration, not a packaged runtime change. + ## Preparing public packages -The current package version is `0.5.0`. +The current package version is `0.5.1`. Android release APKs must use the maintainer's permanent private signing key. Copy `keystore.properties.example` to the ignored `keystore.properties` file and set: @@ -93,13 +95,13 @@ The Windows `upgradeUuid` must remain unchanged for the lifetime of Sync360, and 6. Confirm Send and Receive show the same code, and that a fresh application start creates a new code while navigation and recomposition do not change it. 7. Test one file, multiple files, receiver-busy behavior, the first-connection timeout, and cancellation. 8. Confirm completed files appear in Downloads. -9. Resize the Desktop window and verify compact single-pane navigation and the wider 50/50 Send/Receive layout. +9. Resize the Desktop window and verify that compact bottom navigation switches to a left navigation rail on wider windows, with one screen visible at a time. For Windows testing, check IPv4 and IPv6 with Ethernet, Wi-Fi, VPN, WSL, Docker, Hyper-V, or virtual-machine adapters. Windows DNS-SD browses and registers with interface index `0`, so Windows selects the applicable interfaces. Confirm discovery and resolution, live removal when a nearby app closes, removal of Windows from the other device after the Desktop app closes, manual Stop/Start. On first network use, allow Sync360 on the intended private network when Windows Firewall prompts. The current MSI does not install its own inbound firewall exception; a denied prompt or administrator policy can block incoming HTTP and file-transfer sockets. -Android currently targets SDK 37 but does not yet declare or request Android 17's `ACCESS_LOCAL_NETWORK` runtime permission. Android 17 LAN testing is therefore expected to fail until permission-aware startup is implemented. On Android 13, also test several discoverable devices appearing close together because the legacy resolver is not yet queued. +Android currently targets SDK 37 but does not yet declare or request Android 17's `ACCESS_LOCAL_NETWORK` runtime permission. Android 17 LAN testing is therefore expected to fail until permission-aware startup is implemented. On Android 13, also test several discoverable devices appearing close together to validate the queued legacy resolver. macOS and Linux currently retain JmDNS. Test those systems with multiple adapters as well because JmDNS starts separately on each eligible address. @@ -153,3 +155,9 @@ Avoid large speculative abstractions, networking inside composables, platform AP Automated coverage is still minimal. Add focused tests for pure Kotlin logic where practical. For discovery, socket, storage, or lifecycle changes, include the exact devices, operating systems, network setup, scenarios, and results in the pull request. There is no stable release yet. Treat current builds as development software. + +## 0.5.1 UI validation + +Before publishing the final packages, check the grouped Nearby devices header, rows, and footer with zero, one, and multiple devices. Check discovery off, scanning, and failure/retry states; light and dark themes; compact and wide windows; and the intentionally limited empty-state text at larger font sizes. Check media/document selection and adding more files. The shared default surface shape changed, so inspect other screens that use its default corners too. + +Build and device validation for 0.5.1 have not been recorded in this preparation task. diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig index c9f0ae6..c0019a3 100644 --- a/iosApp/Configuration/Config.xcconfig +++ b/iosApp/Configuration/Config.xcconfig @@ -3,5 +3,5 @@ TEAM_ID= PRODUCT_NAME=Sync360 PRODUCT_BUNDLE_IDENTIFIER=com.liftley.sync360.Sync360$(TEAM_ID) -CURRENT_PROJECT_VERSION=7 -MARKETING_VERSION=0.5.0 +CURRENT_PROJECT_VERSION=8 +MARKETING_VERSION=0.5.1 diff --git a/shared/src/commonMain/composeResources/drawable/camera-icon.png b/shared/src/commonMain/composeResources/drawable/camera-icon.png deleted file mode 100644 index 8d168e5..0000000 Binary files a/shared/src/commonMain/composeResources/drawable/camera-icon.png and /dev/null differ diff --git a/shared/src/commonMain/composeResources/drawable/document-icon.png b/shared/src/commonMain/composeResources/drawable/document-icon.png deleted file mode 100644 index 6009a6f..0000000 Binary files a/shared/src/commonMain/composeResources/drawable/document-icon.png and /dev/null differ diff --git a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/app/components/Sync360Surface.kt b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/app/components/Sync360Surface.kt index 8df4267..c569589 100644 --- a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/app/components/Sync360Surface.kt +++ b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/app/components/Sync360Surface.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.graphics.Color fun Sync360Surface( modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.surface, - shape: CornerBasedShape = MaterialTheme.shapes.large, + shape: CornerBasedShape = MaterialTheme.shapes.extraLarge, content: @Composable (() -> Unit) ) { Surface( diff --git a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/SendScreen.kt b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/SendScreen.kt index 9a4801b..d2a08ef 100644 --- a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/SendScreen.kt +++ b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/SendScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material3.ButtonGroup import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment diff --git a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/FileSelectionContent.kt b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/FileSelectionContent.kt index 375a509..9882080 100644 --- a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/FileSelectionContent.kt +++ b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/FileSelectionContent.kt @@ -1,6 +1,5 @@ package com.liftley.sync360.presentation.send.components -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -31,10 +30,6 @@ import coil3.compose.AsyncImage import com.liftley.sync360.core.designsystem.icons.Close import com.liftley.sync360.domain.model.SelectedFile import com.liftley.sync360.presentation.app.components.Sync360Surface -import org.jetbrains.compose.resources.painterResource -import sync360.shared.generated.resources.Res -import sync360.shared.generated.resources.camera_icon -import sync360.shared.generated.resources.document_icon private val ImageFileExtensions = setOf( "avif", "bmp", "gif", "heic", "heif", "jpeg", "jpg", "png", "webp" @@ -190,8 +185,8 @@ private fun FilePickerActions( ) { if (onPickMedia != null) { Sync360Surface( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - modifier = Modifier.weight(1f).clip(MaterialTheme.shapes.large).clickable { + containerColor = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.weight(1f).clip(MaterialTheme.shapes.extraLarge).clickable { onPickMedia() } ) { @@ -200,29 +195,24 @@ private fun FilePickerActions( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Image( - painterResource(Res.drawable.camera_icon), - contentDescription = null, - modifier = Modifier.size(64.dp) - ) Text( "Select Media", maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onPrimaryContainer ) Text( - "JPG, PNG, MP4", + "Photos · Videos", maxLines = 1, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onPrimaryContainer, overflow = TextOverflow.Ellipsis, ) } } Sync360Surface( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - modifier = Modifier.weight(1f).clip(MaterialTheme.shapes.large).clickable { + containerColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.weight(1f).clip(MaterialTheme.shapes.extraLarge).clickable { onPickFiles() } ) { @@ -231,25 +221,23 @@ private fun FilePickerActions( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Image( - painterResource(Res.drawable.document_icon), - contentDescription = null, - modifier = Modifier.size(64.dp) + Text("Select docs", maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSecondaryContainer ) - Text("Select docs", maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface) Text( - "PDF, DOCX", + "Pdf · Docs · Excel", maxLines = 1, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onSecondaryContainer, overflow = TextOverflow.Ellipsis ) } } } else { Sync360Surface( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - modifier = Modifier.weight(1f).clip(MaterialTheme.shapes.large).clickable { + containerColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.weight(1f).clip(MaterialTheme.shapes.extraLarge).clickable { onPickFiles() } ) { @@ -258,23 +246,17 @@ private fun FilePickerActions( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Image( - painterResource(Res.drawable.document_icon), - contentDescription = null, - modifier = Modifier.size(64.dp) - ) - Text( if (hasSelectedFiles) "Add more files" else "Select files", maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSecondaryContainer ) Text( - "PDF, DOCX", + "Pdf · Docs · Excel", maxLines = 1, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onSecondaryContainer, overflow = TextOverflow.Ellipsis ) } diff --git a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/NearbyDevicesSection.kt b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/NearbyDevicesSection.kt index 91b12e6..937780f 100644 --- a/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/NearbyDevicesSection.kt +++ b/shared/src/commonMain/kotlin/com/liftley/sync360/presentation/send/components/NearbyDevicesSection.kt @@ -3,10 +3,14 @@ package com.liftley.sync360.presentation.send.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -15,7 +19,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,6 +34,7 @@ import com.liftley.sync360.presentation.app.components.Sync360Surface import com.liftley.sync360.presentation.send.model.NearbyDeviceUiModel import com.liftley.sync360.presentation.send.model.SendScreenState +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun NearbyDevicesSection( screenState: SendScreenState, @@ -46,92 +50,156 @@ fun NearbyDevicesSection( DiscoveryStatus.Stopping -> "Stopping discovery…" DiscoveryStatus.CleanupFailed -> "Couldn’t stop discovery" } - - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text( - "Nearby devices", - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.titleLarge - ) - if (screenState.isDiscoveryEnabled || hasDevices) { - OutlinedButton(onClick = { onDiscoveryEnabledChange(!screenState.isDiscoveryEnabled) }) { - Text(if (screenState.isDiscoveryEnabled) "Stop" else "Start") + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Sync360Surface( + shape = MaterialTheme.shapes.large.copy( + bottomStart = CornerSize(8.dp), + bottomEnd = CornerSize(8.dp) + ) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Nearby devices", + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleLarge + ) + if (screenState.isDiscoveryEnabled || hasDevices) { + OutlinedButton(onClick = { onDiscoveryEnabledChange(!screenState.isDiscoveryEnabled) }) { + Text(if (screenState.isDiscoveryEnabled) "Stop" else "Start") + } + } } } - } - if (hasDevices) { - screenState.nearbyDevices.forEach { device -> - NearbyDeviceRow( - device = device, - enabled = screenState.isContentReadyToSend, - actionLabel = screenState.deviceActionLabel, - onClick = { onDeviceClick(device.id) }) - } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (screenState.discoveryStatus == DiscoveryStatus.Running) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) + if (hasDevices) { + screenState.nearbyDevices.forEach { device -> + NearbyDeviceRow( + shape = MaterialTheme.shapes.extraLarge.copy( + topStart = CornerSize(8.dp), + topEnd = CornerSize(8.dp), + bottomStart = CornerSize(8.dp), + bottomEnd = CornerSize(8.dp) + ), + device = device, + enabled = screenState.isContentReadyToSend, + actionLabel = screenState.deviceActionLabel, + onClick = { onDeviceClick(device.id) }) } - Text( - status, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else if (screenState.discoveryErrorMessage == null) { - Sync360Surface( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) + Sync360Surface( + Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.extraLarge.copy( + topStart = CornerSize(8.dp), + topEnd = CornerSize(8.dp), + ) ) { - Sync360Surface(containerColor = MaterialTheme.colorScheme.primaryContainer) { - Icon( - Wifi, - contentDescription = null, - modifier = Modifier.padding(16.dp).size(24.dp), - tint = MaterialTheme.colorScheme.primary + Spacer(Modifier.height(32.dp)) + } + Spacer(modifier = Modifier) + + Sync360Surface { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (screenState.discoveryStatus == DiscoveryStatus.Running) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } + Text( + status, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - Text( - status, - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center - ) - Text( - text = if (screenState.isDiscoveryEnabled) { - "Open Sync360 on the other device and connect both to the same Wi-Fi network or hotspot." - } else { - "Click Start discovery to find nearby devices and let them find you." - }, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - if (!screenState.isDiscoveryEnabled) { - Button(onClick = { onDiscoveryEnabledChange(true) }) { Text("Start discovery") } + } + } else if (screenState.discoveryErrorMessage == null) { + Sync360Surface( + shape = MaterialTheme.shapes.large.copy( + topStart = CornerSize(8.dp), + topEnd = CornerSize(8.dp) + ), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Sync360Surface(containerColor = MaterialTheme.colorScheme.primaryContainer) { + Icon( + Wifi, + contentDescription = null, + modifier = Modifier.padding(16.dp).size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + Text( + status, + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + Text( + text = if (screenState.isDiscoveryEnabled) { + "Open Sync360 on the other device and connect both to the same Wi-Fi network or hotspot." + } else { + "Click Start discovery to find nearby devices and let them find you." + }, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + if (!screenState.isDiscoveryEnabled) { + Button(onClick = { onDiscoveryEnabledChange(true) }) { Text("Start discovery") } + } } } } - } - screenState.discoveryErrorMessage?.let { message -> - Surface( - color = MaterialTheme.colorScheme.errorContainer, shape = MaterialTheme.shapes.large - ) { - Column( - Modifier.fillMaxWidth().padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + screenState.discoveryErrorMessage?.let { message -> + Sync360Surface( + modifier = Modifier.fillMaxWidth(), + shape = if (hasDevices) MaterialTheme.shapes.extraLarge else { + MaterialTheme.shapes.large.copy( + topStart = CornerSize(8.dp), + topEnd = CornerSize(8.dp) + ) + } ) { - Text(message, style = MaterialTheme.typography.bodyMedium) - TextButton(onClick = onRetryDiscovery) { Text("Try again") } + Column( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Sync360Surface(containerColor = MaterialTheme.colorScheme.errorContainer) { + Icon( + imageVector = Wifi, + contentDescription = null, + modifier = Modifier.padding(16.dp).size(24.dp), + tint = MaterialTheme.colorScheme.onErrorContainer + ) + } + Text( + text = "Discovery needs attention", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + OutlinedButton(onClick = onRetryDiscovery) { Text("Try again") } + } } } } @@ -140,6 +208,7 @@ fun NearbyDevicesSection( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun NearbyDeviceRow( + shape: CornerBasedShape, device: NearbyDeviceUiModel, enabled: Boolean, actionLabel: String, @@ -149,7 +218,7 @@ private fun NearbyDeviceRow( onClick = onClick, enabled = enabled, modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.extraExtraLarge, + shape = shape, color = MaterialTheme.colorScheme.surface, ) { Row(