- Move widget entry view into Shared/FuelPriceWidgetViews.swift (FuelPriceWidgetContent, FuelPriceWidgetEntryData) so the app can render the REAL widget faces; widget extension now wraps it (FuelPriceEntry.data) — one source of truth for the face - WidgetMockScreen (-widgets launch arg): app icon + small + medium widget previews for App Store captures; AppIconPreview image set (1024 drip master) - AppStoreScreenshots: 5-8 dark-mode tab captures with real Halifax data, 9 widgets - 95/95 tests green; Release IPA rebuilt + re-served
81 lines
3.2 KiB
Swift
81 lines
3.2 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
|
|
/// Full-screen widget preview (launch arg `-widgets`): the real app icon
|
|
/// plus the REAL widget faces (Shared/FuelPriceWidgetViews.swift — the same
|
|
/// code the widget extension renders) at small + medium sizes. Used for App
|
|
/// Store screenshot captures; not reachable from the UI.
|
|
struct WidgetMockScreen: View {
|
|
var body: some View {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.09, green: 0.10, blue: 0.14),
|
|
Color(red: 0.02, green: 0.03, blue: 0.06)
|
|
],
|
|
startPoint: .top, endPoint: .bottom
|
|
)
|
|
.ignoresSafeArea()
|
|
|
|
VStack(alignment: .leading, spacing: 26) {
|
|
// App icon + name, home-screen style
|
|
HStack(spacing: 16) {
|
|
Image("AppIconPreview")
|
|
.resizable()
|
|
.frame(width: 76, height: 76)
|
|
.clipShape(RoundedRectangle(cornerRadius: 17, style: .continuous))
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text("FuelBoard")
|
|
.font(.title2.bold())
|
|
.foregroundStyle(.white)
|
|
Text("Cheapest fuel near you")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.white.opacity(0.65))
|
|
}
|
|
}
|
|
.padding(.top, 8)
|
|
|
|
widgetCard(.systemSmall, label: "Small widget")
|
|
widgetCard(.systemMedium, label: "Medium widget")
|
|
|
|
Spacer(minLength: 0)
|
|
}
|
|
.padding(30)
|
|
}
|
|
}
|
|
|
|
private func widgetCard(_ family: WidgetFamily, label: String) -> some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text(label.uppercased())
|
|
.font(.caption.weight(.semibold))
|
|
.tracking(1)
|
|
.foregroundStyle(.white.opacity(0.5))
|
|
FuelPriceWidgetContent(entry: mockEntry(family: family), family: family, isWidget: false)
|
|
.padding(14)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Color(.systemBackground), in: RoundedRectangle(cornerRadius: 24, style: .continuous))
|
|
}
|
|
}
|
|
|
|
/// Real stations (loaded dump), cheapest-first for the chosen fuel — the
|
|
/// same selection the widget's own provider makes.
|
|
private func mockEntry(family: WidgetFamily) -> FuelPriceWidgetEntryData {
|
|
let fuel = FuelType.e10
|
|
let all = FuelStore.loadStations()
|
|
let selling = all.filter { $0.prices[fuel] != nil }
|
|
.sorted { ($0.prices[fuel] ?? .infinity) < ($1.prices[fuel] ?? .infinity) }
|
|
let location = FuelStore.loadLocation().map { Coordinate(lat: $0.lat, lng: $0.lng) }
|
|
let count = family == .systemSmall ? 1 : 3
|
|
return FuelPriceWidgetEntryData(
|
|
date: Date(),
|
|
stations: Array(selling.prefix(count)),
|
|
fuel: fuel,
|
|
sort: .cheapest,
|
|
isFavourites: false,
|
|
location: location,
|
|
locationSource: location != nil ? "live" : "none",
|
|
unit: FuelStore.loadDistanceUnit()
|
|
)
|
|
}
|
|
}
|