mirror of
https://github.com/Darkrock-Studios/hammer-editor.git
synced 2026-08-11 10:33:17 +00:00
Add a warning state for conflicted project syncs
This commit is contained in:
parent
db29c001c2
commit
00a9bcafd1
7 changed files with 214 additions and 56 deletions
|
|
@ -36,6 +36,7 @@
|
|||
<string name="account_sync_log_filter_all">All</string>
|
||||
<string name="account_sync_dialog_status_pending">Pending</string>
|
||||
<string name="account_sync_dialog_status_error">Error</string>
|
||||
<string name="account_sync_dialog_status_conflict">Conflict</string>
|
||||
<string name="account_sync_dialog_status_complete">Sync Complete</string>
|
||||
<string name="account_sync_dialog_status_canceled">Canceled</string>
|
||||
<string name="delete_project_title">Delete Project</string>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ interface ProjectsList : HammerComponent, ComponentToaster {
|
|||
Pending,
|
||||
Syncing,
|
||||
Failed,
|
||||
NeedsResolution,
|
||||
Complete,
|
||||
Canceled
|
||||
}
|
||||
|
|
@ -79,8 +80,8 @@ interface ProjectsList : HammerComponent, ComponentToaster {
|
|||
|
||||
/**
|
||||
* True only while a sync is still working. Closing the dialog mid-sync should confirm
|
||||
* cancellation; once every project is in a terminal state (Complete/Failed/Canceled) the
|
||||
* sync is no longer active, so closing should just close — even if [syncComplete] never flipped.
|
||||
* cancellation; once every project is in a terminal state (Complete/Failed/NeedsResolution/Canceled)
|
||||
* the sync is no longer active, so closing should just close — even if [syncComplete] never flipped.
|
||||
*/
|
||||
val ProjectsList.SyncState.hasActiveSync: Boolean
|
||||
get() = !syncComplete && projectsStatus.values.any { status ->
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.darkrockstudios.apps.hammer.common.components.projectselection.proje
|
|||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.decompose.value.getAndUpdate
|
||||
import com.darkrockstudios.apps.hammer.*
|
||||
import com.darkrockstudios.apps.hammer.Res
|
||||
import com.darkrockstudios.apps.hammer.base.http.readTomlOrNull
|
||||
import com.darkrockstudios.apps.hammer.common.components.ComponentToaster
|
||||
import com.darkrockstudios.apps.hammer.common.components.ComponentToasterImpl
|
||||
|
|
@ -22,7 +22,15 @@ import com.darkrockstudios.apps.hammer.common.data.projectmetadata.ProjectMetada
|
|||
import com.darkrockstudios.apps.hammer.common.data.projectsrepository.ProjectsRepository
|
||||
import com.darkrockstudios.apps.hammer.common.data.projectstatistics.ProjectStatisticsCacheReader
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.accountsync.ClientAccountSynchronizer
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.*
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.ClientProjectSynchronizer
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.OnSyncLog
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.SyncLogMessage
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncAccLogE
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncAccLogI
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncAccLogW
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncLogE
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncLogI
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncLogW
|
||||
import com.darkrockstudios.apps.hammer.common.data.temporaryProjectTask
|
||||
import com.darkrockstudios.apps.hammer.common.dependencyinjection.injectMainDispatcher
|
||||
import com.darkrockstudios.apps.hammer.common.fileio.HPath
|
||||
|
|
@ -31,10 +39,26 @@ import com.darkrockstudios.apps.hammer.common.fileio.okio.toOkioPath
|
|||
import com.darkrockstudios.apps.hammer.common.util.NetworkConnectivity
|
||||
import com.darkrockstudios.apps.hammer.common.util.StrRes
|
||||
import com.darkrockstudios.apps.hammer.common.util.lifecycleCoroutineScope
|
||||
import com.darkrockstudios.apps.hammer.projects_list_toast_sync_complete
|
||||
import com.darkrockstudios.apps.hammer.projects_list_toast_sync_failed
|
||||
import com.darkrockstudios.apps.hammer.sync_log_begin_account
|
||||
import com.darkrockstudios.apps.hammer.sync_log_begin_project
|
||||
import com.darkrockstudios.apps.hammer.sync_log_begin_projects
|
||||
import com.darkrockstudios.apps.hammer.sync_log_end_projects
|
||||
import com.darkrockstudios.apps.hammer.sync_log_project_conflict
|
||||
import io.github.aakira.napier.Napier
|
||||
import korlibs.datastructure.iterators.parallelMap
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import net.peanuuutz.tomlkt.Toml
|
||||
import okio.FileSystem
|
||||
import okio.IOException
|
||||
|
|
@ -289,55 +313,80 @@ class ProjectsListComponent(
|
|||
projectDef: ProjectDef,
|
||||
onLog: OnSyncLog,
|
||||
onProgress: suspend (Float, SyncLogMessage?) -> Unit
|
||||
): Boolean {
|
||||
): ProjectSyncOutcome {
|
||||
onLog(syncLogI(strRes.get(Res.string.sync_log_begin_project, projectDef.name), projectDef))
|
||||
|
||||
var success = false
|
||||
temporaryProjectTask(projectDef) { projScope ->
|
||||
val synchronizer: ClientProjectSynchronizer = projScope.get { parametersOf(projectDef) }
|
||||
val conflictBroker: ProjectDataConflictBroker = projScope.get { parametersOf(projectDef) }
|
||||
var conflicted = false
|
||||
|
||||
coroutineScope {
|
||||
// Bulk account sync has no interactive resolver. A project-data conflict reports
|
||||
// to the broker and waits on resolutions forever, leaving the project stuck
|
||||
// "Syncing". Watch for it and abort so the project fails instead of hanging.
|
||||
val conflictWatcher = launch {
|
||||
for (conflict in conflictBroker.conflicts) {
|
||||
onLog(
|
||||
syncLogW(
|
||||
strRes.get(Res.string.sync_log_project_conflict, projectDef.name),
|
||||
projectDef
|
||||
)
|
||||
)
|
||||
conflictBroker.abort()
|
||||
}
|
||||
}
|
||||
try {
|
||||
temporaryProjectTask(projectDef) { projScope ->
|
||||
val synchronizer: ClientProjectSynchronizer =
|
||||
projScope.get { parametersOf(projectDef) }
|
||||
val conflictBroker: ProjectDataConflictBroker =
|
||||
projScope.get { parametersOf(projectDef) }
|
||||
|
||||
try {
|
||||
success = synchronizer.sync(
|
||||
onProgress = onProgress,
|
||||
onLog = { message -> onLog(message) },
|
||||
onConflict = {
|
||||
coroutineScope {
|
||||
// Bulk account sync has no interactive resolver. A project-data conflict reports
|
||||
// to the broker and waits on resolutions forever, leaving the project stuck
|
||||
// "Syncing". Watch for it and abort so the project stops instead of hanging.
|
||||
val conflictWatcher = launch {
|
||||
for (conflict in conflictBroker.conflicts) {
|
||||
onLog(
|
||||
syncLogW(
|
||||
strRes.get(Res.string.sync_log_project_conflict, projectDef.name),
|
||||
strRes.get(
|
||||
Res.string.sync_log_project_conflict,
|
||||
projectDef.name
|
||||
),
|
||||
projectDef
|
||||
)
|
||||
)
|
||||
throw IllegalStateException("Entity conflict must be handled by Project sync")
|
||||
},
|
||||
onComplete = {},
|
||||
onUnauthorized = ::showReauth
|
||||
)
|
||||
} finally {
|
||||
conflictWatcher.cancel()
|
||||
conflicted = true
|
||||
conflictBroker.abort()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
success = synchronizer.sync(
|
||||
onProgress = onProgress,
|
||||
onLog = { message -> onLog(message) },
|
||||
onConflict = {
|
||||
onLog(
|
||||
syncLogW(
|
||||
strRes.get(
|
||||
Res.string.sync_log_project_conflict,
|
||||
projectDef.name
|
||||
),
|
||||
projectDef
|
||||
)
|
||||
)
|
||||
conflicted = true
|
||||
throw IllegalStateException("Entity conflict must be handled by Project sync")
|
||||
},
|
||||
onComplete = {},
|
||||
onUnauthorized = ::showReauth
|
||||
)
|
||||
} finally {
|
||||
conflictWatcher.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
// A conflict aborts the entity sync by throwing; that's a resolvable state, not a failure.
|
||||
if (!conflicted) throw e
|
||||
}
|
||||
|
||||
return success
|
||||
return when {
|
||||
success -> ProjectSyncOutcome.Success
|
||||
conflicted -> ProjectSyncOutcome.NeedsResolution
|
||||
else -> ProjectSyncOutcome.Failed
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ProjectSyncOutcome { Success, NeedsResolution, Failed }
|
||||
|
||||
private suspend fun syncNewProjectStatus(projects: List<ProjectDef>) {
|
||||
val newStatuses = mutableMapOf<String, ProjectsList.ProjectSyncStatus>()
|
||||
projects.forEach { projDef ->
|
||||
|
|
@ -429,11 +478,14 @@ class ProjectsListComponent(
|
|||
if (message != null) onSyncLog(message)
|
||||
}
|
||||
|
||||
val projectSuccess = syncProject(projectDef, ::onSyncLog, ::onProgress)
|
||||
allSuccess = allSuccess && projectSuccess
|
||||
val outcome = syncProject(projectDef, ::onSyncLog, ::onProgress)
|
||||
allSuccess = allSuccess && (outcome == ProjectSyncOutcome.Success)
|
||||
|
||||
val newStatus =
|
||||
if (projectSuccess) ProjectsList.Status.Complete else ProjectsList.Status.Failed
|
||||
val newStatus = when (outcome) {
|
||||
ProjectSyncOutcome.Success -> ProjectsList.Status.Complete
|
||||
ProjectSyncOutcome.NeedsResolution -> ProjectsList.Status.NeedsResolution
|
||||
ProjectSyncOutcome.Failed -> ProjectsList.Status.Failed
|
||||
}
|
||||
syncProgressStatus(projectDef.name, newStatus)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
|
|
|
|||
|
|
@ -59,6 +59,15 @@ class SyncStateActiveTest {
|
|||
assertFalse(state.hasActiveSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Project needing conflict resolution is not active`() {
|
||||
// A conflict stops the project's sync in a terminal state; closing the dialog
|
||||
// should not prompt the cancel confirmation.
|
||||
val state = syncState(syncComplete = false, Status.Complete, Status.NeedsResolution)
|
||||
|
||||
assertFalse(state.hasActiveSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Canceled projects are not active`() {
|
||||
val state = syncState(syncComplete = true, Status.Canceled, Status.Canceled)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ enum class HdStatus {
|
|||
Syncing,
|
||||
Complete,
|
||||
Failed,
|
||||
Warning,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ fun HdStatus.accentColor(): Color {
|
|||
HdStatus.Syncing -> MaterialTheme.colorScheme.primary
|
||||
HdStatus.Complete -> hammer.success
|
||||
HdStatus.Failed -> hammer.danger
|
||||
HdStatus.Warning -> hammer.warning
|
||||
HdStatus.Canceled -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +71,12 @@ fun HdStatusGlyph(
|
|||
}
|
||||
HdStatus.Complete -> Box(modifier = base.background(accent).drawCheck(ink))
|
||||
HdStatus.Failed -> Box(modifier = base.background(accent).drawCross(ink))
|
||||
HdStatus.Warning -> Box(
|
||||
modifier = base.background(accent),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
BangMark(ink, size.value)
|
||||
}
|
||||
HdStatus.Canceled -> Box(
|
||||
modifier = base
|
||||
.border(width = Dp.Hairline, color = MaterialTheme.colorScheme.outlineVariant, shape = RectangleShape)
|
||||
|
|
@ -89,18 +97,24 @@ fun HdWarnGlyph(
|
|||
modifier = modifier.size(size).background(amber),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "!",
|
||||
color = ink,
|
||||
style = TextStyle(
|
||||
fontFamily = MaterialTheme.typography.labelSmall.fontFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = (size.value * 0.6f).sp,
|
||||
),
|
||||
)
|
||||
BangMark(ink, size.value)
|
||||
}
|
||||
}
|
||||
|
||||
/** Mono `!` sized relative to its containing glyph. */
|
||||
@Composable
|
||||
private fun BangMark(ink: Color, sizeValue: Float) {
|
||||
Text(
|
||||
text = "!",
|
||||
color = ink,
|
||||
style = TextStyle(
|
||||
fontFamily = MaterialTheme.typography.labelSmall.fontFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = (sizeValue * 0.6f).sp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.drawCross(color: Color): Modifier = this
|
||||
.fillMaxSize()
|
||||
.drawBehind {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,18 @@ package com.darkrockstudios.apps.hammer.common.projectselection
|
|||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -10,8 +21,15 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
|
|
@ -19,14 +37,36 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.darkrockstudios.apps.hammer.*
|
||||
import com.darkrockstudios.apps.hammer.Res
|
||||
import com.darkrockstudios.apps.hammer.account_sync_confirm_cancel_message
|
||||
import com.darkrockstudios.apps.hammer.account_sync_confirm_cancel_title
|
||||
import com.darkrockstudios.apps.hammer.account_sync_dialog_header
|
||||
import com.darkrockstudios.apps.hammer.account_sync_dialog_status_canceled
|
||||
import com.darkrockstudios.apps.hammer.account_sync_dialog_status_complete
|
||||
import com.darkrockstudios.apps.hammer.account_sync_dialog_status_conflict
|
||||
import com.darkrockstudios.apps.hammer.account_sync_dialog_status_error
|
||||
import com.darkrockstudios.apps.hammer.account_sync_dialog_status_pending
|
||||
import com.darkrockstudios.apps.hammer.account_sync_log_filter_all
|
||||
import com.darkrockstudios.apps.hammer.account_sync_log_filter_label
|
||||
import com.darkrockstudios.apps.hammer.account_sync_log_title
|
||||
import com.darkrockstudios.apps.hammer.account_sync_toast_canceled
|
||||
import com.darkrockstudios.apps.hammer.common.components.projectselection.projectslist.ProjectsList
|
||||
import com.darkrockstudios.apps.hammer.common.components.projectselection.projectslist.hasActiveSync
|
||||
import com.darkrockstudios.apps.hammer.common.compose.AnimatedDialogContainer
|
||||
import com.darkrockstudios.apps.hammer.common.compose.RootSnackbarHostState
|
||||
import com.darkrockstudios.apps.hammer.common.compose.SimpleConfirm
|
||||
import com.darkrockstudios.apps.hammer.common.compose.Ui
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.*
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdFilterMenu
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdFolioDivider
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdHairlineProgressBar
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdLogGlyph
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdMasthead
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdMastheadAction
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdMonoLabel
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdStatus
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdStatusGlyph
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdToolButton
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.accentColor
|
||||
import com.darkrockstudios.apps.hammer.common.compose.resources.get
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.SyncLogMessage
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -293,7 +333,7 @@ private fun ProgressList(projects: List<ProjectsList.ProjectSyncStatus>) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SyncStatusRow(
|
||||
internal fun SyncStatusRow(
|
||||
projectStatus: ProjectsList.ProjectSyncStatus,
|
||||
isLast: Boolean,
|
||||
) {
|
||||
|
|
@ -301,6 +341,7 @@ private fun SyncStatusRow(
|
|||
ProjectsList.Status.Pending -> HdStatus.Pending
|
||||
ProjectsList.Status.Syncing -> HdStatus.Syncing
|
||||
ProjectsList.Status.Failed -> HdStatus.Failed
|
||||
ProjectsList.Status.NeedsResolution -> HdStatus.Warning
|
||||
ProjectsList.Status.Complete -> HdStatus.Complete
|
||||
ProjectsList.Status.Canceled -> HdStatus.Canceled
|
||||
}
|
||||
|
|
@ -308,6 +349,7 @@ private fun SyncStatusRow(
|
|||
ProjectsList.Status.Pending -> Res.string.account_sync_dialog_status_pending.get()
|
||||
ProjectsList.Status.Syncing -> "${(projectStatus.progress * 100).toInt()}%"
|
||||
ProjectsList.Status.Failed -> Res.string.account_sync_dialog_status_error.get()
|
||||
ProjectsList.Status.NeedsResolution -> Res.string.account_sync_dialog_status_conflict.get()
|
||||
ProjectsList.Status.Complete -> Res.string.account_sync_dialog_status_complete.get()
|
||||
ProjectsList.Status.Canceled -> Res.string.account_sync_dialog_status_canceled.get()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.darkrockstudios.apps.hammer.common.Padded
|
||||
import com.darkrockstudios.apps.hammer.common.components.projectselection.projectslist.ProjectsList
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdHairlineProgressBar
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdStatus
|
||||
import com.darkrockstudios.apps.hammer.common.compose.designsystem.HdStatusGlyph
|
||||
|
|
@ -18,6 +19,7 @@ import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncLogE
|
|||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncLogI
|
||||
import com.darkrockstudios.apps.hammer.common.data.sync.projectsync.syncLogW
|
||||
import com.darkrockstudios.apps.hammer.common.projectselection.SyncLogMessageUi
|
||||
import com.darkrockstudios.apps.hammer.common.projectselection.SyncStatusRow
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
@ -34,6 +36,43 @@ private fun SyncLogRowsPreview() = Padded {
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SyncStatusRowsPreview() = Padded {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
val rows = listOf(
|
||||
ProjectsList.ProjectSyncStatus(
|
||||
"Pending Project",
|
||||
progress = 0f,
|
||||
status = ProjectsList.Status.Pending
|
||||
),
|
||||
ProjectsList.ProjectSyncStatus(
|
||||
"Syncing Project",
|
||||
progress = 0.4f,
|
||||
status = ProjectsList.Status.Syncing
|
||||
),
|
||||
ProjectsList.ProjectSyncStatus(
|
||||
"Conflicted Project",
|
||||
progress = 0.6f,
|
||||
status = ProjectsList.Status.NeedsResolution
|
||||
),
|
||||
ProjectsList.ProjectSyncStatus(
|
||||
"Failed Project",
|
||||
progress = 0.6f,
|
||||
status = ProjectsList.Status.Failed
|
||||
),
|
||||
ProjectsList.ProjectSyncStatus(
|
||||
"Synced Project",
|
||||
progress = 1f,
|
||||
status = ProjectsList.Status.Complete
|
||||
),
|
||||
)
|
||||
rows.forEachIndexed { index, row ->
|
||||
SyncStatusRow(projectStatus = row, isLast = index == rows.lastIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StatusGlyphPreview() = Padded {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue