From 4cbf29d27c8e300a1b00fe18ea1405323fe65a1a Mon Sep 17 00:00:00 2001 From: dmpr0 Date: Thu, 25 Jun 2026 14:19:22 +0300 Subject: [PATCH 01/20] Update Astronomy plugin to use dynamic named colors --- Sources/Plugins/Astronomy/AstroUtils.swift | 25 +++++----- Sources/Plugins/Astronomy/StarView.swift | 56 +++++++++++----------- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/Sources/Plugins/Astronomy/AstroUtils.swift b/Sources/Plugins/Astronomy/AstroUtils.swift index 6248e3dec7..de477d39ed 100644 --- a/Sources/Plugins/Astronomy/AstroUtils.swift +++ b/Sources/Plugins/Astronomy/AstroUtils.swift @@ -63,7 +63,7 @@ private final class AstroRedFilterOverlayView: UIView { private func commonInit() { isUserInteractionEnabled = false - backgroundColor = .red + backgroundColor = UIColor(named: "mapNightFilter")! layer.compositingFilter = "multiplyBlendMode" } @@ -352,17 +352,17 @@ enum AstroUtils { static func color(for body: Body) -> UIColor { if body === Body.sun { - return UIColor(red: 1.0, green: 0.69, blue: 0.20, alpha: 1.0) + return UIColor(named: "solarSun")! } else if body === Body.moon { - return UIColor(white: 0.88, alpha: 1.0) + return UIColor(named: "solarMoon")! } else if body === Body.mars { - return UIColor(red: 0.95, green: 0.36, blue: 0.22, alpha: 1.0) + return UIColor(named: "solarMars")! } else if body === Body.jupiter { - return UIColor(red: 0.95, green: 0.73, blue: 0.48, alpha: 1.0) + return UIColor(named: "solarJupiter")! } else if body === Body.saturn { - return UIColor(red: 0.95, green: 0.82, blue: 0.52, alpha: 1.0) + return UIColor(named: "solarSaturn")! } else if body === Body.neptune || body === Body.uranus { - return UIColor(red: 0.42, green: 0.73, blue: 1.0, alpha: 1.0) + return UIColor(named: "solarUranusNeptune")! } else { return UIColor(red: 0.87, green: 0.90, blue: 1.0, alpha: 1.0) } @@ -371,16 +371,15 @@ enum AstroUtils { static func color(for type: SkyObjectType, magnitude: Double?) -> UIColor { switch type { case .STAR: - let brightness = max(0.45, min(1.0, 1.0 - ((magnitude ?? 2.0) / 8.0))) - return UIColor(red: brightness, green: brightness, blue: 1.0, alpha: 1.0) + return UIColor(named: "starDot")! case .GALAXY, .GALAXY_CLUSTER: - return UIColor(red: 0.52, green: 0.74, blue: 1.0, alpha: 1.0) + return UIColor(named: "deepSkyGalaxyDot")! case .NEBULA: - return UIColor(red: 0.85, green: 0.45, blue: 0.95, alpha: 1.0) + return UIColor(named: "deepSkyNebulaDot")! case .OPEN_CLUSTER, .GLOBULAR_CLUSTER: - return UIColor(red: 0.50, green: 0.95, blue: 0.78, alpha: 1.0) + return UIColor(named: "deepSkyClusterDot")! case .BLACK_HOLE: - return UIColor(red: 0.95, green: 0.45, blue: 0.35, alpha: 1.0) + return UIColor(named: "deepSkyBlackHoleDot")! case .CONSTELLATION: return UIColor(red: 0.80, green: 0.86, blue: 1.0, alpha: 1.0) case .SUN, .MOON, .PLANET: diff --git a/Sources/Plugins/Astronomy/StarView.swift b/Sources/Plugins/Astronomy/StarView.swift index 18ab054db0..5a24646c8c 100644 --- a/Sources/Plugins/Astronomy/StarView.swift +++ b/Sources/Plugins/Astronomy/StarView.swift @@ -849,9 +849,9 @@ final class StarView: UIView { private func drawBackground(in context: CGContext) { if isCameraMode { context.clear(bounds) - UIColor.black.withAlphaComponent(0.20).setFill() + UIColor(named: "mapBgZenithColor")!.withAlphaComponent(0.20).setFill() } else { - UIColor.black.setFill() + UIColor(named: "mapBgZenithColor")!.setFill() } context.fill(bounds) } @@ -875,7 +875,7 @@ final class StarView: UIView { var baseSize: CGFloat = 15 if object.type == .STAR && zoomFactor > 0.3 && object.magnitude > 2.5 { baseSize = 8 - color = .gray + color = UIColor(named: "starDotDimmed")! } var radius = max(2, baseSize - CGFloat(object.magnitude) * 2) @@ -930,12 +930,12 @@ final class StarView: UIView { private func labelColor(for object: SkyObject) -> UIColor { if object === selectedObject { - return .red + return UIColor(named: "mapTextSelected")! } if settings.starMap.showCelestialPaths && object.showCelestialPath { - return .yellow + return UIColor(named: "mapObjectPath")! } - return .lightGray + return UIColor(named: "mapTextPrimary")! } private func drawHighlights(in context: CGContext) { @@ -949,7 +949,7 @@ final class StarView: UIView { if let object = selectedObject, let point = skyToScreen(azimuth: object.azimuth, altitude: object.altitude) { - strokeCircle(at: point, radius: pixelsToPoints(25), color: .red, width: pixelsToPoints(3), in: context) + strokeCircle(at: point, radius: pixelsToPoints(25), color: UIColor(named: "mapTextSelected")!, width: pixelsToPoints(3), in: context) } if settings.starMap.showDirections { @@ -976,12 +976,12 @@ final class StarView: UIView { appendSkyLine(to: path, range: stride(from: 0.0, through: 360.0, by: 2.0)) { azimuth in (azimuth, 0.0) } - stroke(path, color: .green, width: 1.2, in: context) + stroke(path, color: UIColor(named: "mapLineHorizon")!, width: 1.2, in: context) - drawOutsideLabel("N", azimuth: 0, altitude: 0, color: .green, offset: 30, in: context) - drawOutsideLabel("E", azimuth: 90, altitude: 0, color: .green, offset: 30, in: context) - drawOutsideLabel("S", azimuth: 180, altitude: 0, color: .green, offset: 30, in: context) - drawOutsideLabel("W", azimuth: 270, altitude: 0, color: .green, offset: 30, in: context) + drawOutsideLabel("N", azimuth: 0, altitude: 0, color: UIColor(named: "mapLineHorizon")!, offset: 30, in: context) + drawOutsideLabel("E", azimuth: 90, altitude: 0, color: UIColor(named: "mapLineHorizon")!, offset: 30, in: context) + drawOutsideLabel("S", azimuth: 180, altitude: 0, color: UIColor(named: "mapLineHorizon")!, offset: 30, in: context) + drawOutsideLabel("W", azimuth: 270, altitude: 0, color: UIColor(named: "mapLineHorizon")!, offset: 30, in: context) } private func drawAzimuthalGrid(in context: CGContext) { @@ -1000,8 +1000,8 @@ final class StarView: UIView { (azimuth, Double(altitude)) } if altitude != 0 { - drawGridLabel("\(altitude)°", azimuth: centerAzimuth, altitude: Double(altitude), align: .left, color: UIColor(white: 0.55, alpha: 1), in: context) - drawGridLabel("\(altitude)°", azimuth: centerAzimuth + 180, altitude: Double(altitude), align: .left, color: UIColor(white: 0.55, alpha: 1), in: context) + drawGridLabel("\(altitude)°", azimuth: centerAzimuth, altitude: Double(altitude), align: .left, color: UIColor(named: "mapGridSecondaryText")!, in: context) + drawGridLabel("\(altitude)°", azimuth: centerAzimuth + 180, altitude: Double(altitude), align: .left, color: UIColor(named: "mapGridSecondaryText")!, in: context) } } @@ -1010,10 +1010,10 @@ final class StarView: UIView { (Double(azimuth), altitude) } if !azimuth.isMultiple(of: 90) { - drawOutsideLabel("\(azimuth)°", azimuth: Double(azimuth), altitude: 0, color: UIColor(white: 0.55, alpha: 1), offset: 25, in: context) + drawOutsideLabel("\(azimuth)°", azimuth: Double(azimuth), altitude: 0, color: UIColor(named: "mapGridSecondaryText")!, offset: 25, in: context) } } - stroke(path, color: UIColor(white: 0.27, alpha: 1), width: 2.0, in: context) + stroke(path, color: UIColor(named: "mapGridSecondary")!, width: 2.0, in: context) } private func updateEquatorialGridCache() { @@ -1124,7 +1124,7 @@ final class StarView: UIView { let hours = totalMinutes / 60 let minutes = totalMinutes % 60 let label = minutes == 0 ? "\(hours)h" : "\(hours)h\(minutes)" - drawText(label, at: CGPoint(x: point.x, y: point.y - 18), color: UIColor(red: 0, green: 0.74, blue: 0.74, alpha: 1), font: .systemFont(ofSize: 11), align: .center) + drawText(label, at: CGPoint(x: point.x, y: point.y - 18), color: UIColor(named: "mapGridMainText")!, font: .systemFont(ofSize: 11), align: .center) } } @@ -1148,12 +1148,12 @@ final class StarView: UIView { if dec != 0 { let ra = Double(bestRaIndex * equRaStepMin) / 60.0 let hor = AstronomyKt.horizon(time: currentTime, observer: observer, ra: ra, dec: Double(dec), refraction: Refraction.normal) - drawGridLabel("\(dec)°", azimuth: hor.azimuth, altitude: hor.altitude, align: .left, color: UIColor(red: 0, green: 0.74, blue: 0.74, alpha: 1), in: context) + drawGridLabel("\(dec)°", azimuth: hor.azimuth, altitude: hor.altitude, align: .left, color: UIColor(named: "mapGridMainText")!, in: context) } } } - stroke(path, color: UIColor(red: 0, green: 0.40, blue: 0.40, alpha: 1), width: 0.8, in: context) + stroke(path, color: UIColor(named: "mapGridMain")!, width: 0.8, in: context) } private func updateEclipticCache() { @@ -1185,7 +1185,7 @@ final class StarView: UIView { private func drawEclipticLine(in context: CGContext) { updateEclipticCache() stroke(cachedLinePath(azimuths: eclipticAzimuths, altitudes: eclipticAltitudes), - color: .yellow, + color: UIColor(named: "mapLineEcliptic")!, width: 1.2, dash: [8, 8], in: context) @@ -1199,7 +1199,7 @@ final class StarView: UIView { appendSkyLine(to: path, range: stride(from: -90.0, through: 90.0, by: 2.0)) { altitude in (180.0, altitude) } - stroke(path, color: .green, width: 1.2, dash: [12, 8], in: context) + stroke(path, color: UIColor(named: "mapLineMeridian")!, width: 1.2, dash: [12, 8], in: context) } private func updateEquatorCache() { @@ -1226,7 +1226,7 @@ final class StarView: UIView { private func drawEquatorLine(in context: CGContext) { updateEquatorCache() stroke(cachedLinePath(azimuths: equatorAzimuths, altitudes: equatorAltitudes), - color: UIColor(red: 0, green: 0.67, blue: 0.67, alpha: 1), + color: UIColor(named: "mapLineEquator")!, width: 1.2, dash: [12, 8], in: context) @@ -1261,7 +1261,7 @@ final class StarView: UIView { private func drawGalacticLine(in context: CGContext) { updateGalacticCache() stroke(cachedLinePath(azimuths: galacticAzimuths, altitudes: galacticAltitudes), - color: .magenta, + color: UIColor(named: "mapLineGalactic")!, width: 1.2, dash: [8, 8], in: context) @@ -1286,7 +1286,7 @@ final class StarView: UIView { path.addLine(to: p2) } stroke(path, - color: isSelected ? UIColor(red: 1, green: 0.84, blue: 0, alpha: 1) : UIColor(red: 0.33, green: 0.60, blue: 1.0, alpha: 0.58), + color: isSelected ? UIColor(named: "constellationSelected")! : UIColor(named: "constellationLine")!, width: isSelected ? 1.8 : 0.9, in: context) } @@ -1304,7 +1304,7 @@ final class StarView: UIView { continue } - let color = isSelected ? UIColor(red: 1, green: 0.84, blue: 0, alpha: 1) : UIColor(red: 0.67, green: 0.73, blue: 1.0, alpha: 1) + let color = isSelected ? UIColor(named: "constellationSelected")! : UIColor(named: "constellationText")! let font = UIFont.italicSystemFont(ofSize: 16) let text = constellation.getDisplayName() let size = text.size(withAttributes: [.font: font]) @@ -1400,7 +1400,7 @@ final class StarView: UIView { } previousPoint = point } - stroke(path, color: .cyan, width: 1.1, dash: [5, 8], in: context) + stroke(path, color: UIColor(named: "mapTrajectoryLine")!, width: 1.1, dash: [5, 8], in: context) var drawnLabels: Set = [] for index in 0.. Date: Wed, 8 Jul 2026 10:46:35 +0300 Subject: [PATCH 02/20] Changed buttons Buttons size and style the same we use on regular map + Liquid glass style by default --- OsmAnd.xcodeproj/project.pbxproj | 16 ++ Sources/Plugins/Astronomy/StarMapButton.swift | 102 +++++++--- .../Astronomy/StarMapGlassBackground.swift | 40 ++++ .../StarMapMagnitudeFilterButton.swift | 109 ++++++++++ .../StarMapMagnitudeSliderPanel.swift | 116 +++++++++++ .../Astronomy/StarMapResetButton.swift | 27 ++- .../Astronomy/StarMapTimeControlButton.swift | 34 +++- .../Astronomy/StarMapTimeControlCard.swift | 111 ++++++++++ .../Astronomy/StarMapViewController.swift | 192 ++++-------------- 9 files changed, 558 insertions(+), 189 deletions(-) create mode 100644 Sources/Plugins/Astronomy/StarMapGlassBackground.swift create mode 100644 Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift create mode 100644 Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift create mode 100644 Sources/Plugins/Astronomy/StarMapTimeControlCard.swift diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index d9452ebbe3..f0c601c5cd 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1706,6 +1706,10 @@ CE4075A32FC48BD9004224AA /* BasePoiIconCollectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4075A22FC48BD6004224AA /* BasePoiIconCollectionHandler.swift */; }; CE4075A52FC48C40004224AA /* ProfileIconCollectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4075A42FC48C2F004224AA /* ProfileIconCollectionHandler.swift */; }; CE71B88C2FEFAB0C008B0759 /* StarMapSearchEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE71B88B2FEFAB0C008B0759 /* StarMapSearchEmptyView.swift */; }; + CE762AEE2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */; }; + CE762AF02FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AEF2FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift */; }; + CE762AF22FFE1197001BF551 /* StarMapGlassBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AF12FFE1197001BF551 /* StarMapGlassBackground.swift */; }; + CE762AF42FFE251B001BF551 /* StarMapTimeControlCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AF32FFE251B001BF551 /* StarMapTimeControlCard.swift */; }; CE7818262FC07944005CCF47 /* WikipediaContextMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */; }; CE7AC1A82FE02A2600495411 /* OATrackPreviewMapRenderer.mm in Sources */ = {isa = PBXBuildFile; fileRef = CE7AC19A2FE02A2600495411 /* OATrackPreviewMapRenderer.mm */; }; CE7AC1A92FE02A2600495411 /* TrackBitmapDrawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AC19B2FE02A2600495411 /* TrackBitmapDrawer.swift */; }; @@ -5749,6 +5753,10 @@ CE4075A22FC48BD6004224AA /* BasePoiIconCollectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasePoiIconCollectionHandler.swift; sourceTree = ""; }; CE4075A42FC48C2F004224AA /* ProfileIconCollectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileIconCollectionHandler.swift; sourceTree = ""; }; CE71B88B2FEFAB0C008B0759 /* StarMapSearchEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchEmptyView.swift; sourceTree = ""; }; + CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMagnitudeFilterButton.swift; sourceTree = ""; }; + CE762AEF2FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMagnitudeSliderPanel.swift; sourceTree = ""; }; + CE762AF12FFE1197001BF551 /* StarMapGlassBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapGlassBackground.swift; sourceTree = ""; }; + CE762AF32FFE251B001BF551 /* StarMapTimeControlCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapTimeControlCard.swift; sourceTree = ""; }; CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikipediaContextMenuCell.swift; sourceTree = ""; }; CE7AC1992FE02A2600495411 /* OATrackPreviewMapRenderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OATrackPreviewMapRenderer.h; sourceTree = ""; }; CE7AC19A2FE02A2600495411 /* OATrackPreviewMapRenderer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OATrackPreviewMapRenderer.mm; sourceTree = ""; }; @@ -10653,7 +10661,11 @@ A5A5002B2F5000010000002B /* StarMapButton.swift */, A5A5002D2F5000010000002D /* StarMapResetButton.swift */, A5A5002F2F5000010000002F /* StarMapTimeControlButton.swift */, + CE762AF32FFE251B001BF551 /* StarMapTimeControlCard.swift */, A5A500312F50000100000031 /* StarCompassButton.swift */, + CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */, + CE762AEF2FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift */, + CE762AF12FFE1197001BF551 /* StarMapGlassBackground.swift */, A5A5000F2F5000010000000F /* StarView.swift */, A5A500112F50000100000011 /* StarMapARModeHelper.swift */, A5A500132F50000100000013 /* StarMapCameraHelper.swift */, @@ -17660,6 +17672,7 @@ DA5A84CE26C563A900F274C7 /* OAQuickActionSelectionBottomSheetViewController.mm in Sources */, 4656BD362C4855D200B69928 /* ColorizationType.swift in Sources */, DA5A816026C563A700F274C7 /* OAParkingPositionPlugin.mm in Sources */, + CE762AEE2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift in Sources */, 2C8E8A562D5104E600746A69 /* OAArabicNormalizer.mm in Sources */, C596D5832E8D634300D13D93 /* AutoZoom3DAngleViewController.swift in Sources */, DA5A843226C563A800F274C7 /* OATransportDetailsTableViewController.mm in Sources */, @@ -17900,6 +17913,7 @@ DA5A812626C563A700F274C7 /* CoreResourcesFromBundleProvider.mm in Sources */, DA5A84B126C563A900F274C7 /* OATargetOptionsBottomSheetViewController.mm in Sources */, DA2F790C2A273D39002B9B83 /* WidgetsInitializer.swift in Sources */, + CE762AF42FFE251B001BF551 /* StarMapTimeControlCard.swift in Sources */, DA5A847C26C563A900F274C7 /* OATurnPathHelper.mm in Sources */, 329E56BA2C88321100DC7B7C /* ProfileAppearanceViewAngleViewController.swift in Sources */, DA5A84F626C563A900F274C7 /* OAMapLayersConfiguration.m in Sources */, @@ -17979,6 +17993,7 @@ 3260870E2F324DE20038A34B /* PoiRowParams.swift in Sources */, DA5A813226C563A700F274C7 /* OACoreResourcesTransportRouteIconProvider.mm in Sources */, 46AB58A729C4C979003B2FB9 /* OAWikipediaImagesSettingsViewController.m in Sources */, + CE762AF22FFE1197001BF551 /* StarMapGlassBackground.swift in Sources */, C588D1EF298D1ECE001FEDBD /* OASearchHistoryTableGroup.m in Sources */, DA5A824D26C563A700F274C7 /* OAScreenAlertsViewController.m in Sources */, FA73410D2BBAED8600CBF7EC /* LeftIconRightStackTitleDescriptionButtonView.swift in Sources */, @@ -18479,6 +18494,7 @@ 27C768E42E6833F100540266 /* CustomInputDeviceProfile.swift in Sources */, DA5A817F26C563A700F274C7 /* OAUnsupportedAction.m in Sources */, 0513AA382A9B530A00CFA2A0 /* OADestinationBarWidget.mm in Sources */, + CE762AF02FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift in Sources */, 32FE7AA72B65138800F82728 /* OAGPXImportUIHelper.mm in Sources */, FA86F41A2AE7CB8D00DC3B2B /* BLEBikeSCDDevice.swift in Sources */, FA86F4152AE7C56E00DC3B2B /* UserDefaults+Extension.swift in Sources */, diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index c3b959b0e9..579f1f9def 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -42,47 +42,101 @@ enum StarMapControlTheme { } } -class StarMapButton: UIButton { +@objcMembers +class StarMapButton: OAHudButton { + var showsHudChrome = true + var active = false { didSet { updateTheme() } } + var nightMode = false { didSet { updateTheme() } } override init(frame: CGRect) { super.init(frame: frame) - imageView?.contentMode = .scaleAspectFit - updateTheme() + commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) - imageView?.contentMode = .scaleAspectFit - updateTheme() - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = min(bounds.width, bounds.height) / 2.0 - } - - func updateTheme() { - tintColor = StarMapControlTheme.foreground(active: active, nightMode: nightMode) - backgroundColor = active - ? StarMapControlTheme.activeBackground() - : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.86) - layer.cornerRadius = min(bounds.width, bounds.height) / 2.0 - layer.borderWidth = StarMapControlTheme.borderWidth(active: active, nightMode: nightMode) - layer.borderColor = StarMapControlTheme.borderColor.cgColor - } - - func setColorFilter(_ color: UIColor) { - tintColor = color + commonInit() } func setIcon(iconName: String, accessibilityLabel: String? = nil) { setImage(AstroIcon.template(iconName), for: .normal) self.accessibilityLabel = accessibilityLabel + updateTheme() + } + + func updateTheme() { + guard showsHudChrome else { + applyPlainTheme() + return + } + + if active { + unpressedColorDay = .mapButtonBgColorActive.light + unpressedColorNight = .mapButtonBgColorActive.dark + tintColorDay = .white + tintColorNight = .white + borderWidthDay = 0 + borderWidthNight = 0 + } else { + unpressedColorDay = .mapButtonBgColorDefault.light + unpressedColorNight = .mapButtonBgColorDefault.dark + tintColorDay = .mapButtonIconColorDefault.light + tintColorNight = .mapButtonIconColorDefault.dark + borderWidthDay = 0 + borderWidthNight = 2 + } + + updateColors(forPressedState: isHighlighted) + if active { + backgroundColor = StarMapControlTheme.activeBackground(alpha: 0.5) + } + } + + private func applyDefaultAppearanceParams() { + let helper = OAMapButtonsHelper.sharedInstance() + var size = helper.getDefaultSizePref().get() + if size <= 0 { size = MapButtonState.defaultSizeDp } + + var cornerRadius = helper.getDefaultCornerRadiusPref().get() + if cornerRadius < 0 { cornerRadius = MapButtonState.roundRadiusDp } + + var glassStyle: Int32 + var opacity: Double + if #available(iOS 26.0, *) { + glassStyle = Int32(UIGlassEffect.Style.clear.rawValue) + opacity = 0.5 + } else { + glassStyle = MapButtonState.defaultGlassStyle + opacity = helper.getDefaultOpacityPref().get() + if opacity < 0 { + opacity = MapButtonState.opaqueAlpha + } + } + + setCustomAppearanceParams(ButtonAppearanceParams( + iconName: nil, + size: Int32(size), + opacity: opacity, + cornerRadius: Int32(cornerRadius), + glassStyle: glassStyle + )) + } + + private func applyPlainTheme() { + backgroundColor = .clear + layer.borderWidth = 0 + layer.shadowOpacity = 0 + } + + private func commonInit() { + imageView?.contentMode = .scaleAspectFit + applyDefaultAppearanceParams() + updateTheme() } } diff --git a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift new file mode 100644 index 0000000000..71d006d23f --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift @@ -0,0 +1,40 @@ +// +// StarMapGlassBackground.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 08.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import Foundation + +enum StarMapGlassBackground { + private static let tag = 9_001 + + static func apply(to view: UIView, active: Bool, nightMode: Bool, cornerRadius: CGFloat) { + if #available(iOS 26.0, *) { + view.subviews.filter { $0.tag == tag }.forEach { $0.removeFromSuperview() } + view.backgroundColor = .clear + + let baseColor = active + ? UIColor.mapButtonBgColorActive + : StarMapControlTheme.resolved(.mapButtonBgColorDefault, nightMode: nightMode) + + let glass = UIGlassEffect(style: .clear) + let effectView = UIVisualEffectView(effect: glass) + effectView.tag = tag + effectView.isUserInteractionEnabled = false + effectView.frame = view.bounds + effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + effectView.layer.cornerRadius = cornerRadius + effectView.clipsToBounds = true + effectView.overrideUserInterfaceStyle = nightMode ? .dark : .light + view.insertSubview(effectView, at: 0) + } else { + view.backgroundColor = active + ? StarMapControlTheme.activeBackground() + : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.5) + view.layer.cornerRadius = cornerRadius + } + } +} diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift new file mode 100644 index 0000000000..b074ff3937 --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift @@ -0,0 +1,109 @@ +// +// StarMapMagnitudeFilterButton.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 08.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit + +final class StarMapMagnitudeFilterButton: StarMapButton { + private static let pillCornerRadius: Int32 = 24 + + private let iconView = UIImageView(image: .icCustomMagnitude) + private let valueLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + setupContent() + applyPillAppearance() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupContent() + applyPillAppearance() + } + + func setValue(_ text: String) { + valueLabel.text = text + } + + override func updateTheme() { + super.updateTheme() + setImage(nil, for: .normal) + imageView?.isHidden = true + + let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) + iconView.tintColor = color + valueLabel.textColor = color + } + + override func layoutSubviews() { + super.layoutSubviews() + guard showsHudChrome, bounds.width > 0, bounds.height > 0 else { return } + if #available(iOS 26.0, *) { + subviews.compactMap { $0 as? UIVisualEffectView }.forEach { + $0.frame = bounds + $0.layer.cornerRadius = bounds.width / 2 + } + } + } + + private func setupContent() { + setImage(nil, for: .normal) + imageView?.isHidden = true + + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + iconView.widthAnchor.constraint(equalToConstant: 30), + iconView.heightAnchor.constraint(equalToConstant: 30) + ]) + + valueLabel.font = .systemFont(ofSize: 15, weight: .bold) + valueLabel.textAlignment = .center + + let stack = UIStackView(arrangedSubviews: [iconView, valueLabel]) + stack.axis = .vertical + stack.alignment = .center + stack.spacing = 4 + stack.isUserInteractionEnabled = false + stack.translatesAutoresizingMaskIntoConstraints = false + addSubview(stack) + + NSLayoutConstraint.activate([ + stack.centerXAnchor.constraint(equalTo: centerXAnchor), + stack.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + + accessibilityLabel = localizedString("astro_min_magnitude") + } + + private func applyPillAppearance() { + let helper = OAMapButtonsHelper.sharedInstance() + var size = helper.getDefaultSizePref().get() + if size <= 0 { size = MapButtonState.defaultSizeDp } + + var opacity = helper.getDefaultOpacityPref().get() + if opacity < 0 { opacity = MapButtonState.opaqueAlpha } + + var glassStyle: Int32 + if #available(iOS 26.0, *) { + glassStyle = Int32(UIGlassEffect.Style.clear.rawValue) + opacity = 0.5 + } else { + glassStyle = MapButtonState.defaultGlassStyle + } + + setCustomAppearanceParams(ButtonAppearanceParams( + iconName: nil, + size: Int32(size), + opacity: opacity, + cornerRadius: Self.pillCornerRadius, + glassStyle: glassStyle + )) + updateTheme() + } +} diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift new file mode 100644 index 0000000000..7b288908e0 --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift @@ -0,0 +1,116 @@ +// +// StarMapMagnitudeSliderPanel.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 08.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit + +final class StarMapMagnitudeSliderPanel: UIView { + static let preferredWidth: CGFloat = 240 + private static let cornerRadius: CGFloat = 16 + + var onValueChanged: ((Double) -> Void)? + + var isExpanded: Bool { + !isHidden + } + + private let titleLabel = UILabel() + private let valueLabel = UILabel() + private let slider = UISlider() + private var nightMode = false + + init(maxMagnitude: Double) { + super.init(frame: .zero) + setupContent(maxMagnitude: maxMagnitude) + isHidden = true + updateTheme(nightMode: OADayNightHelper.instance().isNightMode()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func toggle() { + isHidden.toggle() + updateTheme(nightMode: nightMode) + } + + func setMagnitude(_ value: Double) { + slider.value = Float(value) + valueLabel.text = String(format: "%.1f", value) + } + + func updateTheme(nightMode: Bool) { + self.nightMode = nightMode + StarMapGlassBackground.apply( + to: self, + active: false, + nightMode: nightMode, + cornerRadius: Self.cornerRadius + ) + let color = StarMapControlTheme.foreground(active: true, nightMode: nightMode) + titleLabel.textColor = color + valueLabel.textColor = color + + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: OADayNightHelper.instance().isNightMode(), alpha: 0.5) + } + + override func layoutSubviews() { + super.layoutSubviews() + guard bounds.width > 0, bounds.height > 0 else { return } + if #available(iOS 26.0, *) { + subviews.compactMap { $0 as? UIVisualEffectView }.forEach { + $0.frame = bounds + $0.layer.cornerRadius = Self.cornerRadius + } + } + } + + private func setupContent(maxMagnitude: Double) { + translatesAutoresizingMaskIntoConstraints = false + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.16 + layer.shadowRadius = 6 + layer.shadowOffset = CGSize(width: 0, height: 2) + layer.cornerRadius = Self.cornerRadius + + titleLabel.text = localizedString("astro_min_magnitude") + titleLabel.font = .preferredFont(forTextStyle: .subheadline) + + valueLabel.font = .preferredFont(forTextStyle: .subheadline) + + slider.minimumValue = -1 + slider.maximumValue = Float(maxMagnitude) + slider.addTarget(self, action: #selector(sliderChanged), for: .valueChanged) + + let header = UIStackView(arrangedSubviews: [titleLabel, UIView(), valueLabel]) + header.axis = .horizontal + header.alignment = .center + header.spacing = 8 + + let stack = UIStackView(arrangedSubviews: [header, slider]) + stack.axis = .vertical + stack.spacing = 6 + stack.layoutMargins = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10) + stack.isLayoutMarginsRelativeArrangement = true + stack.translatesAutoresizingMaskIntoConstraints = false + addSubview(stack) + + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + stack.topAnchor.constraint(equalTo: topAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + @objc private func sliderChanged() { + let value = Double(slider.value) + valueLabel.text = String(format: "%.1f", value) + onValueChanged?(value) + } +} diff --git a/Sources/Plugins/Astronomy/StarMapResetButton.swift b/Sources/Plugins/Astronomy/StarMapResetButton.swift index 0adb1a0aea..0c49418bc4 100644 --- a/Sources/Plugins/Astronomy/StarMapResetButton.swift +++ b/Sources/Plugins/Astronomy/StarMapResetButton.swift @@ -8,10 +8,31 @@ import UIKit -final class StarMapResetButton: StarMapButton { - override func updateTheme() { - super.updateTheme() +final class StarMapResetButton: UIButton { + var active = false { + didSet { applyColors() } + } + var nightMode = false { + didSet { applyColors() } + } + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + private func setup() { + backgroundColor = .clear setImage(.icCustomRefresh, for: .normal) + imageView?.contentMode = .scaleAspectFit accessibilityLabel = localizedString("shared_string_reset") + applyColors() + } + private func applyColors() { + let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) + tintColor = color + setTitleColor(color, for: .normal) } } diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift b/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift index 3e2c085098..b1be6ceb48 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift @@ -8,13 +8,33 @@ import UIKit -final class StarMapTimeControlButton: StarMapButton { - override func updateTheme() { +final class StarMapTimeControlButton: UIButton { + var active = false { + didSet { applyColors() } + } + var nightMode = false { + didSet { applyColors() } + } + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + private func setup() { backgroundColor = .clear - layer.borderWidth = 0 - layer.cornerRadius = 0 - let foregroundColor: UIColor = active ? .white : StarMapControlTheme.activeForeground(nightMode: nightMode) - tintColor = foregroundColor - setTitleColor(foregroundColor, for: .normal) + adjustsImageWhenHighlighted = false + titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) + contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + setImage(AstroIcon.template("ic_action_time"), for: .normal) + imageView?.contentMode = .scaleAspectFit + applyColors() + } + private func applyColors() { + let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) + tintColor = color + setTitleColor(color, for: .normal) } } diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift new file mode 100644 index 0000000000..fcf84f60ea --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift @@ -0,0 +1,111 @@ +// +// StarMapTimeControlCard.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 08.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit + +final class StarMapTimeControlCard: UIView { + static let height: CGFloat = 48 + private static let cornerRadius: CGFloat = 24 + private static let smallButtonSize: CGFloat = 40 + + var onTimeButtonTapped: (() -> Void)? + var onResetTapped: (() -> Void)? + + private let timeButton = StarMapTimeControlButton() + private let resetButton = StarMapResetButton() + + override init(frame: CGRect) { + super.init(frame: frame) + setupContent() + updateTheme(nightMode: OADayNightHelper.instance().isNightMode(), active: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setTimeTitle(_ title: String) { + timeButton.setTitle(title, for: .normal) + } + + func setResetVisible(_ visible: Bool) { + resetButton.isHidden = !visible + } + + func updateTheme(nightMode: Bool, active: Bool) { + StarMapGlassBackground.apply( + to: self, + active: active, + nightMode: nightMode, + cornerRadius: Self.cornerRadius + ) + timeButton.nightMode = nightMode + timeButton.active = active + resetButton.nightMode = nightMode + resetButton.active = active + + backgroundColor = active + ? StarMapControlTheme.activeBackground(alpha: 0.5) + : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.5) + } + + override func layoutSubviews() { + super.layoutSubviews() + guard bounds.width > 0, bounds.height > 0 else { return } + if #available(iOS 26.0, *) { + subviews.compactMap { $0 as? UIVisualEffectView }.forEach { + $0.frame = bounds + $0.layer.cornerRadius = Self.cornerRadius + } + } + } + + private func setupContent() { + translatesAutoresizingMaskIntoConstraints = false + layer.cornerRadius = Self.cornerRadius + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.16 + layer.shadowRadius = 6 + layer.shadowOffset = CGSize(width: 0, height: 2) + + timeButton.setImage(AstroIcon.template("ic_action_time"), for: .normal) + timeButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) + timeButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + timeButton.addTarget(self, action: #selector(timeButtonTapped), for: .touchUpInside) + + resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside) + resetButton.isHidden = true + resetButton.widthAnchor.constraint(equalToConstant: Self.smallButtonSize).isActive = true + resetButton.heightAnchor.constraint(equalToConstant: Self.smallButtonSize).isActive = true + + let stack = UIStackView(arrangedSubviews: [timeButton, resetButton]) + stack.axis = .horizontal + stack.alignment = .center + stack.spacing = 4 + stack.layoutMargins = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6) + stack.isLayoutMarginsRelativeArrangement = true + stack.translatesAutoresizingMaskIntoConstraints = false + addSubview(stack) + + NSLayoutConstraint.activate([ + heightAnchor.constraint(equalToConstant: Self.height), + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + stack.topAnchor.constraint(equalTo: topAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + @objc private func timeButtonTapped() { + onTimeButtonTapped?() + } + + @objc private func resetButtonTapped() { + onResetTapped?() + } +} diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index 29e7c680f0..53adf2bd68 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -25,31 +25,20 @@ final class StarMapViewController: UIViewController, StarViewDelegate { static let maxMagnitude: Double = 7.0 static let leftPanelWidth: CGFloat = 393 } - private var settings: AstronomyPluginSettings - private let plugin: AstronomyPlugin - private let dataProvider: AstroDataProvider - private let viewModel: StarObjectsViewModel private let mainLayout = UIView() private let starView = StarView() private let regularMapContainer = UIView() private let mapControlsContainer = StarMapControlsContainer() private let timeSelectionView = DateTimeSelectionView() - private let timeControlCard = UIView() - private let timeControlButton = StarMapTimeControlButton() - private let resetTimeButton = StarMapResetButton() + private let timeControlCard = StarMapTimeControlCard() private let arModeButton = StarMapButton() private let cameraButton = StarMapButton() private let transparencySlider = UISlider() private let sliderContainer = UIView() private let resetFovButton = StarMapButton() - private let magnitudeFilterButton = UIControl() - private let magnitudeFilterIcon = UIImageView() - private let magnitudeFilterText = UILabel() - private let magnitudeSliderCard = UIView() - private let magnitudeSlider = UISlider() - private let magnitudeSliderTitle = UILabel() - private let magnitudeSliderValue = UILabel() + private let magnitudeFilterButton = StarMapMagnitudeFilterButton() + private let magnitudeSliderPanel = StarMapMagnitudeSliderPanel(maxMagnitude: Layout.maxMagnitude) private let closeButton = StarMapButton() private let settingsButton = StarMapButton() private let searchButton = StarMapButton() @@ -57,6 +46,11 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private let arModeHelper = StarMapARModeHelper() private let cameraHelper = StarMapCameraHelper() + + private let plugin: AstronomyPlugin + private let dataProvider: AstroDataProvider + private let viewModel: StarObjectsViewModel + private var settings: AstronomyPluginSettings private var autoTimeUpdateTimer: Timer? private var isTimeAutoUpdateEnabled = true @@ -288,37 +282,10 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func setupTimeControls() { - let nightMode = OADayNightHelper.instance().isNightMode() - timeControlCard.translatesAutoresizingMaskIntoConstraints = false - timeControlCard.backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 1) - timeControlCard.layer.cornerRadius = Layout.buttonSize / 2 - timeControlCard.layer.shadowColor = UIColor.black.cgColor - timeControlCard.layer.shadowOpacity = 0.16 - timeControlCard.layer.shadowRadius = 6 - timeControlCard.layer.shadowOffset = CGSize(width: 0, height: 2) + timeControlCard.onTimeButtonTapped = { [weak self] in self?.toggleTimeSelection() } + timeControlCard.onResetTapped = { [weak self] in self?.resetTimeButtonPressed() } mapControlsContainer.addSubview(timeControlCard) - let stack = UIStackView() - stack.axis = .horizontal - stack.alignment = .center - stack.spacing = 4 - stack.layoutMargins = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6) - stack.isLayoutMarginsRelativeArrangement = true - stack.translatesAutoresizingMaskIntoConstraints = false - timeControlCard.addSubview(stack) - - timeControlButton.setImage(AstroIcon.template("ic_action_time"), for: .normal) - timeControlButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold) - timeControlButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - timeControlButton.addTarget(self, action: #selector(toggleTimeSelection), for: .touchUpInside) - stack.addArrangedSubview(timeControlButton) - - resetTimeButton.addTarget(self, action: #selector(resetTimeButtonPressed), for: .touchUpInside) - resetTimeButton.isHidden = true - resetTimeButton.widthAnchor.constraint(equalToConstant: Layout.smallButtonSize).isActive = true - resetTimeButton.heightAnchor.constraint(equalToConstant: Layout.smallButtonSize).isActive = true - stack.addArrangedSubview(resetTimeButton) - timeSelectionView.translatesAutoresizingMaskIntoConstraints = false timeSelectionView.isHidden = true mapControlsContainer.addSubview(timeSelectionView) @@ -326,106 +293,32 @@ final class StarMapViewController: UIViewController, StarViewDelegate { NSLayoutConstraint.activate([ timeControlCard.centerXAnchor.constraint(equalTo: mapVisibleAreaGuide.centerXAnchor), timeControlCard.bottomAnchor.constraint(equalTo: mapControlsContainer.safeAreaLayoutGuide.bottomAnchor, constant: -Layout.contentPadding), - timeControlCard.heightAnchor.constraint(equalToConstant: Layout.buttonSize), - - stack.leadingAnchor.constraint(equalTo: timeControlCard.leadingAnchor), - stack.trailingAnchor.constraint(equalTo: timeControlCard.trailingAnchor), - stack.topAnchor.constraint(equalTo: timeControlCard.topAnchor), - stack.bottomAnchor.constraint(equalTo: timeControlCard.bottomAnchor), - timeSelectionView.centerXAnchor.constraint(equalTo: mapVisibleAreaGuide.centerXAnchor), timeSelectionView.bottomAnchor.constraint(equalTo: timeControlCard.topAnchor, constant: -Layout.contentPadding) ]) } private func setupMagnitudeControls() { - let nightMode = OADayNightHelper.instance().isNightMode() magnitudeFilterButton.translatesAutoresizingMaskIntoConstraints = false - magnitudeFilterButton.backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.92) - magnitudeFilterButton.layer.cornerRadius = Layout.buttonSize / 2 - magnitudeFilterButton.layer.shadowColor = UIColor.black.cgColor - magnitudeFilterButton.layer.shadowOpacity = 0.16 - magnitudeFilterButton.layer.shadowRadius = 6 - magnitudeFilterButton.layer.shadowOffset = CGSize(width: 0, height: 2) magnitudeFilterButton.addAction(UIAction { [weak self] _ in self?.toggleMagnitudeSlider() }, for: .touchUpInside) mapControlsContainer.addSubview(magnitudeFilterButton) - - let filterStack = UIStackView() - filterStack.axis = .vertical - filterStack.alignment = .center - filterStack.spacing = 4 - filterStack.isUserInteractionEnabled = false - filterStack.translatesAutoresizingMaskIntoConstraints = false - magnitudeFilterButton.addSubview(filterStack) - - magnitudeFilterIcon.image = .icCustomMagnitude - magnitudeFilterIcon.tintColor = StarMapControlTheme.foreground(active: false, nightMode: nightMode) - magnitudeFilterIcon.contentMode = .scaleAspectFit - magnitudeFilterIcon.widthAnchor.constraint(equalToConstant: 30).isActive = true - magnitudeFilterIcon.heightAnchor.constraint(equalToConstant: 30).isActive = true - filterStack.addArrangedSubview(magnitudeFilterIcon) - - magnitudeFilterText.textColor = StarMapControlTheme.foreground(active: false, nightMode: nightMode) - magnitudeFilterText.font = UIFont.systemFont(ofSize: 15, weight: .bold) - filterStack.addArrangedSubview(magnitudeFilterText) - - magnitudeSliderCard.translatesAutoresizingMaskIntoConstraints = false - magnitudeSliderCard.backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.94) - magnitudeSliderCard.layer.cornerRadius = Layout.contentPadding - magnitudeSliderCard.layer.shadowColor = UIColor.black.cgColor - magnitudeSliderCard.layer.shadowOpacity = 0.16 - magnitudeSliderCard.layer.shadowRadius = 6 - magnitudeSliderCard.layer.shadowOffset = CGSize(width: 0, height: 2) - magnitudeSliderCard.isHidden = true - mapControlsContainer.addSubview(magnitudeSliderCard) - - let sliderStack = UIStackView() - sliderStack.axis = .vertical - sliderStack.spacing = 6 - sliderStack.layoutMargins = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10) - sliderStack.isLayoutMarginsRelativeArrangement = true - sliderStack.translatesAutoresizingMaskIntoConstraints = false - magnitudeSliderCard.addSubview(sliderStack) - - let sliderHeader = UIStackView() - sliderHeader.axis = .horizontal - sliderHeader.alignment = .center - sliderHeader.spacing = 8 - magnitudeSliderTitle.text = localizedString("astro_min_magnitude") - magnitudeSliderTitle.textColor = StarMapControlTheme.textColor(nightMode: nightMode) - magnitudeSliderTitle.font = UIFont.systemFont(ofSize: 14) - sliderHeader.addArrangedSubview(magnitudeSliderTitle) - sliderHeader.addArrangedSubview(UIView()) - magnitudeSliderValue.textColor = StarMapControlTheme.activeBackground() - magnitudeSliderValue.font = UIFont.systemFont(ofSize: 14, weight: .semibold) - sliderHeader.addArrangedSubview(magnitudeSliderValue) - sliderStack.addArrangedSubview(sliderHeader) - - magnitudeSlider.minimumValue = -1 - magnitudeSlider.maximumValue = Float(Layout.maxMagnitude) - magnitudeSlider.addTarget(self, action: #selector(magnitudeChanged), for: .valueChanged) - sliderStack.addArrangedSubview(magnitudeSlider) - + + magnitudeSliderPanel.onValueChanged = { [weak self] value in + self?.applyMagnitudeFilter(value) + } + mapControlsContainer.addSubview(magnitudeSliderPanel) + NSLayoutConstraint.activate([ magnitudeFilterButton.widthAnchor.constraint(equalToConstant: Layout.buttonSize), magnitudeFilterButton.heightAnchor.constraint(equalToConstant: Layout.magnitudeButtonHeight), magnitudeFilterButton.centerXAnchor.constraint(equalTo: settingsButton.centerXAnchor), magnitudeFilterButton.bottomAnchor.constraint(equalTo: settingsButton.topAnchor, constant: -Layout.contentPadding), - - filterStack.centerXAnchor.constraint(equalTo: magnitudeFilterButton.centerXAnchor), - filterStack.centerYAnchor.constraint(equalTo: magnitudeFilterButton.centerYAnchor), - - magnitudeSliderCard.widthAnchor.constraint(equalToConstant: Layout.magnitudeSliderWidth), - magnitudeSliderCard.heightAnchor.constraint(equalTo: magnitudeFilterButton.heightAnchor), - magnitudeSliderCard.trailingAnchor.constraint(equalTo: magnitudeFilterButton.leadingAnchor, constant: -Layout.contentPadding), - magnitudeSliderCard.topAnchor.constraint(equalTo: magnitudeFilterButton.topAnchor), - - sliderStack.leadingAnchor.constraint(equalTo: magnitudeSliderCard.leadingAnchor), - sliderStack.trailingAnchor.constraint(equalTo: magnitudeSliderCard.trailingAnchor), - sliderStack.topAnchor.constraint(equalTo: magnitudeSliderCard.topAnchor), - sliderStack.bottomAnchor.constraint(equalTo: magnitudeSliderCard.bottomAnchor) + magnitudeSliderPanel.widthAnchor.constraint(equalToConstant: StarMapMagnitudeSliderPanel.preferredWidth), + magnitudeSliderPanel.heightAnchor.constraint(equalTo: magnitudeFilterButton.heightAnchor), + magnitudeSliderPanel.trailingAnchor.constraint(equalTo: magnitudeFilterButton.leadingAnchor, constant: -Layout.contentPadding), + magnitudeSliderPanel.topAnchor.constraint(equalTo: magnitudeFilterButton.topAnchor) ]) } @@ -716,7 +609,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func setTimeAutoUpdateEnabled(_ enabled: Bool) { isTimeAutoUpdateEnabled = enabled - resetTimeButton.isHidden = enabled + timeControlCard.setResetVisible(!enabled) if enabled { syncCurrentTimeForAutoUpdate(animate: true) } else { @@ -757,7 +650,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { formatter.dateStyle = .short formatter.timeStyle = .short } - timeControlButton.setTitle(formatter.string(from: date), for: .normal) + timeControlCard.setTimeTitle(formatter.string(from: date)) updateTimeControlTheme() } @@ -765,13 +658,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func updateTimeControlTheme() { let nightMode = OADayNightHelper.instance().isNightMode() let active = !timeSelectionView.isHidden - timeControlCard.backgroundColor = active - ? StarMapControlTheme.activeBackground() - : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 1) - timeControlButton.nightMode = nightMode - resetTimeButton.nightMode = nightMode - timeControlButton.active = active - resetTimeButton.active = active + timeControlCard.updateTheme(nightMode: nightMode, active: active) } private func updateMagnitudeControls() { @@ -779,24 +666,19 @@ final class StarMapViewController: UIViewController, StarViewDelegate { if starView.magnitudeFilter == nil || (starView.magnitudeFilter ?? 0) > Layout.maxMagnitude { starView.magnitudeFilter = Layout.maxMagnitude } - magnitudeSlider.value = Float(filterToUse) let text = String(format: "%.1f", filterToUse) - magnitudeFilterText.text = text - magnitudeSliderValue.text = text + magnitudeFilterButton.setValue(text) + magnitudeSliderPanel.setMagnitude(filterToUse) updateMagnitudeFilterTheme() } private func updateMagnitudeFilterTheme() { let nightMode = OADayNightHelper.instance().isNightMode() - let active = !magnitudeSliderCard.isHidden - magnitudeFilterButton.backgroundColor = active - ? StarMapControlTheme.activeBackground() - : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.92) - magnitudeFilterIcon.tintColor = StarMapControlTheme.foreground(active: active, nightMode: nightMode) - magnitudeFilterText.textColor = StarMapControlTheme.foreground(active: active, nightMode: nightMode) - magnitudeSliderCard.backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.94) - magnitudeSliderTitle.textColor = StarMapControlTheme.textColor(nightMode: nightMode) - magnitudeSliderValue.textColor = StarMapControlTheme.activeBackground() + + magnitudeFilterButton.nightMode = nightMode + magnitudeFilterButton.active = magnitudeSliderPanel.isExpanded + + magnitudeSliderPanel.updateTheme(nightMode: nightMode) } private func updateArModeUI(_ enabled: Bool) { @@ -837,8 +719,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { settingsButton.nightMode = nightMode searchButton.nightMode = nightMode compassButton.nightMode = nightMode - resetTimeButton.nightMode = nightMode - timeControlButton.nightMode = nightMode + magnitudeFilterButton.nightMode = nightMode + magnitudeSliderPanel.updateTheme(nightMode: nightMode) } private func updateRedMode(_ enabled: Bool) { @@ -850,7 +732,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { cameraButton, resetFovButton, magnitudeFilterButton, - magnitudeSliderCard, + magnitudeSliderPanel, compassButton, searchButton, closeButton, @@ -1492,17 +1374,17 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } @objc private func toggleMagnitudeSlider() { - magnitudeSliderCard.isHidden.toggle() + magnitudeSliderPanel.toggle() updateMagnitudeFilterTheme() } - @objc private func magnitudeChanged() { - starView.magnitudeFilter = Double(magnitudeSlider.value) + private func applyMagnitudeFilter(_ value: Double) { + starView.magnitudeFilter = value starView.showMagnitudeFilter = true settings.updateStarMapConfig { current in var config = current config.showMagnitudeFilter = true - config.magnitudeFilter = starView.magnitudeFilter + config.magnitudeFilter = value return config } updateMagnitudeControls() From 33044d8fbec6ba1b0d82438685db67c735568112 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Wed, 8 Jul 2026 15:49:25 +0300 Subject: [PATCH 03/20] Added Inertial scrolling & zoom buttons for mac --- .../Astronomy/StarMapViewController.swift | 53 +++++++++++ Sources/Plugins/Astronomy/StarView.swift | 88 +++++++++++++++++-- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index 53adf2bd68..ec971e25fb 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -24,6 +24,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { static let regularMapHeightFractionForPadLandscape: CGFloat = 0.3 static let maxMagnitude: Double = 7.0 static let leftPanelWidth: CGFloat = 393 + static let zoomButtonsBottomPadding: CGFloat = 34 } private let mainLayout = UIView() @@ -43,6 +44,9 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private let settingsButton = StarMapButton() private let searchButton = StarMapButton() private let compassButton = StarCompassButton() + private let zoomInButton = StarMapButton() + private let zoomOutButton = StarMapButton() + private let zoomButtonsVisible: Bool = OAUtilities.isiOSAppOnMac() private let arModeHelper = StarMapARModeHelper() private let cameraHelper = StarMapCameraHelper() @@ -227,6 +231,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { setupTimeControls() setupMagnitudeControls() setupCameraControls() + setupMacZoomControls() updateMapControlThemes() } @@ -359,6 +364,33 @@ final class StarMapViewController: UIViewController, StarViewDelegate { resetFovButton.bottomAnchor.constraint(lessThanOrEqualTo: mapControlsContainer.safeAreaLayoutGuide.bottomAnchor, constant: -Layout.contentPadding) ]) } + + private func setupMacZoomControls() { + guard zoomButtonsVisible else { return } + + addRoundButton(zoomInButton, + iconName: "ic_custom_map_zoom_in", + accessibilityLabel: localizedString("key_hint_zoom_in")) + zoomInButton.addTarget(self, action: #selector(zoomInPressed), for: .touchUpInside) + + addRoundButton(zoomOutButton, + iconName: "ic_custom_map_zoom_out", + accessibilityLabel: localizedString("key_hint_zoom_out")) + zoomOutButton.addTarget(self, action: #selector(zoomOutPressed), for: .touchUpInside) + + NSLayoutConstraint.activate([ + zoomOutButton.centerXAnchor.constraint(equalTo: settingsButton.centerXAnchor), + zoomOutButton.bottomAnchor.constraint(equalTo: magnitudeFilterButton.topAnchor, constant: -Layout.zoomButtonsBottomPadding), + + zoomInButton.centerXAnchor.constraint(equalTo: settingsButton.centerXAnchor), + zoomInButton.bottomAnchor.constraint(equalTo: zoomOutButton.topAnchor, constant: -Layout.contentPadding) + ]) + + starView.onViewAngleChangeListener = { [weak self] _ in + self?.updateZoomButtonsEnabled() + } + updateZoomButtonsEnabled() + } private func addRoundButton(_ button: StarMapButton, iconName: String? = nil, accessibilityLabel: String) { button.translatesAutoresizingMaskIntoConstraints = false @@ -721,6 +753,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { compassButton.nightMode = nightMode magnitudeFilterButton.nightMode = nightMode magnitudeSliderPanel.updateTheme(nightMode: nightMode) + zoomInButton.nightMode = nightMode + zoomOutButton.nightMode = nightMode } private func updateRedMode(_ enabled: Bool) { @@ -738,6 +772,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { closeButton, settingsButton, sliderContainer, + zoomInButton, + zoomOutButton, regularMapContainer) objectInfoController?.applyRedFilter(enabled: enabled) configureSheetController?.applyRedFilter(enabled: enabled) @@ -1390,6 +1426,23 @@ final class StarMapViewController: UIViewController, StarViewDelegate { updateMagnitudeControls() starView.setNeedsDisplay() } + + @objc private func zoomInPressed() { + starView.zoomIn() + updateZoomButtonsEnabled() + } + + @objc private func zoomOutPressed() { + starView.zoomOut() + updateZoomButtonsEnabled() + } + + private func updateZoomButtonsEnabled() { + guard zoomButtonsVisible else { return } + + zoomInButton.isEnabled = starView.canZoomIn() + zoomOutButton.isEnabled = starView.canZoomOut() + } } private final class StarMapControlsContainer: UIView { diff --git a/Sources/Plugins/Astronomy/StarView.swift b/Sources/Plugins/Astronomy/StarView.swift index 18ab054db0..d63352ae1c 100644 --- a/Sources/Plugins/Astronomy/StarView.swift +++ b/Sources/Plugins/Astronomy/StarView.swift @@ -274,6 +274,14 @@ final class StarView: UIView { private var lastEquGridTimeT = -1.0 private var lastEquGridLat = -999.0 private var lastEquGridLon = -999.0 + + private var inertialDisplayLink: CADisplayLink? + private var inertialVelocity: CGPoint = .zero + private var lastInertialTimestamp: CFTimeInterval = 0 + private let inertialDecelerationRate: CGFloat = 0.998 + private let inertialStopSpeed: CGFloat = 5 + private let inertialMinStartSpeed: CGFloat = 80 + private var didPanThisGesture = false var currentTime: Time { get { @@ -319,6 +327,7 @@ final class StarView: UIView { deinit { timeAnimationDisplayLink?.invalidate() focusAnimationDisplayLink?.invalidate() + inertialDisplayLink?.invalidate() } required init?(coder: NSCoder) { @@ -461,6 +470,8 @@ final class StarView: UIView { focusAnimationDisplayLink?.invalidate() focusAnimationDisplayLink = nil focusAnimation = nil + + cancelInertialScroll() } @objc private func handleFocusAnimationFrame(_ displayLink: CADisplayLink) { @@ -786,6 +797,14 @@ final class StarView: UIView { func zoomOut() { updateViewAngle(viewAngle * 1.5) } + + func canZoomIn() -> Bool { + viewAngle > ViewAngleBounds.min + 0.5 + } + + func canZoomOut() -> Bool { + viewAngle < maxViewAngle - 0.5 + } func project(object: SkyObject) -> CGPoint? { updateProjectionCache() @@ -1869,9 +1888,11 @@ final class StarView: UIView { let point = recognizer.location(in: self) switch recognizer.state { case .began: + cancelInertialScroll() cancelFocusAnimation() lastTouchPoint = point isPanning = false + didPanThisGesture = false case .changed: let dx = point.x - lastTouchPoint.x let dy = point.y - lastTouchPoint.y @@ -1879,22 +1900,71 @@ final class StarView: UIView { isPanning = true } else if hypot(dx, dy) > 0 || isPanning { isPanning = true - if settings.starMap.is2DMode { - panX += dx - panY += dy - } else { - let scale = viewAngle / Double(max(bounds.width, 1)) - centerAzimuth = normalizedDegrees(centerAzimuth - Double(dx) * scale) - centerAltitude = max(-90, min(90, centerAltitude + Double(dy) * scale)) - onAzimuthManualChangeListener?(centerAzimuth) - } + didPanThisGesture = true + applyPanDelta(dx: dx, dy: dy) lastTouchPoint = point setNeedsDisplay() } + case .ended, .cancelled: + if didPanThisGesture && !isCameraMode { + startInertialScroll(velocity: recognizer.velocity(in: self)) + } + isPanning = false + didPanThisGesture = false default: isPanning = false } } + + private func applyPanDelta(dx: CGFloat, dy: CGFloat) { + if settings.starMap.is2DMode { + panX += dx + panY += dy + } else { + let scale = viewAngle / Double(max(bounds.width, 1)) + centerAzimuth = normalizedDegrees(centerAzimuth - Double(dx) * scale) + centerAltitude = max(-90, min(90, centerAltitude + Double(dy) * scale)) + onAzimuthManualChangeListener?(centerAzimuth) + } + setNeedsDisplay() + } + + private func startInertialScroll(velocity: CGPoint) { + guard hypot(velocity.x, velocity.y) >= inertialMinStartSpeed else { return } + + cancelInertialScroll() + cancelFocusAnimation() + + inertialVelocity = velocity + lastInertialTimestamp = CACurrentMediaTime() + + let link = CADisplayLink(target: self, selector: #selector(handleInertialFrame(_:))) + inertialDisplayLink = link + link.add(to: .main, forMode: .common) + } + + private func cancelInertialScroll() { + inertialDisplayLink?.invalidate() + inertialDisplayLink = nil + inertialVelocity = .zero + } + + @objc private func handleInertialFrame(_ link: CADisplayLink) { + let now = link.timestamp + let dt = CGFloat(max(0, min(0.05, now - lastInertialTimestamp))) + lastInertialTimestamp = now + guard dt > 0 else { return } + + applyPanDelta(dx: inertialVelocity.x * dt, dy: inertialVelocity.y * dt) + + let decay = pow(inertialDecelerationRate, dt * 1000) + inertialVelocity.x *= decay + inertialVelocity.y *= decay + + if hypot(inertialVelocity.x, inertialVelocity.y) < inertialStopSpeed { + cancelInertialScroll() + } + } @objc private func handlePinch(_ recognizer: UIPinchGestureRecognizer) { guard recognizer.state == .changed || recognizer.state == .ended else { From c792bb0cd198c364514f3388b804680f760e3975 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Wed, 8 Jul 2026 18:32:12 +0300 Subject: [PATCH 04/20] Remove camera button and unify AR/camera behavior --- .../Plugins/Astronomy/StarCompassButton.swift | 15 +++++++ .../Astronomy/StarMapViewController.swift | 40 +++++++------------ 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Sources/Plugins/Astronomy/StarCompassButton.swift b/Sources/Plugins/Astronomy/StarCompassButton.swift index 8985698a31..96af3e77e8 100644 --- a/Sources/Plugins/Astronomy/StarCompassButton.swift +++ b/Sources/Plugins/Astronomy/StarCompassButton.swift @@ -12,6 +12,7 @@ final class StarCompassButton: StarMapButton { var onSingleTap: (() -> Void)? private let arrowView = UIImageView(image: AstroIcon.original("ic_custom_direction_compass")) private var currentRotation: CGFloat = 0 + private var arDirectionEnabled = false override init(frame: CGRect) { super.init(frame: frame) @@ -34,12 +35,26 @@ final class StarCompassButton: StarMapButton { arrowView.transform = transform } } + + func setArDirectionEnabled(_ enabled: Bool) { + arDirectionEnabled = enabled + updateArrowIcon() + } override func updateTheme() { super.updateTheme() setImage(nil, for: .normal) + updateArrowIcon() arrowView.transform = CGAffineTransform(rotationAngle: currentRotation * .pi / 180.0) } + + private func updateArrowIcon() { + let base = arDirectionEnabled + ? "ic_custom_direction_compass" + : "ic_custom_direction_manual" + let suffix = nightMode ? "_night" : "_day" + arrowView.image = AstroIcon.original(base + suffix) + } private func commonInit() { setImage(nil, for: .normal) diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index ec971e25fb..64bdbc4a43 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -34,7 +34,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private let timeSelectionView = DateTimeSelectionView() private let timeControlCard = StarMapTimeControlCard() private let arModeButton = StarMapButton() - private let cameraButton = StarMapButton() private let transparencySlider = UISlider() private let sliderContainer = UIView() private let resetFovButton = StarMapButton() @@ -248,10 +247,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { addRoundButton(arModeButton, iconName: "ic_custom_view_in_ar", accessibilityLabel: localizedString("astro_ar")) arModeButton.addTarget(self, action: #selector(toggleARMode), for: .touchUpInside) - addRoundButton(cameraButton, iconName: "ic_custom_device", accessibilityLabel: localizedString("astro_camera")) - cameraButton.addTarget(self, action: #selector(toggleCamera), for: .touchUpInside) - cameraButton.isHidden = true - addRoundButton(searchButton, iconName: "ic_custom_search", accessibilityLabel: localizedString("shared_string_search")) searchButton.addTarget(self, action: #selector(showSearchDialog), for: .touchUpInside) @@ -262,9 +257,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { arModeButton.centerXAnchor.constraint(equalTo: compassButton.centerXAnchor), arModeButton.topAnchor.constraint(equalTo: compassButton.bottomAnchor, constant: Layout.contentPadding), - cameraButton.centerXAnchor.constraint(equalTo: arModeButton.centerXAnchor), - cameraButton.topAnchor.constraint(equalTo: arModeButton.bottomAnchor, constant: Layout.contentPadding), - searchButton.centerXAnchor.constraint(equalTo: compassButton.centerXAnchor), searchButton.bottomAnchor.constraint(equalTo: mapControlsContainer.safeAreaLayoutGuide.bottomAnchor, constant: -Layout.contentPadding) ]) @@ -344,13 +336,13 @@ final class StarMapViewController: UIViewController, StarViewDelegate { resetFovButton.addTarget(self, action: #selector(resetFov), for: .touchUpInside) resetFovButton.isHidden = true - let cameraSliderTopConstraint = sliderContainer.topAnchor.constraint(equalTo: cameraButton.bottomAnchor, constant: Layout.contentPadding) + let cameraSliderTopConstraint = sliderContainer.topAnchor.constraint(equalTo: arModeButton.bottomAnchor, constant: Layout.contentPadding) cameraSliderTopConstraint.priority = .defaultHigh NSLayoutConstraint.activate([ sliderContainer.widthAnchor.constraint(equalToConstant: Layout.buttonSize), sliderContainer.heightAnchor.constraint(equalToConstant: Layout.transparencySliderHeight), - sliderContainer.centerXAnchor.constraint(equalTo: cameraButton.centerXAnchor), + sliderContainer.centerXAnchor.constraint(equalTo: arModeButton.centerXAnchor), cameraSliderTopConstraint, sliderContainer.topAnchor.constraint(greaterThanOrEqualTo: mapControlsContainer.safeAreaLayoutGuide.topAnchor, constant: Layout.contentPadding), @@ -385,10 +377,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { zoomInButton.centerXAnchor.constraint(equalTo: settingsButton.centerXAnchor), zoomInButton.bottomAnchor.constraint(equalTo: zoomOutButton.topAnchor, constant: -Layout.contentPadding) ]) - - starView.onViewAngleChangeListener = { [weak self] _ in - self?.updateZoomButtonsEnabled() - } updateZoomButtonsEnabled() } @@ -470,6 +458,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { compassButton.update(mapRotation: CGFloat(-azimuth)) } starView.onViewAngleChangeListener = { [weak self] fov in + self?.updateZoomButtonsEnabled() self?.cameraHelper.updateCameraZoom(fov: fov) } @@ -607,7 +596,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { if cameraHelper.isCameraOverlayEnabled { cameraHelper.toggleCameraOverlay() } - cameraButton.isHidden = true if arModeHelper.isArModeEnabled { arModeHelper.toggleArMode(enable: false) } @@ -618,7 +606,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { starView.setCenter(azimuth: previousAzimuth, altitude: previousAltitude) starView.setViewAngle(previousViewAngle) arModeButton.isHidden = false - cameraButton.isHidden = !arModeHelper.isArModeEnabled } starView.setNeedsDisplay() } @@ -714,8 +701,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func updateArModeUI(_ enabled: Bool) { + compassButton.setArDirectionEnabled(enabled) arModeButton.active = enabled - cameraButton.isHidden = !enabled || starView.is2DMode if enabled { starView.isCameraMode = true starView.setNeedsDisplay() @@ -730,7 +717,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func updateCameraUI(_ enabled: Bool) { - cameraButton.active = enabled sliderContainer.isHidden = !enabled resetFovButton.isHidden = !enabled starView.isCameraMode = enabled || arModeHelper.isRunning @@ -745,7 +731,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func updateButtonsNightMode() { let nightMode = OADayNightHelper.instance().isNightMode() arModeButton.nightMode = nightMode - cameraButton.nightMode = nightMode resetFovButton.nightMode = nightMode closeButton.nightMode = nightMode settingsButton.nightMode = nightMode @@ -763,7 +748,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { to: timeControlCard, timeSelectionView, arModeButton, - cameraButton, resetFovButton, magnitudeFilterButton, magnitudeSliderPanel, @@ -1394,11 +1378,17 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } @objc private func toggleARMode() { - arModeHelper.toggleArMode() - } - - @objc private func toggleCamera() { - cameraHelper.toggleCameraOverlay(in: view, below: starView) + if arModeHelper.isArModeEnabled { + if cameraHelper.isCameraOverlayEnabled { + cameraHelper.toggleCameraOverlay(in: mainLayout, below: starView) + } + arModeHelper.toggleArMode(enable: false) + } else { + arModeHelper.toggleArMode(enable: true) + if !cameraHelper.isCameraOverlayEnabled { + cameraHelper.toggleCameraOverlay(in: mainLayout, below: starView) + } + } } @objc private func transparencyChanged() { From 48653f1cab4ac0794906e93109fc201ac211044b Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Thu, 9 Jul 2026 17:24:48 +0300 Subject: [PATCH 05/20] Refactor AR controls into dedicated component --- OsmAnd.xcodeproj/project.pbxproj | 8 + .../Astronomy/AstronomyPluginSettings.swift | 49 +++- .../Astronomy/DateTimeSelectionView.swift | 50 ++-- .../Astronomy/StarMapARModeHelper.swift | 14 +- .../Astronomy/StarMapArControlCard.swift | 188 ++++++++++++ Sources/Plugins/Astronomy/StarMapButton.swift | 18 +- .../Astronomy/StarMapCameraHelper.swift | 57 +++- .../Astronomy/StarMapPlainIconButton.swift | 54 ++++ .../Astronomy/StarMapTimeControlButton.swift | 2 +- .../Astronomy/StarMapTimeControlCard.swift | 37 +-- .../Astronomy/StarMapViewController.swift | 276 ++++++++++-------- Sources/Plugins/Astronomy/StarView.swift | 128 ++++++-- 12 files changed, 682 insertions(+), 199 deletions(-) create mode 100644 Sources/Plugins/Astronomy/StarMapArControlCard.swift create mode 100644 Sources/Plugins/Astronomy/StarMapPlainIconButton.swift diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index f0c601c5cd..79a8b64841 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1724,6 +1724,8 @@ CE7AC1B22FE02A2600495411 /* SelectPointsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AC1A62FE02A2600495411 /* SelectPointsViewController.swift */; }; CE7CE0EE2FED645D001709F0 /* StarMapMyDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CE0ED2FED645D001709F0 /* StarMapMyDataViewController.swift */; }; CE7CE0F02FED8435001709F0 /* StarMapSearchExploreAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CE0EF2FED8435001709F0 /* StarMapSearchExploreAdapter.swift */; }; + CE8215E52FFF6376002A8617 /* StarMapArControlCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */; }; + CE8215E72FFF6AF9002A8617 /* StarMapPlainIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E62FFF6AF9002A8617 /* StarMapPlainIconButton.swift */; }; CE83DB4A2FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE83DB492FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift */; }; CE83DB4C2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE83DB4B2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift */; }; CE8A82A92FCFE11F00EADFD8 /* MapVariantReplacementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */; }; @@ -5772,6 +5774,8 @@ CE7AC1A62FE02A2600495411 /* SelectPointsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectPointsViewController.swift; sourceTree = ""; }; CE7CE0ED2FED645D001709F0 /* StarMapMyDataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMyDataViewController.swift; sourceTree = ""; }; CE7CE0EF2FED8435001709F0 /* StarMapSearchExploreAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchExploreAdapter.swift; sourceTree = ""; }; + CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapArControlCard.swift; sourceTree = ""; }; + CE8215E62FFF6AF9002A8617 /* StarMapPlainIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapPlainIconButton.swift; sourceTree = ""; }; CE83DB492FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchSortFilterChipsView.swift; sourceTree = ""; }; CE83DB4B2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchSortFilterChipsProvider.swift; sourceTree = ""; }; CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVariantReplacementManager.swift; sourceTree = ""; }; @@ -10658,6 +10662,7 @@ A5A501002F51000100000100 /* contextmenu */, A5A503202F51000200000320 /* search */, A5A500292F50000100000029 /* DateTimeSelectionView.swift */, + CE8215E62FFF6AF9002A8617 /* StarMapPlainIconButton.swift */, A5A5002B2F5000010000002B /* StarMapButton.swift */, A5A5002D2F5000010000002D /* StarMapResetButton.swift */, A5A5002F2F5000010000002F /* StarMapTimeControlButton.swift */, @@ -10665,6 +10670,7 @@ A5A500312F50000100000031 /* StarCompassButton.swift */, CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */, CE762AEF2FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift */, + CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */, CE762AF12FFE1197001BF551 /* StarMapGlassBackground.swift */, A5A5000F2F5000010000000F /* StarView.swift */, A5A500112F50000100000011 /* StarMapARModeHelper.swift */, @@ -18542,6 +18548,7 @@ DA5A840126C563A800F274C7 /* OAQuickSearchButtonListItem.mm in Sources */, FA327BFE2BB418F900CFADCF /* MissingMapsCalculator.mm in Sources */, DA5A854426C563A900F274C7 /* OADownloadsItem.mm in Sources */, + CE8215E52FFF6376002A8617 /* StarMapArControlCard.swift in Sources */, 326087132F3251B00038A34B /* PoiAdditionalUiRules.swift in Sources */, 27D33B512E7454EA00AD4F70 /* BaseMapScrollAction.swift in Sources */, FAB744112FB4672B0001FFF8 /* AlertPresenter.swift in Sources */, @@ -18701,6 +18708,7 @@ 320F71272A823FB20071C0E7 /* PopularArticles.swift in Sources */, DA5A850B26C563A900F274C7 /* OAButton.m in Sources */, DA5A81C126C563A700F274C7 /* OAPOIParser.m in Sources */, + CE8215E72FFF6AF9002A8617 /* StarMapPlainIconButton.swift in Sources */, D7B76D0D2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift in Sources */, FAF6C89B2AD013F400A3ED94 /* UIColor+Extension.swift in Sources */, DA5A824C26C563A700F274C7 /* OANavigationLanguageViewController.m in Sources */, diff --git a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift index 2b1caf2e3d..3d940fe3db 100644 --- a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift +++ b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift @@ -55,6 +55,15 @@ struct AstronomyPluginSettings { struct CommonConfig: Equatable { var showRegularMap = false } + + struct MapViewPosition: Equatable { + var azimuth: Double + var altitude: Double + var viewAngle: Double + var roll: Double = 0 + var panX: Double = 0 + var panY: Double = 0 + } struct StarMapConfig: Equatable { var showAzimuthalGrid = true @@ -84,6 +93,7 @@ struct AstronomyPluginSettings { var favorites: [FavoriteConfig] = [] var directions: [DirectionConfig] = [] var celestialPaths: [CelestialPathConfig] = [] + var savedMapPosition: MapViewPosition? = nil } static let storageKey = "astronomy_settings" @@ -119,6 +129,15 @@ struct AstronomyPluginSettings { private static let keyCelestialPaths = "celestialPaths" private static let keyId = "id" private static let keyColorIndex = "colorIndex" + + private static let keySavedMapPosition = "savedMapPosition" + private static let keyLastAzimuth = "lastAzimuth" + private static let keyLastAltitude = "lastAltitude" + private static let keyLastViewAngle = "lastViewAngle" + private static let keyLastRoll = "lastRoll" + private static let keyLastPanX = "lastPanX" + private static let keyLastPanY = "lastPanY" + private static let storageQueue = DispatchQueue(label: "net.osmand.astronomy.settings") var common = CommonConfig() @@ -309,7 +328,25 @@ struct AstronomyPluginSettings { return DirectionConfig(id: id, colorIndex: int(item[keyColorIndex], fallback: nextColor % DirectionColor.allCases.count)) }, - celestialPaths: parseItems(json?[keyCelestialPaths]) { CelestialPathConfig(id: $0) }) + celestialPaths: parseItems(json?[keyCelestialPaths]) { CelestialPathConfig(id: $0) }, + savedMapPosition: parseMapViewPosition(json?[keySavedMapPosition] as? [String: Any])) + } + + private static func parseMapViewPosition(_ json: [String: Any]?) -> MapViewPosition? { + guard let json, + let azimuth = double(json[keyLastAzimuth]), + let altitude = double(json[keyLastAltitude]), + let viewAngle = double(json[keyLastViewAngle]) else { + return nil + } + return MapViewPosition( + azimuth: azimuth, + altitude: altitude, + viewAngle: viewAngle, + roll: double(json[keyLastRoll]) ?? 0, + panX: double(json[keyLastPanX]) ?? 0, + panY: double(json[keyLastPanY]) ?? 0 + ) } private static func serializeStarMapConfig(_ config: StarMapConfig) -> [String: Any] { @@ -350,6 +387,16 @@ struct AstronomyPluginSettings { if !config.celestialPaths.isEmpty { json[keyCelestialPaths] = config.celestialPaths.map { [keyId: $0.id] } } + if let pos = config.savedMapPosition { + json[keySavedMapPosition] = [ + keyLastAzimuth: pos.azimuth, + keyLastAltitude: pos.altitude, + keyLastViewAngle: pos.viewAngle, + keyLastRoll: pos.roll, + keyLastPanX: pos.panX, + keyLastPanY: pos.panY + ] + } return json } diff --git a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift index 8ed094ebeb..ef191b16fc 100644 --- a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift +++ b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift @@ -16,10 +16,11 @@ final class DateTimeSelectionView: UIView { case hour case minute } + + private let calendar = Calendar.current private var currentDate = Date() private var onDateTimeChangeListener: ((Date) -> Void)? - private let calendar = Calendar.current private var labels: [Field: UILabel] = [:] override init(frame: CGRect) { @@ -31,11 +32,24 @@ final class DateTimeSelectionView: UIView { super.init(coder: coder) initViews() } + + func setOnDateTimeChangeListener(_ listener: @escaping (Date) -> Void) { + onDateTimeChangeListener = listener + } + + func setDateTime(_ date: Date) { + currentDate = date + updateDisplay() + } + + func getDateTime() -> Date { + currentDate + } private func initViews() { - backgroundColor = UIColor.black.withAlphaComponent(0.67) - layer.cornerRadius = 0 + layer.cornerRadius = 24 layer.shadowOpacity = 0 + clipsToBounds = true let stack = UIStackView() stack.axis = .horizontal @@ -52,6 +66,15 @@ final class DateTimeSelectionView: UIView { stack.setCustomSpacing(8, after: stack.arrangedSubviews[2]) addColumn(.hour, to: stack) addColumn(.minute, to: stack) + + StarMapGlassBackground.apply( + to: self, + active: false, + nightMode: OADayNightHelper.instance().isNightMode(), + cornerRadius: 24 + ) + + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: OADayNightHelper.instance().isNightMode(), alpha: 0.5) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor), @@ -69,7 +92,9 @@ final class DateTimeSelectionView: UIView { column.spacing = 2 let up = makeStepButton(iconName: "ic_custom_arrow_up") - up.addAction(UIAction { [weak self] _ in self?.step(field, amount: field == .minute ? 5 : 1) }, for: .touchUpInside) + up.addAction(UIAction { [weak self] _ in + self?.step(field, amount: field == .minute ? 5 : 1) + }, for: .touchUpInside) column.addArrangedSubview(up) let label = UILabel() @@ -81,7 +106,9 @@ final class DateTimeSelectionView: UIView { column.addArrangedSubview(label) let down = makeStepButton(iconName: "ic_custom_arrow_down") - down.addAction(UIAction { [weak self] _ in self?.step(field, amount: field == .minute ? -5 : -1) }, for: .touchUpInside) + down.addAction(UIAction { [weak self] _ in + self?.step(field, amount: field == .minute ? -5 : -1) + }, for: .touchUpInside) column.addArrangedSubview(down) parent.addArrangedSubview(column) @@ -125,17 +152,4 @@ final class DateTimeSelectionView: UIView { labels[.hour]?.text = String(format: "%02d", components.hour ?? 0) labels[.minute]?.text = String(format: "%02d", components.minute ?? 0) } - - func setOnDateTimeChangeListener(_ listener: @escaping (Date) -> Void) { - onDateTimeChangeListener = listener - } - - func setDateTime(_ date: Date) { - currentDate = date - updateDisplay() - } - - func getDateTime() -> Date { - currentDate - } } diff --git a/Sources/Plugins/Astronomy/StarMapARModeHelper.swift b/Sources/Plugins/Astronomy/StarMapARModeHelper.swift index 853531b141..ad6afa4abf 100644 --- a/Sources/Plugins/Astronomy/StarMapARModeHelper.swift +++ b/Sources/Plugins/Astronomy/StarMapARModeHelper.swift @@ -98,18 +98,20 @@ final class StarMapARModeHelper { toggleArMode(enable: !isArModeEnabled) } - func toggleArMode(enable: Bool) { + @discardableResult func toggleArMode(enable: Bool) -> Bool { guard isArModeEnabled != enable else { - return + return isArModeEnabled } - if enable { - isArModeEnabled = true - if startMotionUpdates() { - onArModeChanged?(true) + guard startMotionUpdates() else { + return false } + isArModeEnabled = true + onArModeChanged?(true) + return true } else { disableArMode() + return false } } diff --git a/Sources/Plugins/Astronomy/StarMapArControlCard.swift b/Sources/Plugins/Astronomy/StarMapArControlCard.swift new file mode 100644 index 0000000000..98aa1c7e98 --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapArControlCard.swift @@ -0,0 +1,188 @@ +// +// StarMapArControlCard.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 09.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit + +final class StarMapArControlCard: UIView { + static let width: CGFloat = 48 + private static let cornerRadius: CGFloat = 24 + private static let innerButtonSize: CGFloat = 40 + private static let sliderHeight: CGFloat = 150 + private static let stackSpacing: CGFloat = 4 + + var onArTapped: (() -> Void)? + var onResetTapped: (() -> Void)? + var onTransparencyChanged: ((Int) -> Void)? + + private let arButton = StarMapPlainIconButton() + private let resetButton = StarMapPlainIconButton() + private let transparencySlider = UISlider() + private let sliderContainer = UIView() + private let cameraControlsStack = UIStackView() + private let rootStack = UIStackView() + + private var nightMode = OADayNightHelper.instance().isNightMode() + private var arActive = false + + override init(frame: CGRect) { + super.init(frame: frame) + setupContent() + updateTheme(nightMode: nightMode, arActive: false) + setCameraControlsVisible(false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setArActive(_ active: Bool) { + arActive = active + arButton.active = active + updateTheme(nightMode: nightMode, arActive: active) + } + + func setCameraControlsVisible(_ visible: Bool) { + cameraControlsStack.isHidden = !visible + } + + func setTransparencyValue(_ value: Int) { + transparencySlider.value = Float(max(0, min(100, value))) + } + + func updateTheme(nightMode: Bool, arActive: Bool) { + self.nightMode = nightMode + self.arActive = arActive + + StarMapGlassBackground.apply( + to: self, + active: arActive, + nightMode: nightMode, + cornerRadius: Self.cornerRadius + ) + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.5) + + resetButton.nightMode = nightMode + resetButton.active = false + + updateArButtonAppearance() + } + + override func layoutSubviews() { + super.layoutSubviews() + guard bounds.width > 0, bounds.height > 0 else { return } + if #available(iOS 26.0, *) { + subviews.compactMap { $0 as? UIVisualEffectView }.forEach { + $0.frame = bounds + $0.layer.cornerRadius = Self.cornerRadius + } + } + } + + private func setupContent() { + translatesAutoresizingMaskIntoConstraints = false + layer.cornerRadius = Self.cornerRadius + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.16 + layer.shadowRadius = 6 + layer.shadowOffset = CGSize(width: 0, height: 2) + + configurePlainButton( + arButton, + iconName: "ic_custom_view_in_ar", + accessibilityLabel: localizedString("astro_ar"), + action: #selector(arButtonTapped) + ) + configurePlainButton( + resetButton, + iconName: "ic_custom_reset", + accessibilityLabel: localizedString("shared_string_reset"), + action: #selector(resetButtonTapped) + ) + + transparencySlider.minimumValue = 0 + transparencySlider.maximumValue = 100 + transparencySlider.value = Float(StarMapCameraHelper.defaultTransparency) + transparencySlider.transform = CGAffineTransform(rotationAngle: -.pi / 2) + transparencySlider.translatesAutoresizingMaskIntoConstraints = false + transparencySlider.addTarget(self, action: #selector(transparencyChanged), for: .valueChanged) + + sliderContainer.translatesAutoresizingMaskIntoConstraints = false + sliderContainer.addSubview(transparencySlider) + + cameraControlsStack.axis = .vertical + cameraControlsStack.alignment = .center + cameraControlsStack.spacing = 9 + cameraControlsStack.addArrangedSubview(sliderContainer) + cameraControlsStack.addArrangedSubview(resetButton) + + rootStack.axis = .vertical + rootStack.alignment = .center + rootStack.spacing = Self.stackSpacing + rootStack.layoutMargins = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) + rootStack.isLayoutMarginsRelativeArrangement = true + rootStack.addArrangedSubview(arButton) + rootStack.addArrangedSubview(cameraControlsStack) + rootStack.translatesAutoresizingMaskIntoConstraints = false + addSubview(rootStack) + + NSLayoutConstraint.activate([ + widthAnchor.constraint(equalToConstant: Self.width), + + rootStack.leadingAnchor.constraint(equalTo: leadingAnchor), + rootStack.trailingAnchor.constraint(equalTo: trailingAnchor), + rootStack.topAnchor.constraint(equalTo: topAnchor), + rootStack.bottomAnchor.constraint(equalTo: bottomAnchor), + + arButton.widthAnchor.constraint(equalToConstant: Self.innerButtonSize), + arButton.heightAnchor.constraint(equalToConstant: Self.innerButtonSize), + + sliderContainer.widthAnchor.constraint(equalToConstant: Self.width), + sliderContainer.heightAnchor.constraint(equalToConstant: Self.sliderHeight), + + transparencySlider.centerXAnchor.constraint(equalTo: sliderContainer.centerXAnchor), + transparencySlider.centerYAnchor.constraint(equalTo: sliderContainer.centerYAnchor), + transparencySlider.widthAnchor.constraint(equalToConstant: Self.sliderHeight), + transparencySlider.heightAnchor.constraint(equalToConstant: 40), + + resetButton.widthAnchor.constraint(equalToConstant: Self.innerButtonSize), + resetButton.heightAnchor.constraint(equalToConstant: Self.innerButtonSize) + ]) + + setContentCompressionResistancePriority(.defaultLow, for: .vertical) + setContentHuggingPriority(.defaultLow, for: .vertical) + } + + private func configurePlainButton( + _ button: StarMapPlainIconButton, + iconName: String, + accessibilityLabel: String, + action: Selector + ) { + button.setIcon(iconName, accessibilityLabel: accessibilityLabel) + button.addTarget(self, action: action, for: .touchUpInside) + } + + private func updateArButtonAppearance() { + let iconName = arActive ? "ic_custom_view_in_ar_filled" : "ic_custom_view_in_ar" + arButton.setIcon(iconName, accessibilityLabel: localizedString("astro_ar")) + arButton.nightMode = nightMode + arButton.active = arActive + } + + @objc private func arButtonTapped() { + onArTapped?() + } + + @objc private func resetButtonTapped() { + onResetTapped?() + } + + @objc private func transparencyChanged() { + onTransparencyChanged?(Int(transparencySlider.value)) + } +} diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index 579f1f9def..d0dc0bf36c 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -9,6 +9,10 @@ import UIKit enum StarMapControlTheme { + static var borderColor: UIColor { + UIColor(rgb: color_on_map_icon_border_color) + } + static func resolved(_ color: UIColor, nightMode: Bool) -> UIColor { nightMode ? color.dark : color.light } @@ -36,10 +40,6 @@ enum StarMapControlTheme { static func borderWidth(active: Bool, nightMode: Bool) -> CGFloat { active ? 0 : (nightMode ? 2 : 0) } - - static var borderColor: UIColor { - UIColor(rgb: color_on_map_icon_border_color) - } } @objcMembers @@ -132,6 +132,16 @@ class StarMapButton: OAHudButton { backgroundColor = .clear layer.borderWidth = 0 layer.shadowOpacity = 0 + + let color = active + ? StarMapControlTheme.activeForeground(nightMode: nightMode) + : StarMapControlTheme.foreground(active: false, nightMode: nightMode) + + tintColor = color + tintColorDay = color + tintColorNight = color + + updateColors(forPressedState: isHighlighted) } private func commonInit() { diff --git a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift index d5caab912b..fe14405406 100644 --- a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift +++ b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift @@ -11,21 +11,28 @@ import CoreMedia import UIKit final class StarMapCameraHelper { + static let defaultTransparency = 50 + + private let sessionQueue = DispatchQueue(label: "net.osmand.starMap.camera.session") + + var onUnavailable: ((_ message: String) -> Void)? + var onCameraStateChanged: ((_ enabled: Bool) -> Void)? + private var captureSession: AVCaptureSession? private var previewLayer: AVCaptureVideoPreviewLayer? - private weak var hostView: UIView? - private weak var siblingView: UIView? - private weak var starView: StarView? - private var requestedTransparency = 64 + + private var requestedTransparency = defaultTransparency private var rawCameraFov = 60.0 private var cameraAspectRatio = 4.0 / 3.0 private var lastPreviewBounds = CGRect.null private var lastVideoOrientation: AVCaptureVideoOrientation? - var onUnavailable: ((_ message: String) -> Void)? - var onCameraStateChanged: ((_ enabled: Bool) -> Void)? private(set) var isCameraOverlayEnabled = false private(set) var calculatedFov = 60.0 + + private weak var hostView: UIView? + private weak var siblingView: UIView? + private weak var starView: StarView? func bind(starView: StarView) { self.starView = starView @@ -37,7 +44,6 @@ final class StarMapCameraHelper { disableCameraOverlay(notify: true) onUnavailable?(localizedString("recording_camera_not_available")) } else { - starView?.isCameraMode = true starView?.setViewAngle(calculatedFov) updateCameraZoom(fov: calculatedFov) } @@ -94,6 +100,8 @@ final class StarMapCameraHelper { func resetFov() { starView?.setViewAngle(calculatedFov) + updateCameraZoom(fov: calculatedFov) + setTransparency(progress: Self.defaultTransparency) } func updateCameraZoom(fov: Double) { @@ -152,7 +160,6 @@ final class StarMapCameraHelper { return } isCameraOverlayEnabled = true - starView?.isCameraMode = true starView?.setViewAngle(calculatedFov) onCameraStateChanged?(true) } @@ -161,35 +168,42 @@ final class StarMapCameraHelper { let wasEnabled = isCameraOverlayEnabled isCameraOverlayEnabled = false closeCamera() - starView?.isCameraMode = false if notify && wasEnabled { onCameraStateChanged?(false) } } - + private func startPreview(in hostView: UIView, below siblingView: UIView) -> Bool { + assert(Thread.isMainThread) self.hostView = hostView self.siblingView = siblingView - closeCamera() - + + sessionQueue.sync { [weak self] in + guard let self, let session = self.captureSession, session.isRunning else { return } + session.stopRunning() + } + captureSession = nil + previewLayer?.removeFromSuperlayer() + previewLayer = nil do { guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { return false } let input = try AVCaptureDeviceInput(device: camera) let session = AVCaptureSession() + session.beginConfiguration() session.sessionPreset = .high guard session.canAddInput(input) else { + session.commitConfiguration() return false } session.addInput(input) - + session.commitConfiguration() let layer = AVCaptureVideoPreviewLayer(session: session) layer.videoGravity = .resizeAspectFill layer.opacity = Float(requestedTransparency) / 100.0 layer.frame = hostView.bounds hostView.layer.insertSublayer(layer, below: siblingView.layer) - let fovInfo = cameraFovInfo(camera) rawCameraFov = fovInfo.width cameraAspectRatio = fovInfo.aspectRatio @@ -199,7 +213,8 @@ final class StarMapCameraHelper { lastVideoOrientation = videoOrientation() updatePreviewOrientation(lastVideoOrientation) updateEffectiveFov() - DispatchQueue.global(qos: .userInitiated).async { + sessionQueue.async { [weak self] in + guard self?.captureSession === session else { return } session.startRunning() } return true @@ -209,12 +224,22 @@ final class StarMapCameraHelper { } private func closeCamera() { - captureSession?.stopRunning() + let session = captureSession captureSession = nil + previewLayer?.removeFromSuperlayer() previewLayer = nil + lastPreviewBounds = .null lastVideoOrientation = nil + + guard let session else { return } + + sessionQueue.async { + if session.isRunning { + session.stopRunning() + } + } } private func updateEffectiveFov() { diff --git a/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift b/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift new file mode 100644 index 0000000000..01f5dc84e1 --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift @@ -0,0 +1,54 @@ +// +// StarMapPlainIconButton.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 09.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit + +final class StarMapPlainIconButton: UIButton { + var active = false { + didSet { applyColors() } + } + var nightMode = false { + didSet { applyColors() } + } + + override var isHighlighted: Bool { + didSet { applyColors() } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func setIcon(_ iconName: String, accessibilityLabel: String) { + setImage(AstroIcon.template(iconName), for: .normal) + self.accessibilityLabel = accessibilityLabel + applyColors() + } + + private func setup() { + backgroundColor = .clear + imageView?.contentMode = .scaleAspectFit + applyColors() + } + + private func applyColors() { + let color: UIColor + if isHighlighted || active { + color = StarMapControlTheme.activeForeground(nightMode: nightMode) // mapButtonIconColorActive + } else { + color = StarMapControlTheme.foreground(active: false, nightMode: nightMode) + } + tintColor = color + } +} diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift b/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift index b1be6ceb48..1936bb3f6a 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift @@ -25,9 +25,9 @@ final class StarMapTimeControlButton: UIButton { } private func setup() { backgroundColor = .clear - adjustsImageWhenHighlighted = false titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8) setImage(AstroIcon.template("ic_action_time"), for: .normal) imageView?.contentMode = .scaleAspectFit applyColors() diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift index fcf84f60ea..c4259991b4 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift @@ -11,13 +11,13 @@ import UIKit final class StarMapTimeControlCard: UIView { static let height: CGFloat = 48 private static let cornerRadius: CGFloat = 24 - private static let smallButtonSize: CGFloat = 40 var onTimeButtonTapped: (() -> Void)? var onResetTapped: (() -> Void)? private let timeButton = StarMapTimeControlButton() private let resetButton = StarMapResetButton() + private let mainStack = UIStackView() override init(frame: CGRect) { super.init(frame: frame) @@ -35,6 +35,7 @@ final class StarMapTimeControlCard: UIView { func setResetVisible(_ visible: Bool) { resetButton.isHidden = !visible + mainStack.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: resetButton.isHidden ? 16 : 0) } func updateTheme(nightMode: Bool, active: Bool) { @@ -73,31 +74,31 @@ final class StarMapTimeControlCard: UIView { layer.shadowRadius = 6 layer.shadowOffset = CGSize(width: 0, height: 2) - timeButton.setImage(AstroIcon.template("ic_action_time"), for: .normal) timeButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) - timeButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) timeButton.addTarget(self, action: #selector(timeButtonTapped), for: .touchUpInside) + timeButton.heightAnchor.constraint(equalToConstant: Self.height).isActive = true resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside) resetButton.isHidden = true - resetButton.widthAnchor.constraint(equalToConstant: Self.smallButtonSize).isActive = true - resetButton.heightAnchor.constraint(equalToConstant: Self.smallButtonSize).isActive = true - - let stack = UIStackView(arrangedSubviews: [timeButton, resetButton]) - stack.axis = .horizontal - stack.alignment = .center - stack.spacing = 4 - stack.layoutMargins = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6) - stack.isLayoutMarginsRelativeArrangement = true - stack.translatesAutoresizingMaskIntoConstraints = false - addSubview(stack) + resetButton.widthAnchor.constraint(equalToConstant: Self.height).isActive = true + resetButton.heightAnchor.constraint(equalToConstant: Self.height).isActive = true + + mainStack.addArrangedSubview(timeButton) + mainStack.addArrangedSubview(resetButton) + mainStack.axis = .horizontal + mainStack.alignment = .center + mainStack.spacing = 0 + mainStack.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16) + mainStack.isLayoutMarginsRelativeArrangement = true + mainStack.translatesAutoresizingMaskIntoConstraints = false + addSubview(mainStack) NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: Self.height), - stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), - stack.topAnchor.constraint(equalTo: topAnchor), - stack.bottomAnchor.constraint(equalTo: bottomAnchor) + mainStack.leadingAnchor.constraint(equalTo: leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: trailingAnchor), + mainStack.topAnchor.constraint(equalTo: topAnchor), + mainStack.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index 64bdbc4a43..af5e2203c5 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -17,7 +17,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { static let smallButtonSize: CGFloat = 40 static let magnitudeButtonHeight: CGFloat = 76 static let magnitudeSliderWidth: CGFloat = 240 - static let transparencySliderHeight: CGFloat = 150 static let regularMapHeight: CGFloat = 300 static let regularMapHeightLandscape: CGFloat = 110 static let regularMapHeightFractionForPad: CGFloat = 0.33 @@ -33,10 +32,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private let mapControlsContainer = StarMapControlsContainer() private let timeSelectionView = DateTimeSelectionView() private let timeControlCard = StarMapTimeControlCard() - private let arModeButton = StarMapButton() - private let transparencySlider = UISlider() - private let sliderContainer = UIView() - private let resetFovButton = StarMapButton() + private let arControlCard = StarMapArControlCard() private let magnitudeFilterButton = StarMapMagnitudeFilterButton() private let magnitudeSliderPanel = StarMapMagnitudeSliderPanel(maxMagnitude: Layout.maxMagnitude) private let closeButton = StarMapButton() @@ -63,7 +59,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private var previousAltitude = 45.0 private var previousAzimuth = 0.0 private var previousViewAngle = 150.0 - private var manualAzimuth = true private var lastUpdatedAzimuth = -1.0 private var objectInfoController: AstroContextMenuViewController? private var objectInfoNavigationController: UINavigationController? @@ -80,6 +75,13 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private var leftPanelLeadingConstraint: NSLayoutConstraint? private let mapVisibleAreaGuide = UILayoutGuide() private var mapVisibleAreaLeadingConstraint: NSLayoutConstraint? + private var isApplyingControlChange = false + private var isGyroActive: Bool { + arModeHelper.isArModeEnabled + } + private var isArCameraActive: Bool { + cameraHelper.isCameraOverlayEnabled + } private var mapControlsLeadingInset: CGFloat { embeddedLeftPanelNavigationController != nil && OAUtilities.isIPad() @@ -131,8 +133,15 @@ final class StarMapViewController: UIViewController, StarViewDelegate { setupHelpers() setupListeners() applySettings(settings.starMap) + if let pos = settings.starMap.savedMapPosition { + starView.restoreMapPosition(pos) + compassButton.update(mapRotation: CGFloat(-pos.azimuth)) + lastUpdatedAzimuth = pos.azimuth + updateStarMap() + } else { + updateStarMap(updateAzimuth: true) + } updateRegularMapVisibility(settings.common.showRegularMap) - updateStarMap(updateAzimuth: true) viewModel.onDataChanged = { [weak self] in self?.syncObjectsToStarView() @@ -150,9 +159,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } arModeHelper.onResume() cameraHelper.onResume() - if arModeHelper.isArModeEnabled { - updateArModeUI(true) - } + syncControlUI() if isTimeAutoUpdateEnabled { syncCurrentTimeForAutoUpdate(animate: true) } @@ -165,6 +172,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { arModeHelper.onPause() cameraHelper.onPause() starView.isCameraMode = false + starView.isGyroTrackingEnabled = false restoreRegularMapIfNeeded(refresh: true) } @@ -229,7 +237,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { setupRightControls() setupTimeControls() setupMagnitudeControls() - setupCameraControls() setupMacZoomControls() updateMapControlThemes() } @@ -242,23 +249,36 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func setupCompassAndLeftControls() { addRoundButton(compassButton, accessibilityLabel: localizedString("map_widget_compass")) - compassButton.onSingleTap = { [weak self] in self?.setAzimuth(0, animate: true) } - - addRoundButton(arModeButton, iconName: "ic_custom_view_in_ar", accessibilityLabel: localizedString("astro_ar")) - arModeButton.addTarget(self, action: #selector(toggleARMode), for: .touchUpInside) + compassButton.onSingleTap = { [weak self] in + self?.toggleGyroMode() + } addRoundButton(searchButton, iconName: "ic_custom_search", accessibilityLabel: localizedString("shared_string_search")) searchButton.addTarget(self, action: #selector(showSearchDialog), for: .touchUpInside) + + mapControlsContainer.addSubview(arControlCard) + arControlCard.onArTapped = { [weak self] in + self?.toggleARMode() + } + arControlCard.onResetTapped = { [weak self] in + self?.resetFov() + } + arControlCard.onTransparencyChanged = { [weak self] progress in + self?.cameraHelper.setTransparency(progress: progress) + } + searchButton.setContentCompressionResistancePriority(.required, for: .vertical) + compassButton.setContentCompressionResistancePriority(.required, for: .vertical) NSLayoutConstraint.activate([ compassButton.leadingAnchor.constraint(equalTo: mapVisibleAreaGuide.leadingAnchor, constant: Layout.contentPadding), compassButton.topAnchor.constraint(equalTo: mapControlsContainer.safeAreaLayoutGuide.topAnchor, constant: Layout.contentPadding), - arModeButton.centerXAnchor.constraint(equalTo: compassButton.centerXAnchor), - arModeButton.topAnchor.constraint(equalTo: compassButton.bottomAnchor, constant: Layout.contentPadding), - searchButton.centerXAnchor.constraint(equalTo: compassButton.centerXAnchor), - searchButton.bottomAnchor.constraint(equalTo: mapControlsContainer.safeAreaLayoutGuide.bottomAnchor, constant: -Layout.contentPadding) + searchButton.bottomAnchor.constraint(equalTo: mapControlsContainer.safeAreaLayoutGuide.bottomAnchor, constant: -Layout.contentPadding), + + arControlCard.centerXAnchor.constraint(equalTo: compassButton.centerXAnchor), + arControlCard.topAnchor.constraint(equalTo: compassButton.bottomAnchor, constant: Layout.contentPadding), + arControlCard.bottomAnchor.constraint(lessThanOrEqualTo: searchButton.topAnchor, constant: -Layout.contentPadding) ]) } @@ -279,8 +299,12 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func setupTimeControls() { - timeControlCard.onTimeButtonTapped = { [weak self] in self?.toggleTimeSelection() } - timeControlCard.onResetTapped = { [weak self] in self?.resetTimeButtonPressed() } + timeControlCard.onTimeButtonTapped = { [weak self] in + self?.toggleTimeSelection() + } + timeControlCard.onResetTapped = { [weak self] in + self?.resetTimeButtonPressed() + } mapControlsContainer.addSubview(timeControlCard) timeSelectionView.translatesAutoresizingMaskIntoConstraints = false @@ -318,44 +342,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { magnitudeSliderPanel.topAnchor.constraint(equalTo: magnitudeFilterButton.topAnchor) ]) } - - private func setupCameraControls() { - sliderContainer.translatesAutoresizingMaskIntoConstraints = false - sliderContainer.isHidden = true - mapControlsContainer.addSubview(sliderContainer) - - transparencySlider.minimumValue = 0 - transparencySlider.maximumValue = 100 - transparencySlider.value = 50 - transparencySlider.transform = CGAffineTransform(rotationAngle: -.pi / 2) - transparencySlider.translatesAutoresizingMaskIntoConstraints = false - transparencySlider.addTarget(self, action: #selector(transparencyChanged), for: .valueChanged) - sliderContainer.addSubview(transparencySlider) - - addRoundButton(resetFovButton, iconName: "ic_custom_reset", accessibilityLabel: localizedString("shared_string_reset")) - resetFovButton.addTarget(self, action: #selector(resetFov), for: .touchUpInside) - resetFovButton.isHidden = true - - let cameraSliderTopConstraint = sliderContainer.topAnchor.constraint(equalTo: arModeButton.bottomAnchor, constant: Layout.contentPadding) - cameraSliderTopConstraint.priority = .defaultHigh - - NSLayoutConstraint.activate([ - sliderContainer.widthAnchor.constraint(equalToConstant: Layout.buttonSize), - sliderContainer.heightAnchor.constraint(equalToConstant: Layout.transparencySliderHeight), - sliderContainer.centerXAnchor.constraint(equalTo: arModeButton.centerXAnchor), - cameraSliderTopConstraint, - sliderContainer.topAnchor.constraint(greaterThanOrEqualTo: mapControlsContainer.safeAreaLayoutGuide.topAnchor, constant: Layout.contentPadding), - - transparencySlider.centerXAnchor.constraint(equalTo: sliderContainer.centerXAnchor), - transparencySlider.centerYAnchor.constraint(equalTo: sliderContainer.centerYAnchor), - transparencySlider.widthAnchor.constraint(equalToConstant: Layout.transparencySliderHeight), - transparencySlider.heightAnchor.constraint(equalToConstant: 40), - - resetFovButton.centerXAnchor.constraint(equalTo: sliderContainer.centerXAnchor), - resetFovButton.topAnchor.constraint(equalTo: sliderContainer.bottomAnchor, constant: 8), - resetFovButton.bottomAnchor.constraint(lessThanOrEqualTo: mapControlsContainer.safeAreaLayoutGuide.bottomAnchor, constant: -Layout.contentPadding) - ]) - } private func setupMacZoomControls() { guard zoomButtonsVisible else { return } @@ -403,26 +389,25 @@ final class StarMapViewController: UIViewController, StarViewDelegate { guard let self else { return } - updateArModeUI(enabled) if !enabled { - manualAzimuth = true + starView.roll = 0 } + guard !isApplyingControlChange else { return } + syncControlUI() } arModeHelper.onUnavailable = { [weak self] in + self?.disableArExperience(force: true) self?.showMessage(localizedString("astro_ar_unavailable")) } cameraHelper.onUnavailable = { [weak self] message in - self?.updateCameraUI(false) + self?.disableArExperience(force: true) self?.showMessage(message) } - cameraHelper.onCameraStateChanged = { [weak self] enabled in - guard let self else { + cameraHelper.onCameraStateChanged = { [weak self] _ in + guard let self, !isApplyingControlChange else { return } - updateCameraUI(enabled) - if enabled && !arModeHelper.isArModeEnabled { - arModeHelper.toggleArMode(enable: true) - } + syncControlUI() } } @@ -448,13 +433,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } } starView.onAzimuthManualChangeListener = { [weak self] azimuth in - guard let self, !cameraHelper.isCameraOverlayEnabled else { - return - } - if arModeHelper.isArModeEnabled { - arModeHelper.toggleArMode(enable: false) - } - manualAzimuth = true + guard let self else { return } + compassButton.update(mapRotation: CGFloat(-azimuth)) } starView.onViewAngleChangeListener = { [weak self] fov in @@ -546,6 +526,14 @@ final class StarMapViewController: UIViewController, StarViewDelegate { config.is2DMode = starView.is2DMode config.showMagnitudeFilter = starView.showMagnitudeFilter || starView.magnitudeFilter != nil config.magnitudeFilter = starView.magnitudeFilter + config.savedMapPosition = AstronomyPluginSettings.MapViewPosition( + azimuth: starView.getAzimuth(), + altitude: starView.getAltitude(), + viewAngle: starView.getViewAngle(), + roll: starView.roll, + panX: Double(starView.panX), + panY: Double(starView.panY) + ) return config } viewModel.updateSettings(settings) @@ -557,6 +545,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { updated.favorites = current.favorites updated.directions = current.directions updated.celestialPaths = current.celestialPaths + updated.savedMapPosition = config.savedMapPosition ?? current.savedMapPosition return updated } applySettings(updatedConfig) @@ -593,19 +582,13 @@ final class StarMapViewController: UIViewController, StarViewDelegate { previousViewAngle = starView.getViewAngle() starView.is2DMode = true starView.setCenter(azimuth: 180, altitude: 90) - if cameraHelper.isCameraOverlayEnabled { - cameraHelper.toggleCameraOverlay() - } - if arModeHelper.isArModeEnabled { - arModeHelper.toggleArMode(enable: false) - } - arModeButton.isHidden = true - manualAzimuth = true + setArExperienceEnabled(false) + arControlCard.isHidden = true } else { starView.is2DMode = false starView.setCenter(azimuth: previousAzimuth, altitude: previousAltitude) starView.setViewAngle(previousViewAngle) - arModeButton.isHidden = false + arControlCard.isHidden = false } starView.setNeedsDisplay() } @@ -700,26 +683,70 @@ final class StarMapViewController: UIViewController, StarViewDelegate { magnitudeSliderPanel.updateTheme(nightMode: nightMode) } - private func updateArModeUI(_ enabled: Bool) { - compassButton.setArDirectionEnabled(enabled) - arModeButton.active = enabled + private func setArExperienceEnabled(_ enabled: Bool) { if enabled { - starView.isCameraMode = true - starView.setNeedsDisplay() + enableArExperience() } else { - starView.roll = 0 - starView.setNeedsDisplay() - if cameraHelper.isCameraOverlayEnabled { - cameraHelper.toggleCameraOverlay() - } - updateCameraUI(false) + disableArExperience() + } + } + + private func enableArExperience() { + guard !starView.is2DMode else { return } + guard !isArCameraActive else { + syncControlUI() + return + } + guard !isApplyingControlChange else { return } + isApplyingControlChange = true + defer { + isApplyingControlChange = false + syncControlUI() + } + + guard arModeHelper.toggleArMode(enable: true) else { return } + + if !cameraHelper.isCameraOverlayEnabled { + cameraHelper.toggleCameraOverlay(in: mainLayout, below: starView) } } - private func updateCameraUI(_ enabled: Bool) { - sliderContainer.isHidden = !enabled - resetFovButton.isHidden = !enabled - starView.isCameraMode = enabled || arModeHelper.isRunning + private func disableArExperience(force: Bool = false) { + guard force || isGyroActive || isArCameraActive else { + syncControlUI() + return + } + guard force || !isApplyingControlChange else { return } + isApplyingControlChange = true + defer { + isApplyingControlChange = false + syncControlUI() + } + if cameraHelper.isCameraOverlayEnabled { + cameraHelper.toggleCameraOverlay(in: mainLayout, below: starView) + } + if arModeHelper.isArModeEnabled { + arModeHelper.toggleArMode(enable: false) + } + starView.roll = 0 + } + + private func syncControlUI() { + let gyroEnabled = isGyroActive + let cameraEnabled = isArCameraActive + let nightMode = OADayNightHelper.instance().isNightMode() + + compassButton.setArDirectionEnabled(gyroEnabled) + + arControlCard.setArActive(cameraEnabled) + arControlCard.updateTheme(nightMode: nightMode, arActive: cameraEnabled) + arControlCard.setCameraControlsVisible(cameraEnabled) + starView.isCameraMode = cameraEnabled + starView.isGyroTrackingEnabled = gyroEnabled + if !gyroEnabled { + starView.roll = 0 + } + starView.setNeedsDisplay() } private func updateMapControlThemes() { @@ -730,8 +757,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func updateButtonsNightMode() { let nightMode = OADayNightHelper.instance().isNightMode() - arModeButton.nightMode = nightMode - resetFovButton.nightMode = nightMode + arControlCard.updateTheme(nightMode: nightMode, arActive: isArCameraActive) closeButton.nightMode = nightMode settingsButton.nightMode = nightMode searchButton.nightMode = nightMode @@ -747,15 +773,13 @@ final class StarMapViewController: UIViewController, StarViewDelegate { AstroRedFilter.apply(enabled, to: timeControlCard, timeSelectionView, - arModeButton, - resetFovButton, magnitudeFilterButton, magnitudeSliderPanel, compassButton, searchButton, closeButton, settingsButton, - sliderContainer, + arControlCard, zoomInButton, zoomOutButton, regularMapContainer) @@ -1128,7 +1152,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func handleSearchObjectSelected(_ obj: SkyObject) { - manualAzimuth = true selectedObject = obj starView.setSelectedObject(obj) showObjectInfo(obj, centerInVisibleMapOnPresentation: true) @@ -1371,6 +1394,9 @@ final class StarMapViewController: UIViewController, StarViewDelegate { @objc private func toggleTimeSelection() { timeSelectionView.isHidden.toggle() updateTimeControlTheme() + if !timeSelectionView.isHidden && !magnitudeSliderPanel.isHidden { + toggleMagnitudeSlider() + } } @objc private func resetTimeButtonPressed() { @@ -1378,30 +1404,44 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } @objc private func toggleARMode() { - if arModeHelper.isArModeEnabled { - if cameraHelper.isCameraOverlayEnabled { - cameraHelper.toggleCameraOverlay(in: mainLayout, below: starView) - } - arModeHelper.toggleArMode(enable: false) - } else { + setArExperienceEnabled(!isArCameraActive) + } + + @objc private func toggleGyroMode() { + setGyroEnabled(!isGyroActive) + } + + private func setGyroEnabled(_ enabled: Bool) { + guard !starView.is2DMode else { return } + guard isGyroActive != enabled else { + syncControlUI() + return + } + isApplyingControlChange = true + defer { + isApplyingControlChange = false + syncControlUI() + } + if enabled { arModeHelper.toggleArMode(enable: true) - if !cameraHelper.isCameraOverlayEnabled { - cameraHelper.toggleCameraOverlay(in: mainLayout, below: starView) - } + } else { + arModeHelper.toggleArMode(enable: false) + starView.roll = 0 } } - @objc private func transparencyChanged() { - cameraHelper.setTransparency(progress: Int(transparencySlider.value)) - } - @objc private func resetFov() { cameraHelper.resetFov() + arControlCard.setTransparencyValue(StarMapCameraHelper.defaultTransparency) } @objc private func toggleMagnitudeSlider() { magnitudeSliderPanel.toggle() updateMagnitudeFilterTheme() + + if !timeSelectionView.isHidden && !magnitudeSliderPanel.isHidden { + toggleTimeSelection() + } } private func applyMagnitudeFilter(_ value: Double) { diff --git a/Sources/Plugins/Astronomy/StarView.swift b/Sources/Plugins/Astronomy/StarView.swift index d63352ae1c..aaa69d14b4 100644 --- a/Sources/Plugins/Astronomy/StarView.swift +++ b/Sources/Plugins/Astronomy/StarView.swift @@ -39,6 +39,8 @@ final class StarView: UIView { let targetAzimuth: Double let startAltitude: Double let targetAltitude: Double + let startRoll: Double + let targetRoll: Double let startViewAngle: Double let targetViewAngle: Double let startPan: CGPoint @@ -76,6 +78,7 @@ final class StarView: UIView { } var isCameraMode = false + var isGyroTrackingEnabled = false var onAnimationFinished: (() -> Void)? var onAzimuthManualChangeListener: ((Double) -> Void)? var onViewAngleChangeListener: ((Double) -> Void)? @@ -205,8 +208,8 @@ final class StarView: UIView { set { settings.starMap.is2DMode = newValue; setNeedsDisplay() } } - private var panX: CGFloat = 0 - private var panY: CGFloat = 0 + private(set) var panX: CGFloat = 0 + private(set) var panY: CGFloat = 0 private var lastTouchPoint = CGPoint.zero private var isPanning = false @@ -282,6 +285,13 @@ final class StarView: UIView { private let inertialStopSpeed: CGFloat = 5 private let inertialMinStartSpeed: CGFloat = 80 private var didPanThisGesture = false + + private var gyroTargetAzimuth = 0.0 + private var gyroTargetAltitude = 45.0 + private var gyroTargetRoll = 0.0 + private var isGyroPanOverride = false + private var gyroSnapBackWorkItem: DispatchWorkItem? + private let gyroSnapBackDelay: TimeInterval = 0.5 var currentTime: Time { get { @@ -344,6 +354,16 @@ final class StarView: UIView { addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))) } + + func restoreMapPosition(_ position: AstronomyPluginSettings.MapViewPosition) { + setCenter(azimuth: position.azimuth, altitude: position.altitude) + setViewAngle(position.viewAngle) + roll = position.roll + panX = CGFloat(position.panX) + panY = CGFloat(position.panY) + notifyAzimuthChanged() + setNeedsDisplay() + } func resetView() { cancelFocusAnimation() @@ -369,9 +389,16 @@ final class StarView: UIView { } func setCameraOrientation(azimuth: Double, altitude: Double, roll: Double) { + gyroTargetAzimuth = azimuth + gyroTargetAltitude = altitude + gyroTargetRoll = roll + + guard !isGyroPanOverride else { return } + setCenter(azimuth: azimuth, altitude: altitude) self.roll = roll setNeedsDisplay() + notifyAzimuthChanged() } func setCenter(azimuth: Double, altitude: Double, animate: Bool = false) { @@ -450,12 +477,15 @@ final class StarView: UIView { private func animateTo(azimuth: Double, altitude: Double, targetViewAngle: Double? = nil, - targetPan: CGPoint? = nil) { + targetPan: CGPoint? = nil, + targetRoll: Double? = nil) { cancelFocusAnimation() focusAnimation = FocusAnimation(startAzimuth: centerAzimuth, targetAzimuth: normalizedDegrees(azimuth), startAltitude: centerAltitude, targetAltitude: max(-90, min(90, altitude)), + startRoll: roll, + targetRoll: targetRoll ?? roll, startViewAngle: viewAngle, targetViewAngle: targetViewAngle.map { boundedViewAngle($0) } ?? viewAngle, startPan: CGPoint(x: panX, y: panY), @@ -473,6 +503,30 @@ final class StarView: UIView { cancelInertialScroll() } + + private func cancelGyroSnapBack() { + gyroSnapBackWorkItem?.cancel() + gyroSnapBackWorkItem = nil + cancelFocusAnimation() + } + + private func scheduleGyroSnapBack() { + cancelGyroSnapBack() + isGyroPanOverride = true + + let work = DispatchWorkItem { [weak self] in + self?.animateSnapBackToGyro() + } + gyroSnapBackWorkItem = work + DispatchQueue.main.asyncAfter(deadline: .now() + gyroSnapBackDelay, execute: work) + } + + private func animateSnapBackToGyro() { + gyroSnapBackWorkItem = nil + animateTo(azimuth: gyroTargetAzimuth, + altitude: gyroTargetAltitude, + targetRoll: gyroTargetRoll) + } @objc private func handleFocusAnimationFrame(_ displayLink: CADisplayLink) { guard let animation = focusAnimation else { @@ -485,6 +539,7 @@ final class StarView: UIView { applyFocusAnimation(animation, fraction: fraction) if rawFraction >= 1 { applyFocusAnimation(animation, fraction: 1) + isGyroPanOverride = false cancelFocusAnimation() onAnimationFinished?() } @@ -493,6 +548,7 @@ final class StarView: UIView { private func applyFocusAnimation(_ animation: FocusAnimation, fraction: Double) { centerAzimuth = interpolateAngle(start: animation.startAzimuth, end: animation.targetAzimuth, fraction: fraction) centerAltitude = animation.startAltitude + (animation.targetAltitude - animation.startAltitude) * fraction + roll = animation.startRoll + (animation.targetRoll - animation.startRoll) * fraction viewAngle = animation.startViewAngle + (animation.targetViewAngle - animation.startViewAngle) * fraction if let targetPan = animation.targetPan { let cgFraction = CGFloat(fraction) @@ -503,6 +559,7 @@ final class StarView: UIView { onViewAngleChangeListener?(viewAngle) } setNeedsDisplay() + notifyAzimuthChanged() } func getAltitude() -> Double { @@ -528,7 +585,7 @@ final class StarView: UIView { centerAzimuth = normalizedDegrees(azimuth) setNeedsDisplay() } - onAzimuthManualChangeListener?(centerAzimuth) + notifyAzimuthChanged() } func setSkyObjects(_ objects: [SkyObject]) { @@ -850,6 +907,10 @@ final class StarView: UIView { context.restoreGState() } + + private func notifyAzimuthChanged() { + onAzimuthManualChangeListener?(centerAzimuth) + } private func rebuildObjectMap() { skyObjectMap.removeAll(keepingCapacity: true) @@ -922,7 +983,7 @@ final class StarView: UIView { .foregroundColor: labelColor(for: object) ] let size = text.size(withAttributes: attributes) - let origin = CGPoint(x: point.x + radius + pixelsToPoints(5), y: point.y - size.height * 0.75) + let origin = CGPoint(x: point.x + radius + 6, y: point.y - size.height * 0.5) let labelPadding = pixelsToPoints(5) let labelRect = CGRect(origin: origin, size: size).insetBy(dx: -labelPadding, dy: -labelPadding) let overlaps = occupiedRects.contains { $0.intersects(labelRect) } @@ -1014,13 +1075,16 @@ final class StarView: UIView { } let path = CGMutablePath() + var altitudeLabels: [(String, Double, Double)] = [] + var azimuthLabels: [(String, Double)] = [] + for altitude in stride(from: -80, through: 80, by: density.altStep) { appendSkyLine(to: path, range: stride(from: 0.0, through: 360.0, by: Double(density.lineStep))) { azimuth in (azimuth, Double(altitude)) } if altitude != 0 { - drawGridLabel("\(altitude)°", azimuth: centerAzimuth, altitude: Double(altitude), align: .left, color: UIColor(white: 0.55, alpha: 1), in: context) - drawGridLabel("\(altitude)°", azimuth: centerAzimuth + 180, altitude: Double(altitude), align: .left, color: UIColor(white: 0.55, alpha: 1), in: context) + altitudeLabels.append(("\(altitude)°", centerAzimuth, Double(altitude))) + altitudeLabels.append(("\(altitude)°", centerAzimuth + 180, Double(altitude))) } } @@ -1029,10 +1093,19 @@ final class StarView: UIView { (Double(azimuth), altitude) } if !azimuth.isMultiple(of: 90) { - drawOutsideLabel("\(azimuth)°", azimuth: Double(azimuth), altitude: 0, color: UIColor(white: 0.55, alpha: 1), offset: 25, in: context) + azimuthLabels.append(("\(azimuth)°", Double(azimuth))) } } stroke(path, color: UIColor(white: 0.27, alpha: 1), width: 2.0, in: context) + + let labelColor = UIColor(white: 0.55, alpha: 1) + + for (label, az, alt) in altitudeLabels { + drawGridLabel(label, azimuth: az, altitude: alt, align: .left, color: labelColor, in: context) + } + for (label, az) in azimuthLabels { + drawOutsideLabel(label, azimuth: az, altitude: 0, color: labelColor, offset: 25, in: context) + } } private func updateEquatorialGridCache() { @@ -1111,6 +1184,9 @@ final class StarView: UIView { var bestRaIndex = -1 var minCenterDistSq = CGFloat.greatestFiniteMagnitude let center = CGPoint(x: bounds.midX, y: bounds.midY) + + var raLabels: [(String, CGPoint)] = [] + var decLabels: [(String, Double, Double)] = [] for i in 0.. 10 { - isPanning = true - } else if hypot(dx, dy) > 0 || isPanning { + + if hypot(dx, dy) > 0 { isPanning = true didPanThisGesture = true applyPanDelta(dx: dx, dy: dy) @@ -1906,8 +1995,13 @@ final class StarView: UIView { setNeedsDisplay() } case .ended, .cancelled: - if didPanThisGesture && !isCameraMode { + if didPanThisGesture && isGyroTrackingEnabled { + isPanning = false + scheduleGyroSnapBack() + } else if didPanThisGesture && !isGyroTrackingEnabled { startInertialScroll(velocity: recognizer.velocity(in: self)) + } else if isGyroTrackingEnabled { + isGyroPanOverride = false } isPanning = false didPanThisGesture = false @@ -1924,7 +2018,7 @@ final class StarView: UIView { let scale = viewAngle / Double(max(bounds.width, 1)) centerAzimuth = normalizedDegrees(centerAzimuth - Double(dx) * scale) centerAltitude = max(-90, min(90, centerAltitude + Double(dy) * scale)) - onAzimuthManualChangeListener?(centerAzimuth) + notifyAzimuthChanged() } setNeedsDisplay() } From 8892c6aad7871e8391c423e588bca48e35b669f0 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Thu, 9 Jul 2026 19:06:25 +0300 Subject: [PATCH 06/20] Fix SwiftLint warnings --- .../Plugins/Astronomy/StarCompassButton.swift | 4 - .../Astronomy/StarMapARModeHelper.swift | 43 +- Sources/Plugins/Astronomy/StarMapButton.swift | 12 - .../Astronomy/StarMapCameraHelper.swift | 21 +- .../Astronomy/StarMapGlassBackground.swift | 4 - .../Astronomy/StarMapPlainIconButton.swift | 2 +- .../Astronomy/StarMapViewController.swift | 174 ++-- Sources/Plugins/Astronomy/StarView.swift | 888 +++++++++--------- 8 files changed, 556 insertions(+), 592 deletions(-) diff --git a/Sources/Plugins/Astronomy/StarCompassButton.swift b/Sources/Plugins/Astronomy/StarCompassButton.swift index 96af3e77e8..92ef658c6d 100644 --- a/Sources/Plugins/Astronomy/StarCompassButton.swift +++ b/Sources/Plugins/Astronomy/StarCompassButton.swift @@ -73,8 +73,4 @@ final class StarCompassButton: StarMapButton { }, for: .touchUpInside) updateTheme() } - - func shouldShow() -> Bool { - true - } } diff --git a/Sources/Plugins/Astronomy/StarMapARModeHelper.swift b/Sources/Plugins/Astronomy/StarMapARModeHelper.swift index ad6afa4abf..4849cfab70 100644 --- a/Sources/Plugins/Astronomy/StarMapARModeHelper.swift +++ b/Sources/Plugins/Astronomy/StarMapARModeHelper.swift @@ -35,21 +35,25 @@ final class StarMapARModeHelper { self == .magneticNorth } } + + private enum CoreMotionErrorCode { + static let deviceRequiresMovement = 101 + static let trueNorthNotAvailable = 102 + } private struct Vector3 { let x: Double let y: Double let z: Double } + + private(set) var isArModeEnabled = false + private(set) var isRunning = false + + var onOrientationChanged: ((_ azimuth: Double, _ altitude: Double, _ roll: Double) -> Void)? + var onUnavailable: (() -> Void)? + var onArModeChanged: ((_ enabled: Bool) -> Void)? - private enum CoreMotionErrorCode { - // CMErrorDeviceRequiresMovement in CoreMotion/CMError.h. - static let deviceRequiresMovement = 101 - // CMErrorTrueNorthNotAvailable in CoreMotion/CMError.h. - static let trueNorthNotAvailable = 102 - } - - private var motionManager: CMMotionManager? private let queue = OperationQueue() private let minAlpha = 0.03 private let maxAlpha = 0.3 @@ -59,24 +63,12 @@ final class StarMapARModeHelper { private var smoothedAltitude = 45.0 private var activeReferenceFrame: ReferenceFrame? private var geomagneticDeclination: Double? - - var onOrientationChanged: ((_ azimuth: Double, _ altitude: Double, _ roll: Double) -> Void)? - var onUnavailable: (() -> Void)? - var onArModeChanged: ((_ enabled: Bool) -> Void)? - private(set) var isArModeEnabled = false - private(set) var isRunning = false - + private var motionManager: CMMotionManager? + init() { queue.maxConcurrentOperationCount = 1 } - deinit { - onOrientationChanged = nil - onUnavailable = nil - onArModeChanged = nil - stopMotionUpdates(waitUntilFinished: true) - } - func onResume() { if isArModeEnabled { _ = startMotionUpdates() @@ -300,4 +292,11 @@ final class StarMapARModeHelper { } return minAlpha + (absDelta - jitterThresh) * (maxAlpha - minAlpha) / (moveThresh - jitterThresh) } + + deinit { + onOrientationChanged = nil + onUnavailable = nil + onArModeChanged = nil + stopMotionUpdates(waitUntilFinished: true) + } } diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index d0dc0bf36c..c6178448a6 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -9,10 +9,6 @@ import UIKit enum StarMapControlTheme { - static var borderColor: UIColor { - UIColor(rgb: color_on_map_icon_border_color) - } - static func resolved(_ color: UIColor, nightMode: Bool) -> UIColor { nightMode ? color.dark : color.light } @@ -32,14 +28,6 @@ enum StarMapControlTheme { static func activeForeground(nightMode: Bool) -> UIColor { resolved(.mapButtonIconColorActive, nightMode: nightMode) } - - static func textColor(nightMode: Bool) -> UIColor { - resolved(.textColorPrimary, nightMode: nightMode) - } - - static func borderWidth(active: Bool, nightMode: Bool) -> CGFloat { - active ? 0 : (nightMode ? 2 : 0) - } } @objcMembers diff --git a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift index fe14405406..bc96c86533 100644 --- a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift +++ b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift @@ -13,11 +13,14 @@ import UIKit final class StarMapCameraHelper { static let defaultTransparency = 50 - private let sessionQueue = DispatchQueue(label: "net.osmand.starMap.camera.session") - var onUnavailable: ((_ message: String) -> Void)? var onCameraStateChanged: ((_ enabled: Bool) -> Void)? + private(set) var isCameraOverlayEnabled = false + private(set) var calculatedFov = 60.0 + + private let sessionQueue = DispatchQueue(label: "net.osmand.starMap.camera.session") + private var captureSession: AVCaptureSession? private var previewLayer: AVCaptureVideoPreviewLayer? @@ -26,9 +29,6 @@ final class StarMapCameraHelper { private var cameraAspectRatio = 4.0 / 3.0 private var lastPreviewBounds = CGRect.null private var lastVideoOrientation: AVCaptureVideoOrientation? - - private(set) var isCameraOverlayEnabled = false - private(set) var calculatedFov = 60.0 private weak var hostView: UIView? private weak var siblingView: UIView? @@ -66,10 +66,6 @@ final class StarMapCameraHelper { } } - func stopPreview() { - disableCameraOverlay(notify: true) - } - func layoutPreview() { let bounds = hostView?.bounds ?? .zero let orientation = videoOrientation() @@ -117,13 +113,6 @@ final class StarMapCameraHelper { CATransaction.commit() } - func calculateCameraFov() -> Double { - guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { - return 60.0 - } - return cameraFovInfo(camera).width - } - private func requestCameraOverlay() { guard let hostView, let siblingView else { onUnavailable?(localizedString("recording_camera_not_available")) diff --git a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift index 71d006d23f..b259e0e96b 100644 --- a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift +++ b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift @@ -16,10 +16,6 @@ enum StarMapGlassBackground { view.subviews.filter { $0.tag == tag }.forEach { $0.removeFromSuperview() } view.backgroundColor = .clear - let baseColor = active - ? UIColor.mapButtonBgColorActive - : StarMapControlTheme.resolved(.mapButtonBgColorDefault, nightMode: nightMode) - let glass = UIGlassEffect(style: .clear) let effectView = UIVisualEffectView(effect: glass) effectView.tag = tag diff --git a/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift b/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift index 01f5dc84e1..7887c08e97 100644 --- a/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift +++ b/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift @@ -45,7 +45,7 @@ final class StarMapPlainIconButton: UIButton { private func applyColors() { let color: UIColor if isHighlighted || active { - color = StarMapControlTheme.activeForeground(nightMode: nightMode) // mapButtonIconColorActive + color = StarMapControlTheme.activeForeground(nightMode: nightMode) } else { color = StarMapControlTheme.foreground(active: false, nightMode: nightMode) } diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index af5e2203c5..05f52fa3e8 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -42,6 +42,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private let zoomInButton = StarMapButton() private let zoomOutButton = StarMapButton() private let zoomButtonsVisible: Bool = OAUtilities.isiOSAppOnMac() + private let mapVisibleAreaGuide = UILayoutGuide() private let arModeHelper = StarMapARModeHelper() private let cameraHelper = StarMapCameraHelper() @@ -73,7 +74,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private var dayNightModeObserver: OAAutoObserverProxy? private var screenOrientationObserver: NSObjectProtocol? private var leftPanelLeadingConstraint: NSLayoutConstraint? - private let mapVisibleAreaGuide = UILayoutGuide() private var mapVisibleAreaLeadingConstraint: NSLayoutConstraint? private var isApplyingControlChange = false private var isGyroActive: Bool { @@ -116,15 +116,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { fatalError("init(coder:) has not been implemented") } - deinit { - mapLocationObserver?.detach() - dayNightModeObserver?.detach() - if let screenOrientationObserver { - NotificationCenter.default.removeObserver(screenOrientationObserver) - } - restoreRegularMapIfNeeded(refresh: false) - } - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear @@ -182,6 +173,82 @@ final class StarMapViewController: UIViewController, StarViewDelegate { layoutRegularMapRenderer() cameraHelper.layoutPreview() } + + func applyRedFilter(enabled: Bool) { + starView.showRedFilter = enabled + updateRedMode(enabled) + saveStarMapSettings() + } + + func setRegularMapVisibility(enabled: Bool) { + updateRegularMapVisibility(enabled) + saveCommonSettings() + } + + func trackableObjects() -> [SkyObject] { + viewModel.skyObjects + viewModel.constellations.map { $0 as SkyObject } + } + + func findTrackableObjectById(_ id: String) -> SkyObject? { + trackableObjects().first { $0.id == id } + } + + func starView(_ starView: StarView, didSelect object: SkyObject?) { + selectedObject = object + updateTimeControls() + } + + func showSearchDialog(initialCatalogWid: String? = nil) { + clearPreviousSearchDialog() + let controller = StarMapSearchViewController.newInstance(initialCatalogWid: initialCatalogWid, + parent: self, + plugin: plugin) + controller.onObjectSelected = { [weak self] obj in + self?.handleSearchObjectSelected(obj) + } + controller.onDismiss = { [weak self] in + self?.dismissSearchDialog(animated: true) + } + controller.applyRedFilter(enabled: starView.showRedFilter) + searchViewController = controller + + let nav = UINavigationController(rootViewController: controller) + nav.modalPresentationStyle = .fullScreen + nav.navigationBar.prefersLargeTitles = true + searchNavigationController = nav + + if UIDevice.current.userInterfaceIdiom == .pad { + showLeftPanel(nav) + updateMapControlsVisibility() + } else { + nav.modalPresentationStyle = .fullScreen + (presentedViewController ?? self).present(nav, animated: true) + } + } + + func searchableObjects() -> [SkyObject] { + trackableObjects() + } + + func searchConstellations() -> [Constellation] { + viewModel.constellations + } + + func searchObserver() -> Observer { + starView.observer + } + + func searchCurrentDate() -> Date { + currentDate + } + + func searchStarMapConfig() -> AstronomyPluginSettings.StarMapConfig { + settings.starMap + } + + func isSearchRedFilterEnabled() -> Bool { + starView.showRedFilter + } private func setupLayout() { mainLayout.translatesAutoresizingMaskIntoConstraints = false @@ -564,17 +631,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { starView.setNeedsDisplay() } - func applyRedFilter(enabled: Bool) { - starView.showRedFilter = enabled - updateRedMode(enabled) - saveStarMapSettings() - } - - func setRegularMapVisibility(enabled: Bool) { - updateRegularMapVisibility(enabled) - saveCommonSettings() - } - private func apply2DMode(_ is2D: Bool) { if is2D { previousAltitude = starView.getAltitude() @@ -915,14 +971,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { starView.setNeedsDisplay() } - func trackableObjects() -> [SkyObject] { - viewModel.skyObjects + viewModel.constellations.map { $0 as SkyObject } - } - - func findTrackableObjectById(_ id: String) -> SkyObject? { - trackableObjects().first { $0.id == id } - } - private func hideBottomSheet() { hideBottomSheet(clearSelection: true) } @@ -1146,52 +1194,19 @@ final class StarMapViewController: UIViewController, StarViewDelegate { present(alert, animated: true) } - func starView(_ starView: StarView, didSelect object: SkyObject?) { - selectedObject = object - updateTimeControls() - } - private func handleSearchObjectSelected(_ obj: SkyObject) { selectedObject = obj starView.setSelectedObject(obj) showObjectInfo(obj, centerInVisibleMapOnPresentation: true) } - @objc func showSearchDialog() { + @objc private func showSearchDialog() { if UIDevice.current.userInterfaceIdiom == .pad, searchViewController != nil { dismissSearchDialog(animated: true) } else { showSearchDialog(initialCatalogWid: nil) } } - - func showSearchDialog(initialCatalogWid: String? = nil) { - clearPreviousSearchDialog() - let controller = StarMapSearchViewController.newInstance(initialCatalogWid: initialCatalogWid, - parent: self, - plugin: plugin) - controller.onObjectSelected = { [weak self] obj in - self?.handleSearchObjectSelected(obj) - } - controller.onDismiss = { [weak self] in - self?.dismissSearchDialog(animated: true) - } - controller.applyRedFilter(enabled: starView.showRedFilter) - searchViewController = controller - - let nav = UINavigationController(rootViewController: controller) - nav.modalPresentationStyle = .fullScreen - nav.navigationBar.prefersLargeTitles = true - searchNavigationController = nav - - if UIDevice.current.userInterfaceIdiom == .pad { - showLeftPanel(nav) - updateMapControlsVisibility() - } else { - nav.modalPresentationStyle = .fullScreen - (presentedViewController ?? self).present(nav, animated: true) - } - } private func dismissSearchDialog(animated: Bool, completion: (() -> Void)? = nil) { guard let navigationController = searchNavigationController else { @@ -1222,30 +1237,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { dismissSearchDialog(animated: false) } - func searchableObjects() -> [SkyObject] { - trackableObjects() - } - - func searchConstellations() -> [Constellation] { - viewModel.constellations - } - - func searchObserver() -> Observer { - starView.observer - } - - func searchCurrentDate() -> Date { - currentDate - } - - func searchStarMapConfig() -> AstronomyPluginSettings.StarMapConfig { - settings.starMap - } - - func isSearchRedFilterEnabled() -> Bool { - starView.showRedFilter - } - @objc private func close() { dismiss(animated: true) } @@ -1473,6 +1464,15 @@ final class StarMapViewController: UIViewController, StarViewDelegate { zoomInButton.isEnabled = starView.canZoomIn() zoomOutButton.isEnabled = starView.canZoomOut() } + + deinit { + mapLocationObserver?.detach() + dayNightModeObserver?.detach() + if let screenOrientationObserver { + NotificationCenter.default.removeObserver(screenOrientationObserver) + } + restoreRegularMapIfNeeded(refresh: false) + } } private final class StarMapControlsContainer: UIView { diff --git a/Sources/Plugins/Astronomy/StarView.swift b/Sources/Plugins/Astronomy/StarView.swift index aaa69d14b4..d82cee4822 100644 --- a/Sources/Plugins/Astronomy/StarView.swift +++ b/Sources/Plugins/Astronomy/StarView.swift @@ -53,8 +53,21 @@ final class StarView: UIView { static let max2D = 220.0 } - weak var delegate: StarViewDelegate? + var isCameraMode = false + var isGyroTrackingEnabled = false + var onAnimationFinished: (() -> Void)? + var onAzimuthManualChangeListener: ((Double) -> Void)? + var onViewAngleChangeListener: ((Double) -> Void)? + private(set) var panX: CGFloat = 0 + private(set) var panY: CGFloat = 0 + private(set) var centerAzimuth = 180.0 + private(set) var centerAltitude = 45.0 + private(set) var viewAngle = 60.0 + var roll = 0.0 + + weak var delegate: StarViewDelegate? + var viewModel: StarObjectsViewModel? { didSet { rebuildObjectMap() @@ -77,17 +90,6 @@ final class StarView: UIView { } } - var isCameraMode = false - var isGyroTrackingEnabled = false - var onAnimationFinished: (() -> Void)? - var onAzimuthManualChangeListener: ((Double) -> Void)? - var onViewAngleChangeListener: ((Double) -> Void)? - - private(set) var centerAzimuth = 180.0 - private(set) var centerAltitude = 45.0 - private(set) var viewAngle = 60.0 - var roll = 0.0 - var showAzimuthalGrid: Bool { get { settings.starMap.showAzimuthalGrid } set { settings.starMap.showAzimuthalGrid = newValue; setNeedsDisplay() } @@ -207,9 +209,53 @@ final class StarView: UIView { get { settings.starMap.is2DMode } set { settings.starMap.is2DMode = newValue; setNeedsDisplay() } } + + var currentTime: Time { + get { + explicitCurrentTime ?? AstroUtils.astronomyTime(from: Date()) + } + set { + explicitCurrentTime = newValue + } + } + + var observer: Observer { + get { + explicitObserver ?? AstroUtils.observer(from: nil) + } + set { + explicitObserver = newValue + } + } + + private var skyObjects: [SkyObject] { + (manualSkyObjects ?? viewModel?.skyObjects ?? []) + .filter { $0.type != .CONSTELLATION } + .sorted { $0.magnitude < $1.magnitude } + } + + private var constellations: [Constellation] { + manualConstellations ?? viewModel?.constellations ?? [] + } + + private var trackableObjects: [SkyObject] { + skyObjects + constellations.map { $0 as SkyObject } + } + + private var screenScale: CGFloat { + window?.screen.scale ?? UIScreen.main.scale + } + + private let timeAnimationDuration: CFTimeInterval = 0.4 + private let focusAnimationDuration: CFTimeInterval = 0.5 + private let eclipticStep = 10 + private let equatorStep = 2 + private let galacticStep = 5 + private let inertialDecelerationRate: CGFloat = 0.998 + private let inertialStopSpeed: CGFloat = 5 + private let inertialMinStartSpeed: CGFloat = 80 + private let gyroSnapBackDelay: TimeInterval = 0.5 - private(set) var panX: CGFloat = 0 - private(set) var panY: CGFloat = 0 private var lastTouchPoint = CGPoint.zero private var isPanning = false @@ -237,29 +283,24 @@ final class StarView: UIView { private var explicitObserver: Observer? private var timeAnimationDisplayLink: CADisplayLink? private var timeAnimationStartTime: CFTimeInterval = 0 - private let timeAnimationDuration: CFTimeInterval = 0.4 private var focusAnimationDisplayLink: CADisplayLink? private var focusAnimationStartTime: CFTimeInterval = 0 - private let focusAnimationDuration: CFTimeInterval = 0.5 private var focusAnimation: FocusAnimation? private var onObjectClickListener: ((SkyObject?) -> Void)? private var onConstellationClickListener: ((Constellation?) -> Void)? - private let eclipticStep = 10 private var eclipticAzimuths: [Double] = [] private var eclipticAltitudes: [Double] = [] private var lastEclipticTimeT = -1.0 private var lastEclipticLat = -999.0 private var lastEclipticLon = -999.0 - - private let equatorStep = 2 + private var equatorAzimuths: [Double] = [] private var equatorAltitudes: [Double] = [] private var lastEquatorTimeT = -1.0 private var lastEquatorLat = -999.0 private var lastEquatorLon = -999.0 - private let galacticStep = 5 private var galacticAzimuths: [Double] = [] private var galacticAltitudes: [Double] = [] private var lastGalacticTimeT = -1.0 @@ -281,9 +322,6 @@ final class StarView: UIView { private var inertialDisplayLink: CADisplayLink? private var inertialVelocity: CGPoint = .zero private var lastInertialTimestamp: CFTimeInterval = 0 - private let inertialDecelerationRate: CGFloat = 0.998 - private let inertialStopSpeed: CGFloat = 5 - private let inertialMinStartSpeed: CGFloat = 80 private var didPanThisGesture = false private var gyroTargetAzimuth = 0.0 @@ -291,68 +329,55 @@ final class StarView: UIView { private var gyroTargetRoll = 0.0 private var isGyroPanOverride = false private var gyroSnapBackWorkItem: DispatchWorkItem? - private let gyroSnapBackDelay: TimeInterval = 0.5 - - var currentTime: Time { - get { - explicitCurrentTime ?? AstroUtils.astronomyTime(from: Date()) - } - set { - explicitCurrentTime = newValue - } - } - - var observer: Observer { - get { - explicitObserver ?? AstroUtils.observer(from: nil) - } - set { - explicitObserver = newValue - } - } - - private var skyObjects: [SkyObject] { - (manualSkyObjects ?? viewModel?.skyObjects ?? []) - .filter { $0.type != .CONSTELLATION } - .sorted { $0.magnitude < $1.magnitude } - } - - private var constellations: [Constellation] { - manualConstellations ?? viewModel?.constellations ?? [] - } - - private var trackableObjects: [SkyObject] { - skyObjects + constellations.map { $0 as SkyObject } - } - - private var screenScale: CGFloat { - window?.screen.scale ?? UIScreen.main.scale - } override init(frame: CGRect) { super.init(frame: frame) commonInit() } - deinit { - timeAnimationDisplayLink?.invalidate() - focusAnimationDisplayLink?.invalidate() - inertialDisplayLink?.invalidate() - } - required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } - private func commonInit() { - backgroundColor = .clear - isOpaque = false - contentMode = .redraw + updateProjectionCache() + rebuildObjectMap() + occupiedRects.removeAll(keepingCapacity: true) - addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) - addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))) - addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))) + context.saveGState() + drawBackground(in: context) + if settings.starMap.showEquatorialGrid { + drawEquatorialGrid(in: context) + } + if settings.starMap.showAzimuthalGrid { + drawAzimuthalGrid(in: context) + } + if settings.starMap.showEclipticLine { + drawEclipticLine(in: context) + } + if settings.starMap.showMeridianLine { + drawMeridianLine(in: context) + } + if settings.starMap.showEquatorLine { + drawEquatorLine(in: context) + } + if settings.starMap.showGalacticLine { + drawGalacticLine(in: context) + } + drawConstellationLines(in: context) + drawHorizon(in: context) + drawCelestialPaths(in: context) + drawConstellationLabels(in: context) + drawSkyObjects(in: context) + drawHighlights(in: context) + drawDirectionArrows(in: context) + + context.restoreGState() } func restoreMapPosition(_ position: AstronomyPluginSettings.MapViewPosition) { @@ -411,331 +436,408 @@ final class StarView: UIView { centerAltitude = max(-90, min(90, altitude)) setNeedsDisplay() } + + func getAltitude() -> Double { + centerAltitude + } - private func setCenter(azimuth: Double, - altitude: Double, - at targetPoint: CGPoint, - animate: Bool = false, - targetViewAngle: Double? = nil) { - guard bounds.width > 0, bounds.height > 0 else { - if animate { - animateTo(azimuth: azimuth, altitude: altitude, targetViewAngle: targetViewAngle) - } else { - setCenterState(azimuth: azimuth, altitude: altitude, targetViewAngle: targetViewAngle) - } - return - } + func getAzimuth() -> Double { + centerAzimuth + } - let finalViewAngle = boundedViewAngle(targetViewAngle ?? viewAngle) - if settings.starMap.is2DMode { - let targetPan = CGPoint(x: targetPoint.x - bounds.midX, - y: targetPoint.y - bounds.midY) - if animate { - animateTo(azimuth: azimuth, - altitude: altitude, - targetViewAngle: targetViewAngle, - targetPan: targetPan) - } else { - setCenterState(azimuth: azimuth, altitude: altitude, targetViewAngle: targetViewAngle) - panX = targetPan.x - panY = targetPan.y - setNeedsDisplay() - } + func getViewAngle() -> Double { + viewAngle + } + + func setAzimuth(_ azimuth: Double, animate: Bool = false, fps: Int? = 30) { + guard abs(centerAzimuth - azimuth) >= 0.5 else { return } - - let screenOffsetX = Double(targetPoint.x - bounds.midX) - let screenOffsetY = Double(targetPoint.y - bounds.midY) - let rollRad = roll * .pi / 180.0 - let unrotatedX = screenOffsetX * cos(rollRad) + screenOffsetY * sin(rollRad) - let unrotatedY = -screenOffsetX * sin(rollRad) + screenOffsetY * cos(rollRad) - let scale = finalViewAngle / Double(max(bounds.width, 1)) - let targetAzimuth = normalizedDegrees(azimuth - unrotatedX * scale) - let targetAltitude = max(-90, min(90, altitude + unrotatedY * scale)) if animate { - animateTo(azimuth: targetAzimuth, altitude: targetAltitude, targetViewAngle: targetViewAngle) + animateTo(azimuth: azimuth, altitude: centerAltitude) } else { - setCenterState(azimuth: targetAzimuth, altitude: targetAltitude, targetViewAngle: targetViewAngle) + cancelFocusAnimation() + centerAzimuth = normalizedDegrees(azimuth) + setNeedsDisplay() } + notifyAzimuthChanged() } - private func setCenterState(azimuth: Double, altitude: Double, targetViewAngle: Double? = nil) { - cancelFocusAnimation() - centerAzimuth = normalizedDegrees(azimuth) - centerAltitude = max(-90, min(90, altitude)) - if let targetViewAngle { - viewAngle = boundedViewAngle(targetViewAngle) - onViewAngleChangeListener?(viewAngle) + func setSkyObjects(_ objects: [SkyObject]) { + manualSkyObjects = objects.sorted { $0.magnitude < $1.magnitude } + rebuildObjectMap() + rebuildConstellationCenterCache() + recalculatePositions(time: currentTime, updateTargets: false, force: true) + let celestialPathObjects = Set(objects.filter { $0.showCelestialPath }) + let staleObjects = pinnedObjects.filter { !($0 is Constellation) && !celestialPathObjects.contains($0) } + pinnedObjects.subtract(staleObjects) + for object in staleObjects { + pathCache.removeValue(forKey: object.id) } + pinnedObjects.formUnion(celestialPathObjects) setNeedsDisplay() } - private func boundedViewAngle(_ angle: Double) -> Double { - max(ViewAngleBounds.min, min(maxViewAngle, angle)) - } - - private func animateTo(azimuth: Double, - altitude: Double, - targetViewAngle: Double? = nil, - targetPan: CGPoint? = nil, - targetRoll: Double? = nil) { - cancelFocusAnimation() - focusAnimation = FocusAnimation(startAzimuth: centerAzimuth, - targetAzimuth: normalizedDegrees(azimuth), - startAltitude: centerAltitude, - targetAltitude: max(-90, min(90, altitude)), - startRoll: roll, - targetRoll: targetRoll ?? roll, - startViewAngle: viewAngle, - targetViewAngle: targetViewAngle.map { boundedViewAngle($0) } ?? viewAngle, - startPan: CGPoint(x: panX, y: panY), - targetPan: targetPan) - focusAnimationStartTime = CACurrentMediaTime() - let displayLink = CADisplayLink(target: self, selector: #selector(handleFocusAnimationFrame(_:))) - focusAnimationDisplayLink = displayLink - displayLink.add(to: .main, forMode: .common) + func setConstellations(_ list: [Constellation]) { + manualConstellations = list + rebuildObjectMap() + rebuildConstellationCenterCache() + recalculatePositions(time: currentTime, updateTargets: false, force: true) + let celestialPathConstellations = Set(list.filter { $0.showCelestialPath }.map { $0 as SkyObject }) + let staleConstellations = pinnedObjects.filter { $0 is Constellation && !celestialPathConstellations.contains($0) } + pinnedObjects.subtract(staleConstellations) + for constellation in staleConstellations { + pathCache.removeValue(forKey: constellation.id) + } + pinnedObjects.formUnion(celestialPathConstellations) + setNeedsDisplay() } - private func cancelFocusAnimation() { - focusAnimationDisplayLink?.invalidate() - focusAnimationDisplayLink = nil - focusAnimation = nil - - cancelInertialScroll() + func refreshObjects() { + recalculatePositions(time: currentTime, updateTargets: false) + setNeedsDisplay() } - - private func cancelGyroSnapBack() { - gyroSnapBackWorkItem?.cancel() - gyroSnapBackWorkItem = nil - cancelFocusAnimation() + + func updateRedFilter() { + AstroRedFilter.apply(settings.starMap.showRedFilter, to: self) + setNeedsDisplay() } - private func scheduleGyroSnapBack() { - cancelGyroSnapBack() - isGyroPanOverride = true + func setOnObjectClickListener(_ listener: @escaping (SkyObject?) -> Void) { + onObjectClickListener = listener + } - let work = DispatchWorkItem { [weak self] in - self?.animateSnapBackToGyro() - } - gyroSnapBackWorkItem = work - DispatchQueue.main.asyncAfter(deadline: .now() + gyroSnapBackDelay, execute: work) + func setOnConstellationClickListener(_ listener: @escaping (Constellation?) -> Void) { + onConstellationClickListener = listener } - private func animateSnapBackToGyro() { - gyroSnapBackWorkItem = nil - animateTo(azimuth: gyroTargetAzimuth, - altitude: gyroTargetAltitude, - targetRoll: gyroTargetRoll) + func getSelectedConstellationItem() -> Constellation? { + guard let selectedConstellationId else { + return nil + } + return constellations.first { $0.id == selectedConstellationId } } - @objc private func handleFocusAnimationFrame(_ displayLink: CADisplayLink) { - guard let animation = focusAnimation else { - cancelFocusAnimation() + func setSelectedObject(_ object: SkyObject?, center: Bool = false, animate: Bool = false) { + if let constellation = object as? Constellation { + setSelectedConstellation(constellation, center: center, animate: animate) return } - - let rawFraction = min(1.0, max(0.0, (displayLink.timestamp - focusAnimationStartTime) / focusAnimationDuration)) - let fraction = 1.0 - pow(1.0 - rawFraction, 2.0) - applyFocusAnimation(animation, fraction: fraction) - if rawFraction >= 1 { - applyFocusAnimation(animation, fraction: 1) - isGyroPanOverride = false - cancelFocusAnimation() - onAnimationFinished?() + selectedConstellationId = nil + selectedConstellationStarIds.removeAll() + selectedObject = object + if let object { + if object.azimuth == 0 && object.altitude == 0 { + calculatePosition(object) + } + if center { + setCenter(azimuth: object.azimuth, altitude: object.altitude, animate: animate) + } } + setNeedsDisplay() } - private func applyFocusAnimation(_ animation: FocusAnimation, fraction: Double) { - centerAzimuth = interpolateAngle(start: animation.startAzimuth, end: animation.targetAzimuth, fraction: fraction) - centerAltitude = animation.startAltitude + (animation.targetAltitude - animation.startAltitude) * fraction - roll = animation.startRoll + (animation.targetRoll - animation.startRoll) * fraction - viewAngle = animation.startViewAngle + (animation.targetViewAngle - animation.startViewAngle) * fraction - if let targetPan = animation.targetPan { - let cgFraction = CGFloat(fraction) - panX = animation.startPan.x + (targetPan.x - animation.startPan.x) * cgFraction - panY = animation.startPan.y + (targetPan.y - animation.startPan.y) * cgFraction + func setSelectedObject(_ object: SkyObject?, centerAt targetPoint: CGPoint, animate: Bool = false) { + if let constellation = object as? Constellation { + setSelectedConstellation(constellation, centerAt: targetPoint, animate: animate) + return } - if abs(animation.targetViewAngle - animation.startViewAngle) > 0.001 { - onViewAngleChangeListener?(viewAngle) + selectedConstellationId = nil + selectedConstellationStarIds.removeAll() + selectedObject = object + if let object { + if object.azimuth == 0 && object.altitude == 0 { + calculatePosition(object) + } + setCenter(azimuth: object.azimuth, altitude: object.altitude, at: targetPoint, animate: animate) } setNeedsDisplay() - notifyAzimuthChanged() } - func getAltitude() -> Double { - centerAltitude + func setSelectedConstellation(_ constellation: Constellation?, center: Bool = false, animate: Bool = false) { + selectedConstellationId = constellation?.id + selectedObject = constellation + selectedConstellationStarIds.removeAll() + for (first, second) in constellation?.lines ?? [] { + selectedConstellationStarIds.insert(first) + selectedConstellationStarIds.insert(second) + } + for object in skyObjects where selectedConstellationStarIds.contains(object.hip) { + calculatePosition(object, time: currentTime, updateTargets: false) + object.azimuth = object.targetAzimuth + object.altitude = object.targetAltitude + } + if let constellation, center { + let centers = constellationCenters() + if let center = centers[constellation.id] { + let targetAngle = targetViewAngle(for: constellation, center: center) + if animate { + animateTo(azimuth: center.azimuth, altitude: center.altitude, targetViewAngle: targetAngle) + } else { + setCenter(azimuth: center.azimuth, altitude: center.altitude) + setViewAngle(targetAngle) + } + } + } + setNeedsDisplay() } - func getAzimuth() -> Double { - centerAzimuth + func setSelectedConstellation(_ constellation: Constellation?, centerAt targetPoint: CGPoint, animate: Bool = false) { + selectedConstellationId = constellation?.id + selectedObject = constellation + selectedConstellationStarIds.removeAll() + for (first, second) in constellation?.lines ?? [] { + selectedConstellationStarIds.insert(first) + selectedConstellationStarIds.insert(second) + } + for object in skyObjects where selectedConstellationStarIds.contains(object.hip) { + calculatePosition(object, time: currentTime, updateTargets: false) + object.azimuth = object.targetAzimuth + object.altitude = object.targetAltitude + } + if let constellation { + let centers = constellationCenters() + if let center = centers[constellation.id] { + setCenter(azimuth: center.azimuth, + altitude: center.altitude, + at: targetPoint, + animate: animate, + targetViewAngle: targetViewAngle(for: constellation, center: center)) + } + } + setNeedsDisplay() } - - func getViewAngle() -> Double { - viewAngle + + func isObjectPinned(_ object: SkyObject) -> Bool { + pinnedObjects.contains(object) } - func setAzimuth(_ azimuth: Double, animate: Bool = false, fps: Int? = 30) { - guard abs(centerAzimuth - azimuth) >= 0.5 else { - return + func setObjectPinned(_ object: SkyObject, pinned: Bool, forceUpdate: Bool = false) { + if pinned { + pinnedObjects.insert(object) + } else { + pinnedObjects.remove(object) + pathCache.removeValue(forKey: object.id) } + if forceUpdate { + setNeedsDisplay() + } + } + + func setDateTime(_ time: Time, animate: Bool = true) { + timeAnimationDisplayLink?.invalidate() + timeAnimationDisplayLink = nil + if animate { - animateTo(azimuth: azimuth, altitude: centerAltitude) + for object in skyObjects { + object.startAzimuth = object.azimuth + object.startAltitude = object.altitude + } + captureConstellationAnimationStarts() + recalculatePositions(time: time, updateTargets: true, force: true) + currentTime = time + timeAnimationStartTime = CACurrentMediaTime() + let displayLink = CADisplayLink(target: self, selector: #selector(handleTimeAnimationFrame(_:))) + timeAnimationDisplayLink = displayLink + displayLink.add(to: .main, forMode: .common) } else { - cancelFocusAnimation() - centerAzimuth = normalizedDegrees(azimuth) + currentTime = time + recalculatePositions(time: time, updateTargets: true, force: true) + for object in skyObjects { + object.azimuth = object.targetAzimuth + object.altitude = object.targetAltitude + } + var updatedCenters = constellationCenterCache + for (id, center) in constellationCenterCache { + var updated = center + updated.azimuth = center.targetAzimuth + updated.altitude = center.targetAltitude + updatedCenters[id] = updated + } + constellationCenterCache = updatedCenters + updateConstellationObjectPositions() setNeedsDisplay() + onAnimationFinished?() } - notifyAzimuthChanged() + } + + func setViewAngle(_ angle: Double) { + updateViewAngle(angle) } - func setSkyObjects(_ objects: [SkyObject]) { - manualSkyObjects = objects.sorted { $0.magnitude < $1.magnitude } - rebuildObjectMap() - rebuildConstellationCenterCache() - recalculatePositions(time: currentTime, updateTargets: false, force: true) - let celestialPathObjects = Set(objects.filter { $0.showCelestialPath }) - let staleObjects = pinnedObjects.filter { !($0 is Constellation) && !celestialPathObjects.contains($0) } - pinnedObjects.subtract(staleObjects) - for object in staleObjects { - pathCache.removeValue(forKey: object.id) - } - pinnedObjects.formUnion(celestialPathObjects) - setNeedsDisplay() + func zoomIn() { + updateViewAngle(viewAngle / 1.5) } - func setConstellations(_ list: [Constellation]) { - manualConstellations = list - rebuildObjectMap() - rebuildConstellationCenterCache() - recalculatePositions(time: currentTime, updateTargets: false, force: true) - let celestialPathConstellations = Set(list.filter { $0.showCelestialPath }.map { $0 as SkyObject }) - let staleConstellations = pinnedObjects.filter { $0 is Constellation && !celestialPathConstellations.contains($0) } - pinnedObjects.subtract(staleConstellations) - for constellation in staleConstellations { - pathCache.removeValue(forKey: constellation.id) - } - pinnedObjects.formUnion(celestialPathConstellations) - setNeedsDisplay() + func zoomOut() { + updateViewAngle(viewAngle * 1.5) + } + + func canZoomIn() -> Bool { + viewAngle > ViewAngleBounds.min + 0.5 + } + + func canZoomOut() -> Bool { + viewAngle < maxViewAngle - 0.5 } - func updateVisibility() { - recalculatePositions(time: currentTime, updateTargets: false) - setNeedsDisplay() + func project(object: SkyObject) -> CGPoint? { + updateProjectionCache() + return skyToScreen(azimuth: object.azimuth, altitude: object.altitude) + } + + func calculatePosition(_ object: SkyObject) { + calculatePosition(object, time: currentTime, updateTargets: false) } - func refreshObjects() { - recalculatePositions(time: currentTime, updateTargets: false) - setNeedsDisplay() + private func setCenter(azimuth: Double, + altitude: Double, + at targetPoint: CGPoint, + animate: Bool = false, + targetViewAngle: Double? = nil) { + guard bounds.width > 0, bounds.height > 0 else { + if animate { + animateTo(azimuth: azimuth, altitude: altitude, targetViewAngle: targetViewAngle) + } else { + setCenterState(azimuth: azimuth, altitude: altitude, targetViewAngle: targetViewAngle) + } + return + } + + let finalViewAngle = boundedViewAngle(targetViewAngle ?? viewAngle) + if settings.starMap.is2DMode { + let targetPan = CGPoint(x: targetPoint.x - bounds.midX, + y: targetPoint.y - bounds.midY) + if animate { + animateTo(azimuth: azimuth, + altitude: altitude, + targetViewAngle: targetViewAngle, + targetPan: targetPan) + } else { + setCenterState(azimuth: azimuth, altitude: altitude, targetViewAngle: targetViewAngle) + panX = targetPan.x + panY = targetPan.y + setNeedsDisplay() + } + return + } + + let screenOffsetX = Double(targetPoint.x - bounds.midX) + let screenOffsetY = Double(targetPoint.y - bounds.midY) + let rollRad = roll * .pi / 180.0 + let unrotatedX = screenOffsetX * cos(rollRad) + screenOffsetY * sin(rollRad) + let unrotatedY = -screenOffsetX * sin(rollRad) + screenOffsetY * cos(rollRad) + let scale = finalViewAngle / Double(max(bounds.width, 1)) + let targetAzimuth = normalizedDegrees(azimuth - unrotatedX * scale) + let targetAltitude = max(-90, min(90, altitude + unrotatedY * scale)) + if animate { + animateTo(azimuth: targetAzimuth, altitude: targetAltitude, targetViewAngle: targetViewAngle) + } else { + setCenterState(azimuth: targetAzimuth, altitude: targetAltitude, targetViewAngle: targetViewAngle) + } } - func updateRedFilter() { - AstroRedFilter.apply(settings.starMap.showRedFilter, to: self) + private func setCenterState(azimuth: Double, altitude: Double, targetViewAngle: Double? = nil) { + cancelFocusAnimation() + centerAzimuth = normalizedDegrees(azimuth) + centerAltitude = max(-90, min(90, altitude)) + if let targetViewAngle { + viewAngle = boundedViewAngle(targetViewAngle) + onViewAngleChangeListener?(viewAngle) + } setNeedsDisplay() } - func setOnObjectClickListener(_ listener: @escaping (SkyObject?) -> Void) { - onObjectClickListener = listener + private func boundedViewAngle(_ angle: Double) -> Double { + max(ViewAngleBounds.min, min(maxViewAngle, angle)) } - func setOnConstellationClickListener(_ listener: @escaping (Constellation?) -> Void) { - onConstellationClickListener = listener + private func animateTo(azimuth: Double, + altitude: Double, + targetViewAngle: Double? = nil, + targetPan: CGPoint? = nil, + targetRoll: Double? = nil) { + cancelFocusAnimation() + focusAnimation = FocusAnimation(startAzimuth: centerAzimuth, + targetAzimuth: normalizedDegrees(azimuth), + startAltitude: centerAltitude, + targetAltitude: max(-90, min(90, altitude)), + startRoll: roll, + targetRoll: targetRoll ?? roll, + startViewAngle: viewAngle, + targetViewAngle: targetViewAngle.map { boundedViewAngle($0) } ?? viewAngle, + startPan: CGPoint(x: panX, y: panY), + targetPan: targetPan) + focusAnimationStartTime = CACurrentMediaTime() + let displayLink = CADisplayLink(target: self, selector: #selector(handleFocusAnimationFrame(_:))) + focusAnimationDisplayLink = displayLink + displayLink.add(to: .main, forMode: .common) + } + + private func cancelFocusAnimation() { + focusAnimationDisplayLink?.invalidate() + focusAnimationDisplayLink = nil + focusAnimation = nil + + cancelInertialScroll() + } + + private func cancelGyroSnapBack() { + gyroSnapBackWorkItem?.cancel() + gyroSnapBackWorkItem = nil + cancelFocusAnimation() } - func getSelectedConstellationItem() -> Constellation? { - guard let selectedConstellationId else { - return nil + private func scheduleGyroSnapBack() { + cancelGyroSnapBack() + isGyroPanOverride = true + + let work = DispatchWorkItem { [weak self] in + self?.animateSnapBackToGyro() } - return constellations.first { $0.id == selectedConstellationId } + gyroSnapBackWorkItem = work + DispatchQueue.main.asyncAfter(deadline: .now() + gyroSnapBackDelay, execute: work) } - func setSelectedObject(_ object: SkyObject?, center: Bool = false, animate: Bool = false) { - if let constellation = object as? Constellation { - setSelectedConstellation(constellation, center: center, animate: animate) - return - } - selectedConstellationId = nil - selectedConstellationStarIds.removeAll() - selectedObject = object - if let object { - if object.azimuth == 0 && object.altitude == 0 { - calculatePosition(object) - } - if center { - setCenter(azimuth: object.azimuth, altitude: object.altitude, animate: animate) - } - } - setNeedsDisplay() + private func animateSnapBackToGyro() { + gyroSnapBackWorkItem = nil + animateTo(azimuth: gyroTargetAzimuth, + altitude: gyroTargetAltitude, + targetRoll: gyroTargetRoll) } - func setSelectedObject(_ object: SkyObject?, centerAt targetPoint: CGPoint, animate: Bool = false) { - if let constellation = object as? Constellation { - setSelectedConstellation(constellation, centerAt: targetPoint, animate: animate) + @objc private func handleFocusAnimationFrame(_ displayLink: CADisplayLink) { + guard let animation = focusAnimation else { + cancelFocusAnimation() return } - selectedConstellationId = nil - selectedConstellationStarIds.removeAll() - selectedObject = object - if let object { - if object.azimuth == 0 && object.altitude == 0 { - calculatePosition(object) - } - setCenter(azimuth: object.azimuth, altitude: object.altitude, at: targetPoint, animate: animate) - } - setNeedsDisplay() - } - func setSelectedConstellation(_ constellation: Constellation?, center: Bool = false, animate: Bool = false) { - selectedConstellationId = constellation?.id - selectedObject = constellation - selectedConstellationStarIds.removeAll() - for (first, second) in constellation?.lines ?? [] { - selectedConstellationStarIds.insert(first) - selectedConstellationStarIds.insert(second) - } - for object in skyObjects where selectedConstellationStarIds.contains(object.hip) { - calculatePosition(object, time: currentTime, updateTargets: false) - object.azimuth = object.targetAzimuth - object.altitude = object.targetAltitude - } - if let constellation, center { - let centers = constellationCenters() - if let center = centers[constellation.id] { - let targetAngle = targetViewAngle(for: constellation, center: center) - if animate { - animateTo(azimuth: center.azimuth, altitude: center.altitude, targetViewAngle: targetAngle) - } else { - setCenter(azimuth: center.azimuth, altitude: center.altitude) - setViewAngle(targetAngle) - } - } + let rawFraction = min(1.0, max(0.0, (displayLink.timestamp - focusAnimationStartTime) / focusAnimationDuration)) + let fraction = 1.0 - pow(1.0 - rawFraction, 2.0) + applyFocusAnimation(animation, fraction: fraction) + if rawFraction >= 1 { + applyFocusAnimation(animation, fraction: 1) + isGyroPanOverride = false + cancelFocusAnimation() + onAnimationFinished?() } - setNeedsDisplay() } - func setSelectedConstellation(_ constellation: Constellation?, centerAt targetPoint: CGPoint, animate: Bool = false) { - selectedConstellationId = constellation?.id - selectedObject = constellation - selectedConstellationStarIds.removeAll() - for (first, second) in constellation?.lines ?? [] { - selectedConstellationStarIds.insert(first) - selectedConstellationStarIds.insert(second) - } - for object in skyObjects where selectedConstellationStarIds.contains(object.hip) { - calculatePosition(object, time: currentTime, updateTargets: false) - object.azimuth = object.targetAzimuth - object.altitude = object.targetAltitude + private func applyFocusAnimation(_ animation: FocusAnimation, fraction: Double) { + centerAzimuth = interpolateAngle(start: animation.startAzimuth, end: animation.targetAzimuth, fraction: fraction) + centerAltitude = animation.startAltitude + (animation.targetAltitude - animation.startAltitude) * fraction + roll = animation.startRoll + (animation.targetRoll - animation.startRoll) * fraction + viewAngle = animation.startViewAngle + (animation.targetViewAngle - animation.startViewAngle) * fraction + if let targetPan = animation.targetPan { + let cgFraction = CGFloat(fraction) + panX = animation.startPan.x + (targetPan.x - animation.startPan.x) * cgFraction + panY = animation.startPan.y + (targetPan.y - animation.startPan.y) * cgFraction } - if let constellation { - let centers = constellationCenters() - if let center = centers[constellation.id] { - setCenter(azimuth: center.azimuth, - altitude: center.altitude, - at: targetPoint, - animate: animate, - targetViewAngle: targetViewAngle(for: constellation, center: center)) - } + if abs(animation.targetViewAngle - animation.startViewAngle) > 0.001 { + onViewAngleChangeListener?(viewAngle) } setNeedsDisplay() + notifyAzimuthChanged() } private func targetViewAngle(for constellation: Constellation, center: ConstellationCenter) -> Double { @@ -754,59 +856,6 @@ final class StarView: UIView { return maxDistance > 0 ? max(20, min(120, maxDistance * 3.5)) : viewAngle } - func isObjectPinned(_ object: SkyObject) -> Bool { - pinnedObjects.contains(object) - } - - func setObjectPinned(_ object: SkyObject, pinned: Bool, forceUpdate: Bool = false) { - if pinned { - pinnedObjects.insert(object) - } else { - pinnedObjects.remove(object) - pathCache.removeValue(forKey: object.id) - } - if forceUpdate { - setNeedsDisplay() - } - } - - func setDateTime(_ time: Time, animate: Bool = true) { - timeAnimationDisplayLink?.invalidate() - timeAnimationDisplayLink = nil - - if animate { - for object in skyObjects { - object.startAzimuth = object.azimuth - object.startAltitude = object.altitude - } - captureConstellationAnimationStarts() - recalculatePositions(time: time, updateTargets: true, force: true) - currentTime = time - timeAnimationStartTime = CACurrentMediaTime() - let displayLink = CADisplayLink(target: self, selector: #selector(handleTimeAnimationFrame(_:))) - timeAnimationDisplayLink = displayLink - displayLink.add(to: .main, forMode: .common) - } else { - currentTime = time - recalculatePositions(time: time, updateTargets: true, force: true) - for object in skyObjects { - object.azimuth = object.targetAzimuth - object.altitude = object.targetAltitude - } - var updatedCenters = constellationCenterCache - for (id, center) in constellationCenterCache { - var updated = center - updated.azimuth = center.targetAzimuth - updated.altitude = center.targetAltitude - updatedCenters[id] = updated - } - constellationCenterCache = updatedCenters - updateConstellationObjectPositions() - setNeedsDisplay() - onAnimationFinished?() - } - } - private func captureConstellationAnimationStarts() { var updatedCenters = constellationCenterCache for (id, center) in constellationCenterCache { @@ -842,71 +891,6 @@ final class StarView: UIView { onAnimationFinished?() } } - - func setViewAngle(_ angle: Double) { - updateViewAngle(angle) - } - - func zoomIn() { - updateViewAngle(viewAngle / 1.5) - } - - func zoomOut() { - updateViewAngle(viewAngle * 1.5) - } - - func canZoomIn() -> Bool { - viewAngle > ViewAngleBounds.min + 0.5 - } - - func canZoomOut() -> Bool { - viewAngle < maxViewAngle - 0.5 - } - - func project(object: SkyObject) -> CGPoint? { - updateProjectionCache() - return skyToScreen(azimuth: object.azimuth, altitude: object.altitude) - } - - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { - return - } - - updateProjectionCache() - rebuildObjectMap() - occupiedRects.removeAll(keepingCapacity: true) - - context.saveGState() - drawBackground(in: context) - if settings.starMap.showEquatorialGrid { - drawEquatorialGrid(in: context) - } - if settings.starMap.showAzimuthalGrid { - drawAzimuthalGrid(in: context) - } - if settings.starMap.showEclipticLine { - drawEclipticLine(in: context) - } - if settings.starMap.showMeridianLine { - drawMeridianLine(in: context) - } - if settings.starMap.showEquatorLine { - drawEquatorLine(in: context) - } - if settings.starMap.showGalacticLine { - drawGalacticLine(in: context) - } - drawConstellationLines(in: context) - drawHorizon(in: context) - drawCelestialPaths(in: context) - drawConstellationLabels(in: context) - drawSkyObjects(in: context) - drawHighlights(in: context) - drawDirectionArrows(in: context) - - context.restoreGState() - } private func notifyAzimuthChanged() { onAzimuthManualChangeListener?(centerAzimuth) @@ -1644,10 +1628,6 @@ final class StarView: UIView { } } - func calculatePosition(_ object: SkyObject) { - calculatePosition(object, time: currentTime, updateTargets: false) - } - private func calculatePosition(_ object: SkyObject, time: Time, updateTargets: Bool, force: Bool = false) { if !force && object.lastUpdateTime == time.tt && !updateTargets { return @@ -2199,4 +2179,20 @@ final class StarView: UIView { } return value } + + private func commonInit() { + backgroundColor = .clear + isOpaque = false + contentMode = .redraw + + addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) + addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))) + addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))) + } + + deinit { + timeAnimationDisplayLink?.invalidate() + focusAnimationDisplayLink?.invalidate() + inertialDisplayLink?.invalidate() + } } From 8099f4424f4e044f8ef24af7a413dd7ece782552 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Fri, 10 Jul 2026 13:47:25 +0300 Subject: [PATCH 07/20] Changed colors and alpha --- OsmAnd.xcodeproj/project.pbxproj | 16 ++--- .../Astronomy/DateTimeSelectionView.swift | 35 ++++++++-- .../Astronomy/StarMapArControlCard.swift | 29 +++++--- Sources/Plugins/Astronomy/StarMapButton.swift | 22 +++--- .../Astronomy/StarMapGlassBackground.swift | 2 +- .../StarMapMagnitudeFilterButton.swift | 21 ++++-- .../StarMapMagnitudeSliderPanel.swift | 13 ++-- .../Astronomy/StarMapPlainButton.swift | 68 +++++++++++++++++++ .../Astronomy/StarMapPlainIconButton.swift | 54 --------------- .../Astronomy/StarMapResetButton.swift | 38 ----------- .../Astronomy/StarMapTimeControlButton.swift | 40 ----------- .../Astronomy/StarMapTimeControlCard.swift | 44 ++++++++---- 12 files changed, 191 insertions(+), 191 deletions(-) create mode 100644 Sources/Plugins/Astronomy/StarMapPlainButton.swift delete mode 100644 Sources/Plugins/Astronomy/StarMapPlainIconButton.swift delete mode 100644 Sources/Plugins/Astronomy/StarMapResetButton.swift delete mode 100644 Sources/Plugins/Astronomy/StarMapTimeControlButton.swift diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 79a8b64841..21330c5907 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1400,8 +1400,6 @@ A5A500282F50000100000028 /* ConstellationInfoBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A500272F50000100000027 /* ConstellationInfoBottomSheet.swift */; }; A5A5002A2F5000010000002A /* DateTimeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A500292F50000100000029 /* DateTimeSelectionView.swift */; }; A5A5002C2F5000010000002C /* StarMapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A5002B2F5000010000002B /* StarMapButton.swift */; }; - A5A5002E2F5000010000002E /* StarMapResetButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A5002D2F5000010000002D /* StarMapResetButton.swift */; }; - A5A500302F50000100000030 /* StarMapTimeControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A5002F2F5000010000002F /* StarMapTimeControlButton.swift */; }; A5A500322F50000100000032 /* StarCompassButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A500312F50000100000031 /* StarCompassButton.swift */; }; A5A502022F51000100000102 /* AstroContextMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A501022F51000100000102 /* AstroContextMenuItem.swift */; }; A5A502032F51000100000103 /* AstroContextUiState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A501032F51000100000103 /* AstroContextUiState.swift */; }; @@ -1725,7 +1723,7 @@ CE7CE0EE2FED645D001709F0 /* StarMapMyDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CE0ED2FED645D001709F0 /* StarMapMyDataViewController.swift */; }; CE7CE0F02FED8435001709F0 /* StarMapSearchExploreAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CE0EF2FED8435001709F0 /* StarMapSearchExploreAdapter.swift */; }; CE8215E52FFF6376002A8617 /* StarMapArControlCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */; }; - CE8215E72FFF6AF9002A8617 /* StarMapPlainIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E62FFF6AF9002A8617 /* StarMapPlainIconButton.swift */; }; + CE8215E72FFF6AF9002A8617 /* StarMapPlainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E62FFF6AF9002A8617 /* StarMapPlainButton.swift */; }; CE83DB4A2FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE83DB492FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift */; }; CE83DB4C2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE83DB4B2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift */; }; CE8A82A92FCFE11F00EADFD8 /* MapVariantReplacementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */; }; @@ -5259,8 +5257,6 @@ A5A500272F50000100000027 /* ConstellationInfoBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstellationInfoBottomSheet.swift; sourceTree = ""; }; A5A500292F50000100000029 /* DateTimeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeSelectionView.swift; sourceTree = ""; }; A5A5002B2F5000010000002B /* StarMapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapButton.swift; sourceTree = ""; }; - A5A5002D2F5000010000002D /* StarMapResetButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapResetButton.swift; sourceTree = ""; }; - A5A5002F2F5000010000002F /* StarMapTimeControlButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapTimeControlButton.swift; sourceTree = ""; }; A5A500312F50000100000031 /* StarCompassButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarCompassButton.swift; sourceTree = ""; }; A5A501022F51000100000102 /* AstroContextMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstroContextMenuItem.swift; sourceTree = ""; }; A5A501032F51000100000103 /* AstroContextUiState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstroContextUiState.swift; sourceTree = ""; }; @@ -5775,7 +5771,7 @@ CE7CE0ED2FED645D001709F0 /* StarMapMyDataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMyDataViewController.swift; sourceTree = ""; }; CE7CE0EF2FED8435001709F0 /* StarMapSearchExploreAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchExploreAdapter.swift; sourceTree = ""; }; CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapArControlCard.swift; sourceTree = ""; }; - CE8215E62FFF6AF9002A8617 /* StarMapPlainIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapPlainIconButton.swift; sourceTree = ""; }; + CE8215E62FFF6AF9002A8617 /* StarMapPlainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapPlainButton.swift; sourceTree = ""; }; CE83DB492FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchSortFilterChipsView.swift; sourceTree = ""; }; CE83DB4B2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchSortFilterChipsProvider.swift; sourceTree = ""; }; CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVariantReplacementManager.swift; sourceTree = ""; }; @@ -10662,10 +10658,8 @@ A5A501002F51000100000100 /* contextmenu */, A5A503202F51000200000320 /* search */, A5A500292F50000100000029 /* DateTimeSelectionView.swift */, - CE8215E62FFF6AF9002A8617 /* StarMapPlainIconButton.swift */, + CE8215E62FFF6AF9002A8617 /* StarMapPlainButton.swift */, A5A5002B2F5000010000002B /* StarMapButton.swift */, - A5A5002D2F5000010000002D /* StarMapResetButton.swift */, - A5A5002F2F5000010000002F /* StarMapTimeControlButton.swift */, CE762AF32FFE251B001BF551 /* StarMapTimeControlCard.swift */, A5A500312F50000100000031 /* StarCompassButton.swift */, CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */, @@ -17575,8 +17569,6 @@ A5A500282F50000100000028 /* ConstellationInfoBottomSheet.swift in Sources */, A5A5002A2F5000010000002A /* DateTimeSelectionView.swift in Sources */, A5A5002C2F5000010000002C /* StarMapButton.swift in Sources */, - A5A5002E2F5000010000002E /* StarMapResetButton.swift in Sources */, - A5A500302F50000100000030 /* StarMapTimeControlButton.swift in Sources */, A5A500322F50000100000032 /* StarCompassButton.swift in Sources */, A5A502022F51000100000102 /* AstroContextMenuItem.swift in Sources */, A5A502032F51000100000103 /* AstroContextUiState.swift in Sources */, @@ -18708,7 +18700,7 @@ 320F71272A823FB20071C0E7 /* PopularArticles.swift in Sources */, DA5A850B26C563A900F274C7 /* OAButton.m in Sources */, DA5A81C126C563A700F274C7 /* OAPOIParser.m in Sources */, - CE8215E72FFF6AF9002A8617 /* StarMapPlainIconButton.swift in Sources */, + CE8215E72FFF6AF9002A8617 /* StarMapPlainButton.swift in Sources */, D7B76D0D2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift in Sources */, FAF6C89B2AD013F400A3ED94 /* UIColor+Extension.swift in Sources */, DA5A824C26C563A700F274C7 /* OANavigationLanguageViewController.m in Sources */, diff --git a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift index ef191b16fc..9196385f6a 100644 --- a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift +++ b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift @@ -22,6 +22,7 @@ final class DateTimeSelectionView: UIView { private var currentDate = Date() private var onDateTimeChangeListener: ((Date) -> Void)? private var labels: [Field: UILabel] = [:] + private var buttons: [UIButton] = [] override init(frame: CGRect) { super.init(frame: frame) @@ -33,6 +34,13 @@ final class DateTimeSelectionView: UIView { initViews() } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + applyColors() + } + } + func setOnDateTimeChangeListener(_ listener: @escaping (Date) -> Void) { onDateTimeChangeListener = listener } @@ -45,10 +53,27 @@ final class DateTimeSelectionView: UIView { func getDateTime() -> Date { currentDate } + + private func applyColors() { + let isNightMode = OADayNightHelper.instance().isNightMode() + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: isNightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) + layer.borderWidth = isNightMode ? 2 : 0 + let color: UIColor = isNightMode ? .textColorPrimary.dark : .textColorPrimary.light + labels.forEach { + $1.textColor = color + } + buttons.forEach { + $0.tintColor = color + } + } private func initViews() { layer.cornerRadius = 24 - layer.shadowOpacity = 0 + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.35 + layer.shadowRadius = 5 + layer.shadowOffset = CGSize(width: 0, height: 2) + layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor clipsToBounds = true let stack = UIStackView() @@ -73,8 +98,6 @@ final class DateTimeSelectionView: UIView { nightMode: OADayNightHelper.instance().isNightMode(), cornerRadius: 24 ) - - backgroundColor = StarMapControlTheme.defaultBackground(nightMode: OADayNightHelper.instance().isNightMode(), alpha: 0.5) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor), @@ -83,6 +106,7 @@ final class DateTimeSelectionView: UIView { stack.bottomAnchor.constraint(equalTo: bottomAnchor) ]) updateDisplay() + applyColors() } private func addColumn(_ field: Field, to parent: UIStackView) { @@ -98,7 +122,7 @@ final class DateTimeSelectionView: UIView { column.addArrangedSubview(up) let label = UILabel() - label.textColor = .white + label.textColor = OADayNightHelper.instance().isNightMode() ? .textColorPrimary.dark : .textColorPrimary.light label.font = UIFont.monospacedDigitSystemFont(ofSize: 18, weight: .bold) label.textAlignment = .center label.widthAnchor.constraint(greaterThanOrEqualToConstant: field == .year ? 52 : 32).isActive = true @@ -116,10 +140,11 @@ final class DateTimeSelectionView: UIView { private func makeStepButton(iconName: String) -> UIButton { let button = UIButton(type: .system) - button.tintColor = .white + button.tintColor = OADayNightHelper.instance().isNightMode() ? .textColorPrimary.dark : .textColorPrimary.light button.setImage(AstroIcon.template(iconName), for: .normal) button.widthAnchor.constraint(equalToConstant: 40).isActive = true button.heightAnchor.constraint(equalToConstant: 40).isActive = true + buttons.append(button) return button } diff --git a/Sources/Plugins/Astronomy/StarMapArControlCard.swift b/Sources/Plugins/Astronomy/StarMapArControlCard.swift index 98aa1c7e98..7dbe26dbfc 100644 --- a/Sources/Plugins/Astronomy/StarMapArControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapArControlCard.swift @@ -19,8 +19,8 @@ final class StarMapArControlCard: UIView { var onResetTapped: (() -> Void)? var onTransparencyChanged: ((Int) -> Void)? - private let arButton = StarMapPlainIconButton() - private let resetButton = StarMapPlainIconButton() + private let arButton = StarMapPlainButton() + private let resetButton = StarMapPlainButton() private let transparencySlider = UISlider() private let sliderContainer = UIView() private let cameraControlsStack = UIStackView() @@ -42,7 +42,7 @@ final class StarMapArControlCard: UIView { func setArActive(_ active: Bool) { arActive = active - arButton.active = active + arButton.updateTheme(nightmod: nightMode, active: active) updateTheme(nightMode: nightMode, arActive: active) } @@ -64,12 +64,13 @@ final class StarMapArControlCard: UIView { nightMode: nightMode, cornerRadius: Self.cornerRadius ) - backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.5) + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) - resetButton.nightMode = nightMode - resetButton.active = false + resetButton.updateTheme(nightmod: nightMode, active: false) updateArButtonAppearance() + + layer.borderWidth = nightMode ? 2 : 0 } override func layoutSubviews() { @@ -87,9 +88,10 @@ final class StarMapArControlCard: UIView { translatesAutoresizingMaskIntoConstraints = false layer.cornerRadius = Self.cornerRadius layer.shadowColor = UIColor.black.cgColor - layer.shadowOpacity = 0.16 - layer.shadowRadius = 6 + layer.shadowOpacity = 0.35 + layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) + layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor configurePlainButton( arButton, @@ -103,6 +105,12 @@ final class StarMapArControlCard: UIView { accessibilityLabel: localizedString("shared_string_reset"), action: #selector(resetButtonTapped) ) + + arButton.customColorTintActive = StarMapControlTheme.activeForeground(nightMode: nightMode) + arButton.onHighlightChange = { [weak self] isHighlighted in + guard let self else { return } + arButton.updateTheme(nightmod: nightMode, active: arActive) + } transparencySlider.minimumValue = 0 transparencySlider.maximumValue = 100 @@ -158,7 +166,7 @@ final class StarMapArControlCard: UIView { } private func configurePlainButton( - _ button: StarMapPlainIconButton, + _ button: StarMapPlainButton, iconName: String, accessibilityLabel: String, action: Selector @@ -170,8 +178,7 @@ final class StarMapArControlCard: UIView { private func updateArButtonAppearance() { let iconName = arActive ? "ic_custom_view_in_ar_filled" : "ic_custom_view_in_ar" arButton.setIcon(iconName, accessibilityLabel: localizedString("astro_ar")) - arButton.nightMode = nightMode - arButton.active = arActive + arButton.updateTheme(nightmod: nightMode, active: arActive) } @objc private func arButtonTapped() { diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index c6178448a6..4db6151ba3 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -9,6 +9,8 @@ import UIKit enum StarMapControlTheme { + static let defaultBackgroundAlpha: CGFloat = 0.7 + static func resolved(_ color: UIColor, nightMode: Bool) -> UIColor { nightMode ? color.dark : color.light } @@ -18,7 +20,7 @@ enum StarMapControlTheme { } static func activeBackground(alpha: CGFloat = 1) -> UIColor { - UIColor.mapButtonBgColorActive.withAlphaComponent(alpha) + .mapButtonBgColorActive.withAlphaComponent(alpha) } static func foreground(active: Bool, nightMode: Bool) -> UIColor { @@ -59,18 +61,13 @@ class StarMapButton: OAHudButton { } func updateTheme() { - guard showsHudChrome else { - applyPlainTheme() - return - } - if active { unpressedColorDay = .mapButtonBgColorActive.light unpressedColorNight = .mapButtonBgColorActive.dark tintColorDay = .white tintColorNight = .white borderWidthDay = 0 - borderWidthNight = 0 + borderWidthNight = 2 } else { unpressedColorDay = .mapButtonBgColorDefault.light unpressedColorNight = .mapButtonBgColorDefault.dark @@ -79,10 +76,17 @@ class StarMapButton: OAHudButton { borderWidthDay = 0 borderWidthNight = 2 } + + guard showsHudChrome else { + applyPlainTheme() + return + } updateColors(forPressedState: isHighlighted) if active { - backgroundColor = StarMapControlTheme.activeBackground(alpha: 0.5) + backgroundColor = StarMapControlTheme.activeBackground(alpha: StarMapControlTheme.defaultBackgroundAlpha) + } else { + backgroundColor = backgroundColor?.withAlphaComponent(StarMapControlTheme.defaultBackgroundAlpha) } } @@ -118,7 +122,7 @@ class StarMapButton: OAHudButton { private func applyPlainTheme() { backgroundColor = .clear - layer.borderWidth = 0 + layer.borderWidth = nightMode ? 2 : 0 layer.shadowOpacity = 0 let color = active diff --git a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift index b259e0e96b..26060f5486 100644 --- a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift +++ b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift @@ -29,7 +29,7 @@ enum StarMapGlassBackground { } else { view.backgroundColor = active ? StarMapControlTheme.activeBackground() - : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.5) + : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) view.layer.cornerRadius = cornerRadius } } diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift index b074ff3937..40ddf8a114 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift @@ -10,6 +10,12 @@ import UIKit final class StarMapMagnitudeFilterButton: StarMapButton { private static let pillCornerRadius: Int32 = 24 + + override var isHighlighted: Bool { + didSet { + updateTheme() + } + } private let iconView = UIImageView(image: .icCustomMagnitude) private let valueLabel = UILabel() @@ -35,14 +41,21 @@ final class StarMapMagnitudeFilterButton: StarMapButton { setImage(nil, for: .normal) imageView?.isHidden = true - let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) - iconView.tintColor = color - valueLabel.textColor = color + if isHighlighted { + let color = StarMapControlTheme.foreground(active: false, nightMode: nightMode).withAlphaComponent(0.4) + iconView.tintColor = color + valueLabel.textColor = color + } else { + let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) + iconView.tintColor = color + valueLabel.textColor = color + } + + layer.borderWidth = nightMode ? 2 : 0 } override func layoutSubviews() { super.layoutSubviews() - guard showsHudChrome, bounds.width > 0, bounds.height > 0 else { return } if #available(iOS 26.0, *) { subviews.compactMap { $0 as? UIVisualEffectView }.forEach { $0.frame = bounds diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift index 7b288908e0..b723a2093e 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift @@ -52,11 +52,13 @@ final class StarMapMagnitudeSliderPanel: UIView { nightMode: nightMode, cornerRadius: Self.cornerRadius ) - let color = StarMapControlTheme.foreground(active: true, nightMode: nightMode) + let color: UIColor = OADayNightHelper.instance().isNightMode() ? .textColorPrimary.dark : .textColorPrimary.light titleLabel.textColor = color valueLabel.textColor = color - backgroundColor = StarMapControlTheme.defaultBackground(nightMode: OADayNightHelper.instance().isNightMode(), alpha: 0.5) + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: OADayNightHelper.instance().isNightMode(), alpha: StarMapControlTheme.defaultBackgroundAlpha) + + layer.borderWidth = nightMode ? 2 : 0 } override func layoutSubviews() { @@ -72,11 +74,12 @@ final class StarMapMagnitudeSliderPanel: UIView { private func setupContent(maxMagnitude: Double) { translatesAutoresizingMaskIntoConstraints = false + layer.cornerRadius = Self.cornerRadius layer.shadowColor = UIColor.black.cgColor - layer.shadowOpacity = 0.16 - layer.shadowRadius = 6 + layer.shadowOpacity = 0.35 + layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) - layer.cornerRadius = Self.cornerRadius + layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor titleLabel.text = localizedString("astro_min_magnitude") titleLabel.font = .preferredFont(forTextStyle: .subheadline) diff --git a/Sources/Plugins/Astronomy/StarMapPlainButton.swift b/Sources/Plugins/Astronomy/StarMapPlainButton.swift new file mode 100644 index 0000000000..730bcf86c3 --- /dev/null +++ b/Sources/Plugins/Astronomy/StarMapPlainButton.swift @@ -0,0 +1,68 @@ +// +// StarMapPlainIconButton.swift +// OsmAnd Maps +// +// Created by Vitaliy Sova on 09.07.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit + +final class StarMapPlainButton: UIButton { + var onHighlightChange: ((Bool) -> Void)? + + private var active = false + private var nightMode = false + + var customColorTintActive: UIColor? + + override var isHighlighted: Bool { + didSet { + onHighlightChange?(isHighlighted) + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func updateTheme(nightmod: Bool, active: Bool) { + self.nightMode = nightmod + self.active = active + applyColors() + } + + func setIcon(_ iconName: String, accessibilityLabel: String) { + setImage(AstroIcon.template(iconName), for: .normal) + self.accessibilityLabel = accessibilityLabel + applyColors() + } + + private func setup() { + backgroundColor = .clear + imageView?.contentMode = .scaleAspectFit + + applyColors() + } + + private func applyColors() { + let color: UIColor + if isHighlighted { + color = customColorTintActive ?? StarMapControlTheme.foreground(active: false, nightMode: nightMode).withAlphaComponent(0.4) + } else if active, let customColorTintActive { + color = customColorTintActive + } else { + color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) + } + print(color, active, isHighlighted) + + tintColor = color + setTitleColor(color, for: .normal) + } +} diff --git a/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift b/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift deleted file mode 100644 index 7887c08e97..0000000000 --- a/Sources/Plugins/Astronomy/StarMapPlainIconButton.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// StarMapPlainIconButton.swift -// OsmAnd Maps -// -// Created by Vitaliy Sova on 09.07.2026. -// Copyright © 2026 OsmAnd. All rights reserved. -// - -import UIKit - -final class StarMapPlainIconButton: UIButton { - var active = false { - didSet { applyColors() } - } - var nightMode = false { - didSet { applyColors() } - } - - override var isHighlighted: Bool { - didSet { applyColors() } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - func setIcon(_ iconName: String, accessibilityLabel: String) { - setImage(AstroIcon.template(iconName), for: .normal) - self.accessibilityLabel = accessibilityLabel - applyColors() - } - - private func setup() { - backgroundColor = .clear - imageView?.contentMode = .scaleAspectFit - applyColors() - } - - private func applyColors() { - let color: UIColor - if isHighlighted || active { - color = StarMapControlTheme.activeForeground(nightMode: nightMode) - } else { - color = StarMapControlTheme.foreground(active: false, nightMode: nightMode) - } - tintColor = color - } -} diff --git a/Sources/Plugins/Astronomy/StarMapResetButton.swift b/Sources/Plugins/Astronomy/StarMapResetButton.swift deleted file mode 100644 index 0c49418bc4..0000000000 --- a/Sources/Plugins/Astronomy/StarMapResetButton.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// StarMapResetButton.swift -// OsmAnd Maps -// -// Created by Codex on 20.05.2026. -// Copyright (c) 2026 OsmAnd. All rights reserved. -// - -import UIKit - -final class StarMapResetButton: UIButton { - var active = false { - didSet { applyColors() } - } - var nightMode = false { - didSet { applyColors() } - } - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - private func setup() { - backgroundColor = .clear - setImage(.icCustomRefresh, for: .normal) - imageView?.contentMode = .scaleAspectFit - accessibilityLabel = localizedString("shared_string_reset") - applyColors() - } - private func applyColors() { - let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) - tintColor = color - setTitleColor(color, for: .normal) - } -} diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift b/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift deleted file mode 100644 index 1936bb3f6a..0000000000 --- a/Sources/Plugins/Astronomy/StarMapTimeControlButton.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// StarMapTimeControlButton.swift -// OsmAnd Maps -// -// Created by Codex on 20.05.2026. -// Copyright (c) 2026 OsmAnd. All rights reserved. -// - -import UIKit - -final class StarMapTimeControlButton: UIButton { - var active = false { - didSet { applyColors() } - } - var nightMode = false { - didSet { applyColors() } - } - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - private func setup() { - backgroundColor = .clear - titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) - contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8) - setImage(AstroIcon.template("ic_action_time"), for: .normal) - imageView?.contentMode = .scaleAspectFit - applyColors() - } - private func applyColors() { - let color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) - tintColor = color - setTitleColor(color, for: .normal) - } -} diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift index c4259991b4..3a88d36ed0 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift @@ -15,9 +15,11 @@ final class StarMapTimeControlCard: UIView { var onTimeButtonTapped: (() -> Void)? var onResetTapped: (() -> Void)? - private let timeButton = StarMapTimeControlButton() - private let resetButton = StarMapResetButton() + private let timeButton = StarMapPlainButton() + private let resetButton = StarMapPlainButton() private let mainStack = UIStackView() + + private var active: Bool = false override init(frame: CGRect) { super.init(frame: frame) @@ -38,21 +40,29 @@ final class StarMapTimeControlCard: UIView { mainStack.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: resetButton.isHidden ? 16 : 0) } - func updateTheme(nightMode: Bool, active: Bool) { + func updateTheme(nightMode: Bool, active: Bool, pressed: Bool = false) { + self.active = active + StarMapGlassBackground.apply( to: self, active: active, nightMode: nightMode, cornerRadius: Self.cornerRadius ) - timeButton.nightMode = nightMode - timeButton.active = active - resetButton.nightMode = nightMode - resetButton.active = active + timeButton.updateTheme(nightmod: nightMode, active: active) + resetButton.updateTheme(nightmod: nightMode, active: active) + + if pressed { + let pressedColorDay = UIColor(rgb: color_on_map_icon_background_color_tap_light).withAlphaComponent(StarMapControlTheme.defaultBackgroundAlpha) + let pressedColorNight = UIColor(rgb: color_on_map_icon_background_color_tap_dark).withAlphaComponent(StarMapControlTheme.defaultBackgroundAlpha) + backgroundColor = nightMode ? pressedColorNight : pressedColorDay + } else { + backgroundColor = active + ? StarMapControlTheme.activeBackground(alpha: StarMapControlTheme.defaultBackgroundAlpha) + : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) + } - backgroundColor = active - ? StarMapControlTheme.activeBackground(alpha: 0.5) - : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: 0.5) + layer.borderWidth = nightMode ? 2 : 0 } override func layoutSubviews() { @@ -70,14 +80,24 @@ final class StarMapTimeControlCard: UIView { translatesAutoresizingMaskIntoConstraints = false layer.cornerRadius = Self.cornerRadius layer.shadowColor = UIColor.black.cgColor - layer.shadowOpacity = 0.16 - layer.shadowRadius = 6 + layer.shadowOpacity = 0.35 + layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) + layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor + timeButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + timeButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8) + timeButton.setImage(AstroIcon.template("ic_action_time"), for: .normal) timeButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) timeButton.addTarget(self, action: #selector(timeButtonTapped), for: .touchUpInside) timeButton.heightAnchor.constraint(equalToConstant: Self.height).isActive = true + timeButton.onHighlightChange = { [weak self] isHighlighted in + guard let self else { return } + updateTheme(nightMode: OADayNightHelper.instance().isNightMode(), active: active, pressed: isHighlighted) + } + resetButton.setImage(.icCustomReset, for: .normal) + resetButton.accessibilityLabel = localizedString("shared_string_reset") resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside) resetButton.isHidden = true resetButton.widthAnchor.constraint(equalToConstant: Self.height).isActive = true From 90d5a26d37ffb346ab8b3e6919d8e5fb0e9fce51 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Fri, 10 Jul 2026 14:25:23 +0300 Subject: [PATCH 08/20] Use color asset extensions in AstroUtils --- Sources/Plugins/Astronomy/AstroUtils.swift | 84 +++++++++++----------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Sources/Plugins/Astronomy/AstroUtils.swift b/Sources/Plugins/Astronomy/AstroUtils.swift index cbdd69be6b..56cc955c1c 100644 --- a/Sources/Plugins/Astronomy/AstroUtils.swift +++ b/Sources/Plugins/Astronomy/AstroUtils.swift @@ -60,18 +60,18 @@ private final class AstroRedFilterOverlayView: UIView { super.init(coder: coder) commonInit() } - - private func commonInit() { - isUserInteractionEnabled = false - backgroundColor = UIColor(named: "mapNightFilter")! - layer.compositingFilter = "multiplyBlendMode" - } - + override func layoutSubviews() { super.layoutSubviews() layer.cornerRadius = superview?.layer.cornerRadius ?? 0 layer.masksToBounds = layer.cornerRadius > 0 } + + private func commonInit() { + isUserInteractionEnabled = false + backgroundColor = .mapNightFilter + layer.compositingFilter = "multiplyBlendMode" + } } enum AstroRedFilter { @@ -111,6 +111,17 @@ enum AstroRedFilter { } enum AstroUtils { + struct Twilight { + let sunrise: Date? + let sunset: Date? + let civilDawn: Date? + let civilDusk: Date? + let nauticalDawn: Date? + let nauticalDusk: Date? + let astroDawn: Date? + let astroDusk: Date? + } + private static let customStarLock = NSLock() static let solarSystemWikidataIds: [String: Body] = [ @@ -126,17 +137,6 @@ enum AstroUtils { "Q339": Body.pluto ] - struct Twilight { - let sunrise: Date? - let sunset: Date? - let civilDawn: Date? - let civilDusk: Date? - let nauticalDawn: Date? - let nauticalDusk: Date? - let astroDawn: Date? - let astroDusk: Date? - } - static func astronomyTime(from date: Date) -> Time { Time.companion.fromMillisecondsSince1970(millis: Int64(date.timeIntervalSince1970 * 1000.0)) } @@ -303,19 +303,6 @@ enum AstroUtils { return formatter.string(from: date) } - private static func filterRiseSetDate(_ date: Date?, windowStart: Date?, windowEnd: Date?) -> Date? { - guard let date else { - return nil - } - if let windowStart, date < windowStart { - return nil - } - if let windowEnd, date > windowEnd { - return nil - } - return date - } - static func bodyName(_ body: Body) -> String { bodyDisplayName(body) } @@ -352,17 +339,17 @@ enum AstroUtils { static func color(for body: Body) -> UIColor { if body === Body.sun { - return UIColor(named: "solarSun")! + return .solarSun } else if body === Body.moon { - return UIColor(named: "solarMoon")! + return .solarMoon } else if body === Body.mars { - return UIColor(named: "solarMars")! + return .solarMars } else if body === Body.jupiter { - return UIColor(named: "solarJupiter")! + return .solarJupiter } else if body === Body.saturn { - return UIColor(named: "solarSaturn")! + return .solarSaturn } else if body === Body.neptune || body === Body.uranus { - return UIColor(named: "solarUranusNeptune")! + return .solarUranusNeptune } else { return UIColor(red: 0.87, green: 0.90, blue: 1.0, alpha: 1.0) } @@ -371,15 +358,15 @@ enum AstroUtils { static func color(for type: SkyObjectType, magnitude: Double?) -> UIColor { switch type { case .STAR: - return UIColor(named: "starDot")! + return .starDot case .GALAXY, .GALAXY_CLUSTER: - return UIColor(named: "deepSkyGalaxyDot")! + return .deepSkyGalaxyDot case .NEBULA: - return UIColor(named: "deepSkyNebulaDot")! + return .deepSkyNebulaDot case .OPEN_CLUSTER, .GLOBULAR_CLUSTER: - return UIColor(named: "deepSkyClusterDot")! + return .deepSkyClusterDot case .BLACK_HOLE: - return UIColor(named: "deepSkyBlackHoleDot")! + return .deepSkyBlackHoleDot case .CONSTELLATION: return UIColor(red: 0.80, green: 0.86, blue: 1.0, alpha: 1.0) case .SUN, .MOON, .PLANET: @@ -477,4 +464,17 @@ enum AstroUtils { formatter.timeStyle = .short return formatter.string(from: date) } + + private static func filterRiseSetDate(_ date: Date?, windowStart: Date?, windowEnd: Date?) -> Date? { + guard let date else { + return nil + } + if let windowStart, date < windowStart { + return nil + } + if let windowEnd, date > windowEnd { + return nil + } + return date + } } From 08ace4e975f608a1c3b2a796bfb6193e5424563e Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Tue, 14 Jul 2026 11:53:32 +0300 Subject: [PATCH 09/20] Fix buttons opacity --- Sources/Plugins/Astronomy/StarMapButton.swift | 13 ++++++++----- .../Astronomy/StarMapMagnitudeFilterButton.swift | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index 4db6151ba3..4f529b8eea 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -53,6 +53,13 @@ class StarMapButton: OAHudButton { super.init(coder: coder) commonInit() } + + override func updateColors(forPressedState isPressed: Bool) { + super.updateColors(forPressedState: isPressed) + if showsHudChrome { + backgroundColor = backgroundColor?.withAlphaComponent(StarMapControlTheme.defaultBackgroundAlpha) + } + } func setIcon(iconName: String, accessibilityLabel: String? = nil) { setImage(AstroIcon.template(iconName), for: .normal) @@ -102,14 +109,10 @@ class StarMapButton: OAHudButton { var opacity: Double if #available(iOS 26.0, *) { glassStyle = Int32(UIGlassEffect.Style.clear.rawValue) - opacity = 0.5 } else { glassStyle = MapButtonState.defaultGlassStyle - opacity = helper.getDefaultOpacityPref().get() - if opacity < 0 { - opacity = MapButtonState.opaqueAlpha - } } + opacity = Double(StarMapControlTheme.defaultBackgroundAlpha) setCustomAppearanceParams(ButtonAppearanceParams( iconName: nil, diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift index 40ddf8a114..9fa95f2344 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift @@ -105,7 +105,7 @@ final class StarMapMagnitudeFilterButton: StarMapButton { var glassStyle: Int32 if #available(iOS 26.0, *) { glassStyle = Int32(UIGlassEffect.Style.clear.rawValue) - opacity = 0.5 + opacity = Double(StarMapControlTheme.defaultBackgroundAlpha) } else { glassStyle = MapButtonState.defaultGlassStyle } From 9b2abf72c81bb3b52c933078fa83c28737b82479 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Tue, 14 Jul 2026 16:04:05 +0300 Subject: [PATCH 10/20] DayNight cache --- .../Astronomy/DateTimeSelectionView.swift | 26 +++++++++---------- .../Plugins/Astronomy/StarCompassButton.swift | 1 + .../Astronomy/StarMapArControlCard.swift | 2 +- .../StarMapMagnitudeSliderPanel.swift | 5 ++-- .../Astronomy/StarMapTimeControlCard.swift | 7 ++--- .../Astronomy/StarMapViewController.swift | 22 +++++++++++----- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift index 9196385f6a..1ba5e5923f 100644 --- a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift +++ b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift @@ -23,6 +23,7 @@ final class DateTimeSelectionView: UIView { private var onDateTimeChangeListener: ((Date) -> Void)? private var labels: [Field: UILabel] = [:] private var buttons: [UIButton] = [] + private var nightMode: Bool = false override init(frame: CGRect) { super.init(frame: frame) @@ -34,13 +35,6 @@ final class DateTimeSelectionView: UIView { initViews() } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - applyColors() - } - } - func setOnDateTimeChangeListener(_ listener: @escaping (Date) -> Void) { onDateTimeChangeListener = listener } @@ -54,11 +48,15 @@ final class DateTimeSelectionView: UIView { currentDate } + func updateTheme(nightMod: Bool, active: Bool) { + self.nightMode = nightMod + applyColors() + } + private func applyColors() { - let isNightMode = OADayNightHelper.instance().isNightMode() - backgroundColor = StarMapControlTheme.defaultBackground(nightMode: isNightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) - layer.borderWidth = isNightMode ? 2 : 0 - let color: UIColor = isNightMode ? .textColorPrimary.dark : .textColorPrimary.light + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) + layer.borderWidth = nightMode ? 2 : 0 + let color: UIColor = nightMode ? .textColorPrimary.dark : .textColorPrimary.light labels.forEach { $1.textColor = color } @@ -95,7 +93,7 @@ final class DateTimeSelectionView: UIView { StarMapGlassBackground.apply( to: self, active: false, - nightMode: OADayNightHelper.instance().isNightMode(), + nightMode: nightMode, cornerRadius: 24 ) @@ -122,7 +120,7 @@ final class DateTimeSelectionView: UIView { column.addArrangedSubview(up) let label = UILabel() - label.textColor = OADayNightHelper.instance().isNightMode() ? .textColorPrimary.dark : .textColorPrimary.light + label.textColor = nightMode ? .textColorPrimary.dark : .textColorPrimary.light label.font = UIFont.monospacedDigitSystemFont(ofSize: 18, weight: .bold) label.textAlignment = .center label.widthAnchor.constraint(greaterThanOrEqualToConstant: field == .year ? 52 : 32).isActive = true @@ -140,7 +138,7 @@ final class DateTimeSelectionView: UIView { private func makeStepButton(iconName: String) -> UIButton { let button = UIButton(type: .system) - button.tintColor = OADayNightHelper.instance().isNightMode() ? .textColorPrimary.dark : .textColorPrimary.light + button.tintColor = nightMode ? .textColorPrimary.dark : .textColorPrimary.light button.setImage(AstroIcon.template(iconName), for: .normal) button.widthAnchor.constraint(equalToConstant: 40).isActive = true button.heightAnchor.constraint(equalToConstant: 40).isActive = true diff --git a/Sources/Plugins/Astronomy/StarCompassButton.swift b/Sources/Plugins/Astronomy/StarCompassButton.swift index 92ef658c6d..b7f40ae50a 100644 --- a/Sources/Plugins/Astronomy/StarCompassButton.swift +++ b/Sources/Plugins/Astronomy/StarCompassButton.swift @@ -37,6 +37,7 @@ final class StarCompassButton: StarMapButton { } func setArDirectionEnabled(_ enabled: Bool) { + guard enabled != arDirectionEnabled else { return } arDirectionEnabled = enabled updateArrowIcon() } diff --git a/Sources/Plugins/Astronomy/StarMapArControlCard.swift b/Sources/Plugins/Astronomy/StarMapArControlCard.swift index 7dbe26dbfc..56c887db1b 100644 --- a/Sources/Plugins/Astronomy/StarMapArControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapArControlCard.swift @@ -26,7 +26,7 @@ final class StarMapArControlCard: UIView { private let cameraControlsStack = UIStackView() private let rootStack = UIStackView() - private var nightMode = OADayNightHelper.instance().isNightMode() + private var nightMode = false private var arActive = false override init(frame: CGRect) { diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift index b723a2093e..f343370096 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift @@ -27,7 +27,6 @@ final class StarMapMagnitudeSliderPanel: UIView { super.init(frame: .zero) setupContent(maxMagnitude: maxMagnitude) isHidden = true - updateTheme(nightMode: OADayNightHelper.instance().isNightMode()) } required init?(coder: NSCoder) { @@ -52,11 +51,11 @@ final class StarMapMagnitudeSliderPanel: UIView { nightMode: nightMode, cornerRadius: Self.cornerRadius ) - let color: UIColor = OADayNightHelper.instance().isNightMode() ? .textColorPrimary.dark : .textColorPrimary.light + let color: UIColor = nightMode ? .textColorPrimary.dark : .textColorPrimary.light titleLabel.textColor = color valueLabel.textColor = color - backgroundColor = StarMapControlTheme.defaultBackground(nightMode: OADayNightHelper.instance().isNightMode(), alpha: StarMapControlTheme.defaultBackgroundAlpha) + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) layer.borderWidth = nightMode ? 2 : 0 } diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift index 3a88d36ed0..31142e5682 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift @@ -20,11 +20,11 @@ final class StarMapTimeControlCard: UIView { private let mainStack = UIStackView() private var active: Bool = false - + private var nightMode: Bool = false + override init(frame: CGRect) { super.init(frame: frame) setupContent() - updateTheme(nightMode: OADayNightHelper.instance().isNightMode(), active: false) } required init?(coder: NSCoder) { @@ -41,6 +41,7 @@ final class StarMapTimeControlCard: UIView { } func updateTheme(nightMode: Bool, active: Bool, pressed: Bool = false) { + self.nightMode = nightMode self.active = active StarMapGlassBackground.apply( @@ -93,7 +94,7 @@ final class StarMapTimeControlCard: UIView { timeButton.heightAnchor.constraint(equalToConstant: Self.height).isActive = true timeButton.onHighlightChange = { [weak self] isHighlighted in guard let self else { return } - updateTheme(nightMode: OADayNightHelper.instance().isNightMode(), active: active, pressed: isHighlighted) + updateTheme(nightMode: nightMode, active: active, pressed: isHighlighted) } resetButton.setImage(.icCustomReset, for: .normal) diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index cd8d54ff57..b878912e19 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -75,6 +75,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private var screenOrientationObserver: NSObjectProtocol? private var leftPanelLeadingConstraint: NSLayoutConstraint? private var mapVisibleAreaLeadingConstraint: NSLayoutConstraint? + private var nightMode: Bool = false private var isApplyingControlChange = false private var isGyroActive: Bool { arModeHelper.isArModeEnabled @@ -109,6 +110,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { settings = loadedSettings dataProvider = provider viewModel = StarObjectsViewModel(provider: provider, settings: loadedSettings) + nightMode = OADayNightHelper.instance().isNightMode() super.init(nibName: nil, bundle: nil) } @@ -174,6 +176,14 @@ final class StarMapViewController: UIViewController, StarViewDelegate { cameraHelper.layoutPreview() } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + guard traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else { + return + } + updateMapControlThemes() + } + func applyRedFilter(enabled: Bool) { starView.showRedFilter = enabled updateRedMode(enabled) @@ -714,9 +724,9 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func updateTimeControlTheme() { - let nightMode = OADayNightHelper.instance().isNightMode() let active = !timeSelectionView.isHidden timeControlCard.updateTheme(nightMode: nightMode, active: active) + timeSelectionView.updateTheme(nightMod: nightMode, active: active) } private func updateMagnitudeControls() { @@ -731,8 +741,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func updateMagnitudeFilterTheme() { - let nightMode = OADayNightHelper.instance().isNightMode() - magnitudeFilterButton.nightMode = nightMode magnitudeFilterButton.active = magnitudeSliderPanel.isExpanded @@ -790,7 +798,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func syncControlUI() { let gyroEnabled = isGyroActive let cameraEnabled = isArCameraActive - let nightMode = OADayNightHelper.instance().isNightMode() compassButton.setArDirectionEnabled(gyroEnabled) @@ -812,7 +819,6 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func updateButtonsNightMode() { - let nightMode = OADayNightHelper.instance().isNightMode() arControlCard.updateTheme(nightMode: nightMode, arActive: isArCameraActive) closeButton.nightMode = nightMode settingsButton.nightMode = nightMode @@ -927,7 +933,11 @@ final class StarMapViewController: UIViewController, StarViewDelegate { @objc private func onDayNightModeChanged() { DispatchQueue.main.async { [weak self] in - self?.updateMapControlThemes() + guard let self else { return } + let newValue = OADayNightHelper.instance().isNightMode() + guard newValue != nightMode else { return } + nightMode = newValue + updateMapControlThemes() } } From 9a75cc1007c87587b5b4abb2f0b1b2bb2513be6c Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Wed, 15 Jul 2026 10:16:34 +0300 Subject: [PATCH 11/20] Refactor Astronomy plugin UI and settings management - Add mapButtonBorderColor asset and migrate color constants to asset-based system - Make AstronomyPluginSettings and nested types Codable for simplified JSON serialization - Refactor DateTimeSelectionView to use properties instead of setter methods - Update UI components to use typed icon assets instead of string-based icon names - Simplify glass background management with weak view references - Standardize control card theme methods (add border and pressed background) - Remove redundant manual JSON parsing/serialization code --- .../Contents.json | 38 +++ Sources/Constants/OAColors.h | 5 - Sources/Controllers/Map/OAHudButton.m | 6 +- .../Astronomy/AstronomyPluginSettings.swift | 254 +----------------- .../Astronomy/DateTimeSelectionView.swift | 46 ++-- .../Plugins/Astronomy/StarCompassButton.swift | 12 +- .../Astronomy/StarMapArControlCard.swift | 42 ++- Sources/Plugins/Astronomy/StarMapButton.swift | 17 +- .../Astronomy/StarMapCameraHelper.swift | 1 - .../Astronomy/StarMapGlassBackground.swift | 9 +- .../StarMapMagnitudeFilterButton.swift | 4 +- .../StarMapMagnitudeSliderPanel.swift | 27 +- .../Astronomy/StarMapPlainButton.swift | 12 +- .../Astronomy/StarMapTimeControlCard.swift | 30 +-- .../Astronomy/StarMapViewController.swift | 6 +- 15 files changed, 145 insertions(+), 364 deletions(-) create mode 100644 Resources/Images.xcassets/Colors/Map buttons/mapButtonBorderColor.colorset/Contents.json diff --git a/Resources/Images.xcassets/Colors/Map buttons/mapButtonBorderColor.colorset/Contents.json b/Resources/Images.xcassets/Colors/Map buttons/mapButtonBorderColor.colorset/Contents.json new file mode 100644 index 0000000000..e07f474632 --- /dev/null +++ b/Resources/Images.xcassets/Colors/Map buttons/mapButtonBorderColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x4D", + "green" : "0x4D", + "red" : "0x4D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x4D", + "green" : "0x4D", + "red" : "0x4D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Constants/OAColors.h b/Sources/Constants/OAColors.h index 2050769c38..4c91be28e3 100644 --- a/Sources/Constants/OAColors.h +++ b/Sources/Constants/OAColors.h @@ -43,11 +43,6 @@ #define color_dialog_buttons_dark 0xff8800 #define color_on_map_icon_color 0x505050 -#define color_on_map_icon_background_color_tap_light 0xE6E6E6 -#define color_on_map_icon_background_color_tap_dark 0x595959 - -#define color_on_map_icon_border_color 0x4D4D4D - #define color_sky_day 0x8cbed6 #define color_sky_night 0x05142e diff --git a/Sources/Controllers/Map/OAHudButton.m b/Sources/Controllers/Map/OAHudButton.m index 57728f6e29..2d7cbe807b 100644 --- a/Sources/Controllers/Map/OAHudButton.m +++ b/Sources/Controllers/Map/OAHudButton.m @@ -54,11 +54,11 @@ - (void)commonInit { self.unpressedColorDay = [UIColor colorNamed:ACColorNameMapButtonBgColorDefault].light; self.unpressedColorNight = [UIColor colorNamed:ACColorNameMapButtonBgColorDefault].dark; - self.pressedColorDay = UIColorFromRGB(color_on_map_icon_background_color_tap_light); - self.pressedColorNight = UIColorFromRGB(color_on_map_icon_background_color_tap_dark); + self.pressedColorDay = [UIColor colorNamed:ACColorNameMapButtonBgColorTap].light; + self.pressedColorNight = [UIColor colorNamed:ACColorNameMapButtonBgColorTap].dark; self.tintColorDay = [UIColor colorNamed:ACColorNameMapButtonIconColorDefault].light; self.tintColorNight = [UIColor colorNamed:ACColorNameMapButtonIconColorDefault].dark; - self.borderColor = UIColorFromRGB(color_on_map_icon_border_color); + self.borderColor = [UIColor colorNamed:ACColorNameMapButtonBorderColor]; self.borderWidthDay = 0; self.borderWidthNight = 2; diff --git a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift index 3d940fe3db..2af206b26f 100644 --- a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift +++ b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift @@ -9,8 +9,8 @@ import Foundation import UIKit -struct AstronomyPluginSettings { - enum DirectionColor: Int, CaseIterable { +struct AstronomyPluginSettings: Codable { + enum DirectionColor: Int, CaseIterable, Codable { case BLUE case GREEN case ORANGE @@ -39,24 +39,24 @@ struct AstronomyPluginSettings { } } - struct FavoriteConfig: Equatable { + struct FavoriteConfig: Equatable, Codable { var id: String } - struct DirectionConfig: Equatable { + struct DirectionConfig: Equatable, Codable { var id: String var colorIndex: Int = 0 } - struct CelestialPathConfig: Equatable { + struct CelestialPathConfig: Equatable, Codable { var id: String } - struct CommonConfig: Equatable { + struct CommonConfig: Equatable, Codable { var showRegularMap = false } - struct MapViewPosition: Equatable { + struct MapViewPosition: Equatable, Codable { var azimuth: Double var altitude: Double var viewAngle: Double @@ -65,7 +65,7 @@ struct AstronomyPluginSettings { var panY: Double = 0 } - struct StarMapConfig: Equatable { + struct StarMapConfig: Equatable, Codable { var showAzimuthalGrid = true var showEquatorialGrid = false var showEclipticLine = false @@ -97,46 +97,6 @@ struct AstronomyPluginSettings { } static let storageKey = "astronomy_settings" - private static let keyCommon = "common" - private static let keyShowRegularMap = "showRegularMap" - private static let keyStarMap = "star_map" - private static let keyShowAzimuthal = "showAzimuthalGrid" - private static let keyShowEquatorial = "showEquatorialGrid" - private static let keyShowEcliptic = "showEclipticLine" - private static let keyShowMeridian = "showMeridianLine" - private static let keyShowEquator = "showEquatorLine" - private static let keyShowGalactic = "showGalacticLine" - private static let keyShowSun = "showSun" - private static let keyShowMoon = "showMoon" - private static let keyShowPlanets = "showPlanets" - private static let keyShowFavorites = "showFavorites" - private static let keyShowDirections = "showDirections" - private static let keyShowCelestialPaths = "showCelestialPaths" - private static let keyShowRedFilter = "showRedFilter" - private static let keyShowConstellations = "showConstellations" - private static let keyShowStars = "showStars" - private static let keyShowGalaxies = "showGalaxies" - private static let keyShowNebulae = "showNebulae" - private static let keyShowOpenClusters = "showOpenClusters" - private static let keyShowGlobularClusters = "showGlobularClusters" - private static let keyShowGalaxyClusters = "showGalaxyClusters" - private static let keyShowBlackHoles = "showBlackHoles" - private static let keyIs2DMode = "is2DMode" - private static let keyShowMagnitudeFilter = "showMagnitudeFilter" - private static let keyMagnitudeFilter = "magnitudeFilter" - private static let keyFavorites = "favorites" - private static let keyDirections = "directions" - private static let keyCelestialPaths = "celestialPaths" - private static let keyId = "id" - private static let keyColorIndex = "colorIndex" - - private static let keySavedMapPosition = "savedMapPosition" - private static let keyLastAzimuth = "lastAzimuth" - private static let keyLastAltitude = "lastAltitude" - private static let keyLastViewAngle = "lastViewAngle" - private static let keyLastRoll = "lastRoll" - private static let keyLastPanX = "lastPanX" - private static let keyLastPanY = "lastPanY" private static let storageQueue = DispatchQueue(label: "net.osmand.astronomy.settings") @@ -156,26 +116,11 @@ struct AstronomyPluginSettings { } private static func loadUnlocked() -> AstronomyPluginSettings { - guard let root = settingsJsonUnlocked() else { - return AstronomyPluginSettings() - } - var settings = AstronomyPluginSettings() - settings.common = parseCommonConfig(root[Self.keyCommon] as? [String: Any]) - settings.starMap = parseStarMapConfig(root[Self.keyStarMap] as? [String: Any]) - return settings + UserDefaults.standard.object(AstronomyPluginSettings.self, with: storageKey) ?? AstronomyPluginSettings() } private static func saveUnlocked(_ settings: AstronomyPluginSettings) { - var root: [String: Any] = [:] - root[Self.keyCommon] = [ - Self.keyShowRegularMap: settings.common.showRegularMap - ] - root[Self.keyStarMap] = Self.serializeStarMapConfig(settings.starMap) - guard let data = try? JSONSerialization.data(withJSONObject: root), - let string = String(data: data, encoding: .utf8) else { - return - } - UserDefaults.standard.set(string, forKey: Self.storageKey) + UserDefaults.standard.set(object: settings, forKey: storageKey) } func getCommonConfig() -> CommonConfig { @@ -281,183 +226,4 @@ struct AstronomyPluginSettings { return updated } } - - private static func settingsJsonUnlocked() -> [String: Any]? { - guard let json = UserDefaults.standard.string(forKey: storageKey), - !json.isEmpty, - let data = json.data(using: .utf8), - let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return nil - } - return root - } - - private static func parseCommonConfig(_ json: [String: Any]?) -> CommonConfig { - CommonConfig(showRegularMap: bool(json?[keyShowRegularMap], fallback: false)) - } - - private static func parseStarMapConfig(_ json: [String: Any]?) -> StarMapConfig { - var nextColor = 0 - return StarMapConfig(showAzimuthalGrid: bool(json?[keyShowAzimuthal], fallback: true), - showEquatorialGrid: bool(json?[keyShowEquatorial], fallback: false), - showEclipticLine: bool(json?[keyShowEcliptic], fallback: false), - showMeridianLine: bool(json?[keyShowMeridian], fallback: false), - showEquatorLine: bool(json?[keyShowEquator], fallback: false), - showGalacticLine: bool(json?[keyShowGalactic], fallback: false), - showFavorites: bool(json?[keyShowFavorites], fallback: true), - showDirections: bool(json?[keyShowDirections], fallback: true), - showCelestialPaths: bool(json?[keyShowCelestialPaths], fallback: true), - showRedFilter: bool(json?[keyShowRedFilter], fallback: false), - showSun: bool(json?[keyShowSun], fallback: true), - showMoon: bool(json?[keyShowMoon], fallback: true), - showPlanets: bool(json?[keyShowPlanets], fallback: true), - showConstellations: bool(json?[keyShowConstellations], fallback: false), - showStars: bool(json?[keyShowStars], fallback: false), - showGalaxies: bool(json?[keyShowGalaxies], fallback: false), - showNebulae: bool(json?[keyShowNebulae], fallback: false), - showOpenClusters: bool(json?[keyShowOpenClusters], fallback: false), - showGlobularClusters: bool(json?[keyShowGlobularClusters], fallback: false), - showGalaxyClusters: bool(json?[keyShowGalaxyClusters], fallback: false), - showBlackHoles: bool(json?[keyShowBlackHoles], fallback: false), - is2DMode: bool(json?[keyIs2DMode], fallback: false), - showMagnitudeFilter: bool(json?[keyShowMagnitudeFilter], fallback: false), - magnitudeFilter: double(json?[keyMagnitudeFilter]), - favorites: parseItems(json?[keyFavorites]) { FavoriteConfig(id: $0) }, - directions: parseItems(json?[keyDirections]) { item, id in - defer { nextColor += 1 } - return DirectionConfig(id: id, - colorIndex: int(item[keyColorIndex], fallback: nextColor % DirectionColor.allCases.count)) - }, - celestialPaths: parseItems(json?[keyCelestialPaths]) { CelestialPathConfig(id: $0) }, - savedMapPosition: parseMapViewPosition(json?[keySavedMapPosition] as? [String: Any])) - } - - private static func parseMapViewPosition(_ json: [String: Any]?) -> MapViewPosition? { - guard let json, - let azimuth = double(json[keyLastAzimuth]), - let altitude = double(json[keyLastAltitude]), - let viewAngle = double(json[keyLastViewAngle]) else { - return nil - } - return MapViewPosition( - azimuth: azimuth, - altitude: altitude, - viewAngle: viewAngle, - roll: double(json[keyLastRoll]) ?? 0, - panX: double(json[keyLastPanX]) ?? 0, - panY: double(json[keyLastPanY]) ?? 0 - ) - } - - private static func serializeStarMapConfig(_ config: StarMapConfig) -> [String: Any] { - var json: [String: Any] = [ - keyShowAzimuthal: config.showAzimuthalGrid, - keyShowEquatorial: config.showEquatorialGrid, - keyShowEcliptic: config.showEclipticLine, - keyShowMeridian: config.showMeridianLine, - keyShowEquator: config.showEquatorLine, - keyShowGalactic: config.showGalacticLine, - keyShowFavorites: config.showFavorites, - keyShowDirections: config.showDirections, - keyShowCelestialPaths: config.showCelestialPaths, - keyShowRedFilter: config.showRedFilter, - keyShowSun: config.showSun, - keyShowMoon: config.showMoon, - keyShowPlanets: config.showPlanets, - keyShowConstellations: config.showConstellations, - keyShowStars: config.showStars, - keyShowGalaxies: config.showGalaxies, - keyShowNebulae: config.showNebulae, - keyShowOpenClusters: config.showOpenClusters, - keyShowGlobularClusters: config.showGlobularClusters, - keyShowGalaxyClusters: config.showGalaxyClusters, - keyShowBlackHoles: config.showBlackHoles, - keyIs2DMode: config.is2DMode, - keyShowMagnitudeFilter: config.showMagnitudeFilter - ] - if let magnitudeFilter = config.magnitudeFilter { - json[keyMagnitudeFilter] = magnitudeFilter - } - if !config.favorites.isEmpty { - json[keyFavorites] = config.favorites.map { [keyId: $0.id] } - } - if !config.directions.isEmpty { - json[keyDirections] = config.directions.map { [keyId: $0.id, keyColorIndex: $0.colorIndex] } - } - if !config.celestialPaths.isEmpty { - json[keyCelestialPaths] = config.celestialPaths.map { [keyId: $0.id] } - } - if let pos = config.savedMapPosition { - json[keySavedMapPosition] = [ - keyLastAzimuth: pos.azimuth, - keyLastAltitude: pos.altitude, - keyLastViewAngle: pos.viewAngle, - keyLastRoll: pos.roll, - keyLastPanX: pos.panX, - keyLastPanY: pos.panY - ] - } - return json - } - - private static func parseItems(_ value: Any?, factory: (String) -> T) -> [T] { - guard let array = value as? [[String: Any]] else { - return [] - } - return array.compactMap { item in - guard let id = item[keyId] as? String, !id.isEmpty else { - return nil - } - return factory(id) - } - } - - private static func parseItems(_ value: Any?, factory: ([String: Any], String) -> T) -> [T] { - guard let array = value as? [[String: Any]] else { - return [] - } - return array.compactMap { item in - guard let id = item[keyId] as? String, !id.isEmpty else { - return nil - } - return factory(item, id) - } - } - - private static func bool(_ value: Any?, fallback defaultValue: Bool) -> Bool { - if let value = value as? Bool { - return value - } - if let value = value as? NSNumber { - return value.boolValue - } - return defaultValue - } - - private static func int(_ value: Any?, fallback defaultValue: Int) -> Int { - if let value = value as? Int { - return value - } - if let value = value as? NSNumber { - return value.intValue - } - if let value = value as? String { - return Int(value) ?? defaultValue - } - return defaultValue - } - - private static func double(_ value: Any?) -> Double? { - if let value = value as? Double { - return value.isNaN ? nil : value - } - if let value = value as? NSNumber { - let double = value.doubleValue - return double.isNaN ? nil : double - } - if let value = value as? String, let double = Double(value) { - return double.isNaN ? nil : double - } - return nil - } } diff --git a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift index 1ba5e5923f..9a413dd8dc 100644 --- a/Sources/Plugins/Astronomy/DateTimeSelectionView.swift +++ b/Sources/Plugins/Astronomy/DateTimeSelectionView.swift @@ -17,13 +17,18 @@ final class DateTimeSelectionView: UIView { case minute } - private let calendar = Calendar.current + var onDateTimeChange: ((Date) -> Void)? - private var currentDate = Date() - private var onDateTimeChangeListener: ((Date) -> Void)? + var date = Date() { + didSet { + updateDisplay() + } + } + private let calendar = Calendar.current private var labels: [Field: UILabel] = [:] private var buttons: [UIButton] = [] private var nightMode: Bool = false + private weak var glassBackgroundView: UIVisualEffectView? override init(frame: CGRect) { super.init(frame: frame) @@ -35,19 +40,6 @@ final class DateTimeSelectionView: UIView { initViews() } - func setOnDateTimeChangeListener(_ listener: @escaping (Date) -> Void) { - onDateTimeChangeListener = listener - } - - func setDateTime(_ date: Date) { - currentDate = date - updateDisplay() - } - - func getDateTime() -> Date { - currentDate - } - func updateTheme(nightMod: Bool, active: Bool) { self.nightMode = nightMod applyColors() @@ -71,7 +63,7 @@ final class DateTimeSelectionView: UIView { layer.shadowOpacity = 0.35 layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) - layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor + layer.borderColor = StarMapControlTheme.border(nightMode: nightMode).cgColor clipsToBounds = true let stack = UIStackView() @@ -90,9 +82,8 @@ final class DateTimeSelectionView: UIView { addColumn(.hour, to: stack) addColumn(.minute, to: stack) - StarMapGlassBackground.apply( + glassBackgroundView = StarMapGlassBackground.apply( to: self, - active: false, nightMode: nightMode, cornerRadius: 24 ) @@ -113,7 +104,7 @@ final class DateTimeSelectionView: UIView { column.alignment = .center column.spacing = 2 - let up = makeStepButton(iconName: "ic_custom_arrow_up") + let up = makeStepButton(icon: .icCustomArrowUp) up.addAction(UIAction { [weak self] _ in self?.step(field, amount: field == .minute ? 5 : 1) }, for: .touchUpInside) @@ -127,7 +118,7 @@ final class DateTimeSelectionView: UIView { labels[field] = label column.addArrangedSubview(label) - let down = makeStepButton(iconName: "ic_custom_arrow_down") + let down = makeStepButton(icon: .icCustomArrowDown) down.addAction(UIAction { [weak self] _ in self?.step(field, amount: field == .minute ? -5 : -1) }, for: .touchUpInside) @@ -136,10 +127,10 @@ final class DateTimeSelectionView: UIView { parent.addArrangedSubview(column) } - private func makeStepButton(iconName: String) -> UIButton { + private func makeStepButton(icon: UIImage) -> UIButton { let button = UIButton(type: .system) button.tintColor = nightMode ? .textColorPrimary.dark : .textColorPrimary.light - button.setImage(AstroIcon.template(iconName), for: .normal) + button.setImage(icon, for: .normal) button.widthAnchor.constraint(equalToConstant: 40).isActive = true button.heightAnchor.constraint(equalToConstant: 40).isActive = true buttons.append(button) @@ -160,15 +151,14 @@ final class DateTimeSelectionView: UIView { case .minute: component = .minute } - if let date = calendar.date(byAdding: component, value: amount, to: currentDate) { - currentDate = date - updateDisplay() - onDateTimeChangeListener?(currentDate) + if let newDate = calendar.date(byAdding: component, value: amount, to: date) { + date = newDate + onDateTimeChange?(newDate) } } private func updateDisplay() { - let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: currentDate) + let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) labels[.year]?.text = String(format: "%04d", components.year ?? 0) labels[.month]?.text = String(format: "%02d", components.month ?? 0) labels[.day]?.text = String(format: "%02d", components.day ?? 0) diff --git a/Sources/Plugins/Astronomy/StarCompassButton.swift b/Sources/Plugins/Astronomy/StarCompassButton.swift index b7f40ae50a..2ec9b79829 100644 --- a/Sources/Plugins/Astronomy/StarCompassButton.swift +++ b/Sources/Plugins/Astronomy/StarCompassButton.swift @@ -50,11 +50,13 @@ final class StarCompassButton: StarMapButton { } private func updateArrowIcon() { - let base = arDirectionEnabled - ? "ic_custom_direction_compass" - : "ic_custom_direction_manual" - let suffix = nightMode ? "_night" : "_day" - arrowView.image = AstroIcon.original(base + suffix) + if arDirectionEnabled { + let icon: UIImage? = .icCustomDirectionCompass.imageAsset?.image(with: UITraitCollection(userInterfaceStyle: nightMode ? .dark : .light)) + arrowView.image = icon?.withRenderingMode(.alwaysOriginal) + } else { + let icon: UIImage? = .icCustomDirectionManual.imageAsset?.image(with: UITraitCollection(userInterfaceStyle: nightMode ? .dark : .light)) + arrowView.image = icon?.withRenderingMode(.alwaysOriginal) + } } private func commonInit() { diff --git a/Sources/Plugins/Astronomy/StarMapArControlCard.swift b/Sources/Plugins/Astronomy/StarMapArControlCard.swift index 56c887db1b..643eb30298 100644 --- a/Sources/Plugins/Astronomy/StarMapArControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapArControlCard.swift @@ -28,6 +28,8 @@ final class StarMapArControlCard: UIView { private var nightMode = false private var arActive = false + + private weak var glassBackgroundView: UIVisualEffectView? override init(frame: CGRect) { super.init(frame: frame) @@ -58,12 +60,8 @@ final class StarMapArControlCard: UIView { self.nightMode = nightMode self.arActive = arActive - StarMapGlassBackground.apply( - to: self, - active: arActive, - nightMode: nightMode, - cornerRadius: Self.cornerRadius - ) + glassBackgroundView?.overrideUserInterfaceStyle = nightMode ? .dark : .light + backgroundColor = StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) resetButton.updateTheme(nightmod: nightMode, active: false) @@ -75,13 +73,7 @@ final class StarMapArControlCard: UIView { override func layoutSubviews() { super.layoutSubviews() - guard bounds.width > 0, bounds.height > 0 else { return } - if #available(iOS 26.0, *) { - subviews.compactMap { $0 as? UIVisualEffectView }.forEach { - $0.frame = bounds - $0.layer.cornerRadius = Self.cornerRadius - } - } + glassBackgroundView?.frame = bounds } private func setupContent() { @@ -91,25 +83,31 @@ final class StarMapArControlCard: UIView { layer.shadowOpacity = 0.35 layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) - layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor + layer.borderColor = StarMapControlTheme.border(nightMode: nightMode).cgColor + + glassBackgroundView = StarMapGlassBackground.apply( + to: self, + nightMode: nightMode, + cornerRadius: Self.cornerRadius + ) configurePlainButton( arButton, - iconName: "ic_custom_view_in_ar", + icon: .icCustomViewInAr, accessibilityLabel: localizedString("astro_ar"), action: #selector(arButtonTapped) ) configurePlainButton( resetButton, - iconName: "ic_custom_reset", + icon: .icCustomReset, accessibilityLabel: localizedString("shared_string_reset"), action: #selector(resetButtonTapped) ) arButton.customColorTintActive = StarMapControlTheme.activeForeground(nightMode: nightMode) - arButton.onHighlightChange = { [weak self] isHighlighted in + arButton.onHighlightChange = { [weak self] _ in guard let self else { return } - arButton.updateTheme(nightmod: nightMode, active: arActive) + self.arButton.updateTheme(nightmod: nightMode, active: arActive) } transparencySlider.minimumValue = 0 @@ -167,17 +165,17 @@ final class StarMapArControlCard: UIView { private func configurePlainButton( _ button: StarMapPlainButton, - iconName: String, + icon: UIImage?, accessibilityLabel: String, action: Selector ) { - button.setIcon(iconName, accessibilityLabel: accessibilityLabel) + button.setIcon(icon, accessibilityLabel: accessibilityLabel) button.addTarget(self, action: action, for: .touchUpInside) } private func updateArButtonAppearance() { - let iconName = arActive ? "ic_custom_view_in_ar_filled" : "ic_custom_view_in_ar" - arButton.setIcon(iconName, accessibilityLabel: localizedString("astro_ar")) + let icon: UIImage = arActive ? .icCustomViewInArFilled : .icCustomViewInAr + arButton.setIcon(icon, accessibilityLabel: localizedString("astro_ar")) arButton.updateTheme(nightmod: nightMode, active: arActive) } diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index 4f529b8eea..5dc90cc672 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -18,6 +18,10 @@ enum StarMapControlTheme { static func defaultBackground(nightMode: Bool, alpha: CGFloat) -> UIColor { resolved(.mapButtonBgColorDefault, nightMode: nightMode).withAlphaComponent(alpha) } + + static func pressedBackground(nightMode: Bool, alpha: CGFloat) -> UIColor { + resolved(.mapButtonBgColorTap, nightMode: nightMode).withAlphaComponent(alpha) + } static func activeBackground(alpha: CGFloat = 1) -> UIColor { .mapButtonBgColorActive.withAlphaComponent(alpha) @@ -30,6 +34,10 @@ enum StarMapControlTheme { static func activeForeground(nightMode: Bool) -> UIColor { resolved(.mapButtonIconColorActive, nightMode: nightMode) } + + static func border(nightMode: Bool) -> UIColor { + resolved(.mapButtonBorder, nightMode: nightMode) + } } @objcMembers @@ -105,15 +113,12 @@ class StarMapButton: OAHudButton { var cornerRadius = helper.getDefaultCornerRadiusPref().get() if cornerRadius < 0 { cornerRadius = MapButtonState.roundRadiusDp } - var glassStyle: Int32 - var opacity: Double + var glassStyle = MapButtonState.defaultGlassStyle if #available(iOS 26.0, *) { glassStyle = Int32(UIGlassEffect.Style.clear.rawValue) - } else { - glassStyle = MapButtonState.defaultGlassStyle } - opacity = Double(StarMapControlTheme.defaultBackgroundAlpha) - + let opacity = Double(StarMapControlTheme.defaultBackgroundAlpha) + setCustomAppearanceParams(ButtonAppearanceParams( iconName: nil, size: Int32(size), diff --git a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift index bc96c86533..ae22bbe6f9 100644 --- a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift +++ b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift @@ -163,7 +163,6 @@ final class StarMapCameraHelper { } private func startPreview(in hostView: UIView, below siblingView: UIView) -> Bool { - assert(Thread.isMainThread) self.hostView = hostView self.siblingView = siblingView diff --git a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift index 26060f5486..78014b02ac 100644 --- a/Sources/Plugins/Astronomy/StarMapGlassBackground.swift +++ b/Sources/Plugins/Astronomy/StarMapGlassBackground.swift @@ -11,7 +11,7 @@ import Foundation enum StarMapGlassBackground { private static let tag = 9_001 - static func apply(to view: UIView, active: Bool, nightMode: Bool, cornerRadius: CGFloat) { + static func apply(to view: UIView, nightMode: Bool, cornerRadius: CGFloat) -> UIVisualEffectView? { if #available(iOS 26.0, *) { view.subviews.filter { $0.tag == tag }.forEach { $0.removeFromSuperview() } view.backgroundColor = .clear @@ -26,11 +26,8 @@ enum StarMapGlassBackground { effectView.clipsToBounds = true effectView.overrideUserInterfaceStyle = nightMode ? .dark : .light view.insertSubview(effectView, at: 0) - } else { - view.backgroundColor = active - ? StarMapControlTheme.activeBackground() - : StarMapControlTheme.defaultBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) - view.layer.cornerRadius = cornerRadius + return effectView } + return nil } } diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift index 9fa95f2344..efe220c0f1 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift @@ -102,12 +102,10 @@ final class StarMapMagnitudeFilterButton: StarMapButton { var opacity = helper.getDefaultOpacityPref().get() if opacity < 0 { opacity = MapButtonState.opaqueAlpha } - var glassStyle: Int32 + var glassStyle = MapButtonState.defaultGlassStyle if #available(iOS 26.0, *) { glassStyle = Int32(UIGlassEffect.Style.clear.rawValue) opacity = Double(StarMapControlTheme.defaultBackgroundAlpha) - } else { - glassStyle = MapButtonState.defaultGlassStyle } setCustomAppearanceParams(ButtonAppearanceParams( diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift index f343370096..f09f4c9975 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift @@ -22,6 +22,8 @@ final class StarMapMagnitudeSliderPanel: UIView { private let valueLabel = UILabel() private let slider = UISlider() private var nightMode = false + + private weak var glassBackgroundView: UIVisualEffectView? init(maxMagnitude: Double) { super.init(frame: .zero) @@ -45,12 +47,9 @@ final class StarMapMagnitudeSliderPanel: UIView { func updateTheme(nightMode: Bool) { self.nightMode = nightMode - StarMapGlassBackground.apply( - to: self, - active: false, - nightMode: nightMode, - cornerRadius: Self.cornerRadius - ) + + glassBackgroundView?.overrideUserInterfaceStyle = nightMode ? .dark : .light + let color: UIColor = nightMode ? .textColorPrimary.dark : .textColorPrimary.light titleLabel.textColor = color valueLabel.textColor = color @@ -62,13 +61,7 @@ final class StarMapMagnitudeSliderPanel: UIView { override func layoutSubviews() { super.layoutSubviews() - guard bounds.width > 0, bounds.height > 0 else { return } - if #available(iOS 26.0, *) { - subviews.compactMap { $0 as? UIVisualEffectView }.forEach { - $0.frame = bounds - $0.layer.cornerRadius = Self.cornerRadius - } - } + glassBackgroundView?.frame = bounds } private func setupContent(maxMagnitude: Double) { @@ -78,7 +71,13 @@ final class StarMapMagnitudeSliderPanel: UIView { layer.shadowOpacity = 0.35 layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) - layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor + layer.borderColor = StarMapControlTheme.border(nightMode: nightMode).cgColor + + glassBackgroundView = StarMapGlassBackground.apply( + to: self, + nightMode: nightMode, + cornerRadius: Self.cornerRadius + ) titleLabel.text = localizedString("astro_min_magnitude") titleLabel.font = .preferredFont(forTextStyle: .subheadline) diff --git a/Sources/Plugins/Astronomy/StarMapPlainButton.swift b/Sources/Plugins/Astronomy/StarMapPlainButton.swift index 730bcf86c3..a8b4d09f6a 100644 --- a/Sources/Plugins/Astronomy/StarMapPlainButton.swift +++ b/Sources/Plugins/Astronomy/StarMapPlainButton.swift @@ -10,10 +10,6 @@ import UIKit final class StarMapPlainButton: UIButton { var onHighlightChange: ((Bool) -> Void)? - - private var active = false - private var nightMode = false - var customColorTintActive: UIColor? override var isHighlighted: Bool { @@ -22,6 +18,9 @@ final class StarMapPlainButton: UIButton { } } + private var active = false + private var nightMode = false + override init(frame: CGRect) { super.init(frame: frame) setup() @@ -38,8 +37,8 @@ final class StarMapPlainButton: UIButton { applyColors() } - func setIcon(_ iconName: String, accessibilityLabel: String) { - setImage(AstroIcon.template(iconName), for: .normal) + func setIcon(_ icon: UIImage?, accessibilityLabel: String) { + setImage(icon, for: .normal) self.accessibilityLabel = accessibilityLabel applyColors() } @@ -60,7 +59,6 @@ final class StarMapPlainButton: UIButton { } else { color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) } - print(color, active, isHighlighted) tintColor = color setTitleColor(color, for: .normal) diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift index 31142e5682..4a990c24b0 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift @@ -22,6 +22,8 @@ final class StarMapTimeControlCard: UIView { private var active: Bool = false private var nightMode: Bool = false + private weak var glassBackgroundView: UIVisualEffectView? + override init(frame: CGRect) { super.init(frame: frame) setupContent() @@ -44,19 +46,13 @@ final class StarMapTimeControlCard: UIView { self.nightMode = nightMode self.active = active - StarMapGlassBackground.apply( - to: self, - active: active, - nightMode: nightMode, - cornerRadius: Self.cornerRadius - ) timeButton.updateTheme(nightmod: nightMode, active: active) resetButton.updateTheme(nightmod: nightMode, active: active) + glassBackgroundView?.overrideUserInterfaceStyle = nightMode ? .dark : .light + if pressed { - let pressedColorDay = UIColor(rgb: color_on_map_icon_background_color_tap_light).withAlphaComponent(StarMapControlTheme.defaultBackgroundAlpha) - let pressedColorNight = UIColor(rgb: color_on_map_icon_background_color_tap_dark).withAlphaComponent(StarMapControlTheme.defaultBackgroundAlpha) - backgroundColor = nightMode ? pressedColorNight : pressedColorDay + backgroundColor = StarMapControlTheme.pressedBackground(nightMode: nightMode, alpha: StarMapControlTheme.defaultBackgroundAlpha) } else { backgroundColor = active ? StarMapControlTheme.activeBackground(alpha: StarMapControlTheme.defaultBackgroundAlpha) @@ -68,13 +64,7 @@ final class StarMapTimeControlCard: UIView { override func layoutSubviews() { super.layoutSubviews() - guard bounds.width > 0, bounds.height > 0 else { return } - if #available(iOS 26.0, *) { - subviews.compactMap { $0 as? UIVisualEffectView }.forEach { - $0.frame = bounds - $0.layer.cornerRadius = Self.cornerRadius - } - } + glassBackgroundView?.frame = bounds } private func setupContent() { @@ -84,7 +74,13 @@ final class StarMapTimeControlCard: UIView { layer.shadowOpacity = 0.35 layer.shadowRadius = 5 layer.shadowOffset = CGSize(width: 0, height: 2) - layer.borderColor = UIColor(rgb: color_on_map_icon_border_color).cgColor + layer.borderColor = StarMapControlTheme.border(nightMode: nightMode).cgColor + + glassBackgroundView = StarMapGlassBackground.apply( + to: self, + nightMode: nightMode, + cornerRadius: Self.cornerRadius + ) timeButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) timeButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8) diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index b878912e19..edd45a53af 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -489,7 +489,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func setupListeners() { - timeSelectionView.setOnDateTimeChangeListener { [weak self] date in + timeSelectionView.onDateTimeChange = { [weak self] date in self?.setTimeAutoUpdateEnabled(false) self?.updateTime(date, animate: true) } @@ -661,7 +661,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func updateTime(_ date: Date, animate: Bool) { currentDate = date - timeSelectionView.setDateTime(date) + timeSelectionView.date = date starView.setDateTime(AstroUtils.astronomyTime(from: date), animate: animate) updateTimeControls() objectInfoController?.onTimeChanged() @@ -708,7 +708,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func updateTimeControls() { let date = currentDate - timeSelectionView.setDateTime(date) + timeSelectionView.date = date let calendar = Calendar.current let now = Date() let formatter = DateFormatter() From abfdcc5611f31c0161df90fe9578f0168f1f7d6c Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Wed, 15 Jul 2026 13:42:36 +0300 Subject: [PATCH 12/20] Refactor astronomy UI components --- .../Plugins/Astronomy/StarCompassButton.swift | 8 +-- .../Astronomy/StarMapArControlCard.swift | 7 ++- .../Astronomy/StarMapTimeControlCard.swift | 55 +++++++++++++------ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Sources/Plugins/Astronomy/StarCompassButton.swift b/Sources/Plugins/Astronomy/StarCompassButton.swift index 2ec9b79829..45a1014dd8 100644 --- a/Sources/Plugins/Astronomy/StarCompassButton.swift +++ b/Sources/Plugins/Astronomy/StarCompassButton.swift @@ -51,11 +51,11 @@ final class StarCompassButton: StarMapButton { private func updateArrowIcon() { if arDirectionEnabled { - let icon: UIImage? = .icCustomDirectionCompass.imageAsset?.image(with: UITraitCollection(userInterfaceStyle: nightMode ? .dark : .light)) - arrowView.image = icon?.withRenderingMode(.alwaysOriginal) + arrowView.image = .icCustomDirectionCompass.withRenderingMode(.alwaysOriginal) + arrowView.overrideUserInterfaceStyle = nightMode ? .dark : .light } else { - let icon: UIImage? = .icCustomDirectionManual.imageAsset?.image(with: UITraitCollection(userInterfaceStyle: nightMode ? .dark : .light)) - arrowView.image = icon?.withRenderingMode(.alwaysOriginal) + arrowView.image = .icCustomDirectionManual.withRenderingMode(.alwaysOriginal) + arrowView.overrideUserInterfaceStyle = nightMode ? .dark : .light } } diff --git a/Sources/Plugins/Astronomy/StarMapArControlCard.swift b/Sources/Plugins/Astronomy/StarMapArControlCard.swift index 643eb30298..c7459b4988 100644 --- a/Sources/Plugins/Astronomy/StarMapArControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapArControlCard.swift @@ -135,6 +135,9 @@ final class StarMapArControlCard: UIView { rootStack.addArrangedSubview(cameraControlsStack) rootStack.translatesAutoresizingMaskIntoConstraints = false addSubview(rootStack) + + let sliderContainerHeightConstraint = sliderContainer.heightAnchor.constraint(equalToConstant: Self.sliderHeight) + sliderContainerHeightConstraint.priority = .defaultLow NSLayoutConstraint.activate([ widthAnchor.constraint(equalToConstant: Self.width), @@ -148,11 +151,11 @@ final class StarMapArControlCard: UIView { arButton.heightAnchor.constraint(equalToConstant: Self.innerButtonSize), sliderContainer.widthAnchor.constraint(equalToConstant: Self.width), - sliderContainer.heightAnchor.constraint(equalToConstant: Self.sliderHeight), + sliderContainerHeightConstraint, transparencySlider.centerXAnchor.constraint(equalTo: sliderContainer.centerXAnchor), transparencySlider.centerYAnchor.constraint(equalTo: sliderContainer.centerYAnchor), - transparencySlider.widthAnchor.constraint(equalToConstant: Self.sliderHeight), + transparencySlider.widthAnchor.constraint(equalTo: sliderContainer.heightAnchor), transparencySlider.heightAnchor.constraint(equalToConstant: 40), resetButton.widthAnchor.constraint(equalToConstant: Self.innerButtonSize), diff --git a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift index 4a990c24b0..a9aac58509 100644 --- a/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift +++ b/Sources/Plugins/Astronomy/StarMapTimeControlCard.swift @@ -15,8 +15,8 @@ final class StarMapTimeControlCard: UIView { var onTimeButtonTapped: (() -> Void)? var onResetTapped: (() -> Void)? - private let timeButton = StarMapPlainButton() - private let resetButton = StarMapPlainButton() + private let timeButton = UIButton() + private let resetButton = UIButton() private let mainStack = UIStackView() private var active: Bool = false @@ -45,9 +45,9 @@ final class StarMapTimeControlCard: UIView { func updateTheme(nightMode: Bool, active: Bool, pressed: Bool = false) { self.nightMode = nightMode self.active = active - - timeButton.updateTheme(nightmod: nightMode, active: active) - resetButton.updateTheme(nightmod: nightMode, active: active) + + updateButtonTheme(timeButton) + updateButtonTheme(resetButton) glassBackgroundView?.overrideUserInterfaceStyle = nightMode ? .dark : .light @@ -67,6 +67,37 @@ final class StarMapTimeControlCard: UIView { glassBackgroundView?.frame = bounds } + private func updateButtonTheme(_ button: UIButton) { + let color: UIColor + if button.isHighlighted { + color = StarMapControlTheme.foreground(active: false, nightMode: nightMode).withAlphaComponent(0.4) + } else { + color = StarMapControlTheme.foreground(active: active, nightMode: nightMode) + } + var config = button.configuration + config?.baseBackgroundColor = .clear + config?.baseForegroundColor = color + button.configuration = config + } + + private func makeButtonConfiguration(for button: UIButton, icon: UIImage) { + var config = UIButton.Configuration.plain() + config.image = icon + config.imagePlacement = .leading + config.imagePadding = 8 + config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 16) + config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in + var outgoing = incoming + outgoing.font = .systemFont(ofSize: 17, weight: .medium) + return outgoing + } + button.configuration = config + button.configurationUpdateHandler = { [weak self] button in + guard let self else { return } + self.updateTheme(nightMode: self.nightMode, active: self.active, pressed: button.isHighlighted) + } + } + private func setupContent() { translatesAutoresizingMaskIntoConstraints = false layer.cornerRadius = Self.cornerRadius @@ -82,31 +113,21 @@ final class StarMapTimeControlCard: UIView { cornerRadius: Self.cornerRadius ) - timeButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - timeButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8) - timeButton.setImage(AstroIcon.template("ic_action_time"), for: .normal) - timeButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold) timeButton.addTarget(self, action: #selector(timeButtonTapped), for: .touchUpInside) timeButton.heightAnchor.constraint(equalToConstant: Self.height).isActive = true - timeButton.onHighlightChange = { [weak self] isHighlighted in - guard let self else { return } - updateTheme(nightMode: nightMode, active: active, pressed: isHighlighted) - } + makeButtonConfiguration(for: timeButton, icon: .icActionTime) - resetButton.setImage(.icCustomReset, for: .normal) resetButton.accessibilityLabel = localizedString("shared_string_reset") resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside) resetButton.isHidden = true resetButton.widthAnchor.constraint(equalToConstant: Self.height).isActive = true resetButton.heightAnchor.constraint(equalToConstant: Self.height).isActive = true + makeButtonConfiguration(for: resetButton, icon: .icCustomReset) mainStack.addArrangedSubview(timeButton) mainStack.addArrangedSubview(resetButton) mainStack.axis = .horizontal mainStack.alignment = .center - mainStack.spacing = 0 - mainStack.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16) - mainStack.isLayoutMarginsRelativeArrangement = true mainStack.translatesAutoresizingMaskIntoConstraints = false addSubview(mainStack) From bade85ba402898e2b254ca0a201891d1b8d5cec5 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Fri, 17 Jul 2026 11:34:10 +0300 Subject: [PATCH 13/20] Persist astronomy settings and recent chips --- .../Plugins/Astronomy/AstronomyPlugin.swift | 30 +- .../Astronomy/AstronomyPluginSettings.swift | 407 +++++++++++++++--- .../Astronomy/StarMapViewController.swift | 76 +++- .../Astronomy/StarObjectsViewModel.swift | 16 +- .../search/StarMapMyDataViewController.swift | 4 +- .../search/StarMapSearchViewController.swift | 7 +- 6 files changed, 458 insertions(+), 82 deletions(-) diff --git a/Sources/Plugins/Astronomy/AstronomyPlugin.swift b/Sources/Plugins/Astronomy/AstronomyPlugin.swift index 479a53435b..c0cfc801dd 100644 --- a/Sources/Plugins/Astronomy/AstronomyPlugin.swift +++ b/Sources/Plugins/Astronomy/AstronomyPlugin.swift @@ -8,14 +8,36 @@ import UIKit -@objc(AstronomyPlugin) -final class AstronomyPlugin: OAPlugin { - let dataProvider: AstroDataDbProvider +@objc final class AstronomyPlugin: OAPlugin { + private enum PreferenceId { + static let settings = "astronomy_settings" + static let recent = "astronomy_recently_viewed" + } + + let dataProvider = AstroDataDbProvider() + + var astroSettings: AstronomyPluginSettings { astronomySettingsStorage } var recentSearchChips: [StarMapRecentChip] = [] + private let settingsPref: OACommonString = OAAppSettings.sharedManager() + .registerStringPreference(PreferenceId.settings, defValue: "") + .makeProfile() + .makeShared() + + private let recentPref: OACommonString = OAAppSettings.sharedManager() + .registerStringPreference(PreferenceId.recent, defValue: "") + .makeGlobal() + .makeShared() + + private lazy var astronomySettingsStorage = AstronomyPluginSettings(settingsPref: settingsPref, recentPref: recentPref) + override init() { - dataProvider = AstroDataDbProvider() super.init() + recentSearchChips = astronomySettingsStorage.getRecentChips() + } + + func saveRecentSearchChips() { + astronomySettingsStorage.setRecentChips(recentSearchChips) } override func getId() -> String? { diff --git a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift index 2af206b26f..5defe8d4ad 100644 --- a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift +++ b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift @@ -9,8 +9,8 @@ import Foundation import UIKit -struct AstronomyPluginSettings: Codable { - enum DirectionColor: Int, CaseIterable, Codable { +final class AstronomyPluginSettings { + enum DirectionColor: Int, CaseIterable { case BLUE case GREEN case ORANGE @@ -46,6 +46,17 @@ struct AstronomyPluginSettings: Codable { struct DirectionConfig: Equatable, Codable { var id: String var colorIndex: Int = 0 + + init(id: String, colorIndex: Int = 0) { + self.id = id + self.colorIndex = colorIndex + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + colorIndex = try container.decodeIfPresent(Int.self, forKey: .colorIndex) ?? 0 + } } struct CelestialPathConfig: Equatable, Codable { @@ -54,8 +65,17 @@ struct AstronomyPluginSettings: Codable { struct CommonConfig: Equatable, Codable { var showRegularMap = false + + init(showRegularMap: Bool = false) { + self.showRegularMap = showRegularMap + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + showRegularMap = try container.decodeIfPresent(Bool.self, forKey: .showRegularMap) ?? false + } } - + struct MapViewPosition: Equatable, Codable { var azimuth: Double var altitude: Double @@ -63,6 +83,25 @@ struct AstronomyPluginSettings: Codable { var roll: Double = 0 var panX: Double = 0 var panY: Double = 0 + + init(azimuth: Double, altitude: Double, viewAngle: Double, roll: Double = 0, panX: Double = 0, panY: Double = 0) { + self.azimuth = azimuth + self.altitude = altitude + self.viewAngle = viewAngle + self.roll = roll + self.panX = panX + self.panY = panY + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + azimuth = try container.decode(Double.self, forKey: .azimuth) + altitude = try container.decode(Double.self, forKey: .altitude) + viewAngle = try container.decode(Double.self, forKey: .viewAngle) + roll = try container.decodeIfPresent(Double.self, forKey: .roll) ?? 0 + panX = try container.decodeIfPresent(Double.self, forKey: .panX) ?? 0 + panY = try container.decodeIfPresent(Double.self, forKey: .panY) ?? 0 + } } struct StarMapConfig: Equatable, Codable { @@ -93,78 +132,271 @@ struct AstronomyPluginSettings: Codable { var favorites: [FavoriteConfig] = [] var directions: [DirectionConfig] = [] var celestialPaths: [CelestialPathConfig] = [] - var savedMapPosition: MapViewPosition? = nil + var savedMapPosition: MapViewPosition? + + init( + showAzimuthalGrid: Bool = true, + showEquatorialGrid: Bool = false, + showEclipticLine: Bool = false, + showMeridianLine: Bool = false, + showEquatorLine: Bool = false, + showGalacticLine: Bool = false, + showFavorites: Bool = true, + showDirections: Bool = true, + showCelestialPaths: Bool = true, + showRedFilter: Bool = false, + showSun: Bool = true, + showMoon: Bool = true, + showPlanets: Bool = true, + showConstellations: Bool = false, + showStars: Bool = false, + showGalaxies: Bool = false, + showNebulae: Bool = false, + showOpenClusters: Bool = false, + showGlobularClusters: Bool = false, + showGalaxyClusters: Bool = false, + showBlackHoles: Bool = false, + is2DMode: Bool = false, + showMagnitudeFilter: Bool = false, + magnitudeFilter: Double? = nil, + favorites: [FavoriteConfig] = [], + directions: [DirectionConfig] = [], + celestialPaths: [CelestialPathConfig] = [], + savedMapPosition: MapViewPosition? = nil + ) { + self.showAzimuthalGrid = showAzimuthalGrid + self.showEquatorialGrid = showEquatorialGrid + self.showEclipticLine = showEclipticLine + self.showMeridianLine = showMeridianLine + self.showEquatorLine = showEquatorLine + self.showGalacticLine = showGalacticLine + self.showFavorites = showFavorites + self.showDirections = showDirections + self.showCelestialPaths = showCelestialPaths + self.showRedFilter = showRedFilter + self.showSun = showSun + self.showMoon = showMoon + self.showPlanets = showPlanets + self.showConstellations = showConstellations + self.showStars = showStars + self.showGalaxies = showGalaxies + self.showNebulae = showNebulae + self.showOpenClusters = showOpenClusters + self.showGlobularClusters = showGlobularClusters + self.showGalaxyClusters = showGalaxyClusters + self.showBlackHoles = showBlackHoles + self.is2DMode = is2DMode + self.showMagnitudeFilter = showMagnitudeFilter + self.magnitudeFilter = magnitudeFilter + self.favorites = favorites + self.directions = directions + self.celestialPaths = celestialPaths + self.savedMapPosition = savedMapPosition + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + showAzimuthalGrid = try container.decodeIfPresent(Bool.self, forKey: .showAzimuthalGrid) ?? true + showEquatorialGrid = try container.decodeIfPresent(Bool.self, forKey: .showEquatorialGrid) ?? false + showEclipticLine = try container.decodeIfPresent(Bool.self, forKey: .showEclipticLine) ?? false + showMeridianLine = try container.decodeIfPresent(Bool.self, forKey: .showMeridianLine) ?? false + showEquatorLine = try container.decodeIfPresent(Bool.self, forKey: .showEquatorLine) ?? false + showGalacticLine = try container.decodeIfPresent(Bool.self, forKey: .showGalacticLine) ?? false + showFavorites = try container.decodeIfPresent(Bool.self, forKey: .showFavorites) ?? true + showDirections = try container.decodeIfPresent(Bool.self, forKey: .showDirections) ?? true + showCelestialPaths = try container.decodeIfPresent(Bool.self, forKey: .showCelestialPaths) ?? true + showRedFilter = try container.decodeIfPresent(Bool.self, forKey: .showRedFilter) ?? false + showSun = try container.decodeIfPresent(Bool.self, forKey: .showSun) ?? true + showMoon = try container.decodeIfPresent(Bool.self, forKey: .showMoon) ?? true + showPlanets = try container.decodeIfPresent(Bool.self, forKey: .showPlanets) ?? true + showConstellations = try container.decodeIfPresent(Bool.self, forKey: .showConstellations) ?? false + showStars = try container.decodeIfPresent(Bool.self, forKey: .showStars) ?? false + showGalaxies = try container.decodeIfPresent(Bool.self, forKey: .showGalaxies) ?? false + showNebulae = try container.decodeIfPresent(Bool.self, forKey: .showNebulae) ?? false + showOpenClusters = try container.decodeIfPresent(Bool.self, forKey: .showOpenClusters) ?? false + showGlobularClusters = try container.decodeIfPresent(Bool.self, forKey: .showGlobularClusters) ?? false + showGalaxyClusters = try container.decodeIfPresent(Bool.self, forKey: .showGalaxyClusters) ?? false + showBlackHoles = try container.decodeIfPresent(Bool.self, forKey: .showBlackHoles) ?? false + is2DMode = try container.decodeIfPresent(Bool.self, forKey: .is2DMode) ?? false + showMagnitudeFilter = try container.decodeIfPresent(Bool.self, forKey: .showMagnitudeFilter) ?? false + magnitudeFilter = try container.decodeIfPresent(Double.self, forKey: .magnitudeFilter) + favorites = try container.decodeIfPresent([FavoriteConfig].self, forKey: .favorites) ?? [] + directions = try container.decodeIfPresent([DirectionConfig].self, forKey: .directions) ?? [] + celestialPaths = try container.decodeIfPresent([CelestialPathConfig].self, forKey: .celestialPaths) ?? [] + savedMapPosition = try container.decodeIfPresent(MapViewPosition.self, forKey: .savedMapPosition) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(showAzimuthalGrid, forKey: .showAzimuthalGrid) + try container.encode(showEquatorialGrid, forKey: .showEquatorialGrid) + try container.encode(showEclipticLine, forKey: .showEclipticLine) + try container.encode(showMeridianLine, forKey: .showMeridianLine) + try container.encode(showEquatorLine, forKey: .showEquatorLine) + try container.encode(showGalacticLine, forKey: .showGalacticLine) + try container.encode(showFavorites, forKey: .showFavorites) + try container.encode(showDirections, forKey: .showDirections) + try container.encode(showCelestialPaths, forKey: .showCelestialPaths) + try container.encode(showRedFilter, forKey: .showRedFilter) + try container.encode(showSun, forKey: .showSun) + try container.encode(showMoon, forKey: .showMoon) + try container.encode(showPlanets, forKey: .showPlanets) + try container.encode(showConstellations, forKey: .showConstellations) + try container.encode(showStars, forKey: .showStars) + try container.encode(showGalaxies, forKey: .showGalaxies) + try container.encode(showNebulae, forKey: .showNebulae) + try container.encode(showOpenClusters, forKey: .showOpenClusters) + try container.encode(showGlobularClusters, forKey: .showGlobularClusters) + try container.encode(showGalaxyClusters, forKey: .showGalaxyClusters) + try container.encode(showBlackHoles, forKey: .showBlackHoles) + try container.encode(is2DMode, forKey: .is2DMode) + try container.encode(showMagnitudeFilter, forKey: .showMagnitudeFilter) + + try container.encodeIfPresent(magnitudeFilter, forKey: .magnitudeFilter) + if !favorites.isEmpty { + try container.encode(favorites, forKey: .favorites) + } + if !directions.isEmpty { + try container.encode(directions, forKey: .directions) + } + if !celestialPaths.isEmpty { + try container.encode(celestialPaths, forKey: .celestialPaths) + } + try container.encodeIfPresent(savedMapPosition, forKey: .savedMapPosition) + } + + private enum CodingKeys: String, CodingKey { + case showAzimuthalGrid + case showEquatorialGrid + case showEclipticLine + case showMeridianLine + case showEquatorLine + case showGalacticLine + case showFavorites + case showDirections + case showCelestialPaths + case showRedFilter + case showSun + case showMoon + case showPlanets + case showConstellations + case showStars + case showGalaxies + case showNebulae + case showOpenClusters + case showGlobularClusters + case showGalaxyClusters + case showBlackHoles + case is2DMode + case showMagnitudeFilter + case magnitudeFilter + case favorites + case directions + case celestialPaths + case savedMapPosition + } + } + + private struct SettingsStorage: Codable { + var common: CommonConfig? + var starMap: StarMapConfig? + + enum CodingKeys: String, CodingKey { + case common + case starMap = "star_map" + } + } + + private struct RecentChipDTO: Codable { + var label: String + var objectId: String? } - static let storageKey = "astronomy_settings" - - private static let storageQueue = DispatchQueue(label: "net.osmand.astronomy.settings") + private static let encoder = JSONEncoder() + private static let decoder = JSONDecoder() var common = CommonConfig() var starMap = StarMapConfig() - static func load() -> AstronomyPluginSettings { - storageQueue.sync { - loadUnlocked() - } + private let settingsPref: OACommonString? + private let recentPref: OACommonString? + private let lock = NSLock() + + init(settingsPref: OACommonString, recentPref: OACommonString? = nil) { + self.settingsPref = settingsPref + self.recentPref = recentPref + reloadFromPreference() } - func save() { - Self.storageQueue.sync { - Self.saveUnlocked(self) - } + init() { + self.settingsPref = nil + self.recentPref = nil } - private static func loadUnlocked() -> AstronomyPluginSettings { - UserDefaults.standard.object(AstronomyPluginSettings.self, with: storageKey) ?? AstronomyPluginSettings() + func copyForUI() -> AstronomyPluginSettings { + let copy = AstronomyPluginSettings() + copy.common = common + copy.starMap = starMap + return copy } - private static func saveUnlocked(_ settings: AstronomyPluginSettings) { - UserDefaults.standard.set(object: settings, forKey: storageKey) + func reloadFromPreference() { + lock.lock() + defer { lock.unlock() } + let storage = readStorageUnlocked() + common = storage.common ?? CommonConfig() + starMap = storage.starMap ?? StarMapConfig() } func getCommonConfig() -> CommonConfig { - common + lock.lock() + defer { lock.unlock() } + let config = readStorageUnlocked().common ?? CommonConfig() + common = config + return config } - mutating func setCommonConfig(_ config: CommonConfig) { - self = Self.storageQueue.sync { - var settings = Self.loadUnlocked() - settings.common = config - Self.saveUnlocked(settings) - return settings - } + func setCommonConfig(_ config: CommonConfig) { + lock.lock() + defer { lock.unlock() } + var storage = readStorageUnlocked() + storage.common = config + writeStorageUnlocked(storage) + common = config } func getStarMapConfig() -> StarMapConfig { - starMap + lock.lock() + defer { lock.unlock() } + let config = readStorageUnlocked().starMap ?? StarMapConfig() + starMap = config + return config } - mutating func setStarMapConfig(_ config: StarMapConfig) { - self = Self.storageQueue.sync { - var settings = Self.loadUnlocked() - settings.starMap = config - Self.saveUnlocked(settings) - return settings - } + func setStarMapConfig(_ config: StarMapConfig) { + lock.lock() + defer { lock.unlock() } + var storage = readStorageUnlocked() + storage.starMap = config + writeStorageUnlocked(storage) + starMap = config } - @discardableResult - mutating func updateStarMapConfig(_ transform: (StarMapConfig) -> StarMapConfig) -> StarMapConfig { - let result = Self.storageQueue.sync { - var settings = Self.loadUnlocked() - let updated = transform(settings.starMap) - if updated != settings.starMap { - settings.starMap = updated - Self.saveUnlocked(settings) - } - return (settings, updated) + @discardableResult func updateStarMapConfig(_ transform: (StarMapConfig) -> StarMapConfig) -> StarMapConfig { + lock.lock() + defer { lock.unlock() } + var storage = readStorageUnlocked() + let current = storage.starMap ?? StarMapConfig() + let updated = transform(current) + if updated != current { + storage.starMap = updated + writeStorageUnlocked(storage) } - self = result.0 - return result.1 + starMap = updated + return updated } - mutating func addFavorite(id: String) { + func addFavorite(id: String) { updateStarMapConfig { config in guard !config.favorites.contains(where: { $0.id == id }) else { return config @@ -175,7 +407,7 @@ struct AstronomyPluginSettings: Codable { } } - mutating func removeFavorite(id: String) { + func removeFavorite(id: String) { updateStarMapConfig { config in var updated = config updated.favorites.removeAll { $0.id == id } @@ -183,7 +415,7 @@ struct AstronomyPluginSettings: Codable { } } - mutating func addDirection(id: String) -> Int { + func addDirection(id: String) -> Int { var resultColor = 0 updateStarMapConfig { config in if let direction = config.directions.first(where: { $0.id == id }) { @@ -200,7 +432,7 @@ struct AstronomyPluginSettings: Codable { return resultColor } - mutating func removeDirection(id: String) { + func removeDirection(id: String) { updateStarMapConfig { config in var updated = config updated.directions.removeAll { $0.id == id } @@ -208,7 +440,7 @@ struct AstronomyPluginSettings: Codable { } } - mutating func addCelestialPath(id: String) { + func addCelestialPath(id: String) { updateStarMapConfig { config in guard !config.celestialPaths.contains(where: { $0.id == id }) else { return config @@ -219,11 +451,80 @@ struct AstronomyPluginSettings: Codable { } } - mutating func removeCelestialPath(id: String) { + func removeCelestialPath(id: String) { updateStarMapConfig { config in var updated = config updated.celestialPaths.removeAll { $0.id == id } return updated } } + + // MARK: - Recently viewed (global, all profiles) + + func getRecentChips() -> [StarMapRecentChip] { + lock.lock() + defer { lock.unlock() } + return readRecentChipsUnlocked() + } + + func setRecentChips(_ chips: [StarMapRecentChip]) { + lock.lock() + defer { lock.unlock() } + writeRecentChipsUnlocked(chips) + } + + // MARK: - Codable I/O + + private func readStorageUnlocked() -> SettingsStorage { + guard let settingsPref else { + return SettingsStorage() + } + let str = settingsPref.get() + guard !str.isEmpty, let data = str.data(using: .utf8) else { + return SettingsStorage() + } + return (try? Self.decoder.decode(SettingsStorage.self, from: data)) ?? SettingsStorage() + } + + private func writeStorageUnlocked(_ storage: SettingsStorage) { + guard let settingsPref else { + return + } + guard let data = try? Self.encoder.encode(storage), + let string = String(data: data, encoding: .utf8) else { + return + } + settingsPref.set(string) + } + + private func readRecentChipsUnlocked() -> [StarMapRecentChip] { + guard let recentPref else { + return [] + } + let str = recentPref.get() + guard !str.isEmpty, + let data = str.data(using: .utf8), + let dtos = try? Self.decoder.decode([RecentChipDTO].self, from: data) else { + return [] + } + return dtos.compactMap { dto in + let label = dto.label.trimmingCharacters(in: .whitespacesAndNewlines) + guard !label.isEmpty else { + return nil + } + return StarMapRecentChip(label: label, objectId: dto.objectId) + } + } + + private func writeRecentChipsUnlocked(_ chips: [StarMapRecentChip]) { + guard let recentPref else { + return + } + let dtos = chips.map { RecentChipDTO(label: $0.label, objectId: $0.objectId) } + guard let data = try? Self.encoder.encode(dtos), + let string = String(data: data, encoding: .utf8) else { + return + } + recentPref.set(string) + } } diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index edd45a53af..64d64aad7f 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -72,6 +72,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private var regularMapHeightConstraint: NSLayoutConstraint? private var mapLocationObserver: OAAutoObserverProxy? private var dayNightModeObserver: OAAutoObserverProxy? + private var applicationModeObserver: OAAutoObserverProxy? private var screenOrientationObserver: NSObjectProtocol? private var leftPanelLeadingConstraint: NSLayoutConstraint? private var mapVisibleAreaLeadingConstraint: NSLayoutConstraint? @@ -104,7 +105,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } init(plugin: AstronomyPlugin) { - let loadedSettings = AstronomyPluginSettings.load() + let loadedSettings = plugin.astroSettings let provider = plugin.dataProvider self.plugin = plugin settings = loadedSettings @@ -121,20 +122,12 @@ final class StarMapViewController: UIViewController, StarViewDelegate { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear + reloadProfileSettings() setupLayout() setupControls() setupHelpers() setupListeners() - applySettings(settings.starMap) - if let pos = settings.starMap.savedMapPosition { - starView.restoreMapPosition(pos) - compassButton.update(mapRotation: CGFloat(-pos.azimuth)) - lastUpdatedAzimuth = pos.azimuth - updateStarMap() - } else { - updateStarMap(updateAzimuth: true) - } - updateRegularMapVisibility(settings.common.showRegularMap) + applyProfileSettingsToUI(restoreMapPosition: true) viewModel.onDataChanged = { [weak self] in self?.syncObjectsToStarView() @@ -253,7 +246,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } func searchStarMapConfig() -> AstronomyPluginSettings.StarMapConfig { - settings.starMap + settings.getStarMapConfig() } func isSearchRedFilterEnabled() -> Bool { @@ -321,7 +314,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func setupStarView() { starView.delegate = self starView.viewModel = viewModel - starView.settings = settings + starView.settings = settings.copyForUI() } private func setupCompassAndLeftControls() { @@ -527,6 +520,11 @@ final class StarMapViewController: UIViewController, StarViewDelegate { dayNightModeObserver = OAAutoObserverProxy(self, withHandler: #selector(onDayNightModeChanged), andObserve: OsmAndApp.swiftInstance().dayNightModeObservable) + if let appModeObservable = OsmAndApp.swiftInstance()?.applicationModeChangedObservable { + applicationModeObserver = OAAutoObserverProxy(self, + withHandler: #selector(onApplicationModeChanged), + andObserve: appModeObservable) + } screenOrientationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name(ScreenOrientationHelper.screenOrientationChangedKey), object: nil, queue: .main) { [weak self] _ in @@ -534,6 +532,50 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } } + @objc private func onApplicationModeChanged() { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + reloadAndApplyProfileSettings(restoreMapPosition: true) + viewModel.updateSettings(settings) + viewModel.load(preferredLocale: OsmAndApp.swiftInstance()?.getLanguageCode()) + } + } + + private func reloadProfileSettings() { + settings.reloadFromPreference() + } + + private func applyProfileSettingsToUI(restoreMapPosition: Bool) { + let starMap = settings.getStarMapConfig() + let common = settings.getCommonConfig() + applySettings(starMap) + starView.settings = settings.copyForUI() + updateRegularMapVisibility(common.showRegularMap) + if restoreMapPosition, let pos = starMap.savedMapPosition { + starView.restoreMapPosition(pos) + compassButton.update(mapRotation: CGFloat(-pos.azimuth)) + lastUpdatedAzimuth = pos.azimuth + updateStarMap() + } else if restoreMapPosition { + updateStarMap(updateAzimuth: true) + } else { + starView.setNeedsDisplay() + } + updateMagnitudeControls() + configureSheetController?.config = starMap + configureSheetController?.commonConfig = common + if let configureSheetController { + configureSheetController.applyRedFilter(enabled: starMap.showRedFilter) + } + } + + private func reloadAndApplyProfileSettings(restoreMapPosition: Bool) { + reloadProfileSettings() + applyProfileSettingsToUI(restoreMapPosition: restoreMapPosition) + } + private func syncObjectsToStarView() { starView.setSkyObjects(viewModel.skyObjects) starView.setConstellations(viewModel.constellations) @@ -613,6 +655,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { ) return config } + starView.settings = settings.copyForUI() viewModel.updateSettings(settings) } @@ -626,6 +669,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { return updated } applySettings(updatedConfig) + starView.settings = settings.copyForUI() viewModel.updateSettings(settings) } @@ -1265,13 +1309,12 @@ final class StarMapViewController: UIViewController, StarViewDelegate { hideBottomSheet(clearSelection: false) let sheet = AstroConfigureViewBottomSheet() - sheet.config = settings.starMap - sheet.commonConfig = settings.common + sheet.config = settings.getStarMapConfig() + sheet.commonConfig = settings.getCommonConfig() sheet.onConfigChanged = { [weak self] config in self?.setStarMapSettings(config) } sheet.onCommonConfigChanged = { [weak self] config in - self?.settings.common = config self?.updateRegularMapVisibility(config.showRegularMap) self?.saveCommonSettings() } @@ -1477,6 +1520,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { deinit { mapLocationObserver?.detach() dayNightModeObserver?.detach() + applicationModeObserver?.detach() if let screenOrientationObserver { NotificationCenter.default.removeObserver(screenOrientationObserver) } diff --git a/Sources/Plugins/Astronomy/StarObjectsViewModel.swift b/Sources/Plugins/Astronomy/StarObjectsViewModel.swift index 4e6583ba3d..512aefe20a 100644 --- a/Sources/Plugins/Astronomy/StarObjectsViewModel.swift +++ b/Sources/Plugins/Astronomy/StarObjectsViewModel.swift @@ -9,12 +9,14 @@ import Foundation final class StarObjectsViewModel { - private let provider: AstroDataProvider + var onDataChanged: (() -> Void)? + private(set) var settings: AstronomyPluginSettings - var onDataChanged: (() -> Void)? private(set) var skyObjects: [SkyObject] = [] private(set) var constellations: [Constellation] = [] + + private let provider: AstroDataProvider init(provider: AstroDataProvider, settings: AstronomyPluginSettings) { self.provider = provider @@ -36,7 +38,8 @@ final class StarObjectsViewModel { let constellations = provider.getConstellations(preferredLocale: preferredLocale) DispatchQueue.main.async { self.applyObjectSettings(to: objects + constellations.map { $0 as SkyObject }) - let favoriteOrder = Dictionary(uniqueKeysWithValues: self.settings.starMap.favorites.enumerated().map { ($0.element.id, $0.offset) }) + let starMap = self.settings.getStarMapConfig() + let favoriteOrder = Dictionary(uniqueKeysWithValues: starMap.favorites.enumerated().map { ($0.element.id, $0.offset) }) self.skyObjects = objects.sorted { (favoriteOrder[$0.id] ?? Int.max) < (favoriteOrder[$1.id] ?? Int.max) } self.constellations = constellations.sorted { (favoriteOrder[$0.id] ?? Int.max) < (favoriteOrder[$1.id] ?? Int.max) } self.onDataChanged?() @@ -52,9 +55,10 @@ final class StarObjectsViewModel { targetObjects = skyObjects + constellations.map { $0 as SkyObject } } - let favoritesMap = Dictionary(uniqueKeysWithValues: settings.starMap.favorites.map { ($0.id, $0) }) - let directionsMap = Dictionary(uniqueKeysWithValues: settings.starMap.directions.map { ($0.id, $0) }) - let celestialPathsMap = Dictionary(uniqueKeysWithValues: settings.starMap.celestialPaths.map { ($0.id, $0) }) + let starMap = settings.getStarMapConfig() + let favoritesMap = Dictionary(uniqueKeysWithValues: starMap.favorites.map { ($0.id, $0) }) + let directionsMap = Dictionary(uniqueKeysWithValues: starMap.directions.map { ($0.id, $0) }) + let celestialPathsMap = Dictionary(uniqueKeysWithValues: starMap.celestialPaths.map { ($0.id, $0) }) for object in targetObjects { object.isFavorite = favoritesMap[object.id] != nil object.showDirection = directionsMap[object.id] != nil diff --git a/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift b/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift index d541d84852..6310ced14c 100644 --- a/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift +++ b/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift @@ -434,7 +434,7 @@ final class StarMapMyDataViewController: UIViewController { } private func getMyDataInsertionOrderMap(_ quickPresetType: StarMapSearchQuickPresetType) -> [String: Int] { - let config = parentStarMapController?.searchStarMapConfig() ?? AstronomyPluginSettings.load().starMap + let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.getStarMapConfig() let ids: [String] switch quickPresetType { case .MY_DATA_FAVORITES: @@ -454,7 +454,7 @@ final class StarMapMyDataViewController: UIViewController { } private func currentTabHasData() -> Bool { - let config = parentStarMapController?.searchStarMapConfig() ?? AstronomyPluginSettings.load().starMap + let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.getStarMapConfig() switch currentTab { case .favorites: return !config.favorites.isEmpty diff --git a/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift b/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift index 8d168c441a..dd6ee99998 100644 --- a/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift +++ b/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift @@ -751,7 +751,7 @@ final class StarMapSearchViewController: UIViewController { StarMapExploreRowConfig(quickPresetType: .CATEGORY_STAR_CLUSTERS, iconRes: "ic_custom_star_clusters", titleRes: "astro_star_clusters", subtitleRes: nil), StarMapExploreRowConfig(quickPresetType: .CATEGORY_DEEP_SKY, iconRes: "ic_custom_galaxy", titleRes: "astro_deep_sky", subtitleRes: "astro_explore_deep_sky_subtitle") ] - let config = parentStarMapController?.searchStarMapConfig() ?? AstronomyPluginSettings.load().starMap + let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.getStarMapConfig() let myDataItems: [(StarMapExploreRowConfig, Int)] = [ (StarMapExploreRowConfig(quickPresetType: .MY_DATA_FAVORITES, iconRes: "ic_custom_bookmark", titleRes: "favorites_item", subtitleRes: nil), config.favorites.count), (StarMapExploreRowConfig(quickPresetType: .MY_DATA_DAILY_PATH, iconRes: "ic_custom_target_path_on", titleRes: "astro_daily_path", subtitleRes: nil), config.celestialPaths.count), @@ -1008,12 +1008,17 @@ final class StarMapSearchViewController: UIViewController { searchState.addRecentChip(label: entry.displayName, objectId: entry.objectRef.id) plugin.recentSearchChips.removeAll() plugin.recentSearchChips.append(contentsOf: searchState.recentChips) + plugin.saveRecentSearchChips() renderRecentChips() } private func syncRecentChipsWithSession() { + if plugin.recentSearchChips.isEmpty { + plugin.recentSearchChips = plugin.astroSettings.getRecentChips() + } if plugin.recentSearchChips.isEmpty { plugin.recentSearchChips.append(contentsOf: searchState.recentChips) + plugin.saveRecentSearchChips() } else { searchState.replaceRecentChips(plugin.recentSearchChips) } From b871e21fa6452e564912da9c4278a29af5dafcca Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Fri, 17 Jul 2026 17:30:44 +0300 Subject: [PATCH 14/20] Code review fixes --- Sources/Plugins/Astronomy/AstronomyPlugin.swift | 2 +- .../Plugins/Astronomy/AstronomyPluginSettings.swift | 6 +++--- Sources/Plugins/Astronomy/StarMapViewController.swift | 10 +++++----- Sources/Plugins/Astronomy/StarObjectsViewModel.swift | 4 ++-- .../Astronomy/search/StarMapMyDataViewController.swift | 8 ++++---- .../Astronomy/search/StarMapSearchViewController.swift | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/Plugins/Astronomy/AstronomyPlugin.swift b/Sources/Plugins/Astronomy/AstronomyPlugin.swift index c0cfc801dd..592b901419 100644 --- a/Sources/Plugins/Astronomy/AstronomyPlugin.swift +++ b/Sources/Plugins/Astronomy/AstronomyPlugin.swift @@ -33,7 +33,7 @@ import UIKit override init() { super.init() - recentSearchChips = astronomySettingsStorage.getRecentChips() + recentSearchChips = astronomySettingsStorage.recentChips() } func saveRecentSearchChips() { diff --git a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift index 5defe8d4ad..1c08a9621f 100644 --- a/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift +++ b/Sources/Plugins/Astronomy/AstronomyPluginSettings.swift @@ -348,7 +348,7 @@ final class AstronomyPluginSettings { starMap = storage.starMap ?? StarMapConfig() } - func getCommonConfig() -> CommonConfig { + func commonConfig() -> CommonConfig { lock.lock() defer { lock.unlock() } let config = readStorageUnlocked().common ?? CommonConfig() @@ -365,7 +365,7 @@ final class AstronomyPluginSettings { common = config } - func getStarMapConfig() -> StarMapConfig { + func starMapConfig() -> StarMapConfig { lock.lock() defer { lock.unlock() } let config = readStorageUnlocked().starMap ?? StarMapConfig() @@ -461,7 +461,7 @@ final class AstronomyPluginSettings { // MARK: - Recently viewed (global, all profiles) - func getRecentChips() -> [StarMapRecentChip] { + func recentChips() -> [StarMapRecentChip] { lock.lock() defer { lock.unlock() } return readRecentChipsUnlocked() diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index 50bc89e2d9..95f43773da 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -256,7 +256,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } func searchStarMapConfig() -> AstronomyPluginSettings.StarMapConfig { - settings.getStarMapConfig() + settings.starMapConfig() } func isSearchRedFilterEnabled() -> Bool { @@ -560,8 +560,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func applyProfileSettingsToUI(restoreMapPosition: Bool) { - let starMap = settings.getStarMapConfig() - let common = settings.getCommonConfig() + let starMap = settings.starMapConfig() + let common = settings.commonConfig() applySettings(starMap) starView.settings = settings.copyForUI() updateRegularMapVisibility(common.showRegularMap) @@ -1359,8 +1359,8 @@ final class StarMapViewController: UIViewController, StarViewDelegate { hideBottomSheet(clearSelection: false) let sheet = AstroConfigureViewBottomSheet() - sheet.config = settings.getStarMapConfig() - sheet.commonConfig = settings.getCommonConfig() + sheet.config = settings.starMapConfig() + sheet.commonConfig = settings.commonConfig() sheet.onConfigChanged = { [weak self] config in self?.setStarMapSettings(config) } diff --git a/Sources/Plugins/Astronomy/StarObjectsViewModel.swift b/Sources/Plugins/Astronomy/StarObjectsViewModel.swift index 512aefe20a..71095b7f3b 100644 --- a/Sources/Plugins/Astronomy/StarObjectsViewModel.swift +++ b/Sources/Plugins/Astronomy/StarObjectsViewModel.swift @@ -38,7 +38,7 @@ final class StarObjectsViewModel { let constellations = provider.getConstellations(preferredLocale: preferredLocale) DispatchQueue.main.async { self.applyObjectSettings(to: objects + constellations.map { $0 as SkyObject }) - let starMap = self.settings.getStarMapConfig() + let starMap = self.settings.starMapConfig() let favoriteOrder = Dictionary(uniqueKeysWithValues: starMap.favorites.enumerated().map { ($0.element.id, $0.offset) }) self.skyObjects = objects.sorted { (favoriteOrder[$0.id] ?? Int.max) < (favoriteOrder[$1.id] ?? Int.max) } self.constellations = constellations.sorted { (favoriteOrder[$0.id] ?? Int.max) < (favoriteOrder[$1.id] ?? Int.max) } @@ -55,7 +55,7 @@ final class StarObjectsViewModel { targetObjects = skyObjects + constellations.map { $0 as SkyObject } } - let starMap = settings.getStarMapConfig() + let starMap = settings.starMapConfig() let favoritesMap = Dictionary(uniqueKeysWithValues: starMap.favorites.map { ($0.id, $0) }) let directionsMap = Dictionary(uniqueKeysWithValues: starMap.directions.map { ($0.id, $0) }) let celestialPathsMap = Dictionary(uniqueKeysWithValues: starMap.celestialPaths.map { ($0.id, $0) }) diff --git a/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift b/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift index e4dd9acb86..d0174d6b00 100644 --- a/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift +++ b/Sources/Plugins/Astronomy/search/StarMapMyDataViewController.swift @@ -430,7 +430,7 @@ final class StarMapMyDataViewController: UIViewController { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self else { return } - let insertionOrderById = self.getMyDataInsertionOrderMap(stateSnapshot.quickPresetType) + let insertionOrderById = self.myDataInsertionOrderMap(stateSnapshot.quickPresetType) let filteredEntries = stateSnapshot.filterAndSort( preparedEntries: preparedEntriesSnapshot.map { $0.copy() }, visibleTonightProvider: self.searchHelper.getVisibleTonight, @@ -467,8 +467,8 @@ final class StarMapMyDataViewController: UIViewController { } } - private func getMyDataInsertionOrderMap(_ quickPresetType: StarMapSearchQuickPresetType) -> [String: Int] { - let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.getStarMapConfig() + private func myDataInsertionOrderMap(_ quickPresetType: StarMapSearchQuickPresetType) -> [String: Int] { + let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.starMapConfig() let ids: [String] switch quickPresetType { case .MY_DATA_FAVORITES: @@ -488,7 +488,7 @@ final class StarMapMyDataViewController: UIViewController { } private func currentTabHasData() -> Bool { - let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.getStarMapConfig() + let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.starMapConfig() switch currentTab { case .favorites: return !config.favorites.isEmpty diff --git a/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift b/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift index 6a58128b4b..af368f9030 100644 --- a/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift +++ b/Sources/Plugins/Astronomy/search/StarMapSearchViewController.swift @@ -797,7 +797,7 @@ final class StarMapSearchViewController: UIViewController { StarMapExploreRowConfig(quickPresetType: .CATEGORY_STAR_CLUSTERS, iconRes: "ic_custom_star_clusters", titleRes: "astro_star_clusters", subtitleRes: nil), StarMapExploreRowConfig(quickPresetType: .CATEGORY_DEEP_SKY, iconRes: "ic_custom_galaxy", titleRes: "astro_deep_sky", subtitleRes: "astro_explore_deep_sky_subtitle") ] - let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.getStarMapConfig() + let config = parentStarMapController?.searchStarMapConfig() ?? plugin.astroSettings.starMapConfig() let myDataItems: [(StarMapExploreRowConfig, Int)] = [ (StarMapExploreRowConfig(quickPresetType: .MY_DATA_FAVORITES, iconRes: "ic_custom_bookmark", titleRes: "favorites_item", subtitleRes: nil), config.favorites.count), (StarMapExploreRowConfig(quickPresetType: .MY_DATA_DAILY_PATH, iconRes: "ic_custom_target_path_on", titleRes: "astro_daily_path", subtitleRes: nil), config.celestialPaths.count), @@ -1058,7 +1058,7 @@ final class StarMapSearchViewController: UIViewController { private func syncRecentChipsWithSession() { if plugin.recentSearchChips.isEmpty { - plugin.recentSearchChips = plugin.astroSettings.getRecentChips() + plugin.recentSearchChips = plugin.astroSettings.recentChips() } if plugin.recentSearchChips.isEmpty { plugin.recentSearchChips.append(contentsOf: searchState.recentChips) From 37a369e751e5a2e35be6b00918e0b5898f888b43 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Fri, 17 Jul 2026 18:01:56 +0300 Subject: [PATCH 15/20] Migrate Android legacy star_watcher_settings into astronomy_settings on iOS Register the legacy profile/shared preference so cloud and file backups can apply it, then copy into astronomy_settings only when the new pref is empty (after plugin init and profile import/sync). --- Sources/Backup/BackupUtils.swift | 6 ++++++ Sources/Helpers/MigrationManager.swift | 11 +++++++++++ Sources/Plugins/Astronomy/AstronomyPlugin.swift | 17 +++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/Sources/Backup/BackupUtils.swift b/Sources/Backup/BackupUtils.swift index a25324c2c8..9e94d33a2f 100644 --- a/Sources/Backup/BackupUtils.swift +++ b/Sources/Backup/BackupUtils.swift @@ -264,5 +264,11 @@ final class BackupUtils: NSObject { if updatePoiFilters { OAPOIFiltersHelper.sharedInstance().loadSelectedPoiFilters() } + + let hasProfileItems = items.contains { $0 is OAProfileSettingsItem } + if hasProfileItems, + let plugin = OAPluginsHelper.getPlugin(AstronomyPlugin.self) as? AstronomyPlugin { + plugin.migrateLegacyStarWatcherSettingsIfNeeded() + } } } diff --git a/Sources/Helpers/MigrationManager.swift b/Sources/Helpers/MigrationManager.swift index 8a1651e65c..3df2b09065 100644 --- a/Sources/Helpers/MigrationManager.swift +++ b/Sources/Helpers/MigrationManager.swift @@ -22,6 +22,7 @@ final class MigrationManager: NSObject { case migrationHudButtonPositionsKey case migrateRouteRecalculationValues case migrateLocationIconSizeAndCourseIconSize + case migrateAstronomyPreferences } private struct HudMigrationScenario { @@ -104,6 +105,10 @@ final class MigrationManager: NSObject { migrateLocationIconSizeAndCourseIconSize() defaults.set(true, forKey: MigrationKey.migrateLocationIconSizeAndCourseIconSize.rawValue) } + if !defaults.bool(forKey: MigrationKey.migrateAstronomyPreferences.rawValue) { + migrateAstronomyPreferences() + defaults.set(true, forKey: MigrationKey.migrateAstronomyPreferences.rawValue) + } } } @@ -600,6 +605,12 @@ final class MigrationManager: NSObject { } } + private func migrateAstronomyPreferences() { + if let plugin = OAPluginsHelper.getPlugin(AstronomyPlugin.self) as? AstronomyPlugin { + plugin.migrateLegacyStarWatcherSettingsIfNeeded() + } + } + // MARK: - Import old versions func changeJsonMigrationToV2(_ json: [String: String]) -> [String: String] { diff --git a/Sources/Plugins/Astronomy/AstronomyPlugin.swift b/Sources/Plugins/Astronomy/AstronomyPlugin.swift index 592b901419..dab313af05 100644 --- a/Sources/Plugins/Astronomy/AstronomyPlugin.swift +++ b/Sources/Plugins/Astronomy/AstronomyPlugin.swift @@ -11,6 +11,7 @@ import UIKit @objc final class AstronomyPlugin: OAPlugin { private enum PreferenceId { static let settings = "astronomy_settings" + static let legacySettings = "star_watcher_settings" static let recent = "astronomy_recently_viewed" } @@ -24,6 +25,11 @@ import UIKit .makeProfile() .makeShared() + private let legacySettingsPref: OACommonString = OAAppSettings.sharedManager() + .registerStringPreference(PreferenceId.legacySettings, defValue: "") + .makeProfile() + .makeShared() + private let recentPref: OACommonString = OAAppSettings.sharedManager() .registerStringPreference(PreferenceId.recent, defValue: "") .makeGlobal() @@ -33,6 +39,7 @@ import UIKit override init() { super.init() + migrateLegacyStarWatcherSettingsIfNeeded() recentSearchChips = astronomySettingsStorage.recentChips() } @@ -40,6 +47,16 @@ import UIKit astronomySettingsStorage.setRecentChips(recentSearchChips) } + func migrateLegacyStarWatcherSettingsIfNeeded() { + for appMode in OAApplicationMode.allPossibleValues() { + guard legacySettingsPref.isSet(for: appMode), !settingsPref.isSet(for: appMode) else { + continue + } + settingsPref.set(legacySettingsPref.get(appMode), mode: appMode) + } + astronomySettingsStorage.reloadFromPreference() + } + override func getId() -> String? { kInAppId_Addon_Astronomy } From c02d9e7541de1142676506894b3473e3c712e50c Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Mon, 20 Jul 2026 17:08:24 +0300 Subject: [PATCH 16/20] Code review fixes --- Sources/Backup/BackupUtils.swift | 6 ------ Sources/Helpers/MigrationManager.swift | 3 ++- Sources/Plugins/Astronomy/AstronomyPlugin.swift | 2 -- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Sources/Backup/BackupUtils.swift b/Sources/Backup/BackupUtils.swift index 9e94d33a2f..a25324c2c8 100644 --- a/Sources/Backup/BackupUtils.swift +++ b/Sources/Backup/BackupUtils.swift @@ -264,11 +264,5 @@ final class BackupUtils: NSObject { if updatePoiFilters { OAPOIFiltersHelper.sharedInstance().loadSelectedPoiFilters() } - - let hasProfileItems = items.contains { $0 is OAProfileSettingsItem } - if hasProfileItems, - let plugin = OAPluginsHelper.getPlugin(AstronomyPlugin.self) as? AstronomyPlugin { - plugin.migrateLegacyStarWatcherSettingsIfNeeded() - } } } diff --git a/Sources/Helpers/MigrationManager.swift b/Sources/Helpers/MigrationManager.swift index 3df2b09065..f746482d03 100644 --- a/Sources/Helpers/MigrationManager.swift +++ b/Sources/Helpers/MigrationManager.swift @@ -621,7 +621,8 @@ final class MigrationManager: NSObject { "top_widget_panel_order": "widget_top_panel_order", "bottom_widget_panel_order": "widget_bottom_panel_order", "shared_string_automatic": "driving_region_automatic", - "external_input_device": "selected_external_input_device" + "external_input_device": "selected_external_input_device", + "star_watcher_settings": "astronomy_settings" ] let changeWidgetIds = [ diff --git a/Sources/Plugins/Astronomy/AstronomyPlugin.swift b/Sources/Plugins/Astronomy/AstronomyPlugin.swift index dab313af05..379bd1df0d 100644 --- a/Sources/Plugins/Astronomy/AstronomyPlugin.swift +++ b/Sources/Plugins/Astronomy/AstronomyPlugin.swift @@ -33,13 +33,11 @@ import UIKit private let recentPref: OACommonString = OAAppSettings.sharedManager() .registerStringPreference(PreferenceId.recent, defValue: "") .makeGlobal() - .makeShared() private lazy var astronomySettingsStorage = AstronomyPluginSettings(settingsPref: settingsPref, recentPref: recentPref) override init() { super.init() - migrateLegacyStarWatcherSettingsIfNeeded() recentSearchChips = astronomySettingsStorage.recentChips() } From 2f925d015d1d5b634cd2cd98905eca0db07f3bf7 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Tue, 21 Jul 2026 18:04:50 +0300 Subject: [PATCH 17/20] Code review fixes --- .../ic_navbar_close.imageset/Contents.json | 25 ++++++++++++++ .../ic_navbar_close@2x.png | Bin 0 -> 575 bytes .../ic_navbar_close@3x.png | Bin 0 -> 777 bytes .../AstroConfigureViewBottomSheet.swift | 2 +- .../Plugins/Astronomy/AstronomyPlugin.swift | 1 - Sources/Plugins/Astronomy/StarMapButton.swift | 4 +-- .../Astronomy/StarMapCameraHelper.swift | 2 +- .../StarMapMagnitudeFilterButton.swift | 8 ++--- .../StarMapMagnitudeSliderPanel.swift | 10 +++--- .../Astronomy/StarMapViewController.swift | 32 +++++++++--------- Sources/Plugins/Astronomy/StarView.swift | 28 +++++++-------- 11 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 Resources/Images.xcassets/Icons/ic_navbar_close.imageset/Contents.json create mode 100644 Resources/Images.xcassets/Icons/ic_navbar_close.imageset/ic_navbar_close@2x.png create mode 100644 Resources/Images.xcassets/Icons/ic_navbar_close.imageset/ic_navbar_close@3x.png diff --git a/Resources/Images.xcassets/Icons/ic_navbar_close.imageset/Contents.json b/Resources/Images.xcassets/Icons/ic_navbar_close.imageset/Contents.json new file mode 100644 index 0000000000..6d1212439c --- /dev/null +++ b/Resources/Images.xcassets/Icons/ic_navbar_close.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_navbar_close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_navbar_close@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Resources/Images.xcassets/Icons/ic_navbar_close.imageset/ic_navbar_close@2x.png b/Resources/Images.xcassets/Icons/ic_navbar_close.imageset/ic_navbar_close@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cca669f0c069e9ee7a7ce0d24cf3dedde85a6ba6 GIT binary patch literal 575 zcmV-F0>J%=P)YzG2G|?=!_Cv0Y=isA||%=w{LY|znM%jiL8C!OcHA)BEv8Y z!}z}psR7d~-MgnZnrL*RkHZsvQb$Vj+@vOS6CK^-*1fuO&mZRkT?n;ZhM32TCP)ho zVX})VeI%pYGRv5{E-m;?kJr_7C0FG*$0>Df@;4afd7*n=2wuElH*u40f3T{avZ5P2tZO=j^i{SDT$EdD2+%+ zqU1P+04sUgRug~^fBlH;-f_9S_LjvSW+u_?Ja7$fI_33{?5 zfpQ+Gpl4SSEayo#yzLdHs?UVLenq$bEtd(PO*8IKCP(f!1xylio7lO}ovW(t49MT; zGj8IVI|H&ywSpCjmZaQbyJGpJAt}=?HLNYslI;zzz`_|R*|udN*cG58>o=VMEe}Gn zZrc#p&q+&`Z~Q7WtCEssTlb2cxRhl6;ftbNoLWFK@A#~PTrANjnRiCtNiLRZkjy)y zzkW|m&J$|QoyZc%8NI4pjIkyuct-!_M8{roGZ&I`o)Z~{VHk$7$qyo=XFVq3CSgxW9*_u)Rd`o$1k4`zvY*Ffi#|1Gw8I_dl^qVmDA4u$ahKHcq>oSX88C` z`G5E;*V53Hv^RYAz2gH)*{7K3N`6RVJ)mo)L|w(~tR3`K9?$$V73xLIS9y`f`redo z=DDpsMz(g*f$P$$W6S;vR3sfddOnFABifTAu`B1}XDikeW#FcBRn+Js7o zi8Pnu=yjO5G;R|R-@km@S|hr7f37#BzJ*&hzu^dI-X>U~5`XB~ za5$M^g-Yz;NXM}`J8InU-Hw3{X4IGgW`$8BR#g8f5XB)lMpWNPI)m{{HdMc9WW&K> zCRCq=4h|#jEU5NNaU7>D7*K5&iv`R9MWfm+XAGQBi$t|qa7!>d8HK99B%ElfHv(1n zqy@rxb8l3=(`twpA$X$docNVsMTZya{!B;#J^{C0eT%iP`n;;Ds;a80s;a80s#@<2sU6}YWkd6r00000NkvXX Hu0mjfDuGpD literal 0 HcmV?d00001 diff --git a/Sources/Plugins/Astronomy/AstroConfigureViewBottomSheet.swift b/Sources/Plugins/Astronomy/AstroConfigureViewBottomSheet.swift index 1e1b64355d..6bed1bf9ee 100644 --- a/Sources/Plugins/Astronomy/AstroConfigureViewBottomSheet.swift +++ b/Sources/Plugins/Astronomy/AstroConfigureViewBottomSheet.swift @@ -93,7 +93,7 @@ final class AstroConfigureViewBottomSheet: UIViewController, UISheetPresentation } private func configureNavigationBar() { - let imageClose = OAUtilities.resize(UIImage.templateImageNamed("ic_navbar_close"), + let imageClose = OAUtilities.resize(.icNavbarClose, newSize: CGSize(width: 24, height: 24))?.withRenderingMode(.alwaysTemplate) let closeButton = UIBarButtonItem(image: imageClose, style: .plain, target: self, action: #selector(closeAction)) closeButton.tintColor = .label diff --git a/Sources/Plugins/Astronomy/AstronomyPlugin.swift b/Sources/Plugins/Astronomy/AstronomyPlugin.swift index 379bd1df0d..19f1429207 100644 --- a/Sources/Plugins/Astronomy/AstronomyPlugin.swift +++ b/Sources/Plugins/Astronomy/AstronomyPlugin.swift @@ -28,7 +28,6 @@ import UIKit private let legacySettingsPref: OACommonString = OAAppSettings.sharedManager() .registerStringPreference(PreferenceId.legacySettings, defValue: "") .makeProfile() - .makeShared() private let recentPref: OACommonString = OAAppSettings.sharedManager() .registerStringPreference(PreferenceId.recent, defValue: "") diff --git a/Sources/Plugins/Astronomy/StarMapButton.swift b/Sources/Plugins/Astronomy/StarMapButton.swift index 5dc90cc672..2bab5866fa 100644 --- a/Sources/Plugins/Astronomy/StarMapButton.swift +++ b/Sources/Plugins/Astronomy/StarMapButton.swift @@ -69,8 +69,8 @@ class StarMapButton: OAHudButton { } } - func setIcon(iconName: String, accessibilityLabel: String? = nil) { - setImage(AstroIcon.template(iconName), for: .normal) + func setIcon(icon: UIImage?, accessibilityLabel: String? = nil) { + setImage(icon, for: .normal) self.accessibilityLabel = accessibilityLabel updateTheme() } diff --git a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift index ae22bbe6f9..0d4ad5ae3a 100644 --- a/Sources/Plugins/Astronomy/StarMapCameraHelper.swift +++ b/Sources/Plugins/Astronomy/StarMapCameraHelper.swift @@ -77,7 +77,7 @@ final class StarMapCameraHelper { lastVideoOrientation = orientation return } - let currentViewAngle = starView?.getViewAngle() + let currentViewAngle = starView?.viewAngleValue() updateEffectiveFov() if geometryChanged { starView?.setViewAngle(calculatedFov) diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift index efe220c0f1..a91b4d589c 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeFilterButton.swift @@ -32,10 +32,6 @@ final class StarMapMagnitudeFilterButton: StarMapButton { applyPillAppearance() } - func setValue(_ text: String) { - valueLabel.text = text - } - override func updateTheme() { super.updateTheme() setImage(nil, for: .normal) @@ -63,6 +59,10 @@ final class StarMapMagnitudeFilterButton: StarMapButton { } } } + + func setValue(_ text: String) { + valueLabel.text = text + } private func setupContent() { setImage(nil, for: .normal) diff --git a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift index f09f4c9975..d27bb42b09 100644 --- a/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift +++ b/Sources/Plugins/Astronomy/StarMapMagnitudeSliderPanel.swift @@ -34,6 +34,11 @@ final class StarMapMagnitudeSliderPanel: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func layoutSubviews() { + super.layoutSubviews() + glassBackgroundView?.frame = bounds + } func toggle() { isHidden.toggle() @@ -59,11 +64,6 @@ final class StarMapMagnitudeSliderPanel: UIView { layer.borderWidth = nightMode ? 2 : 0 } - override func layoutSubviews() { - super.layoutSubviews() - glassBackgroundView?.frame = bounds - } - private func setupContent(maxMagnitude: Double) { translatesAutoresizingMaskIntoConstraints = false layer.cornerRadius = Self.cornerRadius diff --git a/Sources/Plugins/Astronomy/StarMapViewController.swift b/Sources/Plugins/Astronomy/StarMapViewController.swift index 95f43773da..e7eccf7c3c 100644 --- a/Sources/Plugins/Astronomy/StarMapViewController.swift +++ b/Sources/Plugins/Astronomy/StarMapViewController.swift @@ -335,7 +335,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { self?.toggleGyroMode() } - addRoundButton(searchButton, iconName: "ic_custom_search", accessibilityLabel: localizedString("shared_string_search")) + addRoundButton(searchButton, icon: .icCustomSearch, accessibilityLabel: localizedString("shared_string_search")) searchButton.addTarget(self, action: #selector(showSearchDialog), for: .touchUpInside) mapControlsContainer.addSubview(arControlCard) @@ -365,10 +365,10 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } private func setupRightControls() { - addRoundButton(closeButton, iconName: "ic_navbar_close", accessibilityLabel: localizedString("shared_string_close")) + addRoundButton(closeButton, icon: .icNavbarClose, accessibilityLabel: localizedString("shared_string_close")) closeButton.addTarget(self, action: #selector(close), for: .touchUpInside) - addRoundButton(settingsButton, iconName: "ic_custom_overlay_map", accessibilityLabel: localizedString("shared_string_settings")) + addRoundButton(settingsButton, icon: .icCustomOverlayMap, accessibilityLabel: localizedString("shared_string_settings")) settingsButton.addTarget(self, action: #selector(showConfigureSheet), for: .touchUpInside) NSLayoutConstraint.activate([ @@ -429,12 +429,12 @@ final class StarMapViewController: UIViewController, StarViewDelegate { guard zoomButtonsVisible else { return } addRoundButton(zoomInButton, - iconName: "ic_custom_map_zoom_in", + icon: .icCustomMapZoomIn, accessibilityLabel: localizedString("key_hint_zoom_in")) zoomInButton.addTarget(self, action: #selector(zoomInPressed), for: .touchUpInside) addRoundButton(zoomOutButton, - iconName: "ic_custom_map_zoom_out", + icon: .icCustomMapZoomOut, accessibilityLabel: localizedString("key_hint_zoom_out")) zoomOutButton.addTarget(self, action: #selector(zoomOutPressed), for: .touchUpInside) @@ -448,10 +448,10 @@ final class StarMapViewController: UIViewController, StarViewDelegate { updateZoomButtonsEnabled() } - private func addRoundButton(_ button: StarMapButton, iconName: String? = nil, accessibilityLabel: String) { + private func addRoundButton(_ button: StarMapButton, icon: UIImage? = nil, accessibilityLabel: String) { button.translatesAutoresizingMaskIntoConstraints = false - if let iconName { - button.setIcon(iconName: iconName, accessibilityLabel: accessibilityLabel) + if let icon { + button.setIcon(icon: icon, accessibilityLabel: accessibilityLabel) } else { button.accessibilityLabel = accessibilityLabel } @@ -502,7 +502,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { self?.selectedObject = object if let object { self?.showObjectInfo(object) - } else if self?.starView.getSelectedConstellationItem() == nil { + } else if self?.starView.selectedConstellationItem() == nil { self?.hideBottomSheet() } } @@ -658,9 +658,9 @@ final class StarMapViewController: UIViewController, StarViewDelegate { config.showMagnitudeFilter = starView.showMagnitudeFilter || starView.magnitudeFilter != nil config.magnitudeFilter = starView.magnitudeFilter config.savedMapPosition = AstronomyPluginSettings.MapViewPosition( - azimuth: starView.getAzimuth(), - altitude: starView.getAltitude(), - viewAngle: starView.getViewAngle(), + azimuth: starView.azimuth(), + altitude: starView.altitude(), + viewAngle: starView.viewAngleValue(), roll: starView.roll, panX: Double(starView.panX), panY: Double(starView.panY) @@ -699,9 +699,9 @@ final class StarMapViewController: UIViewController, StarViewDelegate { private func apply2DMode(_ is2D: Bool) { if is2D { - previousAltitude = starView.getAltitude() - previousAzimuth = starView.getAzimuth() - previousViewAngle = starView.getViewAngle() + previousAltitude = starView.altitude() + previousAzimuth = starView.azimuth() + previousViewAngle = starView.viewAngleValue() starView.is2DMode = true starView.setCenter(azimuth: 180, altitude: 90) setArExperienceEnabled(false) @@ -1266,7 +1266,7 @@ final class StarMapViewController: UIViewController, StarViewDelegate { } @objc private func showSearchDialog() { - if UIDevice.current.userInterfaceIdiom == .pad, searchViewController != nil { + if OAUtilities.isIPad(), searchViewController != nil { dismissSearchDialog(animated: true) } else { showSearchDialog(initialCatalogWid: nil) diff --git a/Sources/Plugins/Astronomy/StarView.swift b/Sources/Plugins/Astronomy/StarView.swift index de89186a5c..fa815e98db 100644 --- a/Sources/Plugins/Astronomy/StarView.swift +++ b/Sources/Plugins/Astronomy/StarView.swift @@ -437,15 +437,15 @@ final class StarView: UIView { setNeedsDisplay() } - func getAltitude() -> Double { + func altitude() -> Double { centerAltitude } - func getAzimuth() -> Double { + func azimuth() -> Double { centerAzimuth } - func getViewAngle() -> Double { + func viewAngleValue() -> Double { viewAngle } @@ -511,7 +511,7 @@ final class StarView: UIView { onConstellationClickListener = listener } - func getSelectedConstellationItem() -> Constellation? { + func selectedConstellationItem() -> Constellation? { guard let selectedConstellationId else { return nil } @@ -689,6 +689,16 @@ final class StarView: UIView { func calculatePosition(_ object: SkyObject) { calculatePosition(object, time: currentTime, updateTargets: false) } + + private func commonInit() { + backgroundColor = .clear + isOpaque = false + contentMode = .redraw + + addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) + addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))) + addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))) + } private func setCenter(azimuth: Double, altitude: Double, @@ -2180,16 +2190,6 @@ final class StarView: UIView { return value } - private func commonInit() { - backgroundColor = .clear - isOpaque = false - contentMode = .redraw - - addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) - addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))) - addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))) - } - deinit { timeAnimationDisplayLink?.invalidate() focusAnimationDisplayLink?.invalidate() From 0fa2a7f2f425feb4187828ad4fe2018973d783e6 Mon Sep 17 00:00:00 2001 From: vitaliy-sova-ios Date: Tue, 21 Jul 2026 18:49:45 +0300 Subject: [PATCH 18/20] Remove unused icons and fix StarMap 2D/3D mode handling --- OsmAnd.xcodeproj/project.pbxproj | 18 ++++----------- Resources/Icons/ic_navbar_close@2x.png | Bin 575 -> 0 bytes Resources/Icons/ic_navbar_close@3x.png | Bin 777 -> 0 bytes .../OARoutePlanningHudViewController.xib | 4 ++-- .../OAMapillaryImageViewController.xib | 4 ++-- .../OADownloadMapProgressViewController.xib | 4 ++-- .../Astronomy/StarMapViewController.swift | 4 ++-- Sources/Plugins/Astronomy/StarView.swift | 21 +++++++++++++++--- 8 files changed, 30 insertions(+), 25 deletions(-) delete mode 100644 Resources/Icons/ic_navbar_close@2x.png delete mode 100644 Resources/Icons/ic_navbar_close@3x.png diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index d8d9b568ec..734c86bea7 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -669,7 +669,6 @@ 460679FB27708A980076740B /* OARouteLineAppearanceHudViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 460679FA27708A980076740B /* OARouteLineAppearanceHudViewController.xib */; }; 460767122A49F86400AF63F2 /* WikiAlgorithms.swift in Sources */ = {isa = PBXBuildFile; fileRef = 460767112A49F86400AF63F2 /* WikiAlgorithms.swift */; }; 4609FAA22C5158F100B57F06 /* UIImageView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4609FAA12C5158F100B57F06 /* UIImageView+Extension.swift */; }; - 228EDAF170A7A81DF3A85783 /* UIImage+RouteActivityIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD9FA9C2F7AD66558B5B407 /* UIImage+RouteActivityIcon.swift */; }; 460C1E702BFC9B490090D67B /* QuickActionIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 460C1E6F2BFC9B490090D67B /* QuickActionIds.swift */; }; 460E9AC12B71111000411854 /* CloudTrashItemMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 460E9AC02B71111000411854 /* CloudTrashItemMenuViewController.swift */; }; 4611DF3A28569C5000885FD1 /* OASubscriptionBannerCardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4611DF3928569C5000885FD1 /* OASubscriptionBannerCardView.xib */; }; @@ -1704,11 +1703,11 @@ CE4075A32FC48BD9004224AA /* BasePoiIconCollectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4075A22FC48BD6004224AA /* BasePoiIconCollectionHandler.swift */; }; CE4075A52FC48C40004224AA /* ProfileIconCollectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4075A42FC48C2F004224AA /* ProfileIconCollectionHandler.swift */; }; CE71B88C2FEFAB0C008B0759 /* StarMapSearchEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE71B88B2FEFAB0C008B0759 /* StarMapSearchEmptyView.swift */; }; + CE7339672FF6D01900A9B191 /* TrackPointsKDIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7339662FF6D01900A9B191 /* TrackPointsKDIndex.swift */; }; CE762AEE2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */; }; CE762AF02FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AEF2FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift */; }; CE762AF22FFE1197001BF551 /* StarMapGlassBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AF12FFE1197001BF551 /* StarMapGlassBackground.swift */; }; CE762AF42FFE251B001BF551 /* StarMapTimeControlCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE762AF32FFE251B001BF551 /* StarMapTimeControlCard.swift */; }; - CE7339672FF6D01900A9B191 /* TrackPointsKDIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7339662FF6D01900A9B191 /* TrackPointsKDIndex.swift */; }; CE7818262FC07944005CCF47 /* WikipediaContextMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */; }; CE7AC1A82FE02A2600495411 /* OATrackPreviewMapRenderer.mm in Sources */ = {isa = PBXBuildFile; fileRef = CE7AC19A2FE02A2600495411 /* OATrackPreviewMapRenderer.mm */; }; CE7AC1A92FE02A2600495411 /* TrackBitmapDrawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AC19B2FE02A2600495411 /* TrackBitmapDrawer.swift */; }; @@ -1724,9 +1723,9 @@ CE7AC7A03008E3EE00A864B9 /* AstroWikiBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AC79F3008E3EE00A864B9 /* AstroWikiBridge.swift */; }; CE7CE0EE2FED645D001709F0 /* StarMapMyDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CE0ED2FED645D001709F0 /* StarMapMyDataViewController.swift */; }; CE7CE0F02FED8435001709F0 /* StarMapSearchExploreAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CE0EF2FED8435001709F0 /* StarMapSearchExploreAdapter.swift */; }; + CE7F960C30025BA0000F2F22 /* StarMapObjectContextMenuBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7F960B30025BA0000F2F22 /* StarMapObjectContextMenuBuilder.swift */; }; CE8215E52FFF6376002A8617 /* StarMapArControlCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */; }; CE8215E72FFF6AF9002A8617 /* StarMapPlainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8215E62FFF6AF9002A8617 /* StarMapPlainButton.swift */; }; - CE7F960C30025BA0000F2F22 /* StarMapObjectContextMenuBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7F960B30025BA0000F2F22 /* StarMapObjectContextMenuBuilder.swift */; }; CE83DB4A2FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE83DB492FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift */; }; CE83DB4C2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE83DB4B2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift */; }; CE8A82A92FCFE11F00EADFD8 /* MapVariantReplacementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */; }; @@ -3273,8 +3272,6 @@ DAEC05FB2296C6C500045298 /* ic_custom_show@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAEC05EC2296C6C400045298 /* ic_custom_show@3x.png */; }; DAEC05FD2296C6C500045298 /* ic_custom_date@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAEC05ED2296C6C400045298 /* ic_custom_date@2x.png */; }; DAEC05FF2296C6C500045298 /* ic_custom_date@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAEC05EE2296C6C500045298 /* ic_custom_date@3x.png */; }; - DAEC06042296CA7C00045298 /* ic_navbar_close@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAEC06012296CA7B00045298 /* ic_navbar_close@2x.png */; }; - DAEC06062296CA7C00045298 /* ic_navbar_close@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAEC06022296CA7C00045298 /* ic_navbar_close@3x.png */; }; DAECA4FA26CFE62600241B0B /* OABaseWidgetView.m in Sources */ = {isa = PBXBuildFile; fileRef = DAECA4F926CFE62600241B0B /* OABaseWidgetView.m */; }; DAEDAAA12865A0F100CE54D0 /* OAGenerateBackupInfoTask.m in Sources */ = {isa = PBXBuildFile; fileRef = DAEDAAA02865A0F100CE54D0 /* OAGenerateBackupInfoTask.m */; }; DAF2F804254825BA00967935 /* map_plan_route_point_movable@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAF2F7FB254825B600967935 /* map_plan_route_point_movable@3x.png */; }; @@ -4433,7 +4430,6 @@ 460679FA27708A980076740B /* OARouteLineAppearanceHudViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OARouteLineAppearanceHudViewController.xib; sourceTree = ""; }; 460767112A49F86400AF63F2 /* WikiAlgorithms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiAlgorithms.swift; sourceTree = ""; }; 4609FAA12C5158F100B57F06 /* UIImageView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extension.swift"; sourceTree = ""; }; - 8BD9FA9C2F7AD66558B5B407 /* UIImage+RouteActivityIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+RouteActivityIcon.swift"; sourceTree = ""; }; 460C1E6F2BFC9B490090D67B /* QuickActionIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionIds.swift; sourceTree = ""; }; 460E9AC02B71111000411854 /* CloudTrashItemMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTrashItemMenuViewController.swift; sourceTree = ""; }; 4611DF3928569C5000885FD1 /* OASubscriptionBannerCardView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OASubscriptionBannerCardView.xib; sourceTree = ""; }; @@ -5754,11 +5750,11 @@ CE4075A22FC48BD6004224AA /* BasePoiIconCollectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasePoiIconCollectionHandler.swift; sourceTree = ""; }; CE4075A42FC48C2F004224AA /* ProfileIconCollectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileIconCollectionHandler.swift; sourceTree = ""; }; CE71B88B2FEFAB0C008B0759 /* StarMapSearchEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchEmptyView.swift; sourceTree = ""; }; + CE7339662FF6D01900A9B191 /* TrackPointsKDIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackPointsKDIndex.swift; sourceTree = ""; }; CE762AED2FFE0A2A001BF551 /* StarMapMagnitudeFilterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMagnitudeFilterButton.swift; sourceTree = ""; }; CE762AEF2FFE0F32001BF551 /* StarMapMagnitudeSliderPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMagnitudeSliderPanel.swift; sourceTree = ""; }; CE762AF12FFE1197001BF551 /* StarMapGlassBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapGlassBackground.swift; sourceTree = ""; }; CE762AF32FFE251B001BF551 /* StarMapTimeControlCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapTimeControlCard.swift; sourceTree = ""; }; - CE7339662FF6D01900A9B191 /* TrackPointsKDIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackPointsKDIndex.swift; sourceTree = ""; }; CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikipediaContextMenuCell.swift; sourceTree = ""; }; CE7AC1992FE02A2600495411 /* OATrackPreviewMapRenderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OATrackPreviewMapRenderer.h; sourceTree = ""; }; CE7AC19A2FE02A2600495411 /* OATrackPreviewMapRenderer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OATrackPreviewMapRenderer.mm; sourceTree = ""; }; @@ -5775,9 +5771,9 @@ CE7AC79F3008E3EE00A864B9 /* AstroWikiBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstroWikiBridge.swift; sourceTree = ""; }; CE7CE0ED2FED645D001709F0 /* StarMapMyDataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapMyDataViewController.swift; sourceTree = ""; }; CE7CE0EF2FED8435001709F0 /* StarMapSearchExploreAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchExploreAdapter.swift; sourceTree = ""; }; + CE7F960B30025BA0000F2F22 /* StarMapObjectContextMenuBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapObjectContextMenuBuilder.swift; sourceTree = ""; }; CE8215E42FFF6376002A8617 /* StarMapArControlCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapArControlCard.swift; sourceTree = ""; }; CE8215E62FFF6AF9002A8617 /* StarMapPlainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapPlainButton.swift; sourceTree = ""; }; - CE7F960B30025BA0000F2F22 /* StarMapObjectContextMenuBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapObjectContextMenuBuilder.swift; sourceTree = ""; }; CE83DB492FEE4B20002D7685 /* StarMapSearchSortFilterChipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchSortFilterChipsView.swift; sourceTree = ""; }; CE83DB4B2FEE4B20002D7685 /* StarMapSearchSortFilterChipsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMapSearchSortFilterChipsProvider.swift; sourceTree = ""; }; CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVariantReplacementManager.swift; sourceTree = ""; }; @@ -8148,8 +8144,6 @@ DAEC05EC2296C6C400045298 /* ic_custom_show@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_show@3x.png"; path = "Resources/Icons/ic_custom_show@3x.png"; sourceTree = ""; }; DAEC05ED2296C6C400045298 /* ic_custom_date@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_date@2x.png"; path = "Resources/Icons/ic_custom_date@2x.png"; sourceTree = ""; }; DAEC05EE2296C6C500045298 /* ic_custom_date@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_date@3x.png"; path = "Resources/Icons/ic_custom_date@3x.png"; sourceTree = ""; }; - DAEC06012296CA7B00045298 /* ic_navbar_close@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_navbar_close@2x.png"; path = "Resources/Icons/ic_navbar_close@2x.png"; sourceTree = ""; }; - DAEC06022296CA7C00045298 /* ic_navbar_close@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_navbar_close@3x.png"; path = "Resources/Icons/ic_navbar_close@3x.png"; sourceTree = ""; }; DAECA4F826CFE62600241B0B /* OABaseWidgetView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OABaseWidgetView.h; sourceTree = ""; }; DAECA4F926CFE62600241B0B /* OABaseWidgetView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OABaseWidgetView.m; sourceTree = ""; }; DAEDAA9F2865A0F100CE54D0 /* OAGenerateBackupInfoTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAGenerateBackupInfoTask.h; sourceTree = ""; }; @@ -9907,8 +9901,6 @@ 84F3A8A11B4DA93100D16EBC /* ic_map_pin_small@3x.png */, 84185A621AA8ADFF00F70328 /* ic_map_pin@2x.png */, 84185A631AA8ADFF00F70328 /* ic_map_pin@3x.png */, - DAEC06012296CA7B00045298 /* ic_navbar_close@2x.png */, - DAEC06022296CA7C00045298 /* ic_navbar_close@3x.png */, DAB854A923054C7A002566A6 /* ic_navbar_search@2x.png */, DAB854AA23054C7B002566A6 /* ic_navbar_search@3x.png */, 8431E7851B383A190063C9AF /* ic_operator@2x.png */, @@ -16805,7 +16797,6 @@ DA5A82AC26C563A700F274C7 /* OARouteStatisticsModeCell.xib in Resources */, DA8DB8E4218DA8AA00EBF914 /* ic_ruler_center_light@2x.png in Resources */, DA5A848E26C563A900F274C7 /* OARoutePlanningHudViewController.xib in Resources */, - DAEC06062296CA7C00045298 /* ic_navbar_close@3x.png in Resources */, DA5A829326C563A700F274C7 /* OADestinationCollectionViewCell.xib in Resources */, DA2EC78329FBBE0400ECEB37 /* warnings_railways_us@3x.png in Resources */, DA5A827A26C563A700F274C7 /* OAHeaderRoundCell.xib in Resources */, @@ -17091,7 +17082,6 @@ 0A8D1A1026FBA1750042444C /* ic_small_profile_straight_line@3x.png in Resources */, 84E441B91AF8A07500F87542 /* add_waypoint_to_track@3x.png in Resources */, C508678C2D9D5E32000B7219 /* SegmentTextTableViewCell.xib in Resources */, - DAEC06042296CA7C00045298 /* ic_navbar_close@2x.png in Resources */, DA2EC77329FBBE0400ECEB37 /* warnings_pedestrian@3x.png in Resources */, 0CBDD091245038EC00C94BC2 /* map_marker_direction_arrow_p2_color_pin_1@2x.png in Resources */, DAF345A122F4482C009E2EB6 /* ic_custom_compound_action_hide_top@2x.png in Resources */, diff --git a/Resources/Icons/ic_navbar_close@2x.png b/Resources/Icons/ic_navbar_close@2x.png deleted file mode 100644 index cca669f0c069e9ee7a7ce0d24cf3dedde85a6ba6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 575 zcmV-F0>J%=P)YzG2G|?=!_Cv0Y=isA||%=w{LY|znM%jiL8C!OcHA)BEv8Y z!}z}psR7d~-MgnZnrL*RkHZsvQb$Vj+@vOS6CK^-*1fuO&mZRkT?n;ZhM32TCP)ho zVX})VeI%pYGRv5{E-m;?kJr_7C0FG*$0>Df@;4afd7*n=2wuElH*u40f3T{avZ5P2tZO=j^i{SDT$EdD2+%+ zqU1P+04sUgRug~^fBlH;-f_9S_LjvSW+u_?Ja7$fI_33{?5 zfpQ+Gpl4SSEayo#yzLdHs?UVLenq$bEtd(PO*8IKCP(f!1xylio7lO}ovW(t49MT; zGj8IVI|H&ywSpCjmZaQbyJGpJAt}=?HLNYslI;zzz`_|R*|udN*cG58>o=VMEe}Gn zZrc#p&q+&`Z~Q7WtCEssTlb2cxRhl6;ftbNoLWFK@A#~PTrANjnRiCtNiLRZkjy)y zzkW|m&J$|QoyZc%8NI4pjIkyuct-!_M8{roGZ&I`o)Z~{VHk$7$qyo=XFVq3CSgxW9*_u)Rd`o$1k4`zvY*Ffi#|1Gw8I_dl^qVmDA4u$ahKHcq>oSX88C` z`G5E;*V53Hv^RYAz2gH)*{7K3N`6RVJ)mo)L|w(~tR3`K9?$$V73xLIS9y`f`redo z=DDpsMz(g*f$P$$W6S;vR3sfddOnFABifTAu`B1}XDikeW#FcBRn+Js7o zi8Pnu=yjO5G;R|R-@km@S|hr7f37#BzJ*&hzu^dI-X>U~5`XB~ za5$M^g-Yz;NXM}`J8InU-Hw3{X4IGgW`$8BR#g8f5XB)lMpWNPI)m{{HdMc9WW&K> zCRCq=4h|#jEU5NNaU7>D7*K5&iv`R9MWfm+XAGQBi$t|qa7!>d8HK99B%ElfHv(1n zqy@rxb8l3=(`twpA$X$docNVsMTZya{!B;#J^{C0eT%iP`n;;Ds;a80s;a80s#@<2sU6}YWkd6r00000NkvXX Hu0mjfDuGpD diff --git a/Sources/Controllers/RoutePlanning/OARoutePlanningHudViewController.xib b/Sources/Controllers/RoutePlanning/OARoutePlanningHudViewController.xib index c43f046cf5..de012f1e42 100644 --- a/Sources/Controllers/RoutePlanning/OARoutePlanningHudViewController.xib +++ b/Sources/Controllers/RoutePlanning/OARoutePlanningHudViewController.xib @@ -513,7 +513,7 @@