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
60 changes: 28 additions & 32 deletions app/src/commonMain/kotlin/com/crosspaste/sync/GeneralSyncManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.crosspaste.db.sync.SyncRuntimeInfo
import com.crosspaste.db.sync.SyncRuntimeInfoDao
import com.crosspaste.db.sync.SyncState
import com.crosspaste.dto.sync.SyncInfo
import com.crosspaste.utils.getControlUtils
import com.crosspaste.utils.ioDispatcher
import com.crosspaste.utils.mainDispatcher
import io.github.oshai.kotlinlogging.KotlinLogging
Expand All @@ -26,7 +25,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext

class GeneralSyncManager(
Expand All @@ -37,8 +35,6 @@ class GeneralSyncManager(

private val logger = KotlinLogging.logger {}

private val controlUtils = getControlUtils()

private val _realTimeSyncRuntimeInfos = MutableStateFlow<List<SyncRuntimeInfo>>(listOf())

override val realTimeSyncRuntimeInfos: StateFlow<List<SyncRuntimeInfo>> = _realTimeSyncRuntimeInfos.asStateFlow()
Expand Down Expand Up @@ -148,23 +144,23 @@ class GeneralSyncManager(

private fun resolveSyncs(callback: () -> Unit) {
realTimeSyncScope.launch {
controlUtils.ensureMinExecutionTime(delayTime = 1000L) {
supervisorScope {
val jobs =
getSyncHandlers()
.values
.map {
async {
it.forceResolve()
}
try {
val jobs =
getSyncHandlers()
.values
.map {
async {
it.forceResolve()
}
jobs.awaitAll()
}
jobs.awaitAll()
} catch (e: Exception) {
logger.error(e) { "Exception while resolving sync handlers" }
} finally {
withContext(mainDispatcher) {
callback()
}
}

withContext(mainDispatcher) {
callback()
}
}
}

Expand All @@ -173,23 +169,23 @@ class GeneralSyncManager(
callback: () -> Unit,
) {
realTimeSyncScope.launch {
controlUtils.ensureMinExecutionTime(delayTime = 1000L) {
supervisorScope {
val jobs =
ids
.mapNotNull { getSyncHandler(it) }
.map {
async {
it.forceResolve()
}
try {
val jobs =
ids
.mapNotNull { getSyncHandler(it) }
.map {
async {
it.forceResolve()
}
jobs.awaitAll()
}
jobs.awaitAll()
} catch (e: Exception) {
logger.error(e) { "Exception while resolving sync handlers for ids: $ids" }
} finally {
withContext(mainDispatcher) {
callback()
}
}

withContext(mainDispatcher) {
callback()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.crosspaste.i18n.GlobalCopywriter
import com.crosspaste.ui.theme.AppUISize.small
import com.crosspaste.ui.theme.AppUISize.small3X
Expand All @@ -38,6 +39,7 @@ fun StateTagView(style: StateTagStyle) {
Surface(
color = style.containerColor,
shape = tinyRoundedCornerShape,
shadowElevation = 1.5.dp,
) {
Row(
modifier =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import com.crosspaste.app.AppUpdateService
Expand All @@ -27,15 +28,22 @@ import com.crosspaste.notification.MessageType
import com.crosspaste.notification.NotificationManager
import com.crosspaste.sync.SyncManager
import com.crosspaste.ui.base.GeneralIconButton
import com.crosspaste.utils.getControlUtils
import kotlinx.coroutines.launch
import org.koin.compose.koinInject

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeviceScope.DeviceActionButton() {
fun DeviceScope.DeviceActionButton(
refreshing: Boolean,
updateRefreshing: (Boolean) -> Unit,
) {
val appUpdateService = koinInject<AppUpdateService>()
val notificationManager = koinInject<NotificationManager>()
val syncManager = koinInject<SyncManager>()

val scope = rememberCoroutineScope()

when (syncRuntimeInfo.connectState) {
SyncState.CONNECTING, SyncState.DISCONNECTED,
-> {
Expand Down Expand Up @@ -65,18 +73,23 @@ fun DeviceScope.DeviceActionButton() {
rotationZ = if (refreshing) rotation else 0f
},
) {
runCatching {
refreshing = true
syncManager.refresh(listOf(syncRuntimeInfo.appInstanceId)) {
refreshing = false
scope.launch {
runCatching {
updateRefreshing(true)
getControlUtils().ensureMinExecutionTimeForCallback(2000L) { proceed ->
syncManager.refresh(listOf(syncRuntimeInfo.appInstanceId)) {
proceed()
}
}
updateRefreshing(false)
}.onFailure { e ->
updateRefreshing(false)
notificationManager.sendNotification(
title = { it.getText("refresh_connection_failed") },
message = e.message?.let { message -> { message } },
messageType = MessageType.Error,
)
}
}.onFailure { e ->
refreshing = false
notificationManager.sendNotification(
title = { it.getText("refresh_connection_failed") },
message = e.message?.let { message -> { message } },
messageType = MessageType.Error,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package com.crosspaste.ui.devices

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun DeviceScope.DeviceDetailHeaderView() {
var refreshing by remember { mutableStateOf(false) }
DeviceRowContent(
style = myDeviceDetailStyle,
tagContent = { SyncStateTag() },
trailingContent = { DeviceActionButton() },
tagContent = { SyncStateTag(refreshing) },
trailingContent = {
DeviceActionButton(refreshing) {
refreshing = it
}
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import com.crosspaste.platform.Platform

interface DeviceScope : PlatformScope {

var refreshing: Boolean

var syncRuntimeInfo: SyncRuntimeInfo

override val platform: Platform
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.LinkOff
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Shield
import androidx.compose.material.icons.filled.SyncAlt
import androidx.compose.material.icons.filled.Warning
Expand Down Expand Up @@ -72,8 +73,8 @@ val unmatchedStateStyle
get() =
StateTagStyle(
label = "sync_status_unmatched",
containerColor = LocalThemeExtState.current.special.container,
contentColor = LocalThemeExtState.current.special.onContainer,
containerColor = LocalThemeExtState.current.warning.container,
contentColor = LocalThemeExtState.current.warning.onContainer,
icon = Icons.Default.Warning,
)

Expand All @@ -82,8 +83,8 @@ val unverifiedStateStyle
get() =
StateTagStyle(
label = "sync_status_unverified",
containerColor = LocalThemeExtState.current.special.container,
contentColor = LocalThemeExtState.current.special.onContainer,
containerColor = LocalThemeExtState.current.warning.container,
contentColor = LocalThemeExtState.current.warning.onContainer,
icon = Icons.Default.Shield,
)

Expand All @@ -92,15 +93,27 @@ val incompatibleStateStyle
get() =
StateTagStyle(
label = "sync_status_incompatible",
containerColor = LocalThemeExtState.current.warning.container,
contentColor = LocalThemeExtState.current.warning.onContainer,
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
icon = Icons.Default.Close,
)

val refreshingStateStyle
@Composable @ReadOnlyComposable
get() =
StateTagStyle(
label = "refresh",
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
icon = Icons.Default.Refresh,
)

@Composable
fun DeviceScope.SyncStateTag() {
fun DeviceScope.SyncStateTag(refreshing: Boolean) {
val state = syncRuntimeInfo.connectState
if (state == SyncState.CONNECTED) {
if (refreshing) {
StateTagView(refreshingStateStyle)
} else if (state == SyncState.CONNECTED) {
if (syncRuntimeInfo.allowSend && syncRuntimeInfo.allowReceive) {
StateTagView(syncedStateStyle)
} else if (syncRuntimeInfo.allowSend) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ data class ThemeExt(
companion object {
private val COLOR_SUCCESS = Color(0xFF2E7D32)
private val COLOR_INFO = Color(0xFF0288D1)
private val COLOR_NEUTRAL = Color(0xFF607D8B)
private val COLOR_NEUTRAL = Color(0xFF747775)
private val COLOR_WARNING = Color(0xFFFBC02D)
private val COLOR_SPECIAL = Color(0xFF6750A4)

Expand Down
32 changes: 31 additions & 1 deletion app/src/commonMain/kotlin/com/crosspaste/utils/ControlUtils.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.crosspaste.utils

import com.crosspaste.utils.DateUtils.nowEpochMilliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

expect fun getControlUtils(): ControlUtils

interface ControlUtils {

suspend fun <T> ensureMinExecutionTime(
delayTime: Long = 20L,
delayTime: Long,
action: suspend () -> T,
): Result<T> {
val start = nowEpochMilliseconds()
Expand All @@ -28,6 +32,32 @@ interface ControlUtils {
return result
}

suspend fun ensureMinExecutionTimeForCallback(
delayTime: Long,
action: (proceed: () -> Unit) -> Unit,
): Result<Unit> {
val start = nowEpochMilliseconds()

val result =
runCatching {
suspendCancellableCoroutine { continuation ->
CoroutineScope(continuation.context).launch {
action {
continuation.resume(Unit)
}
}
}
}

val end = nowEpochMilliseconds()
val remainingDelay = delayTime - (end - start)

if (remainingDelay > 0) {
delay(remainingDelay)
}
return result
}

suspend fun <T> exponentialBackoffUntilValid(
initTime: Long,
maxTime: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import com.crosspaste.db.sync.SyncRuntimeInfo
Expand All @@ -17,25 +18,28 @@ class DesktopDeviceScope(
override var syncRuntimeInfo: SyncRuntimeInfo,
) : DeviceScope {

override var refreshing: Boolean by mutableStateOf(false)

@Composable
override fun DeviceConnectView() {
val navigationManager = koinInject<NavigationManager>()

var refreshing by remember { mutableStateOf(false) }

DeviceRowContent(
onClick = {
navigationManager.navigate(DeviceDetail(syncRuntimeInfo.appInstanceId))
},
style = myDeviceStyle,
tagContent = {
SyncStateTag()
SyncStateTag(refreshing)
},
trailingContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(tiny),
verticalAlignment = Alignment.CenterVertically,
) {
DeviceActionButton()
DeviceActionButton(refreshing) {
refreshing = it
}
MyDeviceMenuButton()
}
},
Expand Down