|
@@ -6,7 +6,8 @@ import WebKit
|
|
|
|
|
|
|
|
// MARK: - Public SwiftUI view
|
|
// MARK: - Public SwiftUI view
|
|
|
|
|
|
|
|
-/// Desktop widget embedding Google Maps in a `WKWebView`, with search, traffic, and navigation shortcuts.
|
|
|
|
|
|
|
+/// Google Maps desktop widgets with distinct mobile-style experiences by size:
|
|
|
|
|
+/// small = commute shortcuts, medium = nearby finder, large = interactive live map.
|
|
|
struct MapsInteractiveWidgetView: View {
|
|
struct MapsInteractiveWidgetView: View {
|
|
|
let app: LauncherApp
|
|
let app: LauncherApp
|
|
|
let widgetSize: WidgetSize
|
|
let widgetSize: WidgetSize
|
|
@@ -41,7 +42,7 @@ struct MapsInteractiveWidgetView: View {
|
|
|
if isPreview {
|
|
if isPreview {
|
|
|
previewChrome
|
|
previewChrome
|
|
|
} else {
|
|
} else {
|
|
|
- interactiveChrome
|
|
|
|
|
|
|
+ liveBody
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
@@ -59,10 +60,10 @@ struct MapsInteractiveWidgetView: View {
|
|
|
)
|
|
)
|
|
|
VStack(spacing: 12) {
|
|
VStack(spacing: 12) {
|
|
|
AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
- Text("Live Maps")
|
|
|
|
|
|
|
+ Text(previewTitle)
|
|
|
.font(.system(size: 15, weight: .bold))
|
|
.font(.system(size: 15, weight: .bold))
|
|
|
.foregroundStyle(.white.opacity(0.92))
|
|
.foregroundStyle(.white.opacity(0.92))
|
|
|
- Text("Interactive map loads on your desktop.")
|
|
|
|
|
|
|
+ Text(previewSubtitle)
|
|
|
.font(.system(size: 11, weight: .medium))
|
|
.font(.system(size: 11, weight: .medium))
|
|
|
.foregroundStyle(.white.opacity(0.65))
|
|
.foregroundStyle(.white.opacity(0.65))
|
|
|
.multilineTextAlignment(.center)
|
|
.multilineTextAlignment(.center)
|
|
@@ -72,34 +73,133 @@ struct MapsInteractiveWidgetView: View {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private var interactiveChrome: some View {
|
|
|
|
|
|
|
+ private var previewTitle: String {
|
|
|
|
|
+ switch widgetSize {
|
|
|
|
|
+ case .small: return "Commute"
|
|
|
|
|
+ case .medium: return "Nearby"
|
|
|
|
|
+ case .large: return "Live Map"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var previewSubtitle: String {
|
|
|
|
|
+ switch widgetSize {
|
|
|
|
|
+ case .small: return "Quick access to Home, Work, and traffic."
|
|
|
|
|
+ case .medium: return "Search nearby places with one-tap categories."
|
|
|
|
|
+ case .large: return "Interactive map with live controls and shortcuts."
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @ViewBuilder
|
|
|
|
|
+ private var liveBody: some View {
|
|
|
|
|
+ switch widgetSize {
|
|
|
|
|
+ case .small:
|
|
|
|
|
+ smallCommuteWidget
|
|
|
|
|
+ case .medium:
|
|
|
|
|
+ mediumNearbyWidget
|
|
|
|
|
+ case .large:
|
|
|
|
|
+ largeInteractiveWidget
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var baseBackground: some View {
|
|
|
|
|
+ LinearGradient(
|
|
|
|
|
+ colors: [Color(red: 0.08, green: 0.12, blue: 0.16), Color(red: 0.07, green: 0.26, blue: 0.23)],
|
|
|
|
|
+ startPoint: .topLeading,
|
|
|
|
|
+ endPoint: .bottomTrailing
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var smallCommuteWidget: some View {
|
|
|
ZStack {
|
|
ZStack {
|
|
|
- RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
- .fill(Color.black.opacity(0.35))
|
|
|
|
|
|
|
+ baseBackground
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ AppIconView(app: app, size: 26, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 1) {
|
|
|
|
|
+ Text("Google Maps")
|
|
|
|
|
+ .font(.system(size: 13, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ Text("Commute")
|
|
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
|
|
+ }
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ mapFeatureButton(title: "Home", icon: "house.fill", emphasized: true) {
|
|
|
|
|
+ openDirectionsTo(destination: "home", travelMode: "driving")
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Work", icon: "briefcase.fill", emphasized: true) {
|
|
|
|
|
+ openDirectionsTo(destination: "work", travelMode: "driving")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ mapFeatureButton(title: "Traffic", icon: "car.fill") {
|
|
|
|
|
+ trafficLayerOn = true
|
|
|
|
|
+ mapURL = Self.makeMapURL(center: nil, zoom: nil, layer: "traffic")
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: mapURL, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Open", icon: "arrow.up.right.square") {
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: URL(string: "https://www.google.com/maps")!, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var mediumNearbyWidget: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ baseBackground
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
+ compactHeader(title: "Google Maps", subtitle: "Nearby Finder")
|
|
|
|
|
+ searchBar
|
|
|
|
|
+ travelModesRow
|
|
|
|
|
+ nearbyCategoryRow
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ mapFeatureButton(title: "My location", icon: "location.fill") {
|
|
|
|
|
+ centerOnCurrentLocation()
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Open Maps", icon: "arrow.up.right.square", emphasized: true) {
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: URL(string: "https://www.google.com/maps")!, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var largeInteractiveWidget: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ baseBackground
|
|
|
|
|
|
|
|
VStack(spacing: 0) {
|
|
VStack(spacing: 0) {
|
|
|
- VStack(spacing: 8) {
|
|
|
|
|
- headerBar
|
|
|
|
|
|
|
+ VStack(spacing: 10) {
|
|
|
|
|
+ compactHeader(title: "Google Maps", subtitle: "Live Navigator")
|
|
|
searchBar
|
|
searchBar
|
|
|
- primaryToolbar
|
|
|
|
|
- quickPlacesBar
|
|
|
|
|
|
|
+ largeActionsRow
|
|
|
|
|
+ nearbyCategoryRow
|
|
|
}
|
|
}
|
|
|
- .padding(.horizontal, 4)
|
|
|
|
|
- .padding(.vertical, 6)
|
|
|
|
|
|
|
+ .padding(.horizontal, 8)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
.background(
|
|
.background(
|
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
- .fill(Color(red: 0.07, green: 0.08, blue: 0.1))
|
|
|
|
|
|
|
+ .fill(Color.black.opacity(0.28))
|
|
|
)
|
|
)
|
|
|
.overlay(
|
|
.overlay(
|
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
- .stroke(Color.white.opacity(0.1), lineWidth: 1)
|
|
|
|
|
|
|
+ .stroke(Color.white.opacity(0.14), lineWidth: 1)
|
|
|
)
|
|
)
|
|
|
.zIndex(20)
|
|
.zIndex(20)
|
|
|
.layoutPriority(1)
|
|
.layoutPriority(1)
|
|
|
.allowsHitTesting(true)
|
|
.allowsHitTesting(true)
|
|
|
|
|
|
|
|
MapsWebWidgetView(url: $mapURL)
|
|
MapsWebWidgetView(url: $mapURL)
|
|
|
- .frame(height: mapContentHeight)
|
|
|
|
|
|
|
+ .frame(minHeight: mapContentHeight)
|
|
|
.frame(maxWidth: .infinity)
|
|
.frame(maxWidth: .infinity)
|
|
|
.clipped()
|
|
.clipped()
|
|
|
.compositingGroup()
|
|
.compositingGroup()
|
|
@@ -111,16 +211,32 @@ struct MapsInteractiveWidgetView: View {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private var headerBar: some View {
|
|
|
|
|
|
|
+ private func compactHeader(title: String, subtitle: String) -> some View {
|
|
|
HStack(spacing: 8) {
|
|
HStack(spacing: 8) {
|
|
|
AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
- Text("Google Maps")
|
|
|
|
|
- .font(.system(size: 14, weight: .bold))
|
|
|
|
|
- .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
- .lineLimit(1)
|
|
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 1) {
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .lineLimit(1)
|
|
|
|
|
+ Text(subtitle)
|
|
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.62))
|
|
|
|
|
+ }
|
|
|
Spacer(minLength: 0)
|
|
Spacer(minLength: 0)
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: mapURL, title: app.name)
|
|
|
|
|
+ }) {
|
|
|
|
|
+ Image(systemName: "arrow.up.right.square")
|
|
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.94))
|
|
|
|
|
+ .frame(width: 30, height: 30)
|
|
|
|
|
+ .background(Circle().fill(Color.white.opacity(0.14)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .help("Open current map in full browser")
|
|
|
}
|
|
}
|
|
|
- .padding(.bottom, 6)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private var searchBar: some View {
|
|
private var searchBar: some View {
|
|
@@ -154,96 +270,97 @@ struct MapsInteractiveWidgetView: View {
|
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
.stroke(Color.white.opacity(0.12), lineWidth: 1)
|
|
.stroke(Color.white.opacity(0.12), lineWidth: 1)
|
|
|
)
|
|
)
|
|
|
- .padding(.bottom, 8)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private var primaryToolbar: some View {
|
|
|
|
|
- VStack(alignment: .leading, spacing: 6) {
|
|
|
|
|
|
|
+ private var travelModesRow: some View {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ mapFeatureButton(title: "Drive", icon: "car.fill") {
|
|
|
|
|
+ openTravelMode("driving")
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Transit", icon: "tram.fill") {
|
|
|
|
|
+ openTravelMode("transit")
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Walk", icon: "figure.walk") {
|
|
|
|
|
+ openTravelMode("walking")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var largeActionsRow: some View {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ mapFeatureButton(title: trafficLayerOn ? "Traffic On" : "Traffic", icon: "car.fill", emphasized: trafficLayerOn) {
|
|
|
|
|
+ trafficLayerOn.toggle()
|
|
|
|
|
+ applyTrafficLayer()
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Directions", icon: "arrow.triangle.turn.up.right.diamond.fill") {
|
|
|
|
|
+ openTravelMode("driving")
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "My location", icon: "location.fill") {
|
|
|
|
|
+ centerOnCurrentLocation()
|
|
|
|
|
+ }
|
|
|
|
|
+ mapFeatureButton(title: "Satellite", icon: "globe.americas.fill") {
|
|
|
|
|
+ openMapMode("satellite")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var nearbyCategoryRow: some View {
|
|
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
HStack(spacing: 8) {
|
|
HStack(spacing: 8) {
|
|
|
- MapToolbarButton(
|
|
|
|
|
- title: trafficLayerOn ? "Traffic on" : "Traffic",
|
|
|
|
|
- systemImage: "car.side.fill",
|
|
|
|
|
- isSelected: trafficLayerOn,
|
|
|
|
|
- helpText: "Show or hide live traffic on the map"
|
|
|
|
|
- ) {
|
|
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
- trafficLayerOn.toggle()
|
|
|
|
|
- applyTrafficLayer()
|
|
|
|
|
|
|
+ MapQuickPlaceChip(title: "Food", icon: "fork.knife", helpText: "Search for restaurants nearby") {
|
|
|
|
|
+ activateAndSearch(.restaurants)
|
|
|
}
|
|
}
|
|
|
- MapToolbarButton(
|
|
|
|
|
- title: "Directions",
|
|
|
|
|
- systemImage: "arrow.triangle.turn.up.right.diamond.fill",
|
|
|
|
|
- helpText: "Open driving directions (set start and destination in Maps)"
|
|
|
|
|
- ) {
|
|
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
- loadURLString("https://www.google.com/maps/dir/?api=1&travelmode=driving")
|
|
|
|
|
|
|
+ MapQuickPlaceChip(title: "Gas", icon: "fuelpump.fill", helpText: "Find gas stations") {
|
|
|
|
|
+ activateAndSearch(.gasStations)
|
|
|
}
|
|
}
|
|
|
- MapToolbarButton(
|
|
|
|
|
- title: "My location",
|
|
|
|
|
- systemImage: "location.fill",
|
|
|
|
|
- helpText: "Center the map on your current location"
|
|
|
|
|
- ) {
|
|
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
- locationHelper.requestLocation { coord in
|
|
|
|
|
- let center = "\(coord.latitude),\(coord.longitude)"
|
|
|
|
|
- let layer: String? = trafficLayerOn ? "traffic" : nil
|
|
|
|
|
- let url = MapsInteractiveWidgetView.makeMapURL(center: center, zoom: 15, layer: layer)
|
|
|
|
|
- Task { @MainActor in
|
|
|
|
|
- mapURL = url
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ MapQuickPlaceChip(title: "Hotels", icon: "bed.double.fill", helpText: "Search for hotels") {
|
|
|
|
|
+ activateAndSearch(.hotels)
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- HStack(spacing: 8) {
|
|
|
|
|
- MapToolbarButton(
|
|
|
|
|
- title: "Satellite",
|
|
|
|
|
- systemImage: "globe.americas.fill",
|
|
|
|
|
- helpText: "Switch to satellite imagery"
|
|
|
|
|
- ) {
|
|
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
- loadURLString("https://www.google.com/maps/@?api=1&map_action=map&basemap=satellite")
|
|
|
|
|
|
|
+ MapQuickPlaceChip(title: "Coffee", icon: "cup.and.saucer.fill", helpText: "Find coffee shops") {
|
|
|
|
|
+ activateAndSearch(.coffee)
|
|
|
|
|
+ }
|
|
|
|
|
+ MapQuickPlaceChip(title: "Parking", icon: "parkingsign", helpText: "Find nearby parking") {
|
|
|
|
|
+ activateAndSearch(.parking)
|
|
|
}
|
|
}
|
|
|
- MapToolbarButton(
|
|
|
|
|
- title: "Open",
|
|
|
|
|
- systemImage: "arrow.up.right.square",
|
|
|
|
|
- isEmphasized: true,
|
|
|
|
|
- helpText: "Open this map in the full browser window"
|
|
|
|
|
- ) {
|
|
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
- InAppBrowserWindowManager.shared.open(url: mapURL, title: app.name)
|
|
|
|
|
|
|
+ MapQuickPlaceChip(title: "Pharmacy", icon: "cross.case.fill", helpText: "Find nearby pharmacies") {
|
|
|
|
|
+ activateAndSearch(.pharmacy)
|
|
|
}
|
|
}
|
|
|
- Spacer(minLength: 0)
|
|
|
|
|
}
|
|
}
|
|
|
- .animation(.easeInOut(duration: 0.18), value: trafficLayerOn)
|
|
|
|
|
}
|
|
}
|
|
|
- .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
- .padding(.bottom, 8)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private var quickPlacesBar: some View {
|
|
|
|
|
- HStack(alignment: .center, spacing: 8) {
|
|
|
|
|
- MapQuickPlaceChip(title: "Food", icon: "fork.knife", helpText: "Search for restaurants nearby") {
|
|
|
|
|
- activateAndSearch(.restaurants)
|
|
|
|
|
- }
|
|
|
|
|
- MapQuickPlaceChip(title: "Gas", icon: "fuelpump.fill", helpText: "Find gas stations") {
|
|
|
|
|
- activateAndSearch(.gasStations)
|
|
|
|
|
- }
|
|
|
|
|
- MapQuickPlaceChip(title: "Hotels", icon: "bed.double.fill", helpText: "Search for hotels") {
|
|
|
|
|
- activateAndSearch(.hotels)
|
|
|
|
|
- }
|
|
|
|
|
- MapQuickPlaceChip(title: "Coffee", icon: "cup.and.saucer.fill", helpText: "Find coffee shops") {
|
|
|
|
|
- activateAndSearch(.coffee)
|
|
|
|
|
|
|
+ private func mapFeatureButton(title: String, icon: String, emphasized: Bool = false, action: @escaping () -> Void) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ action()
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 6) {
|
|
|
|
|
+ Image(systemName: icon)
|
|
|
|
|
+ .font(.system(size: 11, weight: .semibold))
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
|
|
+ .lineLimit(1)
|
|
|
}
|
|
}
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
|
|
+ .fill(emphasized ? Color.blue.opacity(0.42) : Color.white.opacity(0.14))
|
|
|
|
|
+ )
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
|
|
+ .stroke(Color.white.opacity(emphasized ? 0.32 : 0.16), lineWidth: 1)
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
- .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
- .padding(.bottom, 2)
|
|
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private var mapContentHeight: CGFloat {
|
|
private var mapContentHeight: CGFloat {
|
|
|
switch widgetSize {
|
|
switch widgetSize {
|
|
|
- case .small: return 220
|
|
|
|
|
- case .medium: return 200
|
|
|
|
|
- case .large: return 340
|
|
|
|
|
|
|
+ case .small: return 210
|
|
|
|
|
+ case .medium: return 220
|
|
|
|
|
+ case .large: return 300
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -259,6 +376,8 @@ struct MapsInteractiveWidgetView: View {
|
|
|
case gasStations = "gas stations"
|
|
case gasStations = "gas stations"
|
|
|
case hotels
|
|
case hotels
|
|
|
case coffee
|
|
case coffee
|
|
|
|
|
+ case parking
|
|
|
|
|
+ case pharmacy
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func activateAndSearch(_ place: QuickPlaceSearch) {
|
|
private func activateAndSearch(_ place: QuickPlaceSearch) {
|
|
@@ -268,9 +387,7 @@ struct MapsInteractiveWidgetView: View {
|
|
|
|
|
|
|
|
private func openSearchQueryString(_ query: String) {
|
|
private func openSearchQueryString(_ query: String) {
|
|
|
guard let url = Self.mapsSearchURL(for: query) else { return }
|
|
guard let url = Self.mapsSearchURL(for: query) else { return }
|
|
|
- DispatchQueue.main.async {
|
|
|
|
|
- mapURL = url
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ openMapDestination(url)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Official search URL: `https://www.google.com/maps/search/?api=1&query=…`
|
|
/// Official search URL: `https://www.google.com/maps/search/?api=1&query=…`
|
|
@@ -286,12 +403,64 @@ struct MapsInteractiveWidgetView: View {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func loadURLString(_ string: String) {
|
|
private func loadURLString(_ string: String) {
|
|
|
- if let u = URL(string: string) { mapURL = u }
|
|
|
|
|
|
|
+ if let u = URL(string: string) {
|
|
|
|
|
+ openMapDestination(u)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openDirections(mode: String) {
|
|
|
|
|
+ loadURLString("https://www.google.com/maps/dir/?api=1&travelmode=\(mode)")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openDirectionsTo(destination: String, travelMode: String) {
|
|
|
|
|
+ var c = URLComponents(string: "https://www.google.com/maps/dir/")!
|
|
|
|
|
+ c.queryItems = [
|
|
|
|
|
+ URLQueryItem(name: "api", value: "1"),
|
|
|
|
|
+ URLQueryItem(name: "destination", value: destination),
|
|
|
|
|
+ URLQueryItem(name: "travelmode", value: travelMode),
|
|
|
|
|
+ ]
|
|
|
|
|
+ if let url = c.url {
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openTravelMode(_ mode: String) {
|
|
|
|
|
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ if q.isEmpty {
|
|
|
|
|
+ openDirections(mode: mode)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ openDirectionsTo(destination: q, travelMode: mode)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openMapMode(_ basemap: String) {
|
|
|
|
|
+ loadURLString("https://www.google.com/maps/@?api=1&map_action=map&basemap=\(basemap)")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func centerOnCurrentLocation() {
|
|
|
|
|
+ locationHelper.requestLocation { coord in
|
|
|
|
|
+ let center = "\(coord.latitude),\(coord.longitude)"
|
|
|
|
|
+ let layer: String? = trafficLayerOn ? "traffic" : nil
|
|
|
|
|
+ let url = MapsInteractiveWidgetView.makeMapURL(center: center, zoom: 15, layer: layer)
|
|
|
|
|
+ openMapDestination(url)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func applyTrafficLayer() {
|
|
private func applyTrafficLayer() {
|
|
|
let layer: String? = trafficLayerOn ? "traffic" : nil
|
|
let layer: String? = trafficLayerOn ? "traffic" : nil
|
|
|
- mapURL = MapsInteractiveWidgetView.makeMapURL(center: nil, zoom: nil, layer: layer)
|
|
|
|
|
|
|
+ let next = MapsInteractiveWidgetView.makeMapURL(center: nil, zoom: nil, layer: layer)
|
|
|
|
|
+ openMapDestination(next)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Large widget updates embedded map; small/medium open in-app browser directly.
|
|
|
|
|
+ private func openMapDestination(_ url: URL) {
|
|
|
|
|
+ if widgetSize == .large {
|
|
|
|
|
+ DispatchQueue.main.async {
|
|
|
|
|
+ mapURL = url
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|