Skip to content

Commit 522be17

Browse files
committed
allow selection of multiple events for scanning
cleanup
1 parent d3bbc78 commit 522be17

File tree

20 files changed

+932
-115
lines changed

20 files changed

+932
-115
lines changed

pretixscan/composeApp/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44
alias(libs.plugins.kotlinMultiplatform)
55
alias(libs.plugins.jetbrainsCompose)
66
alias(libs.plugins.compose.compiler)
7+
alias(libs.plugins.kotlinx.serialization)
78
id("kotlin-kapt")
89
alias(libs.plugins.osdetector)
910
alias(libs.plugins.composeHotReload)
@@ -34,6 +35,7 @@ kotlin {
3435
implementation(libs.org.json)
3536
implementation(libs.joda.time)
3637
implementation(libs.vanniktech.multiplatform.locale)
38+
implementation(libs.kotlinx.serialization.json)
3739

3840
// play short audio files
3941
implementation(libs.gadulka)

pretixscan/composeApp/src/commonMain/composeResources/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@
3333
<string name="debug_info_device">Device: %1$s</string>
3434
<string name="action_search">Search</string>
3535
<string name="action_manual">Manual setup</string>
36+
<string name="action_reload_events">Reload Events</string>
3637
<string name="operation_select_event">Select Event</string>
3738
<string name="operation_select_events">Select Events</string>
3839
<string name="events_selected">%s events selected</string>
3940
<string name="operation_select_checkinlist">Select Check-in list</string>
41+
<string name="advanced_mode">Advanced Mode</string>
42+
<string name="advanced_mode_description">Select multiple events for concurrent scanning</string>
43+
<string name="change_event_selection">Change event selection</string>
4044
<string name="settings_label_auto_sync">Automatic Synchronization</string>
4145
<string name="settings_label_orders_sync">Download orders</string>
4246
<string name="settings_summary_orders_sync">Without this option, pretixSCAN will not download order data and not be able to scan offline tickets with pretix\'s default settings.</string>
@@ -205,4 +209,6 @@
205209
<string name="settings_printer_layout_landscape">Landscape</string>
206210
<string name="settings_printer_layout_portrait">Portrait</string>
207211
<string name="settings_label_badge_layout">Orientation</string>
212+
<string name="sync_completed">Sync completed successfully</string>
213+
<string name="sync_error">Sync completed with errors</string>
208214
</resources>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package eu.pretix.desktop.app.sync
2+
3+
import androidx.compose.foundation.layout.*
4+
import androidx.compose.material.icons.Icons
5+
import androidx.compose.material.icons.filled.CheckCircle
6+
import androidx.compose.material.icons.filled.Error
7+
import androidx.compose.material3.CircularProgressIndicator
8+
import androidx.compose.material3.Icon
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Text
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.unit.dp
15+
import eu.pretix.desktop.cache.EventSelection
16+
import org.jetbrains.compose.resources.stringResource
17+
import pretixscan.composeapp.generated.resources.Res
18+
import pretixscan.composeapp.generated.resources.sync_completed
19+
import pretixscan.composeapp.generated.resources.sync_error
20+
21+
@Composable
22+
fun MultiEventSyncProgress(
23+
syncState: SyncState,
24+
eventSyncStates: Map<String, EventSyncState>,
25+
events: List<EventSelection>
26+
) {
27+
Column(
28+
modifier = Modifier.fillMaxWidth().padding(16.dp),
29+
verticalArrangement = Arrangement.spacedBy(8.dp)
30+
) {
31+
Text(
32+
text = when (syncState) {
33+
is SyncState.InProgress -> syncState.message
34+
is SyncState.Success -> stringResource(Res.string.sync_completed)
35+
is SyncState.Error -> stringResource(Res.string.sync_error)
36+
else -> ""
37+
},
38+
style = MaterialTheme.typography.titleMedium
39+
)
40+
41+
// Show per-event progress
42+
events.forEach { event ->
43+
val state = eventSyncStates[event.eventSlug] ?: EventSyncState.Pending
44+
EventSyncProgressItem(event = event, state = state)
45+
}
46+
}
47+
}
48+
49+
@Composable
50+
private fun EventSyncProgressItem(
51+
event: EventSelection,
52+
state: EventSyncState
53+
) {
54+
Row(
55+
modifier = Modifier.fillMaxWidth(),
56+
horizontalArrangement = Arrangement.spacedBy(12.dp),
57+
verticalAlignment = Alignment.CenterVertically
58+
) {
59+
when (state) {
60+
EventSyncState.Pending -> CircularProgressIndicator(
61+
modifier = Modifier.size(20.dp),
62+
strokeWidth = 2.dp
63+
)
64+
is EventSyncState.InProgress -> CircularProgressIndicator(
65+
modifier = Modifier.size(20.dp),
66+
strokeWidth = 2.dp
67+
)
68+
EventSyncState.Success -> Icon(
69+
Icons.Default.CheckCircle,
70+
contentDescription = null,
71+
tint = MaterialTheme.colorScheme.primary,
72+
modifier = Modifier.size(20.dp)
73+
)
74+
is EventSyncState.Error -> Icon(
75+
Icons.Default.Error,
76+
contentDescription = null,
77+
tint = MaterialTheme.colorScheme.error,
78+
modifier = Modifier.size(20.dp)
79+
)
80+
}
81+
82+
Column(modifier = Modifier.weight(1f)) {
83+
Text(
84+
text = event.eventName,
85+
style = MaterialTheme.typography.bodyMedium
86+
)
87+
when (state) {
88+
is EventSyncState.InProgress -> {
89+
Text(
90+
text = state.message,
91+
style = MaterialTheme.typography.bodySmall,
92+
color = MaterialTheme.colorScheme.onSurfaceVariant
93+
)
94+
}
95+
is EventSyncState.Error -> {
96+
Text(
97+
text = state.message,
98+
style = MaterialTheme.typography.bodySmall,
99+
color = MaterialTheme.colorScheme.error
100+
)
101+
}
102+
else -> {}
103+
}
104+
}
105+
}
106+
}

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/desktop/app/sync/SyncRootService.kt

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class SyncRootService(private val appConfig: AppConfig) : ViewModel() {
2424
private val _minimumSyncState = MutableStateFlow<SyncState>(SyncState.Idle)
2525
val minimumSyncState: StateFlow<SyncState> = _minimumSyncState.asStateFlow()
2626

27+
private val _eventSyncStates = MutableStateFlow<Map<String, EventSyncState>>(emptyMap())
28+
val eventSyncStates: StateFlow<Map<String, EventSyncState>> = _eventSyncStates.asStateFlow()
2729

2830
private val _showMainSyncProgress = MutableStateFlow(false)
2931
val showMainSyncProgress: StateFlow<Boolean> = _showMainSyncProgress.asStateFlow()
@@ -65,22 +67,65 @@ class SyncRootService(private val appConfig: AppConfig) : ViewModel() {
6567
_syncState.value = SyncState.Idle
6668
return
6769
}
68-
log.info("performing a sync")
69-
_syncState.value = SyncState.InProgress("")
70+
71+
val events = appConfig.eventSelections
72+
if (events.isEmpty()) {
73+
log.info("no events to sync")
74+
_syncState.value = SyncState.Idle
75+
return
76+
}
77+
78+
log.info("performing sync for ${events.size} events")
79+
80+
// Initialize event sync states
81+
_eventSyncStates.value = events.associate {
82+
it.eventSlug to EventSyncState.Pending
83+
}
84+
85+
_syncState.value = SyncState.InProgress("Syncing ${events.size} event(s)...")
7086
val syncManager = GlobalContext.get().get<SyncManager>()
71-
syncManager.sync(force) {
87+
88+
syncManager.sync(force) { message ->
7289
runBlocking {
7390
withContext(Dispatchers.Main) {
74-
_syncState.value = SyncState.InProgress(it)
91+
_syncState.value = SyncState.InProgress(message)
92+
93+
// Update per-event state based on message content
94+
events.forEach { event ->
95+
if (message.contains(event.eventSlug, ignoreCase = true) ||
96+
message.contains(event.eventName, ignoreCase = true)) {
97+
_eventSyncStates.update { states ->
98+
states + (event.eventSlug to EventSyncState.InProgress(message))
99+
}
100+
}
101+
}
75102
}
76103
}
77104
}
105+
106+
// Mark all as success
107+
_eventSyncStates.value = events.associate {
108+
it.eventSlug to EventSyncState.Success
109+
}
110+
78111
_syncState.value = SyncState.Success(lastSync = nowMillis)
79112
appConfig.lastFailedSync = 0L
80113
appConfig.lastSync = nowMillis
81114
appConfig.lastDownload = nowMillis
82115
} catch (e: Exception) {
83116
log.warning("sync failed: ${e.stackTraceToString()}")
117+
118+
// Mark in-progress events as error
119+
_eventSyncStates.update { states ->
120+
states.mapValues { (_, state) ->
121+
when (state) {
122+
is EventSyncState.InProgress -> EventSyncState.Error(e.localizedMessage ?: "Sync failed")
123+
EventSyncState.Pending -> EventSyncState.Error(e.localizedMessage ?: "Sync failed")
124+
else -> state
125+
}
126+
}
127+
}
128+
84129
_syncState.value = SyncState.Error(e.localizedMessage ?: "Unknown error")
85130
appConfig.lastFailedSync = nowMillis
86131
appConfig.lastFailedSyncMsg = e.localizedMessage ?: "Unknown error"
@@ -160,6 +205,13 @@ sealed class SyncState {
160205
data class Error(val message: String) : SyncState()
161206
}
162207

208+
sealed class EventSyncState {
209+
object Pending : EventSyncState()
210+
data class InProgress(val message: String) : EventSyncState()
211+
object Success : EventSyncState()
212+
data class Error(val message: String) : EventSyncState()
213+
}
214+
163215
val LocalSyncRootService = compositionLocalOf<SyncRootService> {
164216
error("No SyncRootService found! Make sure to provide it in SyncRoot.")
165217
}

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/desktop/app/ui/RequiredTextLabel.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// ABOUTME: Questions with custom components sometimes use stand-alone Text for their label
2-
31
package eu.pretix.desktop.app.ui
42

53
import androidx.compose.foundation.layout.padding

0 commit comments

Comments
 (0)