Files
TREK/client/ios/App/TREKWidgets/TripIntent.swift
T
Maurice fe7847fc08 feat(native): pick the trip a widget shows in Edit Widget
Widgets followed the dashboard's trip only, which is the one under way, so a
packing list or to-dos for a trip still being planned could not sit on the
home screen. The trip, today, bookings, to-dos, packing and group widgets now
take a trip in Edit Widget (an AppIntent with the trips running or ahead,
Automatic by default, which keeps following the dashboard); the world map
needs none.

- The app writes a snapshot per trip running or ahead (at most six, the
  dashboard's order) and the list to pick from, under their own keys
  (feature widgetTrips, so an older binary never mistakes one for the
  dashboard's); bags, chat and polls are kept ten minutes, covers per trip
- The widget extension targets iOS 17, which configurable widgets need; the
  apps are not released, so no installed widget goes away
- The choices and their texts in the 28 languages of the gallery
2026-11-06 21:41:21 +01:00

73 lines
2.6 KiB
Swift

import AppIntents
import WidgetKit
/// The trip a widget shows, picked in "Edit Widget": the dashboard's trip by
/// default ("Automatic"), or any trip the app listed in widget.trips.
struct TripChoice: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "widget.intent.trip"
static var defaultQuery = TripChoiceQuery()
/// The id of the automatic choice, which follows the dashboard's trip.
static let automaticID = "auto"
var id: String
var title: String
var dates: String?
var displayRepresentation: DisplayRepresentation {
if id == TripChoice.automaticID {
return DisplayRepresentation(title: "widget.intent.auto", subtitle: "widget.intent.autoHint")
}
return DisplayRepresentation(title: "\(title)", subtitle: dates.map { "\($0)" })
}
static var automatic: TripChoice { TripChoice(id: automaticID, title: "", dates: nil) }
}
struct TripChoiceQuery: EntityQuery {
func entities(for identifiers: [TripChoice.ID]) async throws -> [TripChoice] {
all().filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws -> [TripChoice] {
all()
}
func defaultResult() async -> TripChoice? {
.automatic
}
/// Automatic first, then the trips the app sent, soonest first.
private func all() -> [TripChoice] {
[.automatic] + WidgetSnapshot.tripIndex().map {
TripChoice(id: String($0.id), title: $0.title, dates: RelativeDay.range($0.start, $0.end))
}
}
}
struct SelectTripIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "widget.intent.title"
static var description = IntentDescription("widget.intent.description")
@Parameter(title: "widget.intent.trip")
var trip: TripChoice?
}
/// Like TrekProvider, for the widgets that show one trip: the snapshot of the
/// trip picked for the widget, or the dashboard's.
struct TrekTripProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> TrekEntry {
TrekEntry(date: Date(), snapshot: .sample(running: false))
}
func snapshot(for configuration: SelectTripIntent, in context: Context) async -> TrekEntry {
let stored = WidgetSnapshot.load(trip: configuration.trip?.id)
let snapshot: WidgetSnapshot? = (stored?.trip != nil || !context.isPreview) ? stored : WidgetSnapshot.sample(running: false)
return TrekEntry(date: Date(), snapshot: snapshot)
}
func timeline(for configuration: SelectTripIntent, in context: Context) async -> Timeline<TrekEntry> {
TrekProvider.timeline(WidgetSnapshot.load(trip: configuration.trip?.id))
}
}