Refine trends and debug harness; clean up warnings

This commit is contained in:
FuelBoard Contributor
2026-08-17 14:06:26 +01:00
parent 68d970720a
commit 63084322f6
16 changed files with 398 additions and 80 deletions
@@ -3,6 +3,17 @@
// Lives in the widget extension (the standard host for ActivityConfiguration).
// Shows the cheapest station for the pinned fuel within the app's chosen
// radius. Tapping anywhere opens Apple Maps directions to that station.
//
// ADAPTIVE LAYOUT: ActivityConfiguration shares ONE content view across the
// Lock Screen, banner, and the CarPlay small slot there is no per-platform
// closure. So this view provides two layouts and lets ViewThatFits pick by
// available width:
// richBody the full three-column design (glyph · fuel+station · price),
// wins wherever there's Lock Screen width (it carries an
// explicit minWidth so it can never be squeezed into the car).
// compactBody a minimal price-strip (glyph+fuel+price, station caption
// below) that wins in the CarPlay/Apple Watch Smart Stack
// small slot.
import ActivityKit
import SwiftUI
@@ -17,7 +28,7 @@ import WidgetKit
struct FuelBoardLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: FuelBoardLiveActivityAttributes.self) { context in
// Lock Screen / banner presentation
// Lock Screen / banner / CarPlay small slot presentation
FuelBoardLiveActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
@@ -44,43 +55,81 @@ struct FuelBoardLiveActivity: Widget {
}
}
/// Lock Screen / banner body the main presentation.
/// Lock Screen / banner / CarPlay body adaptive.
private struct FuelBoardLiveActivityView: View {
let context: ActivityViewContext<FuelBoardLiveActivityAttributes>
var body: some View {
Link(destination: context.state.mapsURL) {
HStack(spacing: 12) {
// LEFT station brand glyph
Image(systemName: "fuelpump.circle.fill")
.font(.system(size: 32))
.foregroundStyle(.green, .white)
.frame(width: 40, height: 40)
// MIDDLE fuel + station
VStack(alignment: .leading, spacing: 2) {
Text("Cheapest \(context.state.fuel.displayName)")
.font(.headline)
Text("\(context.state.stationName) · \(context.state.distanceText)")
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
// RIGHT price
VStack(alignment: .trailing, spacing: 2) {
FuelStore.priceTextAttributed(context.state.pricePence,
style: context.state.priceDisplayStyle,
size: 22, weight: .bold)
Text("Tap for directions")
.font(.caption2)
.foregroundStyle(.secondary)
}
ViewThatFits(in: .horizontal) {
// rich first wins on full-width Lock Screen / banner
richBody
// CarPlay / Watch small slot is far narrower than this,
// so ViewThatFits reliably falls through to compactBody.
.frame(minWidth: 280)
// compact fallback the car's small supplemental slot
compactBody
}
.padding()
}
}
/// Full three-column design (unchanged): brand glyph · fuel+station · price.
private var richBody: some View {
HStack(spacing: 12) {
// LEFT station brand glyph
Image(systemName: "fuelpump.circle.fill")
.font(.system(size: 32))
.foregroundStyle(.green, .white)
.frame(width: 40, height: 40)
// MIDDLE fuel + station
VStack(alignment: .leading, spacing: 2) {
Text("Cheapest \(context.state.fuel.displayName)")
.font(.headline)
Text("\(context.state.stationName) · \(context.state.distanceText)")
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
// RIGHT price
VStack(alignment: .trailing, spacing: 2) {
FuelStore.priceTextAttributed(context.state.pricePence,
style: context.state.priceDisplayStyle,
size: 22, weight: .bold)
Text("Tap for directions")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding()
}
/// Minimal strip for the small CarPlay / Watch Smart Stack slot:
/// glyph + fuel left, bold price right, truncated station below.
private var compactBody: some View {
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 5) {
Image(systemName: "fuelpump.fill")
.font(.caption2)
.foregroundStyle(.green)
Text(context.state.fuel.displayName)
.font(.caption2.weight(.semibold))
.lineLimit(1)
Spacer(minLength: 4)
FuelStore.priceTextAttributed(context.state.pricePence,
style: context.state.priceDisplayStyle,
size: 15, weight: .bold)
.lineLimit(1)
}
Text(context.state.stationName)
.font(.system(size: 9))
.foregroundStyle(.secondary)
.lineLimit(1)
}
.padding(8)
}
}
/// Dynamic Island expanded regions + compact trailing price only.
@@ -140,4 +189,4 @@ extension FuelBoardLiveActivityAttributes.ContentState {
var mapsURL: URL {
URL(string: "maps://?daddr=\(lat),\(lng)&t=d")!
}
}
}
+3 -3
View File
@@ -156,7 +156,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
URLQueryItem(name: "price", value: String(first?.prices[d.fuel] ?? -1)),
]
guard let url = components.url else { return }
var request = RelayFuelProvider.relayRequest(url, client: "widget", timeout: 2)
let request = RelayFuelProvider.relayRequest(url, client: "widget", timeout: 2)
Task {
_ = try? await URLSession.shared.data(for: request)
}
@@ -312,7 +312,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
URLQueryItem(name: "radius", value: String(radiusKM)),
URLQueryItem(name: "limit", value: "500"),
]
var request = RelayFuelProvider.relayRequest(components.url!, client: "widget", timeout: 5)
let request = RelayFuelProvider.relayRequest(components.url!, client: "widget", timeout: 5)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
@@ -340,7 +340,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
URLQueryItem(name: "fuel", value: fuel.rawValue),
URLQueryItem(name: "limit", value: String(limit)),
]
var request = RelayFuelProvider.relayRequest(components.url!, client: "widget", timeout: 5)
let request = RelayFuelProvider.relayRequest(components.url!, client: "widget", timeout: 5)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }