mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix(native): watch screens fit every size and language
Countdowns use the system's short relative style in every language
("через 2 ч", "in 5 Min."), and durations take the watch's language;
a stop on now shows its range, not a countdown. The top-level pages drop
the titles that collided with the scroll indicator, the pages keep the
standard margins, the day header carries a chevron instead of a crowded
icon, the booking page fits on one screen with its reference and seat,
the day timeline opens at the stop that is on, the map is drawn from
above without labels, the bookings list has the trip's light, and the
complication preview adapts to the case size. The sample trip speaks
German and Russian in those languages.
This commit is contained in:
@@ -40,7 +40,7 @@ struct DaysListView: View {
|
||||
|
||||
private func summary(_ day: WatchSnapshot.Day) -> String {
|
||||
guard let first = day.stops.first else { return L("day.empty") }
|
||||
return day.stops.count > 1 ? "\(first.name) +\(day.stops.count - 1)" : first.name
|
||||
return first.booking.map { "\(WatchLook.kindName($0)) \(first.name)" } ?? first.name
|
||||
}
|
||||
|
||||
private func isPast(_ day: WatchSnapshot.Day) -> Bool {
|
||||
@@ -60,6 +60,10 @@ struct DayView: View {
|
||||
let day = snapshot.days.indices.contains(index) ? snapshot.days[index] : nil
|
||||
let isToday = day?.date == TrekDate.iso(now)
|
||||
let nowMinutes = isToday ? TrekDate.minutesOfDay(now) : (isPastDay(day) ? 24 * 60 : -1)
|
||||
let stopsOfDay = day?.stops ?? []
|
||||
// Today opens at the stop that is on or next, with the one before it for context.
|
||||
let focus = isToday ? stopsOfDay.firstIndex { ($0.endMinutes ?? $0.minutes ?? -1) > nowMinutes } : nil
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
@@ -87,9 +91,14 @@ struct DayView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.id(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let focus, focus > 1 { proxy.scrollTo(focus - 1, anchor: .top) }
|
||||
}
|
||||
}
|
||||
.navigationTitle(day?.dateValue.map { WatchFormat.weekday($0) } ?? L("trip.days"))
|
||||
.containerBackground(WatchLook.tripGradient(snapshot.trip?.title ?? "TREK"), for: .navigation)
|
||||
}
|
||||
@@ -219,8 +228,8 @@ struct MiniMap: View {
|
||||
|
||||
var body: some View {
|
||||
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
|
||||
// Flat and muted, without points of interest, so the stop's own pin is
|
||||
// what stands out; Apple's attribution stays in its corner, untouched.
|
||||
// From above, without street names or points of interest, so the stop's
|
||||
// own pin is what stands out; Apple's attribution stays in its corner.
|
||||
Map(initialPosition: .region(MKCoordinateRegion(center: coordinate, latitudinalMeters: 700, longitudinalMeters: 700)), interactionModes: []) {
|
||||
Annotation(name, coordinate: coordinate, anchor: .center) {
|
||||
Circle()
|
||||
@@ -231,7 +240,7 @@ struct MiniMap: View {
|
||||
}
|
||||
.annotationTitles(.hidden)
|
||||
}
|
||||
.mapStyle(.standard(elevation: .flat, emphasis: .muted, pointsOfInterest: .excludingAll, showsTraffic: false))
|
||||
.mapStyle(.imagery(elevation: .flat))
|
||||
.frame(height: 104)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.accessibilityHidden(true)
|
||||
@@ -358,6 +367,7 @@ struct BookingsListView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(L("booking.all"))
|
||||
.containerBackground(WatchLook.tripGradient(snapshot.trip?.title ?? "TREK"), for: .navigation)
|
||||
}
|
||||
|
||||
private func when(_ booking: WatchSnapshot.Booking) -> String {
|
||||
|
||||
@@ -1,42 +1,64 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Screenshot mode only (-TrekDemo faces): every complication in every
|
||||
/// family, on a dark dial, to review them without setting up a watch face.
|
||||
/// Screenshot mode only (-TrekDemo faces): every complication in every family
|
||||
/// it ships in, at the sizes a watch face gives them on this watch, to review
|
||||
/// them without setting up a face. Circular and corner ones three to a row,
|
||||
/// rectangular and inline ones across the screen's width.
|
||||
struct FacesView: View {
|
||||
let snapshot: WatchSnapshot?
|
||||
let now: Date
|
||||
/// A circular complication's diameter, from the screen: 42 pt on 40 mm, 50 on 49 mm.
|
||||
@State private var dial: CGFloat = 44
|
||||
|
||||
var body: some View {
|
||||
let state = ComplicationState(snapshot, now: now)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
caption("circular")
|
||||
HStack(spacing: 8) {
|
||||
dial(.circular) { NextStopComplication(state: state, family: .circular, now: now) }
|
||||
dial(.circular) { CountdownComplication(state: state, family: .circular, now: now) }
|
||||
dial(.circular) { TripDayComplication(state: state, family: .circular) }
|
||||
round { NextStopComplication(state: state, family: .circular, now: now) }
|
||||
round { CountdownComplication(state: state, family: .circular, now: now) }
|
||||
round { TripDayComplication(state: state, family: .circular) }
|
||||
}
|
||||
dial(.rectangular) { NextStopComplication(state: state, family: .rectangular, now: now) }
|
||||
dial(.rectangular) { CountdownComplication(state: state, family: .rectangular, now: now) }
|
||||
dial(.rectangular) { TripDayComplication(state: state, family: .rectangular) }
|
||||
dial(.inline) { NextStopComplication(state: state, family: .inline, now: now) }
|
||||
dial(.inline) { CountdownComplication(state: state, family: .inline, now: now) }
|
||||
dial(.inline) { TripDayComplication(state: state, family: .inline) }
|
||||
caption("corner")
|
||||
HStack(spacing: 8) {
|
||||
round { NextStopComplication(state: state, family: .corner, now: now) }
|
||||
round { CountdownComplication(state: state, family: .corner, now: now) }
|
||||
round { TripDayComplication(state: state, family: .corner) }
|
||||
}
|
||||
caption("rectangular")
|
||||
wide(56) { NextStopComplication(state: state, family: .rectangular, now: now) }
|
||||
wide(56) { CountdownComplication(state: state, family: .rectangular, now: now) }
|
||||
wide(56) { TripDayComplication(state: state, family: .rectangular) }
|
||||
caption("inline")
|
||||
wide(18) { NextStopComplication(state: state, family: .inline, now: now) }
|
||||
wide(18) { CountdownComplication(state: state, family: .inline, now: now) }
|
||||
wide(18) { TripDayComplication(state: state, family: .inline) }
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.tint(Color(hex: 0xFF9F0A))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onGeometryChange(for: CGFloat.self, of: { $0.size.width }) { width in
|
||||
dial = min(50, max(38, (width - 16) / 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dial<Content: View>(_ family: ComplicationFamily, @ViewBuilder _ content: () -> Content) -> some View {
|
||||
let size: CGSize = switch family {
|
||||
case .circular: CGSize(width: 50, height: 50)
|
||||
case .rectangular: CGSize(width: 170, height: 64)
|
||||
case .corner: CGSize(width: 40, height: 40)
|
||||
case .inline: CGSize(width: 170, height: 18)
|
||||
}
|
||||
return content()
|
||||
.frame(width: size.width, height: size.height)
|
||||
.clipped()
|
||||
.overlay(RoundedRectangle(cornerRadius: family == .circular ? 25 : 6).stroke(Color.white.opacity(0.08)))
|
||||
private func caption(_ text: String) -> some View {
|
||||
Text(text).watchEyebrow(WatchLook.faint)
|
||||
}
|
||||
|
||||
private func round<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {
|
||||
content()
|
||||
.frame(width: dial, height: dial)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private func wide<Content: View>(_ height: CGFloat, @ViewBuilder _ content: () -> Content) -> some View {
|
||||
content()
|
||||
.frame(maxWidth: .infinity, minHeight: height, maxHeight: height, alignment: .leading)
|
||||
.padding(.horizontal, 6)
|
||||
.background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.06)))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,21 +39,11 @@ struct TodayPage: View {
|
||||
DoneCard(title: L("today.free"), detail: L("today.freeBody"), symbol: "sun.max.fill")
|
||||
} else {
|
||||
DoneCard(title: L("today.done"), detail: L("today.doneBody"), symbol: "checkmark.circle.fill")
|
||||
if let todayIndex {
|
||||
NavigationLink(value: Route.day(todayIndex)) {
|
||||
Label(L("today.plan"), systemImage: "list.bullet")
|
||||
.font(.system(.footnote, design: .rounded).weight(.semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.watchCard(cornerRadius: 14)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentMargins(.horizontal, 5, for: .scrollContent)
|
||||
.containerBackground(WatchLook.tripGradient(snapshot.trip?.title ?? "TREK"), for: .tabView)
|
||||
.navigationTitle(L("today.title"))
|
||||
.toolbar {
|
||||
if let weather = snapshot.weather {
|
||||
ToolbarItem(placement: .topBarTrailing) { WeatherBadge(weather: weather) }
|
||||
@@ -66,19 +56,19 @@ struct TodayPage: View {
|
||||
private var header: some View {
|
||||
if case .running(let index, let total) = snapshot.phase(at: now), let todayIndex = snapshot.todayIndex(at: now) {
|
||||
NavigationLink(value: Route.day(todayIndex)) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
HStack(spacing: 3) {
|
||||
Text(WatchFormat.dayOf(index, total)).watchEyebrow(.white.opacity(0.85))
|
||||
Text(snapshot.trip?.title ?? "")
|
||||
.font(.system(.footnote, design: .rounded).weight(.semibold))
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 9, weight: .heavy))
|
||||
.foregroundStyle(WatchLook.muted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Image(systemName: "list.bullet")
|
||||
Text(snapshot.trip?.title ?? "")
|
||||
.font(.system(.footnote, design: .rounded).weight(.semibold))
|
||||
.foregroundStyle(WatchLook.muted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -104,23 +94,28 @@ struct FocusCard: View {
|
||||
Text(L(isNow ? "today.now" : "today.next"))
|
||||
.watchEyebrow(isNow ? Trek.confirmed : WatchLook.muted)
|
||||
Spacer(minLength: 0)
|
||||
if let target = countdownTarget {
|
||||
Text(WatchFormat.relative(target, now: now))
|
||||
.font(.system(.caption, design: .rounded).weight(.bold))
|
||||
.foregroundStyle(isNow ? WatchLook.muted : Trek.confirmed)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
}
|
||||
}
|
||||
Text(stop.name)
|
||||
.font(.system(.title3, design: .rounded).weight(.bold))
|
||||
.foregroundStyle(WatchLook.ink)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.75)
|
||||
if let times = timeRange {
|
||||
Text(times)
|
||||
.font(.system(.footnote, design: .rounded).weight(.medium).monospacedDigit())
|
||||
.foregroundStyle(WatchLook.muted)
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
if let times = timeRange {
|
||||
Text(times)
|
||||
.font(.system(.footnote, design: .rounded).weight(.medium).monospacedDigit())
|
||||
.foregroundStyle(WatchLook.muted)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
Spacer(minLength: 2)
|
||||
if let target = countdownTarget, target > now {
|
||||
Text(WatchFormat.relative(target, now: now))
|
||||
.font(.system(.footnote, design: .rounded).weight(.bold))
|
||||
.foregroundStyle(isNow ? WatchLook.muted : Trek.confirmed)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
}
|
||||
}
|
||||
if let then {
|
||||
Divider().overlay(Color.white.opacity(0.12)).padding(.vertical, 2)
|
||||
@@ -148,8 +143,9 @@ struct FocusCard: View {
|
||||
|
||||
/// Next: until it starts. Now: until it ends.
|
||||
private var countdownTarget: Date? {
|
||||
let minutes = isNow ? stop.endMinutes : stop.minutes
|
||||
return minutes.map { TrekDate.at($0, on: now) }
|
||||
guard !isNow, let minutes = stop.minutes else { return nil }
|
||||
let start = TrekDate.at(minutes, on: now)
|
||||
return start.timeIntervalSince(now) >= 60 ? start : nil
|
||||
}
|
||||
|
||||
private var timeRange: String? {
|
||||
@@ -285,6 +281,7 @@ struct CountdownPage: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding(.horizontal, 5)
|
||||
.containerBackground(for: .tabView) {
|
||||
TripBackdrop(trip: widgetTrip(trip), shade: 0.7)
|
||||
}
|
||||
@@ -311,12 +308,13 @@ struct BookingPage: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
NavigationLink(value: Route.booking(booking.id)) {
|
||||
BookingSummary(booking: booking, hour12: hour12)
|
||||
BookingSummary(booking: booking, hour12: hour12, compactFacts: true)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.contentMargins(.horizontal, 5, for: .scrollContent)
|
||||
.containerBackground(WatchLook.tint(Trek.booking(booking.kind).color), for: .tabView)
|
||||
.navigationTitle(L("booking.next"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +323,8 @@ struct BookingSummary: View {
|
||||
let booking: WatchSnapshot.Booking
|
||||
let hour12: Bool
|
||||
var showFacts = true
|
||||
/// The page's single row of facts rather than all of them.
|
||||
var compactFacts = false
|
||||
@Environment(\.trekNow) private var now
|
||||
|
||||
var body: some View {
|
||||
@@ -364,31 +364,34 @@ struct BookingSummary: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
/// The time it starts (or, for a stay under way, ends) and how long until then.
|
||||
/// The time it starts (or, for a stay under way, ends), its day, and how long until then, in one line.
|
||||
@ViewBuilder
|
||||
private var timing: some View {
|
||||
let running = booking.kind == "hotel" && (booking.start ?? .distantFuture) < now
|
||||
let moment = running ? booking.end : booking.start
|
||||
let clock = running ? booking.checkOut ?? booking.endTime : booking.time
|
||||
if let moment {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 5) {
|
||||
if let clock, let minutes = TrekDate.minutes(clock) {
|
||||
Text(WatchFormat.time(minutes: minutes, hour12: hour12))
|
||||
.font(.system(.title3, design: .rounded).weight(.bold).monospacedDigit())
|
||||
.layoutPriority(2)
|
||||
}
|
||||
Text(dayLabel(moment))
|
||||
Text(running ? L("booking.checkOut") : dayLabel(moment))
|
||||
.font(.system(.caption2, design: .rounded).weight(.semibold))
|
||||
.foregroundStyle(WatchLook.muted)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
if running {
|
||||
Text(L("booking.checkOut") + " · " + WatchFormat.relative(moment, now: now))
|
||||
.font(.system(.caption, design: .rounded).weight(.bold))
|
||||
.foregroundStyle(Trek.booking(booking.kind).color)
|
||||
} else if moment > now {
|
||||
Text(WatchFormat.relative(moment, now: now))
|
||||
.font(.system(.caption, design: .rounded).weight(.bold))
|
||||
.foregroundStyle(Trek.booking(booking.kind).color.mix(with: .white, by: 0.35))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 2)
|
||||
if moment > now {
|
||||
Text(WatchFormat.relative(moment, now: now))
|
||||
.font(.system(.caption2, design: .rounded).weight(.bold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 3)
|
||||
.background(Capsule().fill(Trek.booking(booking.kind).color.opacity(0.85)))
|
||||
.layoutPriority(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,9 +413,25 @@ struct BookingSummary: View {
|
||||
return WatchFormat.day(date)
|
||||
}
|
||||
|
||||
/// On the page one row, the reference and the seat; in the detail all of them.
|
||||
@ViewBuilder
|
||||
private var facts: some View {
|
||||
let tiles = factTiles
|
||||
if compactFacts {
|
||||
HStack(spacing: 6) {
|
||||
if let code = booking.code { FactTile(label: L("booking.ref"), value: code, mono: true) }
|
||||
if let first = tiles.first {
|
||||
FactTile(label: first.label, value: first.value)
|
||||
.frame(maxWidth: booking.code == nil ? .infinity : 64)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
factsInFull(tiles)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func factsInFull(_ tiles: [(label: String, value: String)]) -> some View {
|
||||
if !tiles.isEmpty {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(tiles.prefix(2), id: \.label) { tile in
|
||||
@@ -492,8 +511,9 @@ struct TripPage: View {
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(WatchLook.ink)
|
||||
}
|
||||
.contentMargins(.horizontal, 5, for: .scrollContent)
|
||||
.containerBackground(WatchLook.tripGradient(trip?.title ?? "TREK"), for: .tabView)
|
||||
.navigationTitle(L("trip.title"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,8 +587,9 @@ struct ListsPage: View {
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.horizontal, 5)
|
||||
.containerBackground(WatchLook.tint(WatchLook.packing), for: .tabView)
|
||||
.navigationTitle(L("lists.title"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ struct CountdownComplication: View {
|
||||
|
||||
/// The single biggest unit: "2h", "35m", "12d".
|
||||
private func short(_ target: Date) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
let formatter = WatchFormat.components()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
formatter.maximumUnitCount = 1
|
||||
let seconds = max(target.timeIntervalSince(now), 60)
|
||||
|
||||
@@ -7,7 +7,7 @@ enum WatchSample {
|
||||
enum Scenario: String {
|
||||
/// Day 3 of 9, early afternoon.
|
||||
case running
|
||||
/// Day 1, the flight out leaves in a few hours.
|
||||
/// Day 1, the flight out leaves in the afternoon.
|
||||
case travelday
|
||||
/// Twelve days before the trip.
|
||||
case upcoming
|
||||
@@ -22,52 +22,61 @@ enum WatchSample {
|
||||
}
|
||||
let calendar = Calendar.current
|
||||
let iso: (Int) -> String = { TrekDate.iso(calendar.date(byAdding: .day, value: startOffset + $0, to: now) ?? now) }
|
||||
// The flight out leaves in the afternoon, so the screenshot mode can run
|
||||
// every scenario at about one o'clock, the hour the capture sets its clock to.
|
||||
let stops: [[WatchSnapshot.Stop]] = [
|
||||
[
|
||||
stop("LH 716", 13 * 60 + 5, symbol: "airplane", color: nil, booking: "flight", lat: 50.0379, lng: 8.5622, address: "Frankfurt Airport"),
|
||||
stop("Hotel Kanra Kyoto", 15 * 60, symbol: "bed.double.fill", color: "#8E6CD9", booking: "hotel", lat: 34.9922, lng: 135.7585, address: "190 Kitamachi, Shimogyo Ward, Kyoto"),
|
||||
stop("LH 716", 15 * 60 + 35, symbol: "airplane", color: nil, booking: "flight", lat: 50.0379, lng: 8.5622, address: "Frankfurt Airport"),
|
||||
],
|
||||
[
|
||||
stop("Arashiyama Bamboo Grove", 9 * 60, symbol: "tree.fill", color: "#2FA37A", lat: 35.0170, lng: 135.6713, address: "Sagaogurayama Tabuchiyamacho, Kyoto"),
|
||||
stop("Tenryu-ji", 10 * 60 + 30, symbol: "building.columns.fill", color: "#D6273B", lat: 35.0157, lng: 135.6738, address: "68 Sagatenryuji Susukinobabacho, Kyoto"),
|
||||
stop("Kinkaku-ji", 14 * 60, symbol: "building.columns.fill", color: "#D6273B", lat: 35.0394, lng: 135.7292, address: "1 Kinkakujicho, Kita Ward, Kyoto"),
|
||||
stop("Shinkansen Nozomi 21", 13 * 60 + 33, symbol: "tram.fill", color: nil, booking: "train", lat: 35.6812, lng: 139.7671, address: "Tokyo Station"),
|
||||
stop("Hotel Kanra Kyoto", 16 * 60 + 30, symbol: "bed.double.fill", color: nil, booking: "hotel", lat: 34.9922, lng: 135.7585, address: "190 Kitamachi, Shimogyo Ward, Kyoto"),
|
||||
],
|
||||
[
|
||||
stop("Fushimi Inari Taisha", 8 * 60 + 30, end: 10 * 60 + 30, symbol: "building.columns.fill", color: "#D6273B", lat: 34.9671, lng: 135.7727, address: "68 Fukakusa Yabunouchicho, Fushimi Ward, Kyoto"),
|
||||
stop("Nishiki Market", 11 * 60, end: 12 * 60 + 30, symbol: "basket.fill", color: "#E8A13A", lat: 35.0050, lng: 135.7649, address: "Nishikikoji-dori, Nakagyo Ward, Kyoto"),
|
||||
stop("Kiyomizu-dera", 13 * 60 + 30, end: 15 * 60 + 30, symbol: "leaf.fill", color: "#2FA37A", lat: 34.9949, lng: 135.7850, address: "1-294 Kiyomizu, Higashiyama Ward, Kyoto", note: "Go up the Sannenzaka steps for the view."),
|
||||
stop("Gion walk", 16 * 60, symbol: "figure.walk", color: "#4A7DDB", lat: 35.0037, lng: 135.7750, address: "Hanamikoji-dori, Higashiyama Ward, Kyoto"),
|
||||
stop("Kaiseki at Kikunoi", 19 * 60, symbol: "fork.knife", color: nil, booking: "restaurant", lat: 34.9989, lng: 135.7812, address: "459 Shimokawaracho, Higashiyama Ward, Kyoto"),
|
||||
stop(t("Nishiki Market"), 11 * 60, end: 12 * 60 + 30, symbol: "basket.fill", color: "#E8A13A", lat: 35.0050, lng: 135.7649, address: "Nishikikoji-dori, Nakagyo Ward, Kyoto"),
|
||||
stop("Kiyomizu-dera", 13 * 60 + 30, end: 15 * 60 + 30, symbol: "leaf.fill", color: "#2FA37A", lat: 34.9949, lng: 135.7850, address: "1-294 Kiyomizu, Higashiyama Ward, Kyoto", note: t("Go up the Sannenzaka steps for the view.")),
|
||||
stop(t("Gion walk"), 16 * 60, symbol: "figure.walk", color: "#4A7DDB", lat: 35.0037, lng: 135.7750, address: "Hanamikoji-dori, Higashiyama Ward, Kyoto"),
|
||||
stop(t("Kaiseki at Kikunoi"), 19 * 60, symbol: "fork.knife", color: nil, booking: "restaurant", lat: 34.9989, lng: 135.7812, address: "459 Shimokawaracho, Higashiyama Ward, Kyoto"),
|
||||
],
|
||||
[stop("Nara Park", 10 * 60, symbol: "leaf.fill", color: "#2FA37A", lat: 34.6851, lng: 135.8430, address: "Nara"), stop("Todai-ji", 11 * 60 + 30, symbol: "building.columns.fill", color: "#D6273B", lat: 34.6890, lng: 135.8398, address: "406-1 Zoshicho, Nara")],
|
||||
[stop("Philosopher's Path", 9 * 60 + 30, symbol: "figure.walk", color: "#4A7DDB", lat: 35.0266, lng: 135.7948, address: "Sakyo Ward, Kyoto"), stop("Ginkaku-ji", 11 * 60, symbol: "building.columns.fill", color: "#D6273B", lat: 35.0270, lng: 135.7982, address: "2 Ginkakujicho, Sakyo Ward, Kyoto")],
|
||||
[stop("Tea ceremony", 10 * 60 + 30, symbol: "ticket.fill", color: nil, booking: "event", lat: 35.0036, lng: 135.7788, address: "Camellia Flower, Higashiyama")],
|
||||
[stop("Fushimi sake district", 14 * 60, symbol: "wineglass.fill", color: "#8E6CD9", lat: 34.9318, lng: 135.7601, address: "Fushimi Ward, Kyoto")],
|
||||
[stop("Pontocho alley", 18 * 60 + 30, symbol: "fork.knife", color: "#E8A13A", lat: 35.0050, lng: 135.7708, address: "Pontocho, Nakagyo Ward, Kyoto")],
|
||||
[stop("Shinkansen to Osaka", 9 * 60 + 12, symbol: "tram.fill", color: nil, booking: "train", lat: 34.9858, lng: 135.7588, address: "Kyoto Station"), stop("LH 741", 13 * 60 + 25, symbol: "airplane", color: nil, booking: "flight", lat: 34.4320, lng: 135.2304, address: "Kansai International Airport")],
|
||||
[
|
||||
stop(t("Arashiyama Bamboo Grove"), 9 * 60, symbol: "tree.fill", color: "#2FA37A", lat: 35.0170, lng: 135.6713, address: "Sagaogurayama Tabuchiyamacho, Kyoto"),
|
||||
stop("Tenryu-ji", 10 * 60 + 30, symbol: "building.columns.fill", color: "#D6273B", lat: 35.0157, lng: 135.6738, address: "68 Sagatenryuji Susukinobabacho, Kyoto"),
|
||||
stop("Kinkaku-ji", 14 * 60, symbol: "building.columns.fill", color: "#D6273B", lat: 35.0394, lng: 135.7292, address: "1 Kinkakujicho, Kita Ward, Kyoto"),
|
||||
],
|
||||
[stop(t("Nara Park"), 10 * 60, symbol: "leaf.fill", color: "#2FA37A", lat: 34.6851, lng: 135.8430, address: "Nara"), stop("Todai-ji", 11 * 60 + 30, symbol: "building.columns.fill", color: "#D6273B", lat: 34.6890, lng: 135.8398, address: "406-1 Zoshicho, Nara")],
|
||||
[stop(t("Philosopher's Path"), 9 * 60 + 30, symbol: "figure.walk", color: "#4A7DDB", lat: 35.0266, lng: 135.7948, address: "Sakyo Ward, Kyoto"), stop("Ginkaku-ji", 11 * 60, symbol: "building.columns.fill", color: "#D6273B", lat: 35.0270, lng: 135.7982, address: "2 Ginkakujicho, Sakyo Ward, Kyoto")],
|
||||
[stop(t("Tea ceremony"), 10 * 60 + 30, symbol: "ticket.fill", color: nil, booking: "event", lat: 35.0036, lng: 135.7788, address: "Camellia Flower, Higashiyama")],
|
||||
[stop(t("Fushimi sake district"), 14 * 60, symbol: "wineglass.fill", color: "#8E6CD9", lat: 34.9318, lng: 135.7601, address: "Fushimi Ward, Kyoto"), stop(t("Pontocho alley"), 18 * 60 + 30, symbol: "fork.knife", color: "#E8A13A", lat: 35.0050, lng: 135.7708, address: "Pontocho, Nakagyo Ward, Kyoto")],
|
||||
[stop(t("Shinkansen to Osaka"), 9 * 60 + 12, symbol: "tram.fill", color: nil, booking: "train", lat: 34.9858, lng: 135.7588, address: "Kyoto Station"), stop("LH 741", 13 * 60 + 25, symbol: "airplane", color: nil, booking: "flight", lat: 34.4320, lng: 135.2304, address: "Kansai International Airport")],
|
||||
]
|
||||
let days = stops.enumerated().map { index, stops in
|
||||
WatchSnapshot.Day(date: iso(index), number: index + 1, title: nil, stops: stops)
|
||||
}
|
||||
let bookings: [WatchSnapshot.Booking] = [
|
||||
WatchSnapshot.Booking(
|
||||
id: 1, title: "LH 716", kind: "flight", confirmed: true, date: iso(0), time: "13:05", endDate: iso(1), endTime: "08:50",
|
||||
id: 1, title: "LH 716", kind: "flight", confirmed: true, date: iso(0), time: "15:35", endDate: iso(1), endTime: "11:20",
|
||||
zone: nil, from: .init(name: "Frankfurt", code: "FRA", lat: 50.0379, lng: 8.5622),
|
||||
to: .init(name: "Tokyo Haneda", code: "HND", lat: 35.5494, lng: 139.7798), code: "K7Q2XD",
|
||||
carrier: "Lufthansa LH 716", seat: "42A", platform: nil, location: nil, lat: nil, lng: nil, checkIn: nil, checkOut: nil
|
||||
),
|
||||
WatchSnapshot.Booking(
|
||||
id: 2, title: "Hotel Kanra Kyoto", kind: "hotel", confirmed: true, date: iso(0), time: "15:00", endDate: iso(6), endTime: "11:00",
|
||||
id: 7, title: "Shinkansen Nozomi 21", kind: "train", confirmed: true, date: iso(1), time: "13:33", endDate: iso(1), endTime: "15:48",
|
||||
zone: nil, from: .init(name: "Tokyo", code: nil, lat: 35.6812, lng: 139.7671), to: .init(name: "Kyoto", code: nil, lat: 34.9858, lng: 135.7588),
|
||||
code: "JR-5521", carrier: "JR Nozomi 21", seat: "7D", platform: "14", location: nil, lat: nil, lng: nil, checkIn: nil, checkOut: nil
|
||||
),
|
||||
WatchSnapshot.Booking(
|
||||
id: 2, title: "Hotel Kanra Kyoto", kind: "hotel", confirmed: true, date: iso(1), time: "16:30", endDate: iso(7), endTime: "11:00",
|
||||
zone: nil, from: nil, to: nil, code: "HK-208431", carrier: nil, seat: nil, platform: nil,
|
||||
location: "Hotel Kanra Kyoto", lat: 34.9922, lng: 135.7585, checkIn: "15:00", checkOut: "11:00"
|
||||
),
|
||||
WatchSnapshot.Booking(
|
||||
id: 3, title: "Kaiseki at Kikunoi", kind: "restaurant", confirmed: false, date: iso(2), time: "19:00", endDate: nil, endTime: nil,
|
||||
id: 3, title: t("Kaiseki at Kikunoi"), kind: "restaurant", confirmed: false, date: iso(2), time: "19:00", endDate: nil, endTime: nil,
|
||||
zone: nil, from: nil, to: nil, code: "KK-77", carrier: nil, seat: nil, platform: nil,
|
||||
location: "459 Shimokawaracho, Higashiyama", lat: 34.9989, lng: 135.7812, checkIn: nil, checkOut: nil
|
||||
),
|
||||
WatchSnapshot.Booking(
|
||||
id: 4, title: "Tea ceremony", kind: "event", confirmed: true, date: iso(5), time: "10:30", endDate: nil, endTime: nil,
|
||||
id: 4, title: t("Tea ceremony"), kind: "event", confirmed: true, date: iso(5), time: "10:30", endDate: nil, endTime: nil,
|
||||
zone: nil, from: nil, to: nil, code: nil, carrier: nil, seat: nil, platform: nil,
|
||||
location: "Camellia Flower", lat: 35.0036, lng: 135.7788, checkIn: nil, checkOut: nil
|
||||
),
|
||||
@@ -84,10 +93,10 @@ enum WatchSample {
|
||||
),
|
||||
]
|
||||
let packingItems: [(String, String, Bool)] = [
|
||||
("Passport", "Documents", true), ("JR Pass voucher", "Documents", true), ("Travel insurance", "Documents", true),
|
||||
("Rain jacket", "Clothes", false), ("Walking shoes", "Clothes", true), ("Warm sweater", "Clothes", false), ("Socks", "Clothes", true),
|
||||
("Camera", "Tech", true), ("Power bank", "Tech", false), ("Type A adapter", "Tech", false),
|
||||
("Toothbrush", "Toiletries", false), ("Sunscreen", "Toiletries", true),
|
||||
(t("Passport"), t("Documents"), true), (t("JR Pass voucher"), t("Documents"), true), (t("Travel insurance"), t("Documents"), true),
|
||||
(t("Rain jacket"), t("Clothes"), false), (t("Walking shoes"), t("Clothes"), true), (t("Warm sweater"), t("Clothes"), false), (t("Socks"), t("Clothes"), true),
|
||||
(t("Camera"), t("Tech"), true), (t("Power bank"), t("Tech"), false), (t("Type A adapter"), t("Tech"), false),
|
||||
(t("Toothbrush"), t("Toiletries"), false), (t("Sunscreen"), t("Toiletries"), true),
|
||||
]
|
||||
var packingList: [WatchSnapshot.Item] = []
|
||||
for (offset, entry) in packingItems.enumerated() {
|
||||
@@ -100,9 +109,9 @@ enum WatchSample {
|
||||
)
|
||||
// Due dates in days from now, priority, done.
|
||||
let todoItems: [(String, Int?, Int, Bool)] = [
|
||||
("Reserve the Ghibli Museum", 2, 3, false), ("Buy a Suica card", nil, 2, false),
|
||||
("Book the tea ceremony", nil, 1, false), ("Print the hotel voucher", nil, 0, false),
|
||||
("Order yen", -1, 0, true), ("Renew passport", -20, 0, true),
|
||||
(t("Reserve the Ghibli Museum"), 2, 3, false), (t("Buy a Suica card"), nil, 2, false),
|
||||
(t("Book the tea ceremony"), nil, 1, false), (t("Print the hotel voucher"), nil, 0, false),
|
||||
(t("Order yen"), -1, 0, true), (t("Renew passport"), -20, 0, true),
|
||||
]
|
||||
var todoList: [WatchSnapshot.Item] = []
|
||||
for (offset, entry) in todoItems.enumerated() {
|
||||
@@ -113,16 +122,62 @@ enum WatchSample {
|
||||
return WatchSnapshot(
|
||||
generatedAt: ISO8601DateFormatter().string(from: now.addingTimeInterval(-4 * 60)),
|
||||
hour12: false,
|
||||
trip: .init(id: 1, title: "Autumn in Kyoto", start: iso(0), end: iso(8), place: "Kyoto, Japan", cover: nil),
|
||||
trip: .init(id: 1, title: t("Autumn in Kyoto"), start: iso(0), end: iso(8), place: t("Kyoto, Japan"), cover: nil),
|
||||
days: days,
|
||||
bookings: bookings,
|
||||
weather: .init(temp: 18, high: 21, low: 11, main: scenario == .upcoming ? "Clouds" : "Clear", label: "Mostly sunny", place: "Kyoto", rain: 10),
|
||||
weather: .init(temp: 18, high: 21, low: 11, main: scenario == .upcoming ? "Clouds" : "Clear", label: t("Mostly sunny"), place: "Kyoto", rain: 10),
|
||||
packing: packing,
|
||||
todos: todos,
|
||||
applied: []
|
||||
)
|
||||
}
|
||||
|
||||
/// The sample's own texts in German and Russian, so the screenshots in those
|
||||
/// languages show a trip written in them; every other language gets the English.
|
||||
private static let translations: [String: [String: String]] = [
|
||||
"Autumn in Kyoto": ["de": "Herbst in Kyoto", "ru": "Осень в Киото"],
|
||||
"Kyoto, Japan": ["de": "Kyoto, Japan", "ru": "Киото, Япония"],
|
||||
"Nishiki Market": ["de": "Nishiki-Markt", "ru": "Рынок Нисики"],
|
||||
"Gion walk": ["de": "Spaziergang durch Gion", "ru": "Прогулка по Гиону"],
|
||||
"Kaiseki at Kikunoi": ["de": "Kaiseki im Kikunoi", "ru": "Кайсэки в Kikunoi"],
|
||||
"Arashiyama Bamboo Grove": ["de": "Bambuswald Arashiyama", "ru": "Бамбуковая роща Арасиямы"],
|
||||
"Nara Park": ["de": "Nara-Park", "ru": "Парк Нара"],
|
||||
"Philosopher's Path": ["de": "Philosophenweg", "ru": "Тропа философа"],
|
||||
"Tea ceremony": ["de": "Teezeremonie", "ru": "Чайная церемония"],
|
||||
"Fushimi sake district": ["de": "Sake-Viertel Fushimi", "ru": "Саке-квартал Фусими"],
|
||||
"Pontocho alley": ["de": "Gasse Pontocho", "ru": "Улочка Понтотё"],
|
||||
"Shinkansen to Osaka": ["de": "Shinkansen nach Osaka", "ru": "Синкансэн в Осаку"],
|
||||
"Go up the Sannenzaka steps for the view.": ["de": "Für den Ausblick die Sannenzaka-Treppe hinauf.", "ru": "Ради вида поднимитесь по лестнице Саннэндзака."],
|
||||
"Mostly sunny": ["de": "Überwiegend sonnig", "ru": "Преимущественно солнечно"],
|
||||
"Passport": ["de": "Reisepass", "ru": "Паспорт"],
|
||||
"JR Pass voucher": ["de": "JR-Pass-Gutschein", "ru": "Ваучер JR Pass"],
|
||||
"Travel insurance": ["de": "Reiseversicherung", "ru": "Страховка"],
|
||||
"Rain jacket": ["de": "Regenjacke", "ru": "Дождевик"],
|
||||
"Walking shoes": ["de": "Wanderschuhe", "ru": "Удобная обувь"],
|
||||
"Warm sweater": ["de": "Warmer Pullover", "ru": "Тёплый свитер"],
|
||||
"Socks": ["de": "Socken", "ru": "Носки"],
|
||||
"Camera": ["de": "Kamera", "ru": "Фотоаппарат"],
|
||||
"Power bank": ["de": "Powerbank", "ru": "Пауэрбанк"],
|
||||
"Type A adapter": ["de": "Reiseadapter Typ A", "ru": "Переходник типа A"],
|
||||
"Toothbrush": ["de": "Zahnbürste", "ru": "Зубная щётка"],
|
||||
"Sunscreen": ["de": "Sonnencreme", "ru": "Солнцезащитный крем"],
|
||||
"Documents": ["de": "Dokumente", "ru": "Документы"],
|
||||
"Clothes": ["de": "Kleidung", "ru": "Одежда"],
|
||||
"Tech": ["de": "Technik", "ru": "Техника"],
|
||||
"Toiletries": ["de": "Kulturbeutel", "ru": "Гигиена"],
|
||||
"Reserve the Ghibli Museum": ["de": "Ghibli-Museum reservieren", "ru": "Забронировать музей Гибли"],
|
||||
"Buy a Suica card": ["de": "Suica-Karte kaufen", "ru": "Купить карту Suica"],
|
||||
"Book the tea ceremony": ["de": "Teezeremonie buchen", "ru": "Записаться на чайную церемонию"],
|
||||
"Print the hotel voucher": ["de": "Hotelgutschein drucken", "ru": "Распечатать ваучер отеля"],
|
||||
"Order yen": ["de": "Yen bestellen", "ru": "Заказать иены"],
|
||||
"Renew passport": ["de": "Reisepass verlängern", "ru": "Продлить паспорт"],
|
||||
]
|
||||
|
||||
private static func t(_ english: String) -> String {
|
||||
let language = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
return translations[english]?[language] ?? english
|
||||
}
|
||||
|
||||
private static func stop(_ name: String, _ minutes: Int, end: Int? = nil, symbol: String, color: String?, booking: String? = nil,
|
||||
lat: Double, lng: Double, address: String, note: String? = nil) -> WatchSnapshot.Stop {
|
||||
WatchSnapshot.Stop(name: name, minutes: minutes, endMinutes: end, symbol: symbol, color: color, address: address,
|
||||
|
||||
@@ -51,7 +51,7 @@ enum WatchFormat {
|
||||
if abs(date.timeIntervalSince(now)) < 60 { return L("time.now") }
|
||||
// Against `now`, not the system clock, so the screenshot mode's clock counts too.
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
formatter.unitsStyle = .short
|
||||
formatter.dateTimeStyle = .numeric
|
||||
return formatter.localizedString(for: date, relativeTo: now)
|
||||
}
|
||||
@@ -59,8 +59,8 @@ enum WatchFormat {
|
||||
/// The time to a moment as a short count: "25 min", "2 h 15 min", "3 d".
|
||||
static func countdown(_ date: Date, now: Date) -> String {
|
||||
let seconds = max(date.timeIntervalSince(now), 0)
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
let formatter = WatchFormat.components()
|
||||
formatter.unitsStyle = .short
|
||||
formatter.maximumUnitCount = 2
|
||||
formatter.allowedUnits = seconds >= 86_400 ? [.day, .hour] : seconds >= 3_600 ? [.hour, .minute] : [.minute]
|
||||
return formatter.string(from: max(seconds, 60)) ?? ""
|
||||
@@ -70,7 +70,7 @@ enum WatchFormat {
|
||||
/// "дней", "日"), taken from the system's own phrase so no plural rules
|
||||
/// have to be kept here: the number is formatted and then cut out.
|
||||
static func daysUnit(_ count: Int) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
let formatter = WatchFormat.components()
|
||||
formatter.unitsStyle = .full
|
||||
formatter.allowedUnits = [.day]
|
||||
let phrase = formatter.string(from: DateComponents(day: count)) ?? ""
|
||||
@@ -78,6 +78,16 @@ enum WatchFormat {
|
||||
return String(String.UnicodeScalarView(word)).trimmingCharacters(in: .whitespacesAndNewlines.union(.punctuationCharacters))
|
||||
}
|
||||
|
||||
/// A duration formatter in the watch's language: it takes its words from
|
||||
/// its calendar's locale, which is not the app's own unless set.
|
||||
static func components() -> DateComponentsFormatter {
|
||||
let formatter = DateComponentsFormatter()
|
||||
var calendar = Calendar.current
|
||||
calendar.locale = Locale.current
|
||||
formatter.calendar = calendar
|
||||
return formatter
|
||||
}
|
||||
|
||||
static func dayOf(_ index: Int, _ total: Int) -> String {
|
||||
String(format: L("day.of"), index, total)
|
||||
}
|
||||
|
||||
@@ -42,11 +42,15 @@ boot() {
|
||||
echo "$udid"
|
||||
}
|
||||
|
||||
# A fixed-offset zone in which it is now <hour> o'clock, so the clock the
|
||||
# watch draws in the corner agrees with the time the screen is about.
|
||||
zone_for() {
|
||||
local offset=$(( ($1 - 10#$(date -u +%H) + 36) % 24 - 12 ))
|
||||
if [ "$offset" -ge 0 ]; then echo "Etc/GMT-$offset"; else echo "Etc/GMT+$(( -offset ))"; fi
|
||||
# The watch draws the Mac's clock in its corner, so the Mac goes to a zone in
|
||||
# which it is about one o'clock: the hour the sample trip's screens are made for
|
||||
# (WatchSample.swift), and the clock and the countdowns agree.
|
||||
set_clock() {
|
||||
local offset=$(( (13 - 10#$(date -u +%H) + 36) % 24 - 12 ))
|
||||
local zone
|
||||
if [ "$offset" -ge 0 ]; then zone="Etc/GMT-$offset"; else zone="Etc/GMT+$(( -offset ))"; fi
|
||||
sudo ln -sf "/usr/share/zoneinfo/$zone" /etc/localtime
|
||||
echo "clock: $zone, $(date +%H:%M)"
|
||||
}
|
||||
|
||||
# Waits for the ready file the app writes once the screen has drawn (DemoMode.signalReady).
|
||||
@@ -61,19 +65,21 @@ wait_ready() {
|
||||
}
|
||||
|
||||
TOKEN=0
|
||||
# shot <udid> <file> <screen> <scenario> <hour> <language>: one launch, retried
|
||||
# shot <udid> <file> <screen> <scenario> <time or -> <language>: one launch, retried
|
||||
# after 2, 5 and 10 seconds when the simulator refuses it.
|
||||
shot() {
|
||||
local udid=$1 file=$2 screen=$3 scenario=$4 hour=$5 lang=$6
|
||||
local udid=$1 file=$2 screen=$3 scenario=$4 clock=$5 lang=$6
|
||||
local locale=${lang//-/_} attempt
|
||||
for attempt in 0 2 5 10; do
|
||||
[ "$attempt" -gt 0 ] && { echo "retrying $(basename "$file") in ${attempt}s"; sleep "$attempt"; }
|
||||
TOKEN=$((TOKEN + 1))
|
||||
xcrun simctl terminate "$udid" "$BUNDLE" >/dev/null 2>&1 || true
|
||||
sleep 0.5
|
||||
SIMCTL_CHILD_TZ=$(zone_for "$hour") xcrun simctl launch "$udid" "$BUNDLE" \
|
||||
local extra=()
|
||||
[ "$clock" = - ] || extra=(-TrekNow "$clock")
|
||||
xcrun simctl launch "$udid" "$BUNDLE" \
|
||||
-AppleLanguages "($lang)" -AppleLocale "$locale" -TrekDemo "$screen" -TrekScenario "$scenario" \
|
||||
-TrekReady "$TOKEN" >/dev/null 2>&1 || continue
|
||||
-TrekReady "$TOKEN" "${extra[@]}" >/dev/null 2>&1 || continue
|
||||
wait_ready "$udid" "$TOKEN" || continue
|
||||
# The map's tiles come over the network after the screen is up.
|
||||
[ "$screen" = place ] && sleep 6
|
||||
@@ -82,50 +88,51 @@ shot() {
|
||||
echo "gave up on $(basename "$file")"
|
||||
}
|
||||
|
||||
# name screen scenario hour: every screen, and the states a screen has.
|
||||
# name screen scenario clock: every screen, and the states a screen has; a
|
||||
# clock only where the state needs another hour than the real one.
|
||||
SHOTS="
|
||||
today today running 13
|
||||
today-travelday today travelday 10
|
||||
today-done today running 22
|
||||
day day running 13
|
||||
place place running 13
|
||||
booking booking travelday 10
|
||||
booking-dinner booking running 17
|
||||
bookings bookings travelday 10
|
||||
booking-detail booking-detail travelday 10
|
||||
trip trip running 13
|
||||
trip-upcoming trip upcoming 9
|
||||
days days running 13
|
||||
countdown countdown upcoming 9
|
||||
lists lists upcoming 19
|
||||
packing packing upcoming 19
|
||||
todos todos upcoming 19
|
||||
empty empty running 12
|
||||
nosync nosync running 12
|
||||
faces faces running 13
|
||||
today today running -
|
||||
today-travelday today travelday -
|
||||
today-done today running 22:30
|
||||
day day running -
|
||||
place place running -
|
||||
booking booking travelday -
|
||||
booking-dinner booking running -
|
||||
bookings bookings travelday -
|
||||
booking-detail booking-detail travelday -
|
||||
trip trip running -
|
||||
trip-upcoming trip upcoming -
|
||||
days days running -
|
||||
countdown countdown upcoming -
|
||||
lists lists upcoming -
|
||||
packing packing upcoming -
|
||||
todos todos upcoming -
|
||||
empty empty running -
|
||||
nosync nosync running -
|
||||
faces faces running -
|
||||
"
|
||||
MAIN="today booking countdown trip lists packing day"
|
||||
LANGS="ar az ca cs de el en es et fr he hu id it ja ko nl pl pt-BR ru sk sv th tr uk vi zh-Hans zh-Hant"
|
||||
|
||||
EXPECTED=()
|
||||
run() {
|
||||
local size=$1 udid=$2 lang shotname screen scenario hour
|
||||
local size=$1 udid=$2 lang shotname screen scenario clock
|
||||
for lang in en de ru; do
|
||||
while read -r shotname screen scenario hour; do
|
||||
while read -r shotname screen scenario clock; do
|
||||
[ -n "$shotname" ] || continue
|
||||
EXPECTED+=("$size-$lang-$shotname.png")
|
||||
shot "$udid" "$OUT/$size-$lang-$shotname.png" "$screen" "$scenario" "$hour" "$lang"
|
||||
shot "$udid" "$OUT/$size-$lang-$shotname.png" "$screen" "$scenario" "$clock" "$lang"
|
||||
done <<< "$SHOTS"
|
||||
done
|
||||
if [ "$size" = small ]; then
|
||||
for lang in $LANGS; do
|
||||
case "$lang" in en|de|ru) continue ;; esac
|
||||
while read -r shotname screen scenario hour; do
|
||||
while read -r shotname screen scenario clock; do
|
||||
[ -n "$shotname" ] || continue
|
||||
case " $MAIN " in
|
||||
*" $shotname "*)
|
||||
EXPECTED+=("$size-$lang-$shotname.png")
|
||||
shot "$udid" "$OUT/$size-$lang-$shotname.png" "$screen" "$scenario" "$hour" "$lang"
|
||||
shot "$udid" "$OUT/$size-$lang-$shotname.png" "$screen" "$scenario" "$clock" "$lang"
|
||||
;;
|
||||
esac
|
||||
done <<< "$SHOTS"
|
||||
@@ -136,6 +143,7 @@ run() {
|
||||
|
||||
# One watch at a time: launches on several booted simulators at once is what
|
||||
# makes SpringBoard refuse them ("Scene update failed").
|
||||
set_clock
|
||||
run large "$(boot "$LARGE" | tail -1)"
|
||||
run medium "$(boot "$MEDIUM" | tail -1)"
|
||||
run small "$(boot "$SMALL" | tail -1)"
|
||||
|
||||
Reference in New Issue
Block a user