Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.6.1] - 2026-10-01

### Changed

- Separate nearby-network presentation by user intent: Send now reports device search, while Receive reports whether this device is visible to nearby peers.
- Keep the shared network-service start, stop, and retry behavior while presenting discovery and advertising failures on their relevant screens.
- Add failure badges to the Send and Receive navigation items so a problem on the other screen remains visible without mixing both statuses into one card.
- Redesign the idle Receive screen around the advertised device name, file-transfer code, and nearby-visibility control.
- Show the advertised device name on Android, Desktop, and iOS, and keep the Desktop hostname lookup off the UI thread.
- Refine the Send discovery presentation with clearer transition controls and expressive search progress.
- Prepare Android, Desktop, and iOS packages as `0.6.1`; Android and iOS build numbers are `11`.

Release builds and cross-platform manual validation have not yet been recorded for `0.6.1`.

## [0.6.0] - 2026-09-30

### Changed
Expand Down
4 changes: 2 additions & 2 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ android {
applicationId = "com.liftley.sync360"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 10
versionName = "0.6.0"
versionCode = 11
versionName = "0.6.1"
}

buildFeatures {
Expand Down
2 changes: 1 addition & 1 deletion desktopApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ compose.desktop {
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Sync360"
packageVersion = "0.6.0"
packageVersion = "0.6.1"
appResourcesRootDir.set(
project.layout.projectDirectory.dir("packaging/app-resources")
)
Expand Down
8 changes: 4 additions & 4 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The Desktop `hotRun` task is configured to use a Java 23 toolchain when it is a

## Preparing public packages

The current package version is `0.6.0`.
The current package version is `0.6.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:

Expand Down Expand Up @@ -156,8 +156,8 @@ Automated coverage is still minimal. Add focused tests for pure Kotlin logic whe

There is no stable release yet. Treat current builds as development software.

## 0.6.0 validation
## 0.6.1 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.
Before publishing the final packages, check the Send screen with zero, one, and multiple devices and confirm that it reports only device-search status. Check the Receive screen's displayed device name, file-transfer code, and nearby-visibility states on each supported platform. Force discovery and registration failures separately and confirm that only the corresponding Send or Receive navigation item shows its warning badge and retry UI. Check light and dark themes, compact and wide windows, and larger font sizes.

Build and device validation for 0.6.0 have not been recorded in this preparation task.
Build and device validation for 0.6.1 have not been recorded in this preparation task.
4 changes: 2 additions & 2 deletions iosApp/Configuration/Config.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ TEAM_ID=
PRODUCT_NAME=Sync360
PRODUCT_BUNDLE_IDENTIFIER=com.liftley.sync360.Sync360$(TEAM_ID)

CURRENT_PROJECT_VERSION=10
MARKETING_VERSION=0.6.0
CURRENT_PROJECT_VERSION=11
MARKETING_VERSION=0.6.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.liftley.sync360.presentation.receive.components

import android.os.Build
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.liftley.sync360.presentation.app.components.Sync360Surface

@Composable
actual fun YouWillAppearAs() {
val manufacturer = Build.MANUFACTURER.trim().replaceFirstChar { it.titlecase() }
val model = Build.MODEL.trim()
val name = if (model.startsWith(
manufacturer,
ignoreCase = true
)
) model else "$manufacturer $model"


Column(
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Text("You'll appear as")

Spacer(modifier = Modifier.height(16.dp))

Sync360Surface(modifier = Modifier.fillMaxWidth()) {
Text(
name,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
modifier = Modifier.padding(16.dp)
)
}
}
}
65 changes: 49 additions & 16 deletions shared/src/commonMain/kotlin/com/liftley/sync360/Sync360Root.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -34,6 +36,8 @@ import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import com.liftley.sync360.core.designsystem.icons.Download
import com.liftley.sync360.core.designsystem.icons.Send
import com.liftley.sync360.domain.model.DiscoveryStatus
import com.liftley.sync360.domain.model.RegistrationStatus
import com.liftley.sync360.presentation.navigation.NavScreen
import com.liftley.sync360.presentation.navigation.NavigationViewModel
import com.liftley.sync360.presentation.receive.ReceiveScreen
Expand All @@ -53,6 +57,7 @@ fun Sync360Root() {
val sendScreenViewModel = koinInject<SendScreenViewModel>()

val receiveScreenState by receiveScreenViewModel.screenState.collectAsStateWithLifecycle()
val registrationStatus by receiveScreenViewModel.registrationStatus.collectAsStateWithLifecycle()
val sendScreenState by sendScreenViewModel.sendScreenState.collectAsStateWithLifecycle()
val currentScreen = navigationViewModel.currentScreen()

Expand All @@ -66,6 +71,10 @@ fun Sync360Root() {

val isReceivingFiles = receiveScreenState is ReceiveState.ReceivingFiles
val receivedText = receiveScreenState as? ReceiveState.ReceivedText
val sendNeedsAttention = sendScreenState.discoveryStatus == DiscoveryStatus.FailedToStart ||
sendScreenState.discoveryStatus == DiscoveryStatus.CleanupFailed
val receiveNeedsAttention = registrationStatus == RegistrationStatus.FailedToStart ||
registrationStatus == RegistrationStatus.CleanupFailed

val receiveTitle = when (receiveScreenState) {
is ReceiveState.Idle -> "Sync360"
Expand Down Expand Up @@ -114,10 +123,16 @@ fun Sync360Root() {
)
},
icon = {
Icon(
imageVector = Download,
contentDescription = null
)
BadgedBox(
badge = {
if (receiveNeedsAttention) Badge()
}
) {
Icon(
imageVector = Download,
contentDescription = null
)
}
}
)
NavigationBarItem(
Expand All @@ -133,10 +148,16 @@ fun Sync360Root() {
)
},
icon = {
Icon(
imageVector = Send,
contentDescription = null
)
BadgedBox(
badge = {
if (sendNeedsAttention) Badge()
}
) {
Icon(
imageVector = Send,
contentDescription = null
)
}
}
)

Expand Down Expand Up @@ -185,10 +206,16 @@ fun Sync360Root() {
selected = currentScreen == NavScreen.ReceiveScreen,
onClick = { navigationViewModel.navigateTo(NavScreen.ReceiveScreen) },
icon = {
Icon(
imageVector = Download,
contentDescription = null
)
BadgedBox(
badge = {
if (receiveNeedsAttention) Badge()
}
) {
Icon(
imageVector = Download,
contentDescription = null
)
}
},
label = { Text("Receive") }
)
Expand All @@ -198,10 +225,16 @@ fun Sync360Root() {
selected = currentScreen == NavScreen.SendScreen,
onClick = { navigationViewModel.navigateTo(NavScreen.SendScreen) },
icon = {
Icon(
imageVector = Send,
contentDescription = null
)
BadgedBox(
badge = {
if (sendNeedsAttention) Badge()
}
) {
Icon(
imageVector = Send,
contentDescription = null
)
}
},
label = { Text("Send") }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module

val appModule = module {
single<ReceiveScreenViewModel> { ReceiveScreenViewModel(get(), get(), get()) }
single<ReceiveScreenViewModel> { ReceiveScreenViewModel(get(), get(), get(), get()) }
single<SendScreenViewModel> {
SendScreenViewModel(get(), get(), get(), get())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class NetworkServicesController(
}
}

fun retryNearbySharing() {
fun retryNetworkServices() {
controllerScope.launch {
if (hasCleanupFailure()) {
stopNearbySharing()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,37 @@ package com.liftley.sync360.presentation.app.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

@Composable
fun FileReceiveCodeCard(
modifier: Modifier,
fileReceiveCode: String
) {
Sync360Surface(
containerColor = MaterialTheme.colorScheme.surface
Column(
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
"Your file transfer code",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text("Your file transfer code")

Spacer(modifier = Modifier.height(16.dp))

Sync360Surface(modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.primaryContainer) {
Text(
fileReceiveCode,
style = MaterialTheme.typography.displayMedium
)
Text(
"Share this code with the sender",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(16.dp)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ import org.koin.compose.koinInject
fun ReceiveScreen() {
val receiveScreenViewModel = koinInject<ReceiveScreenViewModel>()
val receiveScreenState by receiveScreenViewModel.screenState.collectAsStateWithLifecycle()
val registrationStatus by receiveScreenViewModel.registrationStatus.collectAsStateWithLifecycle()
Sync360Surface(
modifier = Modifier.fillMaxSize(),
containerColor = MaterialTheme.colorScheme.surfaceContainer
) {
when (val state = receiveScreenState) {
is ReceiveState.Idle -> {
IdleReceiveStateUi(
fileReceiveCode = state.fileReceiveCode
fileReceiveCode = state.fileReceiveCode,
registrationStatus = registrationStatus,
onStartNetworkServices = receiveScreenViewModel::startNetworkServices,
onStopNetworkServices = receiveScreenViewModel::stopNetworkServices,
onRetryNetworkServices = receiveScreenViewModel::retryNetworkServices
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.liftley.sync360.presentation.receive
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.liftley.sync360.data.IncomingServerRequestsController
import com.liftley.sync360.data.NetworkServicesController
import com.liftley.sync360.domain.model.ClientServerState
import com.liftley.sync360.domain.repository.ClipboardProvider
import com.liftley.sync360.domain.repository.DownloadsFolderOpener
Expand All @@ -15,7 +16,8 @@ import kotlinx.coroutines.launch
class ReceiveScreenViewModel(
private val incomingServerRequestsController: IncomingServerRequestsController,
private val clipboardProvider: ClipboardProvider,
private val downloadsFolderOpener: DownloadsFolderOpener
private val downloadsFolderOpener: DownloadsFolderOpener,
private val networkServicesController: NetworkServicesController
) : ViewModel() {

private val _screenState = MutableStateFlow<ReceiveState>(
Expand All @@ -24,6 +26,7 @@ class ReceiveScreenViewModel(
)
)
val screenState: StateFlow<ReceiveState> = _screenState.asStateFlow()
val registrationStatus = networkServicesController.registrationServiceStatus

init {
viewModelScope.launch {
Expand All @@ -49,6 +52,18 @@ class ReceiveScreenViewModel(
downloadsFolderOpener.openDownloads()
}

fun startNetworkServices() {
networkServicesController.startNetworkServices()
}

fun stopNetworkServices() {
networkServicesController.stopNetworkServices()
}

fun retryNetworkServices() {
networkServicesController.retryNetworkServices()
}

}

private fun ClientServerState.toReceiveScreenState(
Expand Down
Loading
Loading