Compare commits
12
Commits
f0c13a79d6
...
f04a0fe0f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04a0fe0f5 | ||
|
|
f35f605b58 | ||
|
|
7da277547b | ||
|
|
93df94ddc9 | ||
|
|
78b43ce6d7 | ||
|
|
7dbb3d1a15 | ||
|
|
13967c3782 | ||
|
|
6e534ed5b7 | ||
|
|
4718e9b76a | ||
|
|
73db893f4f | ||
|
|
6dfe732199 | ||
|
|
7da09928ca |
@@ -0,0 +1,69 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// The "How Best Value works" explanation as its own sheet.
|
||||||
|
///
|
||||||
|
/// It used to sit inline in the Key sheet as a `Section`; it is one shared view
|
||||||
|
/// now so the Key sheet and the More info sheet can both open it, and the copy is
|
||||||
|
/// unchanged (the same `Localizable.strings` keys are reused — the old section
|
||||||
|
/// header string is the sheet's title).
|
||||||
|
struct HowBestValueSheet: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Best Value compares each station with the closest one. It works out what you could save at the pump, then takes off the fuel used for the extra return journey.")
|
||||||
|
Text("Your fill-up amount and MPG are used to show whether you could save overall.")
|
||||||
|
Text("Results are marked “Approx.” until road distances are available.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("How Best Value works")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Done") { dismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.presentationDetents([.fraction(0.42)])
|
||||||
|
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The row that opens `HowBestValueSheet`. A tappable row with a chevron, like
|
||||||
|
/// the other rows in both sheets. `.buttonStyle(.plain)` keeps it from becoming a
|
||||||
|
/// row-wide tap target that could swallow a neighbouring control (see the
|
||||||
|
/// two-buttons-in-one-row rule).
|
||||||
|
struct HowBestValueRow: View {
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text("How Best Value works")
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
Image(systemName: "chevron.forward")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
/// QA hook support: `-showBestValueInfo` opens this sheet without a tap, so it can
|
||||||
|
/// be screenshotted (simctl has no tap command) — the same pattern as
|
||||||
|
/// `-showKeySheet` / `-showDetail`.
|
||||||
|
enum BestValueInfoQA {
|
||||||
|
static var isRequested: Bool {
|
||||||
|
ProcessInfo.processInfo.arguments.contains("-showBestValueInfo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -98,7 +98,7 @@ struct OnboardingView: View {
|
|||||||
private var welcomePage: some View {
|
private var welcomePage: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
AnimatedOnboardingIcon(tint: .accentColor) {
|
AnimatedOnboardingIcon(tint: .accentColor, showsHalo: false) {
|
||||||
// The app icon itself, with the standard iOS rounded-corner
|
// The app icon itself, with the standard iOS rounded-corner
|
||||||
// treatment (~22% of the size), so onboarding matches the
|
// treatment (~22% of the size), so onboarding matches the
|
||||||
// Home Screen.
|
// Home Screen.
|
||||||
@@ -393,6 +393,9 @@ struct AnimatedOnboardingIcon<Content: View>: View {
|
|||||||
|
|
||||||
let tint: Color
|
let tint: Color
|
||||||
var motion: Motion = .gentle
|
var motion: Motion = .gentle
|
||||||
|
/// The soft circle behind the glyph. The welcome page turns it off: it shows
|
||||||
|
/// the app icon itself and reads better with no container around it.
|
||||||
|
var showsHalo: Bool = true
|
||||||
@ViewBuilder var content: Content
|
@ViewBuilder var content: Content
|
||||||
|
|
||||||
@State private var settled = false
|
@State private var settled = false
|
||||||
@@ -402,16 +405,18 @@ struct AnimatedOnboardingIcon<Content: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
if showsHalo {
|
||||||
.fill(tint.opacity(0.10))
|
Circle()
|
||||||
.frame(width: 108, height: 108)
|
.fill(tint.opacity(0.10))
|
||||||
.scaleEffect(settled ? 1.0 : 0.94)
|
.frame(width: 108, height: 108)
|
||||||
|
.scaleEffect(settled ? 1.0 : 0.94)
|
||||||
|
|
||||||
Circle()
|
Circle()
|
||||||
.stroke(tint.opacity(0.18), lineWidth: 1)
|
.stroke(tint.opacity(0.18), lineWidth: 1)
|
||||||
.frame(width: 118, height: 118)
|
.frame(width: 118, height: 118)
|
||||||
.scaleEffect(settled ? 1.03 : 0.97)
|
.scaleEffect(settled ? 1.03 : 0.97)
|
||||||
.opacity(settled ? 1.0 : 0.55)
|
.opacity(settled ? 1.0 : 0.55)
|
||||||
|
}
|
||||||
|
|
||||||
content
|
content
|
||||||
.scaleEffect(iconScale)
|
.scaleEffect(iconScale)
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ struct StationDetailView: View {
|
|||||||
var onToggleFavourite: (() -> Void)? = nil
|
var onToggleFavourite: (() -> Void)? = nil
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var showingReportIssueConfirmation = false
|
@State private var showingReportIssueConfirmation = false
|
||||||
/// Transient "Copied" confirmation on the Address row. Cleared by a task so
|
/// Transient "Postcode copied" confirmation on the Address row. Cleared by a task so
|
||||||
/// the caption fades out on its own.
|
/// the caption fades out on its own.
|
||||||
@State private var showingCopied = false
|
@State private var showingCopied = false
|
||||||
@State private var copyResetTask: Task<Void, Never>?
|
@State private var copyResetTask: Task<Void, Never>?
|
||||||
|
/// The "How Best Value works" sheet, opened from the row at the bottom of the
|
||||||
|
/// Best Value card (the Key sheet opens the same shared view).
|
||||||
|
@State private var showBestValueInfo = false
|
||||||
|
|
||||||
private let reportIssueURL = URL(string: "https://www.gov.uk/guidance/report-an-error-in-fuel-prices-or-forecourt-details")!
|
private let reportIssueURL = URL(string: "https://www.gov.uk/guidance/report-an-error-in-fuel-prices-or-forecourt-details")!
|
||||||
|
|
||||||
@@ -118,12 +121,92 @@ struct StationDetailView: View {
|
|||||||
.accessibilityHint(String(localized: "Opens Apple Maps with driving directions",
|
.accessibilityHint(String(localized: "Opens Apple Maps with driving directions",
|
||||||
comment: "Accessibility hint for the tappable map preview in the More info sheet"))
|
comment: "Accessibility hint for the tappable map preview in the More info sheet"))
|
||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets())
|
// No hairline between the map and the Address row: the map
|
||||||
}
|
// already reads as its own block, so the line only added
|
||||||
// The two station actions live in their OWN section, so they
|
// clutter. The Address/Distance and Distance/actions lines
|
||||||
// render as a separate card BELOW the map instead of sharing the
|
// stay.
|
||||||
// map's card and looking attached to the map tap target.
|
.listRowSeparator(.hidden, edges: .bottom)
|
||||||
Section {
|
// The map, the address, the distance, any notices and the two
|
||||||
|
// station actions all share ONE card (the list's own row
|
||||||
|
// hairlines separate them), so no hand-rolled Divider here —
|
||||||
|
// adding one would double the line.
|
||||||
|
//
|
||||||
|
// The station name is NOT repeated: the sheet header already
|
||||||
|
// shows it.
|
||||||
|
//
|
||||||
|
// The whole row is a copy control: it puts JUST the postcode
|
||||||
|
// on the clipboard so it can be pasted into another
|
||||||
|
// navigation app. The copy glyph sits ON the address line,
|
||||||
|
// and the "Postcode copied" confirmation is a capsule that floats OVER
|
||||||
|
// THIS ROW — it confirms the address, so it must not hover
|
||||||
|
// over the map. It is an overlay, so nothing moves while it is
|
||||||
|
// up, and `allowsHitTesting(false)` keeps the row tappable
|
||||||
|
// through the capsule.
|
||||||
|
Button {
|
||||||
|
copyPostcode()
|
||||||
|
} label: {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
|
Text("Address")
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
Text(addressLine)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
// Stays the plain copy glyph — the capsule carries
|
||||||
|
// the confirmation, so swapping this to a checkmark
|
||||||
|
// would say it twice.
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(postcodeForCopying.isEmpty)
|
||||||
|
.accessibilityLabel(String(localized: "Address: \(addressLine)",
|
||||||
|
comment: "Accessibility label for the address row in the More info sheet"))
|
||||||
|
.accessibilityHint(postcodeForCopying.isEmpty
|
||||||
|
? ""
|
||||||
|
: String(localized: "Copies the postcode",
|
||||||
|
comment: "Accessibility hint for the address row in the More info sheet"))
|
||||||
|
// Horizontally centred over the address line itself.
|
||||||
|
.overlay(alignment: .center) {
|
||||||
|
if showingCopied {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
Text("Postcode copied")
|
||||||
|
}
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
// A material base keeps it legible where the tint
|
||||||
|
// alone (the notice-capsule recipe) washes out.
|
||||||
|
.background {
|
||||||
|
Capsule()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
.overlay(Capsule().fill(Color.green.opacity(0.22)))
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let location {
|
||||||
|
let distance = FuelStore.stationDistanceDisplay(
|
||||||
|
station: station, userLat: location.lat, userLng: location.lng
|
||||||
|
)
|
||||||
|
LabeledContent("Distance", value: distance.labelWithReturn(unit: distanceUnit))
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
ForEach(StationNotice.notices(for: station)) { notice in
|
||||||
|
Label(notice.title, systemImage: notice.symbolName)
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Capsule().fill(Color.orange.opacity(0.14)))
|
||||||
|
}
|
||||||
|
// The two station actions are the LAST row of the card: half
|
||||||
|
// the width each, on one line, below Address and Distance.
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button {
|
Button {
|
||||||
openDirections()
|
openDirections()
|
||||||
@@ -145,65 +228,6 @@ struct StationDetailView: View {
|
|||||||
// button its own hit area — the half-row frame above.
|
// button its own hit area — the half-row frame above.
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
Section {
|
|
||||||
// The station name is NOT repeated here: the sheet header
|
|
||||||
// (navigationTitle, inline) already shows it, and the address
|
|
||||||
// line below has any leading repeat of the name stripped, so
|
|
||||||
// a "Name" row only said it twice.
|
|
||||||
//
|
|
||||||
// The whole row is a copy control: it puts JUST the postcode
|
|
||||||
// on the clipboard so it can be pasted into another
|
|
||||||
// navigation app. The copy glyph and its "Copied"
|
|
||||||
// confirmation share a SECOND line that is always there, so
|
|
||||||
// the confirmation can never reflow the address above it.
|
|
||||||
Button {
|
|
||||||
copyPostcode()
|
|
||||||
} label: {
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
||||||
Text("Address")
|
|
||||||
Spacer(minLength: 8)
|
|
||||||
Text(addressLine)
|
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
if showingCopied {
|
|
||||||
Text("Copied")
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
Image(systemName: showingCopied ? "checkmark" : "doc.on.doc")
|
|
||||||
.foregroundStyle(showingCopied ? Color.green : Color.secondary)
|
|
||||||
}
|
|
||||||
.font(.footnote.weight(.semibold))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.disabled(postcodeForCopying.isEmpty)
|
|
||||||
.accessibilityLabel(String(localized: "Address: \(addressLine)",
|
|
||||||
comment: "Accessibility label for the address row in the More info sheet"))
|
|
||||||
.accessibilityHint(postcodeForCopying.isEmpty
|
|
||||||
? ""
|
|
||||||
: String(localized: "Copies the postcode",
|
|
||||||
comment: "Accessibility hint for the address row in the More info sheet"))
|
|
||||||
if let location {
|
|
||||||
let distance = FuelStore.stationDistanceDisplay(
|
|
||||||
station: station, userLat: location.lat, userLng: location.lng
|
|
||||||
)
|
|
||||||
LabeledContent("Distance", value: distance.labelWithReturn(unit: distanceUnit))
|
|
||||||
.monospacedDigit()
|
|
||||||
}
|
|
||||||
ForEach(StationNotice.notices(for: station)) { notice in
|
|
||||||
Label(notice.title, systemImage: notice.symbolName)
|
|
||||||
.font(.subheadline.weight(.semibold))
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
.padding(.horizontal, 10)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(Capsule().fill(Color.orange.opacity(0.14)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Only shown while Fuel Savings backs it. `fuelSavings` is nil
|
// Only shown while Fuel Savings backs it. `fuelSavings` is nil
|
||||||
// when the feature is off — `savingsLookup` returns nil when it
|
// when the feature is off — `savingsLookup` returns nil when it
|
||||||
// is not configured — so the heading cannot be left dangling
|
// is not configured — so the heading cannot be left dangling
|
||||||
@@ -219,6 +243,13 @@ struct StationDetailView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("Best Value")
|
Text("Best Value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The explanation lives in its own sheet now, opened from this
|
||||||
|
// row. The row sits INSIDE the `if let` so it carries the same
|
||||||
|
// gate as the card above: no card, no row.
|
||||||
|
Section {
|
||||||
|
HowBestValueRow { showBestValueInfo = true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -244,6 +275,8 @@ struct StationDetailView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser.")
|
Text("Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser.")
|
||||||
}
|
}
|
||||||
|
// Nested sheet: the More info sheet stays put behind the explanation.
|
||||||
|
.sheet(isPresented: $showBestValueInfo) { HowBestValueSheet() }
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
Button("Done") { dismiss() }
|
Button("Done") { dismiss() }
|
||||||
@@ -263,12 +296,12 @@ struct StationDetailView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The "Copied" fade-out task must not outlive the sheet.
|
// The "Postcode copied" fade-out task must not outlive the sheet.
|
||||||
.onDisappear { copyResetTask?.cancel() }
|
.onDisappear { copyResetTask?.cancel() }
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// QA hook: `-qaCopyPostcode` runs the REAL copy on open, so the
|
// QA hook: `-qaCopyPostcode` runs the REAL copy on open, so the
|
||||||
// clipboard value (read back with `xcrun simctl pbpaste`) and the
|
// clipboard value (read back with `xcrun simctl pbpaste`) and the
|
||||||
// "Copied" confirmation can be verified without a tap — simctl
|
// "Postcode copied" confirmation can be verified without a tap — simctl
|
||||||
// cannot drive taps into the Simulator.
|
// cannot drive taps into the Simulator.
|
||||||
.task {
|
.task {
|
||||||
guard ProcessInfo.processInfo.arguments.contains("-qaCopyPostcode") else { return }
|
guard ProcessInfo.processInfo.arguments.contains("-qaCopyPostcode") else { return }
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ struct StationsView: View {
|
|||||||
/// info button in the header — keeps the list focused on stations.
|
/// info button in the header — keeps the list focused on stations.
|
||||||
@State private var showKey = false
|
@State private var showKey = false
|
||||||
|
|
||||||
|
/// The "How Best Value works" sheet. The explanation used to be a section in
|
||||||
|
/// the Key sheet; it is its own sheet now, opened from a row at the bottom of
|
||||||
|
/// the Key sheet and from the More info sheet.
|
||||||
|
@State private var showBestValueInfo = false
|
||||||
|
|
||||||
/// Station picked for the More-info sheet (tap action = showDetails).
|
/// Station picked for the More-info sheet (tap action = showDetails).
|
||||||
@State private var detailStation: FuelStation?
|
@State private var detailStation: FuelStation?
|
||||||
|
|
||||||
@@ -218,6 +223,18 @@ struct StationsView: View {
|
|||||||
if ProcessInfo.processInfo.arguments.contains("-showKeySheet") {
|
if ProcessInfo.processInfo.arguments.contains("-showKeySheet") {
|
||||||
showKey = true
|
showKey = true
|
||||||
}
|
}
|
||||||
|
// QA hook: `-showBestValueInfo` opens the Key sheet and then the
|
||||||
|
// explanation on top of it, so the copy can be screenshotted
|
||||||
|
// without a tap. It has to chain: the explanation's `.sheet` lives
|
||||||
|
// inside the Key sheet's content, so it cannot present while the
|
||||||
|
// Key sheet is shut.
|
||||||
|
if BestValueInfoQA.isRequested {
|
||||||
|
showKey = true
|
||||||
|
Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: 1_200_000_000)
|
||||||
|
showBestValueInfo = true
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.task(id: stations.count) {
|
.task(id: stations.count) {
|
||||||
@@ -299,15 +316,15 @@ struct StationsView: View {
|
|||||||
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
|
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("How Best Value works") {
|
// The explanation is its own sheet now, opened from this
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
// row. Gated exactly like the More info row and the Best
|
||||||
Text("Best Value compares each station with the closest one. It works out what you could save at the pump, then takes off the fuel used for the extra return journey.")
|
// Value card itself: `isConfigured` is the same condition
|
||||||
Text("Your fill-up amount and MPG are used to show whether you could save overall.")
|
// `savingsForStation` uses, so the row appears and
|
||||||
Text("Results are marked “Approx.” until road distances are available.")
|
// disappears with the feature.
|
||||||
.foregroundStyle(.secondary)
|
if fuelSavingsSettings.isConfigured {
|
||||||
|
Section {
|
||||||
|
HowBestValueRow { showBestValueInfo = true }
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Key")
|
.navigationTitle("Key")
|
||||||
@@ -317,6 +334,8 @@ struct StationsView: View {
|
|||||||
Button("Done") { showKey = false }
|
Button("Done") { showKey = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Nested sheet: the Key sheet stays behind the explanation.
|
||||||
|
.sheet(isPresented: $showBestValueInfo) { HowBestValueSheet() }
|
||||||
}
|
}
|
||||||
.presentationDetents([.fraction(0.6)])
|
.presentationDetents([.fraction(0.6)])
|
||||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||||
|
|||||||
@@ -71,8 +71,8 @@
|
|||||||
"Name" = "Name";
|
"Name" = "Name";
|
||||||
"Address" = "Address";
|
"Address" = "Address";
|
||||||
"Distance" = "Distance";
|
"Distance" = "Distance";
|
||||||
"Copied" = "Copied";
|
|
||||||
"Copies the postcode" = "Copies the postcode";
|
"Copies the postcode" = "Copies the postcode";
|
||||||
|
"Postcode copied" = "Postcode copied";
|
||||||
"Address: %@" = "Address: %@";
|
"Address: %@" = "Address: %@";
|
||||||
"Directions to %@" = "Directions to %@";
|
"Directions to %@" = "Directions to %@";
|
||||||
"Opens Apple Maps with driving directions" = "Opens Apple Maps with driving directions";
|
"Opens Apple Maps with driving directions" = "Opens Apple Maps with driving directions";
|
||||||
|
|||||||
Reference in New Issue
Block a user