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
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "pretixscan/libpretixsync-repo"]
path = pretixscan/libpretixsync-repo
url = https://github.com/pretix/libpretixsync.git
branch = sqldelight2
branch = master
[submodule "pretixscan/libpretixprint-repo"]
path = pretixscan/libpretixprint-repo
url = https://github.com/pretix/libpretixprint.git
Expand Down
5 changes: 5 additions & 0 deletions pretixscan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ For example, create a distribution package for the current OS:

By default, packages are created under `pretixscan/composeApp/build/compose/binaries`.

## Translations

- Guidelines for localizing a KMP project can be
found [here](https://kotlinlang.org/docs/multiplatform/compose-localize-strings.html#what-s-next).
- Base locale is stored at `pretixscan/composeApp/src/commonMain/composeResources/values/strings.xml`

## Acknowledgements

Expand Down
1 change: 1 addition & 0 deletions pretixscan/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.app.cash.sqldelight) apply false
alias(libs.plugins.osdetector) apply false
alias(libs.plugins.composeHotReload) apply false
alias(libs.plugins.gmazzo) apply false
}

buildscript {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">pretixSCAN</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
<string name="settings_label_orders_sync">Download orders</string>
<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>
<string name="settings_label_auto_switch">Automatic event selection</string>
<string name="settings_summary_auto_switch">With this option, pretixSCAN will ask the server during synchronization if the server suggests switching the app to a different event and check-in list and then switches automatically.</string>
<string name="settings_summary_auto_switch">With this option, pretixSCAN will ask the server during synchronization
if the server suggests switching the app to a different event and check-in list and then switch automatically.
</string>
<plurals name="sync_status_time_minutes">
<item quantity="one">synchronized %1$d minute ago</item>
<item quantity="other">synchronized %1$d minutes ago</item>
Expand Down Expand Up @@ -75,7 +77,9 @@
<string name="first_scanned">First scanned: </string>
<string name="datetime_format">%1$tY-%1$tm-%1$td %1$tH:%1$tM</string>
<string name="ok">OK</string>
<string name="tutorial_setup">To get started, create a new device in the "Devices" section of your organizer account in the pretix backend. You will be then presented with a configuration QR code to scan.</string>
<string name="tutorial_setup">To get started, create a new device in the "Devices" section of your organizer account
in the pretix backend. You will then be presented with a configuration QR code to scan.
</string>
<string name="setup_error_legacy_qr_code">The QR code you scanned is for an old predecessor of this app. Please go to the "Devices" section in your pretix organizer account and create a new device there.</string>
<string name="setup_error_invalid_qr_code">The QR code you scanned is not a valid configuration code. Please make sure you created the code in the "Devices" section of a pretix organizer account.</string>
<string name="setup_error_version_too_high">The QR code you scanned is from a newer version of pretix than this app has been built for. Please check if you can get an update for this app!</string>
Expand Down Expand Up @@ -114,7 +118,10 @@
<item>20 seconds or errors</item>
<item>Only errors or connection loss</item>
</string-array>
<string name="settings_summary_scan_offline">When scanning offline, your device will verify all input with its internal database instead with the server and synchronize its database with the server occasionally. This is more reliable, but allows a ticket to be scanned twice if you scan with multiple devices.</string>
<string name="settings_summary_scan_offline">When scanning offline, your device will verify all input with its
internal database instead of with the server and synchronize its database with the server occasionally. This is
more reliable, but allows a ticket to be scanned twice if you scan with multiple devices.
</string>
<string name="settings_label_ui_reduce_motion">Reduce motion</string>
<string name="settings_label_ui_reduce_motion_summary">Disables certain features like blinking for special tickets.
</string>
Expand Down Expand Up @@ -156,7 +163,7 @@
<string name="error_sync_in_background">A synchronization is already running in the background.</string>
<string name="dialog_unpaid_title">Unpaid order</string>
<string name="dialog_unpaid_text">This ticket belongs to an order that has not yet been paid. Do you still want to check it in? This will not mark the order as paid.</string>
<string name="dialog_unpaid_retry">Check in anyways</string>
<string name="dialog_unpaid_retry">Check in anyway</string>
<string name="preference_badgeprint_enable">Enable badge printing</string>
<string name="preference_autobadgeprint_enable">Print badges automatically</string>
<string name="preference_badgeprint_install_pretixprint">Printing badges requires the separate pretixPRINT app. Do you want to install it?</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fun createSyncDatabase(
price_after_voucherAdapter = bigDecimalAdapter,
tax_rateAdapter = bigDecimalAdapter,
tax_valueAdapter = bigDecimalAdapter,
line_price_grossAdapter = bigDecimalAdapter,
),
ReceiptAdapter = Receipt.Adapter(
datetime_closedAdapter = dateAdapter,
Expand All @@ -48,5 +49,9 @@ fun createSyncDatabase(
date_toAdapter = dateAdapter,
),
QueuedCheckInAdapter = QueuedCheckIn.Adapter(datetimeAdapter = dateAdapter),
DiscountAdapter = Discount.Adapter(
available_fromAdapter = dateAdapter,
available_untilAdapter = dateAdapter,
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,23 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.Divider
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import eu.pretix.desktop.app.ui.ListDivider
import eu.pretix.desktop.app.ui.SelectListRow
import eu.pretix.libpretixsync.setup.RemoteEvent
import eu.pretix.scan.tickets.presentation.formatDateForDisplay
import eu.pretix.scan.main.utils.formatEventDateTimeRange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.stringResource
Expand Down Expand Up @@ -104,7 +102,7 @@ fun SelectEventList(
Row(
Modifier
.selectable(
selected = item.slug in selectedEventSlugs || item.slug == selectedEvent?.slug,
selected = isEventSelected(item, selectedEvent, selectedEventSlugs, advancedMode),
onClick = { onSelectEvent(item) },
indication = LocalIndication.current,
interactionSource = null,
Expand All @@ -114,7 +112,7 @@ fun SelectEventList(
.padding(vertical = 8.dp)
.then(
if (!advancedMode) {
Modifier.selectableGroup() // Accessibility for radio group
Modifier.selectableGroup()
} else {
Modifier
}
Expand All @@ -123,20 +121,20 @@ fun SelectEventList(
horizontalArrangement = Arrangement.Start,
) {

Checkbox(
checked = item.slug in selectedEventSlugs || item.slug == selectedEvent?.slug,
onCheckedChange = { onSelectEvent(item) }
)
if (advancedMode) {
Checkbox(
checked = isEventSelected(item, selectedEvent, selectedEventSlugs, true),
onCheckedChange = { onSelectEvent(item) }
)
} else {
RadioButton(
selected = isEventSelected(item, selectedEvent, selectedEventSlugs, false),
onClick = { onSelectEvent(item) }
)
}
Column {
Text(item.name, fontWeight = FontWeight.Bold)
val startDate = formatEventDate(item.date_from)
val endDate = if (item.date_to != null) formatEventDate(item.date_to) else null
val dateText = if (endDate != null && endDate.isNotEmpty()) {
"$startDate - $endDate"
} else {
startDate
}
Text(dateText)
Text(formatEventDateTimeRange(item.date_from, item.date_to))
}
}
ListDivider(index, list.lastIndex)
Expand All @@ -155,29 +153,16 @@ fun SelectEventList(
}


/**
* Safely formats a Joda Time DateTime for display using locale-aware formatting.
* Falls back to raw date string if formatting fails.
*/
private fun formatEventDate(dateTime: org.joda.time.DateTime?): String {
if (dateTime == null) return ""

return try {
// Convert to LocalDate and get yyyy-MM-dd format
val dateString = dateTime.toLocalDate().toString() // This produces "yyyy-MM-dd"
// Use existing formatter which handles locale-aware display
val formatted = formatDateForDisplay(dateString)
if (formatted.isNotEmpty() && formatted != dateString) {
formatted
} else {
dateString // Fallback to ISO format if locale formatting failed
}
} catch (e: Exception) {
// Ultimate fallback
try {
dateTime.toLocalDate().toString()
} catch (e2: Exception) {
dateTime.toString()
}
internal fun isEventSelected(
item: RemoteEvent,
selectedEvent: RemoteEvent?,
selectedSlugs: Set<String>,
advancedMode: Boolean
): Boolean {
return if (advancedMode) {
item.slug in selectedSlugs
} else {
item.slug == selectedEvent?.slug &&
item.subevent_id == selectedEvent.subevent_id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.Divider
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -98,9 +95,9 @@ fun SelectCheckInList(
horizontalArrangement = Arrangement.Start,
) {

Checkbox(
checked = item.id == selectedCheckInList?.id,
onCheckedChange = { onSelectCheckInList(item) }
RadioButton(
selected = item.id == selectedCheckInList?.id,
onClick = { onSelectCheckInList(item) }
)

Column {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package eu.pretix.scan.main.utils

import java.util.logging.Logger

private val logger = Logger.getLogger("DateFormatUtils")

fun org.joda.time.DateTime.isMidnight(): Boolean {
return hourOfDay == 0 && minuteOfHour == 0 && secondOfMinute == 0 && millisOfSecond == 0
}

fun formatEventDateTime(dateTime: org.joda.time.DateTime?): String {
if (dateTime == null) return ""

return try {
if (dateTime.isMidnight()) {
org.joda.time.format.DateTimeFormat.mediumDate().print(dateTime)
} else {
org.joda.time.format.DateTimeFormat.mediumDateTime().print(dateTime)
}
} catch (e: Exception) {
logger.warning("Failed to format date time: ${e.message}")
dateTime.toString()
}
}

/**
* Formats an event date range with optional end date.
* Returns the formatted start date, or a range if an end date is provided.
*/
fun formatEventDateTimeRange(from: org.joda.time.DateTime?, to: org.joda.time.DateTime?): String {
if (from == null) return ""

val fromFormatted = formatEventDateTime(from)
if (to == null) return fromFormatted

val toFormatted = formatEventDateTime(to)
return if (toFormatted.isNotEmpty()) {
"$fromFormatted - $toFormatted"
} else {
fromFormatted
}
}
Loading