Skip to content
Draft
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
3 changes: 3 additions & 0 deletions pretixscan/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ kotlin {
desktopTest.dependencies {
implementation(compose.desktop.uiTestJUnit4)
implementation(compose.desktop.currentOs)
implementation(libs.mockk)
implementation(libs.koin.test)
implementation(libs.koin.test.junit4)
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package eu.pretix.desktop.app.scan

import androidx.compose.runtime.Composable
import eu.pretix.scan.main.presentation.MainUiState
import eu.pretix.scan.main.presentation.MainUiStateData
import kotlinx.coroutines.flow.StateFlow

@Composable
expect fun GlobalScanSetup(
stateFlow: StateFlow<MainUiState<MainUiStateData>>,
onHandleDirectScan: suspend (String) -> Unit
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package eu.pretix.desktop.app.ui

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import eu.pretix.scan.tickets.presentation.QuestionImagePreview
import org.jetbrains.compose.resources.stringResource
import pretixscan.composeapp.generated.resources.Res
import pretixscan.composeapp.generated.resources.delete_photo
import pretixscan.composeapp.generated.resources.question_input_invalid
import pretixscan.composeapp.generated.resources.question_input_required
import pretixscan.composeapp.generated.resources.take_a_photo

@Composable
fun FiledFileUpload(
label: String? = null,
required: Boolean = false,
validation: FieldValidationState?,
selectedFilePath: String?,
onSelectFile: () -> Unit = {},
onDeleteFile: () -> Unit = {},
) {
Column(
horizontalAlignment = Alignment.Start
) {
if (label != null) {
RequiredTextLabel(label = label, required = required, fontWeight = FontWeight.SemiBold)
}
Row {
Column(
modifier = Modifier.weight(2f)
.padding(end = 16.dp)
) {
Button(onClick = onSelectFile) {
Text(stringResource(Res.string.take_a_photo))
}
if (selectedFilePath != null) {
Button(
onClick = onDeleteFile) {
Icon(
Icons.Filled.Delete,
contentDescription = stringResource(Res.string.delete_photo)
)
Text(stringResource(Res.string.delete_photo))
}
}
}

if (selectedFilePath != null) {
Box(modifier = Modifier.weight(1f)) {
QuestionImagePreview(filePath = selectedFilePath)
}
}
}

if (validation != null) {
when (validation) {
FieldValidationState.INVALID -> {
Text(
stringResource(Res.string.question_input_invalid),
color = Color.Red,
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp)
)
}

FieldValidationState.MISSING -> {
Text(
stringResource(Res.string.question_input_required),
color = Color.Red,
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp)
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import eu.pretix.desktop.app.navigation.Route
import eu.pretix.desktop.app.scan.GlobalScanSetup
import eu.pretix.desktop.app.ui.ScreenContentRoot
import eu.pretix.desktop.app.ui.autoScanEventListener
import eu.pretix.scan.main.presentation.selectevent.SelectEventDialog
import eu.pretix.scan.main.presentation.selectlist.SelectCheckInListDialog
import eu.pretix.scan.main.presentation.selectlist.SelectCheckInListForMultiEventDialog
Expand All @@ -32,6 +29,14 @@ fun MainScreen(
val uiState by viewModel.uiState.collectAsState()
val coroutineScope = rememberCoroutineScope()

GlobalScanSetup(
stateFlow = viewModel.uiState,
onHandleDirectScan = { secret ->
coroutineScope.launch {
viewModel.onHandleDirectScan(secret)
}
}
)

when (uiState) {
MainUiState.SelectEvent -> {
Expand Down Expand Up @@ -78,13 +83,7 @@ fun MainScreen(

is MainUiState.ReadyToScan -> {
val data = (uiState as MainUiState.ReadyToScan<MainUiStateData>).data
Column(
modifier = Modifier.autoScanEventListener(
onAlphanumericKey = { _ ->
// Event propagates to SearchTextField which auto-focuses on mount
}
)
) {
Column {
MainToolbar(
viewModel = viewModel,
eventSelection = data.eventSelection,
Expand Down Expand Up @@ -116,11 +115,7 @@ fun MainScreen(

is MainUiState.HandlingTicket -> {
val data = (uiState as MainUiState.HandlingTicket<MainUiStateData>).data
Column(
modifier = Modifier.autoScanEventListener { _ ->
// Event propagates to SearchTextField which maintains focus
}
) {
Column {
MainToolbar(
viewModel = viewModel,
eventSelection = data.eventSelection
Expand All @@ -143,7 +138,9 @@ fun MainScreen(
}
TicketHandlingDialog(
secret = data.secret,
onDismiss = viewModel::onHandleTicketHandlingDismissed
scanTimestamp = data.scanTimestamp,
onDismiss = viewModel::onHandleTicketHandlingDismissed,
onResultStateChanged = viewModel::onTicketResultDetermined
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.pretix.scan.main.presentation

import eu.pretix.desktop.cache.EventSelection
import eu.pretix.scan.tickets.data.ResultState
import org.joda.time.DateTime

// Presentation model for multi-event selection
Expand Down Expand Up @@ -28,7 +29,12 @@ sealed class MainUiState<out T> {
) : MainUiState<Nothing>()
}

data class MainUiStateData(val eventSelection: EventSelection, val secret: String? = null)
data class MainUiStateData(
val eventSelection: EventSelection,
val secret: String? = null,
val scanTimestamp: Long = 0L,
val resultState: ResultState? = null
)

fun MainUiStateData.secret(secret: String?): MainUiStateData {
return this.copy(secret = secret)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import eu.pretix.desktop.cache.Version
import eu.pretix.libpretixsync.check.TicketCheckProvider
import eu.pretix.libpretixsync.setup.RemoteEvent
import eu.pretix.libpretixsync.sqldelight.CheckInList
import eu.pretix.scan.tickets.data.ResultState
import eu.pretix.scan.tickets.data.requiresUserInteraction
import kotlinx.coroutines.flow.*
import java.util.logging.Logger

Expand Down Expand Up @@ -156,9 +158,37 @@ class MainViewModel(
suspend fun onHandleDirectScan(secret: String) {
log.info("AutoScan: Handling direct scan for ticket")
val currentState = _uiState.value
if (currentState is MainUiState.ReadyToScan) {
_uiState.update {
MainUiState.HandlingTicket(currentState.data.secret(secret))

when (currentState) {
is MainUiState.ReadyToScan -> {
_uiState.update {
MainUiState.HandlingTicket(
currentState.data
.secret(secret)
.copy(scanTimestamp = System.currentTimeMillis())
)
}
}
is MainUiState.HandlingTicket -> {
val currentResultState = currentState.data.resultState

if (currentResultState?.requiresUserInteraction() == true) {
log.info("AutoScan: Ignoring new scan - current dialog requires user interaction (state: $currentResultState)")
return
}

log.info("AutoScan: New scan while handling auto-dismissible dialog, interrupting")
_uiState.update { MainUiState.ReadyToScan(currentState.data.secret(null)) }
_uiState.update {
MainUiState.HandlingTicket(
currentState.data
.secret(secret)
.copy(scanTimestamp = System.currentTimeMillis())
)
}
}
else -> {
log.info("AutoScan: Ignoring scan in state: ${currentState::class.simpleName}")
}
}
}
Expand All @@ -172,6 +202,17 @@ class MainViewModel(
}
}

fun onTicketResultDetermined(resultState: ResultState) {
val currentState = _uiState.value
if (currentState is MainUiState.HandlingTicket) {
_uiState.update {
MainUiState.HandlingTicket(
currentState.data.copy(resultState = resultState)
)
}
}
}

suspend fun selectMultipleEvents(events: List<RemoteEvent>) {
if (events.isEmpty()) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fun SelectEventDialog(
Spacer(modifier = Modifier.height(16.dp))
}

// Event List - now supports both modes
// Event List
SelectEventList(
advancedMode = advancedMode,
selectedEvent = selectedEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ fun ResultState.color(): Color {
}
}

fun ResultState.requiresUserInteraction(): Boolean {
return this == ResultState.DIALOG_UNPAID || this == ResultState.DIALOG_QUESTIONS
}

fun ResultState.isAutoDismissible(): Boolean {
return this == ResultState.SUCCESS || this == ResultState.SUCCESS_EXIT
}

data class ResultStateData(
val resultState: ResultState,
val resultText: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import com.vanniktech.locale.Country


fun Country.readableName(): String {
return name.lowercase().replace(Regex("""(?:^|_+)(\p{L})""")) { (if (it.value.startsWith("_")) " " else "") + it.groupValues[1].uppercase() } // LIKE_THIS -> "Like This"
if (name.length <= 4 && name.all { it.isUpperCase() || it == '_' }) {
return name
}
return name.lowercase().replace(Regex("""(?:^|_+)(\p{L})""")) { (if (it.value.startsWith("_")) " " else "") + it.groupValues[1].uppercase() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fun QuestionPhoneNumber(
var country by remember { mutableStateOf(calculateDefaultCountry(selectedValue)) }
country.callingCodes.first()

LaunchedEffect(Unit, selectedValue) {
LaunchedEffect(Unit) {
country = calculateDefaultCountry(selectedValue)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,46 +210,14 @@ fun QuestionsDialogView(
}

QuestionType.F -> {
Column(
horizontalAlignment = Alignment.Start
) {
RequiredTextLabel(
label = field.label,
required = field.required,
fontWeight = FontWeight.SemiBold
)
Row {
Column(
modifier = Modifier.weight(2f)
.padding(end = 16.dp)
) {
Button(
onClick = {
viewModel.showModal(field)
}) {
Text(stringResource(Res.string.take_a_photo))
}
if (field.value != null) {
Button(
onClick = {
viewModel.updateAnswer(field.id, null)
}) {
Icon(
Icons.Filled.Delete,
contentDescription = stringResource(Res.string.delete_photo)
)
Text(stringResource(Res.string.delete_photo))
}
}
}

if (field.value != null) {
Box(modifier = Modifier.weight(1f)) {
QuestionImagePreview(filePath = field.value!!)
}
}
}
}
FiledFileUpload(
label = field.label,
required = field.required,
validation = field.validation,
selectedFilePath = field.value,
onSelectFile = { viewModel.showModal(field) },
onDeleteFile = { viewModel.updateAnswer(field.id, null) }
)
}

QuestionType.D -> {
Expand Down
Loading