mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
fe7847fc08
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
250 lines
9.6 KiB
Swift
250 lines
9.6 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
|
|
/// The home-screen and lock-screen widgets; all but the Atlas show one trip,
|
|
/// the dashboard's or the one picked in Edit Widget (TripIntent.swift). Their gallery names and
|
|
/// descriptions are in <language>.lproj/Localizable.strings. Each reads the snapshot the app
|
|
/// last wrote (WidgetSnapshot) and draws it with the views in *Content.swift,
|
|
/// which know nothing of WidgetKit so the preview renderer can draw them too.
|
|
struct TrekEntry: TimelineEntry {
|
|
let date: Date
|
|
let snapshot: WidgetSnapshot?
|
|
}
|
|
|
|
struct TrekProvider: TimelineProvider {
|
|
func placeholder(in context: Context) -> TrekEntry {
|
|
TrekEntry(date: Date(), snapshot: .sample(running: false))
|
|
}
|
|
|
|
/// The gallery shows the user's own trip when the app has written one, a
|
|
/// sample trip before that.
|
|
func getSnapshot(in context: Context, completion: @escaping (TrekEntry) -> Void) {
|
|
let stored = WidgetSnapshot.load()
|
|
let snapshot: WidgetSnapshot? = (stored?.trip != nil || !context.isPreview) ? stored : WidgetSnapshot.sample(running: false)
|
|
completion(TrekEntry(date: Date(), snapshot: snapshot))
|
|
}
|
|
|
|
/// An entry every quarter hour for the next six hours, so the now line and
|
|
/// the next stop move on without the app; the app reloads the timeline
|
|
/// whenever it writes a new snapshot.
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<TrekEntry>) -> Void) {
|
|
completion(TrekProvider.timeline(WidgetSnapshot.load()))
|
|
}
|
|
|
|
static func timeline(_ snapshot: WidgetSnapshot?) -> Timeline<TrekEntry> {
|
|
let now = Date()
|
|
let calendar = Calendar.current
|
|
let minute = calendar.component(.minute, from: now)
|
|
let firstStep = calendar.date(byAdding: .minute, value: 15 - minute % 15, to: now) ?? now
|
|
var entries = [TrekEntry(date: now, snapshot: snapshot)]
|
|
for step in 0..<24 {
|
|
if let date = calendar.date(byAdding: .minute, value: step * 15, to: firstStep) {
|
|
entries.append(TrekEntry(date: calendar.date(bySetting: .second, value: 0, of: date) ?? date, snapshot: snapshot))
|
|
}
|
|
}
|
|
return Timeline(entries: entries, policy: .atEnd)
|
|
}
|
|
}
|
|
|
|
extension TrekFamily {
|
|
init(_ family: WidgetFamily) {
|
|
switch family {
|
|
case .systemSmall: self = .small
|
|
case .systemMedium: self = .medium
|
|
case .accessoryCircular: self = .circular
|
|
case .accessoryRectangular: self = .rectangular
|
|
case .accessoryInline: self = .inline
|
|
default: self = .large
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The widget's ground: the content on its background, through the container
|
|
/// background on iOS 17 (which the system can tint and remove), the plain way before.
|
|
struct TrekContainer<Content: View, Background: View>: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let link: URL?
|
|
@ViewBuilder var content: () -> Content
|
|
@ViewBuilder var background: () -> Background
|
|
|
|
var body: some View {
|
|
let accessory = TrekFamily(family).isAccessory
|
|
Group {
|
|
if #available(iOSApplicationExtension 17.0, *) {
|
|
content().containerBackground(for: .widget) {
|
|
if accessory { Color.clear } else { background() }
|
|
}
|
|
} else {
|
|
content().padding(accessory ? 0 : 16).background(accessory ? nil : background())
|
|
}
|
|
}
|
|
.widgetURL(link)
|
|
}
|
|
}
|
|
|
|
// MARK: - Trip
|
|
|
|
struct NextTripWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
// The kind stays "NextTrip" so widgets placed before the redesign keep working.
|
|
AppIntentConfiguration(kind: "NextTrip", intent: SelectTripIntent.self, provider: TrekTripProvider()) { entry in
|
|
TripWidgetView(entry: entry)
|
|
}
|
|
.configurationDisplayName("widget.trip.name")
|
|
.description("widget.trip.description")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryCircular, .accessoryRectangular, .accessoryInline])
|
|
}
|
|
}
|
|
|
|
struct TripWidgetView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
|
|
var body: some View {
|
|
let snapshot = entry.snapshot
|
|
TrekContainer(link: snapshot?.trip.flatMap { WidgetSnapshot.link($0.path) }) {
|
|
TripContent(snapshot: snapshot, family: TrekFamily(family), now: entry.date)
|
|
} background: {
|
|
if snapshot?.trip != nil { TripBackdrop(trip: snapshot?.trip) } else { TrekPaper() }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Today
|
|
|
|
struct TodayWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(kind: "TripToday", intent: SelectTripIntent.self, provider: TrekTripProvider()) { entry in
|
|
TrekContainer(link: entry.snapshot?.trip.flatMap { WidgetSnapshot.link($0.path) }) {
|
|
TodayFamilyView(entry: entry)
|
|
} background: {
|
|
TrekPaper()
|
|
}
|
|
}
|
|
.configurationDisplayName("widget.today.name")
|
|
.description("widget.today.description")
|
|
.supportedFamilies([.systemMedium, .systemLarge])
|
|
}
|
|
}
|
|
|
|
private struct TodayFamilyView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
var body: some View { TodayContent(snapshot: entry.snapshot, family: TrekFamily(family), now: entry.date) }
|
|
}
|
|
|
|
// MARK: - Bookings
|
|
|
|
struct BookingsWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(kind: "TripBookings", intent: SelectTripIntent.self, provider: TrekTripProvider()) { entry in
|
|
TrekContainer(link: entry.snapshot?.bookings?.first.flatMap { WidgetSnapshot.link($0.path) }) {
|
|
BookingsFamilyView(entry: entry)
|
|
} background: {
|
|
TrekPaper()
|
|
}
|
|
}
|
|
.configurationDisplayName("widget.bookings.name")
|
|
.description("widget.bookings.description")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular, .accessoryInline])
|
|
}
|
|
}
|
|
|
|
private struct BookingsFamilyView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
var body: some View { BookingsContent(snapshot: entry.snapshot, family: TrekFamily(family), now: entry.date) }
|
|
}
|
|
|
|
// MARK: - To-dos
|
|
|
|
struct TodosWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(kind: "TripTodos", intent: SelectTripIntent.self, provider: TrekTripProvider()) { entry in
|
|
TrekContainer(link: entry.snapshot?.todos.flatMap { WidgetSnapshot.link($0.path) }) {
|
|
TodosFamilyView(entry: entry)
|
|
} background: {
|
|
TrekPaper()
|
|
}
|
|
}
|
|
.configurationDisplayName("widget.todos.name")
|
|
.description("widget.todos.description")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryCircular, .accessoryRectangular])
|
|
}
|
|
}
|
|
|
|
private struct TodosFamilyView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
var body: some View { TodosContent(snapshot: entry.snapshot, family: TrekFamily(family), now: entry.date) }
|
|
}
|
|
|
|
// MARK: - Packing
|
|
|
|
struct PackingWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(kind: "TripPacking", intent: SelectTripIntent.self, provider: TrekTripProvider()) { entry in
|
|
TrekContainer(link: entry.snapshot?.packing.flatMap { WidgetSnapshot.link($0.path) }) {
|
|
PackingFamilyView(entry: entry)
|
|
} background: {
|
|
TrekPaper()
|
|
}
|
|
}
|
|
.configurationDisplayName("widget.packing.name")
|
|
.description("widget.packing.description")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryCircular, .accessoryRectangular])
|
|
}
|
|
}
|
|
|
|
private struct PackingFamilyView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
var body: some View { PackingContent(snapshot: entry.snapshot, family: TrekFamily(family), now: entry.date) }
|
|
}
|
|
|
|
// MARK: - Atlas
|
|
|
|
struct AtlasWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: "TripAtlas", provider: TrekProvider()) { entry in
|
|
TrekContainer(link: WidgetSnapshot.link("/atlas")) {
|
|
AtlasFamilyView(entry: entry)
|
|
} background: {
|
|
TrekPaper(contours: false)
|
|
}
|
|
}
|
|
.configurationDisplayName("widget.atlas.name")
|
|
.description("widget.atlas.description")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryCircular, .accessoryRectangular, .accessoryInline])
|
|
}
|
|
}
|
|
|
|
private struct AtlasFamilyView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
var body: some View { AtlasContent(snapshot: entry.snapshot, family: TrekFamily(family)) }
|
|
}
|
|
|
|
// MARK: - Travel group
|
|
|
|
struct CollabWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(kind: "TripCollab", intent: SelectTripIntent.self, provider: TrekTripProvider()) { entry in
|
|
TrekContainer(link: entry.snapshot?.collab.flatMap { WidgetSnapshot.link($0.path) }) {
|
|
CollabFamilyView(entry: entry)
|
|
} background: {
|
|
TrekPaper()
|
|
}
|
|
}
|
|
.configurationDisplayName("widget.collab.name")
|
|
.description("widget.collab.description")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular])
|
|
}
|
|
}
|
|
|
|
private struct CollabFamilyView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TrekEntry
|
|
var body: some View { CollabContent(snapshot: entry.snapshot, family: TrekFamily(family)) }
|
|
}
|