diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index e6db8a4..1abb11f 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -433,7 +433,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 9Y64TRM77N; @@ -464,7 +464,7 @@ INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode"; INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; INFOPLIST_KEY_NSDesktopFolderUsageDescription = "Atoll needs access to save screenshots to your desktop."; - INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read AI assistant files."; + INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read shelf dragged files."; INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Atoll needs access to read files in the downloads folder."; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSLocationUsageDescription = "Atoll uses your location to display accurate lock screen weather."; @@ -475,7 +475,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.0.1; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll.dev; PRODUCT_NAME = Atoll; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -505,7 +505,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 9Y64TRM77N; @@ -536,7 +536,7 @@ INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode"; INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; INFOPLIST_KEY_NSDesktopFolderUsageDescription = "Atoll needs access to save screenshots to your desktop."; - INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read AI assistant files."; + INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read shelf dragged files."; INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Atoll needs access to read files in the downloads folder."; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSLocationUsageDescription = "Atoll uses your location to display accurate lock screen weather."; @@ -547,7 +547,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.0.1; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll; PRODUCT_NAME = Atoll; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b422434..fed7c00 100644 --- a/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/Ebullioscopic/AtollExtensionKit", "state" : { "branch" : "main", - "revision" : "ebe35277572a349fad26abeb6fc552cb5251d584" + "revision" : "e2d30afedbbcc259bec7eeb1bea0757e52f4ca01" } }, { diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 37c8bbc..8909c88 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -150,6 +150,7 @@ struct ContentView: View { @State private var hoverClickMonitor: Any? @State private var hoverClickLocalMonitor: Any? @State private var stickyTerminalClickMonitor: Any? + @State private var hiddenEdgeHoverPollingTask: Task? @State private var gestureProgress: CGFloat = .zero @State private var skipGestureActiveDirection: MusicManager.SkipDirection? @@ -257,22 +258,40 @@ struct ContentView: View { /// Whether the current screen should render as a Dynamic Island pill /// rather than the standard notch shape. Always false on physical notch screens. private var isDynamicIslandMode: Bool { - shouldUseDynamicIslandMode(for: vm.screen ?? coordinator.selectedScreen) + shouldUseDynamicIslandMode(for: currentScreenName) + } + + private var currentScreenName: String { + vm.screen ?? coordinator.selectedScreen } /// Whether the current screen lacks a physical notch. private var isNonNotchScreen: Bool { - let screenName = vm.screen ?? coordinator.selectedScreen - guard let screen = NSScreen.screens.first(where: { $0.localizedName == screenName }) else { + guard let screen = NSScreen.screens.first(where: { $0.localizedName == currentScreenName }) else { return true } return screen.safeAreaInsets.top <= 0 } + /// Whether the global sneak peek is visible on this specific screen. + private var isSneakPeekVisibleOnCurrentScreen: Bool { + guard coordinator.sneakPeek.show else { return false } + guard Defaults[.showOnAllDisplays] else { return true } + guard let targetScreenName = coordinator.sneakPeek.targetScreenName else { return true } + return currentScreenName == targetScreenName + } + /// Whether the notch/island should hide off-screen when closed on a non-notch display. /// Temporarily reveals the notch when a sneakPeek HUD (volume, brightness, music, etc.) is active. private var shouldHideUntilHover: Bool { - hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed && !coordinator.sneakPeek.show + hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed && !isSneakPeekVisibleOnCurrentScreen + } + + /// Whether the fallback top-edge hover detector should run. + /// This is only needed when the notch is fully hidden off-screen and + /// regular `.onHover` hit-testing may not trigger reliably. + private var shouldUseHiddenEdgeHoverPolling: Bool { + shouldHideUntilHover && !lockScreenManager.isLocked } /// Pill shape for Dynamic Island mode with animated corner radius transitions. @@ -524,6 +543,7 @@ struct ContentView: View { clearMusicControlVisibilityDeadline() } enqueueMusicControlWindowSync(forceRefresh: true) + startHiddenEdgeHoverPolling() } .onChange(of: vm.notchState) { _, state in if state == .open { @@ -615,6 +635,7 @@ struct ContentView: View { hoverTask?.cancel() stopHoverClickMonitor() removeStickyTerminalClickMonitor() + stopHiddenEdgeHoverPolling() cancelMusicControlWindowSync() hideMusicControlWindow() cancelMusicControlVisibilityTimer() @@ -696,7 +717,7 @@ struct ContentView: View { .frame(width: 76, alignment: .trailing) } .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) - } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && !coordinator.sneakPeek.type.isExtensionPayload && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) { + } else if isSneakPeekVisibleOnCurrentScreen && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && !coordinator.sneakPeek.type.isExtensionPayload && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) .transition( coordinator.sneakPeek.type == .capsLock @@ -743,7 +764,7 @@ struct ContentView: View { Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight) } - if coordinator.sneakPeek.show { + if isSneakPeekVisibleOnCurrentScreen { if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .capsLock) && !coordinator.sneakPeek.type.isExtensionPayload && !Defaults[.inlineHUD] && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) { SystemEventIndicatorModifier(eventType: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, sendEventBack: { _ in // @@ -970,7 +991,7 @@ struct ContentView: View { .background( Image(nsImage: musicManager.albumArt) .resizable() - .aspectRatio(contentMode: .fill) + .aspectRatio(contentMode: .fit) ) .clipped() .clipShape(RoundedRectangle(cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed)) @@ -1552,6 +1573,48 @@ struct ContentView: View { } } + private func hiddenHoverActivationContainsMouse(_ location: NSPoint = NSEvent.mouseLocation) -> Bool { + guard let screen = NSScreen.screens.first(where: { $0.localizedName == currentScreenName }) else { + return false + } + + let horizontalPadding: CGFloat = 8 + let activationWidth = vm.closedNotchSize.width + horizontalPadding * 2 + let activationHeight = max(vm.closedNotchSize.height + zeroHeightHoverPadding, 14) + let activationRect = CGRect( + x: screen.frame.midX - activationWidth / 2, + y: screen.frame.maxY - activationHeight, + width: activationWidth, + height: activationHeight + ) + + return activationRect.contains(location) + } + + private func startHiddenEdgeHoverPolling() { + guard hiddenEdgeHoverPollingTask == nil else { return } + + hiddenEdgeHoverPollingTask = Task { @MainActor in + while !Task.isCancelled { + if self.shouldUseHiddenEdgeHoverPolling { + let hovering = self.hiddenHoverActivationContainsMouse() + if hovering != self.isHovering { + self.handleHover(hovering) + } + } + + try? await Task.sleep(for: .milliseconds(50)) + } + + self.hiddenEdgeHoverPollingTask = nil + } + } + + private func stopHiddenEdgeHoverPolling() { + hiddenEdgeHoverPollingTask?.cancel() + hiddenEdgeHoverPollingTask = nil + } + private func startHoverClickMonitor() { guard hoverClickMonitor == nil else { return } @@ -1637,7 +1700,7 @@ struct ContentView: View { let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive && !enableMinimalisticUI guard vm.notchState == .closed, - !coordinator.sneakPeek.show, + !isSneakPeekVisibleOnCurrentScreen, (Defaults[.openNotchOnHover] || shouldFocusTimerTab) else { return } hoverTask = Task { @@ -1647,7 +1710,7 @@ struct ContentView: View { await MainActor.run { guard self.vm.notchState == .closed, self.isHovering, - !self.coordinator.sneakPeek.show else { return } + !self.isSneakPeekVisibleOnCurrentScreen else { return } if shouldFocusTimerTab { withAnimation(.smooth) { @@ -2162,7 +2225,7 @@ struct ContentView: View { #endif private func shouldFixSizeForSneakPeek() -> Bool { - guard coordinator.sneakPeek.show else { return false } + guard isSneakPeekVisibleOnCurrentScreen else { return false } let style = resolvedSneakPeekStyle() // Check for extension sneak peek diff --git a/DynamicIsland/DynamicIsland.entitlements b/DynamicIsland/DynamicIsland.entitlements index fbf9fd4..8d7db4e 100644 --- a/DynamicIsland/DynamicIsland.entitlements +++ b/DynamicIsland/DynamicIsland.entitlements @@ -2,21 +2,21 @@ - com.apple.security.mach-lookup.global-name - - com.ebullioscopic.Atoll.xpc - - com.apple.security.automation.apple-events - - com.apple.security.temporary-exception.apple-events - - com.spotify.client - com.apple.Music - - com.apple.security.temporary-exception.mach-lookup.global-name - - $(PRODUCT_BUNDLE_IDENTIFIER)-spks - $(PRODUCT_BUNDLE_IDENTIFIER)-spki - + com.apple.security.mach-services + + com.ebullioscopic.Atoll.xpc + + com.apple.security.automation.apple-events + + com.apple.security.temporary-exception.apple-events + + com.spotify.client + com.apple.Music + + com.apple.security.temporary-exception.mach-lookup.global-name + + $(PRODUCT_BUNDLE_IDENTIFIER)-spks + $(PRODUCT_BUNDLE_IDENTIFIER)-spki + diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index d84ee76..0cac7ea 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -111,6 +111,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let mediaControlsStateCoordinator = MediaControlsStateCoordinator.shared let systemTimerBridge = SystemTimerBridge.shared let extensionXPCServiceHost = ExtensionXPCServiceHost.shared + let extensionRPCServer = ExtensionRPCServer.shared var closeNotchWorkItem: DispatchWorkItem? private var previousScreens: [NSScreen]? private var onboardingWindowController: NSWindowController? @@ -155,10 +156,24 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationWillTerminate(_ notification: Notification) { + let userInfo: [String: Any] = [ + AtollDistributedNotifications.UserInfoKey.sourcePID: NSNumber(value: ProcessInfo.processInfo.processIdentifier) + ] + DistributedNotificationCenter.default().postNotificationName( + AtollDistributedNotifications.didBecomeIdle, + object: nil, + userInfo: userInfo, + deliverImmediately: true + ) + // Cancel any pending window size updates windowSizeUpdateWorkItem?.cancel() NotificationCenter.default.removeObserver(self) extensionXPCServiceHost.stop() + extensionRPCServer.stop() + + // Restore Lunar's native OSD if integration was active + LunarManager.shared.appWillTerminate() } @objc func onScreenLocked(_: Notification) { @@ -388,15 +403,39 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(_ notification: Notification) { + let userInfo: [String: Any] = [ + AtollDistributedNotifications.UserInfoKey.sourcePID: NSNumber(value: ProcessInfo.processInfo.processIdentifier) + ] + DistributedNotificationCenter.default().postNotificationName( + AtollDistributedNotifications.didBecomeActive, + object: nil, + userInfo: userInfo, + deliverImmediately: true + ) + LockScreenLiveActivityWindowManager.shared.configure(viewModel: vm) LockScreenManager.shared.configure(viewModel: vm) extensionXPCServiceHost.start() + extensionRPCServer.start() // Migrate legacy progress bar settings Defaults.Keys.migrateProgressBarStyle() Defaults.Keys.migrateMusicAuxControls() Defaults.Keys.migrateMusicControlSlots() Defaults.Keys.migrateCapsLockTintMode() + Defaults.Keys.migrateThirdPartyDDCIntegration() + + Defaults.publisher(.enableThirdPartyDDCIntegration, options: []) + .sink { _ in + Defaults.Keys.syncLegacyThirdPartyDDCKeys() + } + .store(in: &cancellables) + + Defaults.publisher(.thirdPartyDDCProvider, options: []) + .sink { _ in + Defaults.Keys.syncLegacyThirdPartyDDCKeys() + } + .store(in: &cancellables) // Initialize idle animations (load bundled + built-in face) idleAnimationManager.initializeDefaultAnimations() @@ -415,6 +454,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Setup BetterDisplay integration BetterDisplayManager.shared.configure(coordinator: coordinator) + + // Setup Lunar integration + LunarManager.shared.configure(coordinator: coordinator) // Setup ScreenRecording Manager if Defaults[.enableScreenRecordingDetection] { diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 7c55430..c6ed432 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -84,6 +84,7 @@ struct sneakPeek { var subtitle: String = "" var accentColor: Color? var styleOverride: SneakPeekStyle? = nil + var targetScreenName: String? = nil } enum BrowserType { @@ -323,7 +324,8 @@ class DynamicIslandViewCoordinator: ObservableObject { title: String = "", subtitle: String = "", accentColor: Color? = nil, - styleOverride: SneakPeekStyle? = nil + styleOverride: SneakPeekStyle? = nil, + onScreen targetScreen: NSScreen? = nil ) { let resolvedDuration: TimeInterval switch type { @@ -360,6 +362,7 @@ class DynamicIslandViewCoordinator: ObservableObject { self.sneakPeek.subtitle = subtitle self.sneakPeek.accentColor = accentColor self.sneakPeek.styleOverride = styleOverride + self.sneakPeek.targetScreenName = targetScreen?.localizedName } } } diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index e68f7ec..94d116b 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -103,13 +103,6 @@ }, " – %lld" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : " – %lld" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -184,7 +177,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : " – %lld" } }, @@ -265,13 +258,6 @@ }, "'Now Playing' was the only option on previous versions and works with all media apps." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "'Сейчас воспроизводится' был единственным вариантом в предыдущих версиях, он работает со всеми медиа приложениями." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -346,8 +332,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "'Сейчас играть' была единственной опцией в предыдущих версиях и работает со всеми медиа-приложениями." + "state" : "translated", + "value" : "'Сейчас воспроизводится' был единственным вариантом в предыдущих версиях, он работает со всеми медиа приложениями." } }, "tr" : { @@ -372,13 +358,6 @@ }, "(%@)" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "(%@)" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -453,7 +432,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "(%@)" } }, @@ -479,13 +458,6 @@ }, "%.0f seconds" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%.0f секунд" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -560,8 +532,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%.0f секунды" + "state" : "translated", + "value" : "%.0f секунд" } }, "tr" : { @@ -586,13 +558,6 @@ }, "%.1fs" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%.1fс" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -667,8 +632,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%.1fs" + "state" : "translated", + "value" : "%.1fс" } }, "tr" : { @@ -694,13 +659,6 @@ "%@" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -775,7 +733,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%@" } }, @@ -803,13 +761,6 @@ "comment" : "A secondary line of text that provides additional information about the air quality index, including its scale label and category name.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ · %2$@" - } - }, - "en" : { "stringUnit" : { "state" : "new", @@ -821,6 +772,12 @@ "state" : "translated", "value" : "%1$@ · %2$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ · %2$@" + } } } }, @@ -880,18 +837,17 @@ "comment" : "A row that combines a calendar event with a focus indicator. The first argument is the text of the calendar event. The second argument is the name of the focus.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@. Активный фокус: %2$@" - } - }, - "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@. Focus active: %2$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@. Активный фокус: %2$@" + } } } }, @@ -909,13 +865,6 @@ }, "%d%%" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d%%" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -990,7 +939,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%d%%" } }, @@ -1021,25 +970,17 @@ "comment" : "A text label showing the number of apps with which the current app has granted permissions. The argument is the count of such apps.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld %2$@" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld" - } - }, - "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld %2$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld %2$@" + } } }, "shouldTranslate" : false @@ -1094,18 +1035,17 @@ "comment" : "Text that appears in the \"Time\" column of the \"Account History\" view, indicating how long ago a particular event occurred. The number in the parentheses is replaced with the actual number of days.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld дней%2$@ назад" - } - }, - "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld day%2$@ ago" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld дней%2$@ назад" + } } } }, @@ -1113,18 +1053,17 @@ "comment" : "Text for a time string, showing how many hours have passed since a given date.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld часов%2$@ назад" - } - }, - "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld hour%2$@ ago" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld часов%2$@ назад" + } } } }, @@ -1137,25 +1076,17 @@ "comment" : "A string describing how long ago a particular event occurred, based on the number of minutes that have passed.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld минут%2$@ назад" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld мин" - } - }, - "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld minute%2$@ ago" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld минут%2$@ назад" + } } } }, @@ -1177,51 +1108,22 @@ "comment" : "A label displaying the number of enabled tabs, the recommended minimum notch width, and a button to reset the notch width to its recommended minimum value. The first argument is the count of enabled tabs. The second argument is the recommended minimum notch width as a formatted pixel count.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld вкладок%2$@ включено · мин %3$lld пкс" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld с" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld пкс" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld пт" - } - }, - "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld tab%2$@ enabled · min %3$lld px" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld вкладок%2$@ включено · мин %3$lld пкс" + } } } }, "%lld%%" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld%%" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -1296,7 +1198,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%lld%%" } }, @@ -1343,46 +1245,17 @@ }, "%lldx" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldм назад" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldм" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldч назад" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldч" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldд назад" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld fois" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldм назад" + } } }, "shouldTranslate" : false @@ -1402,13 +1275,6 @@ }, "• Bug fixes" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "• Исправления ошибок" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -1483,7 +1349,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "• Исправления ошибок" } }, @@ -1521,13 +1387,6 @@ }, "• Improved performance" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "• Улучшена производительность" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -1602,8 +1461,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "• повышение эффективности" + "state" : "translated", + "value" : "• Улучшена производительность" } }, "tr" : { @@ -1640,13 +1499,6 @@ }, "• New feature 1" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "• Новая функция 1" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -1721,8 +1573,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "• New feature 1" + "state" : "translated", + "value" : "• Новая функция 1" } }, "tr" : { @@ -1789,13 +1641,6 @@ }, "+%lld more" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -1807,13 +1652,15 @@ "state" : "translated", "value" : "+%lld mais" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "••••••••" + } } } }, - "±10 Seconds" : { - "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", - "isCommentAutoGenerated" : true - }, "0" : { "comment" : "A label displayed in a circular gauge to indicate the minimum battery level.", "isCommentAutoGenerated" : true, @@ -1838,53 +1685,17 @@ "1x" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "1 мин" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "1 час" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "00:05:00" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "00" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "±10 Секунд" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "1x" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 мин" + } } } }, @@ -1893,13 +1704,6 @@ }, "3 items" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "2 часа" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -1911,6 +1715,12 @@ "state" : "translated", "value" : "3 Itens" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "2 часа" + } } } }, @@ -1951,13 +1761,6 @@ }, "7 items" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "5 мин" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -1969,6 +1772,12 @@ "state" : "translated", "value" : "7 Itens" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "5 мин" + } } } }, @@ -2009,13 +1818,6 @@ }, "15 colors" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 мин" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -2027,6 +1829,12 @@ "state" : "translated", "value" : "15 Cores" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "10 мин" + } } } }, @@ -2036,18 +1844,17 @@ "20" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "15 мин" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "20" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "15 мин" + } } } }, @@ -2091,45 +1898,23 @@ }, "100%" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "100" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "45 мин" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "30 мин" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "100%" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" + } } }, "shouldTranslate" : false }, "About" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "О программе" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -2204,8 +1989,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "About" + "state" : "translated", + "value" : "О программе" } }, "tr" : { @@ -2228,26 +2013,8 @@ } } }, - "Accent" : { - "comment" : "Display name for the \"Accent\" caps lock indicator tint mode.", - "isCommentAutoGenerated" : true - }, "Accent color" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выделяющий цвет" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Акцент" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -2322,8 +2089,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Accent color" + "state" : "translated", + "value" : "Выделяющий цвет" } }, "tr" : { @@ -2360,13 +2127,6 @@ }, "Access Denied" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Доступ запрещён" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -2441,8 +2201,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Access Denied" + "state" : "translated", + "value" : "Доступ запрещён" } }, "tr" : { @@ -2598,26 +2358,8 @@ } } }, - "Adaptive gradient" : { - "comment" : "Name of the timer icon color mode option that uses an adaptive gradient.", - "isCommentAutoGenerated" : true - }, "Add" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Адаптивный градиент" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -2692,8 +2434,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add" + "state" : "translated", + "value" : "Добавить" } }, "tr" : { @@ -2780,20 +2522,6 @@ "Add manually" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить вручную" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить значок" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -2868,8 +2596,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add manually" + "state" : "translated", + "value" : "Добавить вручную" } }, "tr" : { @@ -2912,13 +2640,6 @@ }, "Add new visualizer" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить новую визуализацию" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -2993,8 +2714,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add new visualizer" + "state" : "translated", + "value" : "Добавить новую визуализацию" } }, "tr" : { @@ -3031,13 +2752,6 @@ }, "Additional features" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дополнительные функции" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -3112,8 +2826,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Additional features" + "state" : "translated", + "value" : "Дополнительные функции" } }, "tr" : { @@ -3141,18 +2855,17 @@ }, "Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить выпадающий терминал в стиле Guake. Сессия терминала сохраняется при сворачивании/разворачивании выреза." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adiciona um HUD no notch quando o Caps Lock está ativado, com controles opcionais de etiqueta e tonalidade." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включить выпадающий терминал в стиле Guake. Сессия терминала сохраняется при сворачивании/разворачивании выреза." + } } } }, @@ -3267,13 +2980,6 @@ "Air quality index %d, %@" : { "comment" : "Air quality accessibility label", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Индекс качества воздуха %1$d, %2$@" - } - }, - "en" : { "stringUnit" : { "state" : "new", @@ -3285,6 +2991,12 @@ "state" : "translated", "value" : "Nível de qualidade de ar %1$d, %2$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Индекс качества воздуха %1$d, %2$@" + } } } }, @@ -3310,13 +3022,6 @@ }, "AirDrop" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "AirDrop" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -3391,7 +3096,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "AirDrop" } }, @@ -3418,13 +3123,6 @@ }, "AirDrop service not available" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Служба AirDrop недоступна" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -3499,8 +3197,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "AirDrop service not available" + "state" : "translated", + "value" : "Служба AirDrop недоступна" } }, "tr" : { @@ -3528,30 +3226,22 @@ }, "All" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выравнивание" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tudo" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выравнивание" + } } } }, "All-day" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Весь день" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -3626,8 +3316,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "All-day" + "state" : "translated", + "value" : "Весь день" } }, "tr" : { @@ -3664,13 +3354,6 @@ }, "Allow Access" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить доступ" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -3745,8 +3428,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Allow Access" + "state" : "translated", + "value" : "Разрешить доступ" } }, "tr" : { @@ -3796,53 +3479,17 @@ "comment" : "A label describing the allowed features for an app extension.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить отправку сообщений о действиях мыши" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить упрощенные переопределения/настройки" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить интерактивный веб-контент" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить функции выреза в расширениях" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить виджеты расширений на экране блокировки" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разрешить эфиры активности расширений" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Recursos Permitidos" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разрешить отправку сообщений о действиях мыши" + } } } }, @@ -3853,20 +3500,6 @@ }, "Always hide in fullscreen" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Всегда скрывать в полноэкранном режиме" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Альфа" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -3941,8 +3574,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Always hide in fullscreen" + "state" : "translated", + "value" : "Всегда скрывать в полноэкранном режиме" } }, "tr" : { @@ -3967,13 +3600,6 @@ }, "Always show tabs" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Всегда показывать вкладки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -4048,8 +3674,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Always show tabs" + "state" : "translated", + "value" : "Всегда показывать вкладки" } }, "tr" : { @@ -4141,13 +3767,6 @@ }, "App icon" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Значок приложения" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -4222,8 +3841,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "App icon" + "state" : "translated", + "value" : "Значок приложения" } }, "tr" : { @@ -4260,13 +3879,6 @@ }, "Appearance" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Внешний вид" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -4341,8 +3953,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Appearance" + "state" : "translated", + "value" : "Внешний вид" } }, "tr" : { @@ -4365,24 +3977,19 @@ } } }, - "Apple Music" : { - "comment" : "Names of the different music streaming services.", - "isCommentAutoGenerated" : true - }, "Applies the notch-style parallax effect to the lock screen media widget album art." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apple Music" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplica o efeito de paralaxe ao notch à arte do álbum do widget de mídia da tela de bloqueio." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Music" + } } } }, @@ -4405,13 +4012,6 @@ "Are you sure you want to delete \"%@\"?" : { "comment" : "A confirmation prompt asking the user to confirm the deletion of a custom idle animation. The placeholder text inside the quotation marks should be replaced with the name of the animation.", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "AQI" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -4423,6 +4023,12 @@ "state" : "translated", "value" : "Tem certeza de que deseja excluir “%@”?" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "AQI" + } } } }, @@ -4469,6 +4075,10 @@ }, "shouldTranslate" : false }, + "Atoll always listens to selected-provider brightness events, and listens to provider volume events only when external volume listener is enabled." : { + "comment" : "A footer text that explains the behavior of the third-party DDC integration settings.", + "isCommentAutoGenerated" : true + }, "Atoll can show all your upcoming events in one place. Access to your calendar is needed to display your schedule." : { "localizations" : { "pt-BR" : { @@ -4489,6 +4099,10 @@ } } }, + "Atoll keeps native volume key interception. External provider volume payloads are ignored while this is off." : { + "comment" : "A description of the behavior of Atoll's volume key interception when the external volume control listener is disabled.", + "isCommentAutoGenerated" : true + }, "Atoll needs Full Disk Access to read the system notification database. If Atoll does not appear in the list, use 'Reveal App in Finder' and drag it into the settings window." : { "extractionState" : "stale", "localizations" : { @@ -4500,6 +4114,10 @@ } } }, + "Atoll's built-in volume key interception is disabled while external volume listening is on. Volume HUD/OSD will follow %@ payloads." : { + "comment" : "A description below the toggle that explains the effect of enabling or disabling the feature.", + "isCommentAutoGenerated" : true + }, "Attached Files" : { "localizations" : { "fr" : { @@ -4566,13 +4184,6 @@ }, "Automatically check for updates" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматически проверять обновления" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -4647,8 +4258,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for updates" + "state" : "translated", + "value" : "Автоматически проверять обновления" } }, "tr" : { @@ -4673,13 +4284,6 @@ }, "Automatically download updates" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматически загружать уведомления" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -4754,8 +4358,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically download updates" + "state" : "translated", + "value" : "Автоматически загружать уведомления" } }, "tr" : { @@ -4803,41 +4407,22 @@ "comment" : "A control for adjusting the keyboard backlight intensity.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Фон" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Luz de Fundi" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фон" + } } } }, - "Bar" : { - "comment" : "Localized name for the \"Bar\" timer progress style.", - "isCommentAutoGenerated" : true - }, "Battery" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Батарея" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Полоса" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -4912,8 +4497,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Battery" + "state" : "translated", + "value" : "Батарея" } }, "tr" : { @@ -4994,13 +4579,6 @@ }, "Battery Information" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Информация о батарее" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -5075,8 +4653,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Battery Information" + "state" : "translated", + "value" : "Информация о батарее" } }, "tr" : { @@ -5101,13 +4679,6 @@ }, "Battery Settings" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настройки батареи" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -5182,8 +4753,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Battery Settings" + "state" : "translated", + "value" : "Настройки батареи" } }, "tr" : { @@ -5218,13 +4789,6 @@ }, "Battery Status" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Статус батареи" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -5299,8 +4863,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Battery Status" + "state" : "translated", + "value" : "Статус батареи" } }, "tr" : { @@ -5347,59 +4911,10 @@ "comment" : "A label indicating a beta version.", "isCommentAutoGenerated" : true, "shouldTranslate" : false - }, - "BetterDisplay" : { - "comment" : "The name of the BetterDisplay integration option.", - "isCommentAutoGenerated" : true - }, - "BetterDisplay Integration" : { - "comment" : "The header text for the \"BetterDisplay Integration\" section in the settings view.", - "isCommentAutoGenerated" : true - }, - "BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration." : { - - }, - "BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration." : { - }, "Blue dashed box shows actual size in Atoll" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Экранные индикаторы BetterDisplay будут отображаться в активном стиле панели Atoll. Убедитесь, что интеграция индикатора включена в BetterDisplay: Настройки › Приложение › Интеграция." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "BetterDisplay установлен, но не включен. Запустите BetterDisplay для работы интеграции." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Интеграция BetterDisplay" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "BetterDisplay" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Бета" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -5411,6 +4926,12 @@ "state" : "translated", "value" : "A caixa tracelhada azul mostra o tamanho real no Atol" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Экранные индикаторы BetterDisplay будут отображаться в активном стиле панели Atoll. Убедитесь, что интеграция индикатора включена в BetterDisplay: Настройки › Приложение › Интеграция." + } } } }, @@ -5447,13 +4968,6 @@ "Bluetooth device %@ at %d percent" : { "comment" : "Bluetooth device battery", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth-устройство %1$@ на %2$d %" - } - }, - "en" : { "stringUnit" : { "state" : "new", @@ -5465,6 +4979,12 @@ "state" : "translated", "value" : "Dispositivo Bluetooth %1$@ em %2$d por cento" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth-устройство %1$@ на %2$d %" + } } } }, @@ -5473,20 +4993,6 @@ }, "Boost your productivity with Clipboard Manager" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повысьте свою продуктивность с помощью менеджера буфера обмена." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Жирный текст как яркие цвета" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -5561,8 +5067,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Boost your productivity with Clipboard Manager" + "state" : "translated", + "value" : "Повысьте свою продуктивность с помощью менеджера буфера обмена." } }, "tr" : { @@ -5588,13 +5094,6 @@ "Both" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Оба" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -5612,6 +5111,12 @@ "state" : "translated", "value" : "Ambos" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оба" + } } } }, @@ -5623,13 +5128,6 @@ "comment" : "A label displayed above the breakdown of memory usage.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перерыв" - } - }, - "de" : { "stringUnit" : { "state" : "translated", @@ -5647,6 +5145,12 @@ "state" : "needs_review", "value" : "Breakdown" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перерыв" + } } } }, @@ -5654,18 +5158,17 @@ "comment" : "A control that adjusts the brightness of the device.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пауза" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Brilho" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пауза" + } } } }, @@ -5761,13 +5264,6 @@ }, "Calendar" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Календарь" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -5842,8 +5338,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Calendar" + "state" : "translated", + "value" : "Календарь" } }, "tr" : { @@ -5869,13 +5365,6 @@ "Calendar access is denied. Please enable it in System Settings." : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Доступ к календарю запрещен. Пожалуйста, включите его в настройках системы." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -5950,8 +5439,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Calendar access is denied. Please enable it in System Settings." + "state" : "translated", + "value" : "Доступ к календарю запрещен. Пожалуйста, включите его в настройках системы." } }, "tr" : { @@ -5997,13 +5486,6 @@ }, "Calendars" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Виджет календаря" - } - }, - "de" : { "stringUnit" : { "state" : "translated", @@ -6021,6 +5503,12 @@ "state" : "translated", "value" : "Calendários" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Виджет календаря" + } } } }, @@ -6094,13 +5582,6 @@ }, "Cancel" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отмена" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -6175,8 +5656,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Cancel" + "state" : "translated", + "value" : "Отмена" } }, "tr" : { @@ -6204,18 +5685,17 @@ }, "Caps Lock color" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Caps Lock" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cor do Caps Lock" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caps Lock" + } } } }, @@ -6271,13 +5751,6 @@ "Charging" : { "comment" : "Charging fallback label when no estimate is available", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зарядка" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -6352,8 +5825,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Charging" + "state" : "translated", + "value" : "Зарядка" } }, "tr" : { @@ -6382,20 +5855,6 @@ }, "Charging on Hold: Desktop Mode" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зарядка приостановлена: Настольный режим" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зарядка батареи" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -6470,8 +5929,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Charging on Hold: Desktop Mode" + "state" : "translated", + "value" : "Зарядка приостановлена: Настольный режим" } }, "tr" : { @@ -6496,13 +5955,6 @@ }, "Check for Updates…" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Проверить обновления..." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -6577,8 +6029,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Check for Updates…" + "state" : "translated", + "value" : "Проверить обновления..." } }, "tr" : { @@ -6632,13 +6084,6 @@ }, "Choose a Music Source" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выберите источник музыки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -6713,8 +6158,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Choose a Music Source" + "state" : "translated", + "value" : "Выберите источник музыки" } }, "tr" : { @@ -6885,13 +6330,6 @@ }, "Circle" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Круг" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -6966,8 +6404,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Circle" + "state" : "translated", + "value" : "Круг" } }, "tr" : { @@ -7002,25 +6440,17 @@ "comment" : "A description of the \"Classic mode\" option in the Lock Screen Integration settings.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Классический" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Круговой" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O modo clássico mantém o fundo preto translúcido original." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Классический" + } } } }, @@ -7031,13 +6461,6 @@ "Clear" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Классическая форма выреза, плавно переходящая в верхний край экрана" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -7049,6 +6472,12 @@ "state" : "translated", "value" : "Limpar" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Классическая форма выреза, плавно переходящая в верхний край экрана" + } } } }, @@ -7178,6 +6607,10 @@ } } }, + "Clear conversation and attachments" : { + "comment" : "A help text for the \"Reset Context\" button in the chat messages view.", + "isCommentAutoGenerated" : true + }, "Clear Data" : { "localizations" : { "fr" : { @@ -7362,13 +6795,6 @@ }, "Close" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Закрыть" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -7443,8 +6869,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Close" + "state" : "translated", + "value" : "Закрыть" } }, "tr" : { @@ -7491,18 +6917,17 @@ "comment" : "A footer for the \"Diagnostics\" section of the lock screen settings.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разработка и отладка: цветовая пипетка, мониторинг статистики и экранный помощник." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Colete o relatório de falhas mais recente para compartilhar com o desenvolvedor ao relatar problemas de tela de bloqueio ou sobreposição." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разработка и отладка: цветовая пипетка, мониторинг статистики и экранный помощник." + } } } }, @@ -7675,20 +7100,6 @@ }, "Coming soon" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скоро будет" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цвета" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -7763,8 +7174,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Coming soon" + "state" : "translated", + "value" : "Скоро будет" } }, "tr" : { @@ -7878,27 +7289,6 @@ }, "Continue" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Продолжить" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Подключается напрямую к приложению Spotify." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Подключается напрямую к приложению Apple Music." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -7973,8 +7363,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Continue" + "state" : "translated", + "value" : "Продолжить" } }, "tr" : { @@ -8050,18 +7440,17 @@ }, "Controls the optional timer widget that floats above the media panel, including its classic, frosted, or liquid glass surface independent of the global material setting." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Управляет областью напоминаний на экране блокировки и её расположением." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Controla o widget de temporizador opcional que flutua acima do painel de mídia, incluindo sua superfície de vidro clássico, fosco ou líquido, independentemente da configuração global do material." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управляет областью напоминаний на экране блокировки и её расположением." + } } } }, @@ -8190,13 +7579,6 @@ }, "CPU Usage" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "ЦП" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -8208,6 +7590,12 @@ "state" : "translated", "value" : "Uso de CPU" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ЦП" + } } } }, @@ -8217,18 +7605,17 @@ }, "Create from Clipboard" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Создание и дизайн: пипетка, зеркало, визуальные эффекты" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Criar a partir da Área de Transferência" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создание и дизайн: пипетка, зеркало, визуальные эффекты" + } } } }, @@ -8338,38 +7725,23 @@ "Custom" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль указателя" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Указатель" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Personnalisé" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стиль указателя" + } } } }, "Custom color" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пользовательский цвет" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -8381,18 +7753,17 @@ "state" : "translated", "value" : "Couleur personnalisée" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пользовательский цвет" + } } } }, "Custom height" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пользовательская высота" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -8467,8 +7838,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Custom height" + "state" : "translated", + "value" : "Пользовательская высота" } }, "tr" : { @@ -8498,18 +7869,17 @@ "comment" : "A description of the custom liquid blur option in the media settings.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настраиваемая жидкость" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O Liquid Glass personalizado já renderiza com o Liquid Glass da Apple, então essa opção é gerenciada automaticamente." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настраиваемая жидкость" + } } } }, @@ -8518,18 +7888,17 @@ }, "Custom Liquid settings require the Liquid Glass material." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настраиваемая жидкость доступна на macOS 26+" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "As configurações do Liquid Glass personalizadas requerem o material de Liquid Glass." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настраиваемая жидкость доступна на macOS 26+" + } } } }, @@ -8538,20 +7907,6 @@ }, "Custom music live activity animation" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пользовательская анимация эфира активности музыки" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Свой вариант жидкости" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -8626,8 +7981,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Custom music live activity animation" + "state" : "translated", + "value" : "Пользовательская анимация эфира активности музыки" } }, "tr" : { @@ -8652,13 +8007,6 @@ }, "Custom notch size - %.0f" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пользовательский размер выреза - %.0f" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -8733,8 +8081,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Custom notch size - %.0f" + "state" : "translated", + "value" : "Пользовательский размер выреза - %.0f" } }, "tr" : { @@ -8810,13 +8158,6 @@ }, "Custom vizualizers (Lottie)" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пользовательская визуализация (Lottie)" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -8891,8 +8232,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Custom vizualizers (Lottie)" + "state" : "translated", + "value" : "Пользовательская визуализация (Lottie)" } }, "tr" : { @@ -8961,13 +8302,6 @@ }, "Customize in Settings" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Персонализировать в Настройках" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -9042,8 +8376,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Customize in Settings" + "state" : "translated", + "value" : "Персонализировать в Настройках" } }, "tr" : { @@ -9084,20 +8418,6 @@ }, "Default" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "По умолчанию" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Концентрация" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -9172,8 +8492,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Default" + "state" : "translated", + "value" : "По умолчанию" } }, "tr" : { @@ -9200,18 +8520,17 @@ "comment" : "A label displayed above the section that lets users configure a custom timer.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стандартный" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Temporizador Personalizado Padrão" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стандартный" + } } } }, @@ -9318,13 +8637,6 @@ "Description" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Описание" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -9399,8 +8711,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Description" + "state" : "translated", + "value" : "Описание" } }, "tr" : { @@ -9434,20 +8746,6 @@ "comment" : "The header for a section that displays whether the screen recording detection feature is currently active or inactive.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дизайнер" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отменить всё выделение" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -9459,6 +8757,12 @@ "state" : "translated", "value" : "Status de Detecção" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дизайнер" + } } } }, @@ -9470,18 +8774,17 @@ "comment" : "The title of the \"Devices\" section in the settings menu.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Разработчик" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dispositivos" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разработчик" + } } } }, @@ -9497,37 +8800,25 @@ } } }, - "Digital" : { - "comment" : "The display name for the \"Digital\" reminder presentation style.", - "isCommentAutoGenerated" : true - }, "Dimensions" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цифровой" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dimensões" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цифровой" + } } } }, "Disable" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключить" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -9602,8 +8893,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Disable" + "state" : "translated", + "value" : "Отключить" } }, "tr" : { @@ -9631,20 +8922,6 @@ }, "Disabled" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключено" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключите минималистичный интерфейс для настройки стандартных элементов управления медиа в вырезе" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -9719,8 +8996,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Disabled" + "state" : "translated", + "value" : "Отключено" } }, "tr" : { @@ -9743,25 +9020,24 @@ } } }, - "Disabled while BetterDisplay integration is active — BetterDisplay uses Cmd+Brightness keys for its own controls." : { + "Disabled while external display integration is active — brightness keys are handled by the external app." : { }, "Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%)." : { "comment" : "A description of how the color-coded progress bars work when not using smooth transitions.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключено при активной интеграции BetterDisplay — приложение использует Cmd+Клавиши яркости для своего управления." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Transições discretas se encaixam entre Verde (0–60%), Amarelo (60–85%) e Vermelho (85–100%)." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отключено при активной интеграции BetterDisplay — приложение использует Cmd+Клавиши яркости для своего управления." + } } } }, @@ -9771,13 +9047,6 @@ }, "Disk Read" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Диск" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -9789,6 +9058,12 @@ "state" : "translated", "value" : "Leitura em Disco" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Диск" + } } } }, @@ -9842,13 +9117,6 @@ "comment" : "A footer for the \"Bluetooth Audio Devices\" section in the HUD settings.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль дисплея" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -9860,6 +9128,12 @@ "state" : "translated", "value" : "Exibe uma notificação HUD quando os dispositivos de áudio Bluetooth (fones de ouvido, AirPods, alto-falantes) se conectam, mostrando o nome do dispositivo e o nível da bateria." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стиль дисплея" + } } } }, @@ -9888,18 +9162,17 @@ }, "Do Not Disturb" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отображает ближайшее предстоящее событие календаря над или под виджетом погоды. Выбор календаря здесь не зависит от фильтра календаря в динамическом острове." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não Perturbe" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отображает ближайшее предстоящее событие календаря над или под виджетом погоды. Выбор календаря здесь не зависит от фильтра календаря в динамическом острове." + } } } }, @@ -9911,30 +9184,22 @@ "comment" : "A button label that indicates that the user has finished editing a note.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поддержать" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Feito" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поддержать" + } } } }, "Download" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Загрузка" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -10009,8 +9274,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Download" + "state" : "translated", + "value" : "Загрузка" } }, "tr" : { @@ -10049,13 +9314,6 @@ "Download icon style" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль значка загрузки" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -10073,18 +9331,17 @@ "state" : "translated", "value" : "Estilo do ícone de download" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стиль значка загрузки" + } } } }, "Download indicator style" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль индикатора загрузки" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -10102,6 +9359,12 @@ "state" : "translated", "value" : "Estilo do indicador de download" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стиль индикатора загрузки" + } } } }, @@ -10118,13 +9381,6 @@ }, "Downloads" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Загрузки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -10199,8 +9455,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Downloads" + "state" : "translated", + "value" : "Загрузки" } }, "tr" : { @@ -10265,20 +9521,6 @@ }, "Drop files here" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перетащите файлы сюда" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перетащите файл PNG, JPEG, TIFF или ICNS, чтобы добавить его в библиотеку значков." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -10353,8 +9595,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Drop files here" + "state" : "translated", + "value" : "Перетащите файлы сюда" } }, "tr" : { @@ -10385,18 +9627,17 @@ "comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Динамический остров" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barra de Progresso da Dynamic Island" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Динамический остров" + } } } }, @@ -10405,18 +9646,17 @@ }, "dynamic.island" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Видимость динамического острова" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "dynamic.island" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видимость динамического острова" + } } }, "shouldTranslate" : false @@ -10443,20 +9683,6 @@ }, "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Легко копируйте, храните и управляйте наиболее часто используемым контентом. Обновите подписку прямо сейчас, чтобы получить доступ к расширенным функциям, таким как мульи-элементное хранение и быстрый доступ!" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "EAQI" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -10531,8 +9757,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" + "state" : "translated", + "value" : "Легко копируйте, храните и управляйте наиболее часто используемым контентом. Обновите подписку прямо сейчас, чтобы получить доступ к расширенным функциям, таким как мульи-элементное хранение и быстрый доступ!" } }, "tr" : { @@ -10573,13 +9799,6 @@ }, "Edit layout" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Редактировать макет" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -10654,8 +9873,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Edit layout" + "state" : "translated", + "value" : "Редактировать макет" } }, "tr" : { @@ -10688,9 +9907,13 @@ } } } + }, + "Enable \"Idle Animation\" to customize animations" : { + }, "Enable \"Show cool face animation while inactivity\" to customize animations" : { "comment" : "A message displayed below the section header in the IdleAnimationsSettingsSection, explaining that users can customize animations by enabling this feature.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "pt-BR" : { @@ -10758,13 +9981,6 @@ "Enable colored spectrograms" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить цветную спектрограмму" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -10839,8 +10055,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enable colored spectrograms" + "state" : "translated", + "value" : "Включить цветную спектрограмму" } }, "tr" : { @@ -10902,28 +10118,24 @@ "comment" : "A footer text that explains the purpose of enabling extensions.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить журнал диагностики для расширений" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить элементы управления медиа в динамическом острове для управления панелью на экране блокировки." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ative extensões para permitir que aplicativos de terceiros exibam atividades ao vivo e widgets de tela de bloqueio no Atoll." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включить журнал диагностики для расширений" + } } } }, + "Enable external volume control listener" : { + "comment" : "A toggle that enables external volume control listener.", + "isCommentAutoGenerated" : true + }, "Enable global keyboard shortcuts above to customize your shortcuts." : { "localizations" : { "pt-BR" : { @@ -10949,13 +10161,6 @@ "Enable haptics" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить тактильную обратную связь" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11030,8 +10235,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enable haptics" + "state" : "translated", + "value" : "Включить тактильную обратную связь" } }, "tr" : { @@ -11057,13 +10262,6 @@ "Enable HUD replacement" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить замену системной панели" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11138,8 +10336,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enable HUD replacement" + "state" : "translated", + "value" : "Включить замену системной панели" } }, "tr" : { @@ -11169,30 +10367,22 @@ "comment" : "A toggle switch that enables or disables the blur effect on the media panel.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включите в Настройках → Терминал" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar desfoque do painel de mídia" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включите в Настройках → Терминал" + } } } }, "Enable music live activity" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить эфир активности для музыки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11267,8 +10457,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enable music live activity" + "state" : "translated", + "value" : "Включить эфир активности для музыки" } }, "tr" : { @@ -11371,45 +10561,47 @@ } } }, + "Enable third-party DDC app integration" : { + "comment" : "A toggle switch that enables or disables integration with a third-party Dynamic Display Control (DDC) app.", + "isCommentAutoGenerated" : true + }, "Enable third-party extensions" : { "comment" : "Toggle to enable or disable third-party extensions.", "isCommentAutoGenerated" : true }, "Enable timer live activity" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включить сторонние расширения" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar a atividade ao vivo do temporizador" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включить сторонние расширения" + } } } }, - "Enable to route BetterDisplay's brightness and volume changes through Atoll's HUD instead of the system OSD." : { - "comment" : "A description of the benefit of enabling BetterDisplay integration.", + "Enable to route BetterDisplay or Lunar display adjustments through Atoll's active HUD style." : { + "comment" : "A description of the feature that allows third-party display integrations to be enabled.", "isCommentAutoGenerated" : true }, "Enable when scaling beyond 30x20px so the Atoll opens gracefully." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включите, чтобы отображать изменения яркости и громкости BetterDisplay через панель Atoll вместо системных экранных индикаторов." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ative ao escalar além de 30x20 px para que o Atol abra graciosamente." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включите, чтобы отображать изменения яркости и громкости BetterDisplay через панель Atoll вместо системных экранных индикаторов." + } } } }, @@ -11433,13 +10625,6 @@ }, "Enhance your experience with HUDs" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повысьте удобство работы с панелями" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11514,8 +10699,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enhance your experience with HUDs" + "state" : "translated", + "value" : "Повысьте удобство работы с панелями" } }, "tr" : { @@ -11540,13 +10725,6 @@ }, "Enjoy your free time!" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Наслаждайтесь свободным временем!" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11621,8 +10799,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enjoy your free time!" + "state" : "translated", + "value" : "Наслаждайтесь свободным временем!" } }, "tr" : { @@ -11663,13 +10841,6 @@ }, "Error" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ошибка" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11744,8 +10915,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Error" + "state" : "translated", + "value" : "Ошибка" } }, "tr" : { @@ -11776,31 +10947,23 @@ "comment" : "A section header in the calendar settings view, describing the list of calendar events.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цвет события" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lista de eventos" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цвет события" + } } } }, "Exclude apps" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Исключить приложения" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -11812,18 +10975,17 @@ "state" : "translated", "value" : "Exclure des applications" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исключить приложения" + } } } }, "Exit" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выход" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -11898,8 +11060,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Exit" + "state" : "translated", + "value" : "Выход" } }, "tr" : { @@ -11970,13 +11132,6 @@ }, "Extensions" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Расширения" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12051,8 +11206,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Extensions" + "state" : "translated", + "value" : "Расширения" } }, "tr" : { @@ -12109,32 +11264,17 @@ "comment" : "A description below the currently selected quick share provider, explaining that files dropped on the shelf will be shared via that provider.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Файл" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Избранное" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль на внешнем дисплее" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Os arquivos deixados na prateleira serão compartilhados através deste serviço" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Файл" + } } } }, @@ -12150,13 +11290,6 @@ }, "Finish" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Завершить" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12231,8 +11364,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Finish" + "state" : "translated", + "value" : "Завершить" } }, "tr" : { @@ -12285,18 +11418,17 @@ "comment" : "An accessibility label for the focus widget, describing the currently active focus mode.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Фокус" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Foc ativo: %@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фокус" + } } } }, @@ -12306,18 +11438,17 @@ }, "Focus Status" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Режим обнаружения фокуса" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Status do Foco" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Режим обнаружения фокуса" + } } } }, @@ -12330,6 +11461,9 @@ } } } + }, + "Font family" : { + }, "Font size" : { "comment" : "A label displayed above a slider that controls the font size of the terminal", @@ -12342,20 +11476,6 @@ "comment" : "A label displaying the amount of free storage on a disk device. The argument is the number of free bytes on the disk.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Текст" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Размер шрифта" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -12367,6 +11487,12 @@ "state" : "translated", "value" : "Livre · %@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текст" + } } } }, @@ -12381,25 +11507,17 @@ "Full Disk Access required" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Заряжено" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Матовое стекло" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Acesso completo ao disco necessário" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Заряжено" + } } } }, @@ -12432,13 +11550,6 @@ }, "General" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Общие" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12513,8 +11624,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "General" + "state" : "translated", + "value" : "Общие" } }, "tr" : { @@ -12539,13 +11650,6 @@ }, "Gesture control" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Управление жестами" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12620,8 +11724,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Gesture control" + "state" : "translated", + "value" : "Управление жестами" } }, "tr" : { @@ -12646,13 +11750,6 @@ }, "Gesture sensitivity" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Чувствительность жестов" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12727,8 +11824,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Gesture sensitivity" + "state" : "translated", + "value" : "Чувствительность жестов" } }, "tr" : { @@ -12765,13 +11862,6 @@ }, "Get started" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Приступить" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12846,8 +11936,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Get started" + "state" : "translated", + "value" : "Приступить" } }, "tr" : { @@ -12888,13 +11978,6 @@ }, "GitHub" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "GitHub" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -12969,7 +12052,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "GitHub" } }, @@ -13000,18 +12083,17 @@ }, "Glass mode" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стекло" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo Vidro" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стекло" + } } } }, @@ -13029,13 +12111,6 @@ }, "Got it!" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Есть!" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13110,8 +12185,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Got it!" + "state" : "translated", + "value" : "Есть!" } }, "tr" : { @@ -13140,13 +12215,6 @@ }, "GPU Usage" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "ГП" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -13158,18 +12226,17 @@ "state" : "translated", "value" : "Uso de GPU" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ГП" + } } } }, "Gradient" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Градиент" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13244,8 +12311,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Gradient" + "state" : "translated", + "value" : "Градиент" } }, "tr" : { @@ -13284,20 +12351,9 @@ } } }, - "Green" : { - "comment" : "Display name for the \"Green\" Caps Lock Indicator Tint Mode.", - "isCommentAutoGenerated" : true - }, "Height:" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зелёный" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -13309,6 +12365,12 @@ "state" : "translated", "value" : "Altura:" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зелёный" + } } } }, @@ -13383,20 +12445,6 @@ }, "Hide only when NowPlaying app is in fullscreen" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скрывать только во время полноэкранного режима медиа приложения" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скрыть предварительный просмотр экрана блокировки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13471,8 +12519,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Hide only when NowPlaying app is in fullscreen" + "state" : "translated", + "value" : "Скрывать только во время полноэкранного режима медиа приложения" } }, "tr" : { @@ -13497,13 +12545,6 @@ }, "Hierarchical" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ступенчатый" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13578,8 +12619,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Hierarchical" + "state" : "translated", + "value" : "Ступенчатый" } }, "tr" : { @@ -13605,13 +12646,6 @@ "High" : { "comment" : "High battery level label", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Высокий" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13686,8 +12720,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "High" + "state" : "translated", + "value" : "Высокий" } }, "tr" : { @@ -13726,13 +12760,6 @@ }, "History Size" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "История" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -13744,6 +12771,12 @@ "state" : "translated", "value" : "Tamanho do Histórico" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "История" + } } } }, @@ -13774,13 +12807,6 @@ }, "https://github.com/th-ch/youtube-music" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "https://github.com/th-ch/youtube-music" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13855,7 +12881,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "https://github.com/th-ch/youtube-music" } }, @@ -13886,30 +12912,22 @@ }, "HUD Position" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль значка в панели" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Posição do HUD" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стиль значка в панели" + } } } }, "HUD style" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль панели" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -13984,8 +13002,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "HUD style" + "state" : "translated", + "value" : "Стиль панели" } }, "tr" : { @@ -14011,13 +13029,6 @@ "HUDs" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Панели" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -14092,8 +13103,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "HUDs" + "state" : "translated", + "value" : "Панели" } }, "tr" : { @@ -14171,13 +13182,6 @@ }, "Import" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изображение" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -14189,6 +13193,12 @@ "state" : "translated", "value" : "Importar" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изображение" + } } } }, @@ -14230,13 +13240,6 @@ }, "In progress" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "В процессе" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -14311,8 +13314,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "In progress" + "state" : "translated", + "value" : "В процессе" } }, "tr" : { @@ -14353,13 +13356,6 @@ }, "Inline" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Строкой" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -14434,8 +13430,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Inline" + "state" : "translated", + "value" : "Строкой" } }, "tr" : { @@ -14470,35 +13466,10 @@ }, "Input" : { - }, - "Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD." : { - "comment" : "A description of the benefits of installing BetterDisplay.", - "isCommentAutoGenerated" : true }, "Install updates automatically" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматически устанавливать обновления" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Установите [BetterDisplay](https://betterdisplay.pro) для управления яркостью и громкостью внешнего дисплея через панель Atoll." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ввод" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -14510,19 +13481,18 @@ "state" : "translated", "value" : "Installer les mises à jour automatiquement" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматически устанавливать обновления" + } } } }, "Installed extensions" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Установленные расширения" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -14597,8 +13567,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Installed extensions" + "state" : "translated", + "value" : "Установленные расширения" } }, "tr" : { @@ -14680,20 +13650,6 @@ }, "Keyboard Backlight HUD" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Только что" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "только что" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -14705,6 +13661,12 @@ "state" : "translated", "value" : "HUD de Brilho do Teclado" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Только что" + } } } }, @@ -14778,13 +13740,6 @@ }, "Later" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Позже" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -14859,8 +13814,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Later" + "state" : "translated", + "value" : "Позже" } }, "tr" : { @@ -14890,18 +13845,17 @@ }, "Layout Preview" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Макет" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pré-vizualização do Layout" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Макет" + } } } }, @@ -14932,18 +13886,17 @@ }, "Line Width: %lldpx" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Лёгкий режим" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Largura da Linha: %lld px" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лёгкий режим" + } } } }, @@ -14965,13 +13918,6 @@ }, "Liquid Glass requires macOS 26 or later." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Жидкое стекло" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -14983,6 +13929,12 @@ "state" : "translated", "value" : "O Liquid Glass requer MacOS 26 ou posterior." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Жидкое стекло" + } } } }, @@ -15107,13 +14059,6 @@ }, "Lock Screen" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Загрузка..." - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -15125,6 +14070,12 @@ "state" : "translated", "value" : "Tela de Bloqueio" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Загрузка..." + } } } }, @@ -15240,13 +14191,6 @@ }, "Lottie JSON URL" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ссылка на JSON Lottie" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -15321,8 +14265,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Lottie JSON URL" + "state" : "translated", + "value" : "Ссылка на JSON Lottie" } }, "tr" : { @@ -15348,13 +14292,6 @@ "Low" : { "comment" : "Low battery level label", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Низкий" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -15429,8 +14366,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Low" + "state" : "translated", + "value" : "Низкий" } }, "tr" : { @@ -15459,20 +14396,6 @@ }, "Low Power Mode" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Режим энергосбережения" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Низкий заряд батареи" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -15547,8 +14470,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Low Power Mode" + "state" : "translated", + "value" : "Режим энергосбережения" } }, "tr" : { @@ -15578,18 +14501,17 @@ "Mac battery at %d percent" : { "comment" : "Mac battery gauge accessibility label", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Режим энергосбережения: %@" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bateria do Mac em %d porcento" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Режим энергосбережения: %d" + } } } }, @@ -15624,18 +14546,17 @@ "comment" : "A description of how to manage the microphone privacy indicator preferences.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль главного экрана" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerencie as preferências do indicador em Configurações → Privacidade. Não é necessário alternar manualmente o microfone." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стиль главного экрана" + } } } }, @@ -15665,13 +14586,6 @@ }, "Match menubar height" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Соответствовать высоте строки меню" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -15746,8 +14660,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Match menubar height" + "state" : "translated", + "value" : "Соответствовать высоте строки меню" } }, "tr" : { @@ -15772,13 +14686,6 @@ }, "Match real notch size" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Соответствовать реальным размерам выреза" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -15853,8 +14760,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Match real notch size" + "state" : "translated", + "value" : "Соответствовать реальным размерам выреза" } }, "tr" : { @@ -15886,30 +14793,22 @@ "comment" : "A label describing the material options for the custom OSD.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Материал" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Opções de Material:" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Материал" + } } } }, "Max Capacity: %lld%%" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Максимальная ёмкость: %lld%%" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -15984,8 +14883,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Max Capacity: %lld%%" + "state" : "translated", + "value" : "Максимальная ёмкость: %lld%%" } }, "tr" : { @@ -16014,20 +14913,6 @@ }, "Media" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Медиа" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Максимальная высота" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16102,8 +14987,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Media" + "state" : "translated", + "value" : "Медиа" } }, "tr" : { @@ -16131,20 +15016,6 @@ "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Средний" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переключение медиа с помощью горизонтальных жестов" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16219,8 +15090,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Media change with horizontal gestures" + "state" : "translated", + "value" : "Средний" } }, "tr" : { @@ -16245,13 +15116,6 @@ }, "Media controls" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Элементы управления медиа" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16326,8 +15190,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Media controls" + "state" : "translated", + "value" : "Элементы управления медиа" } }, "tr" : { @@ -16352,13 +15216,6 @@ }, "Media inactivity timeout" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таймаут отсутствия активности медиа" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16433,8 +15290,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Media inactivity timeout" + "state" : "translated", + "value" : "Таймаут отсутствия активности медиа" } }, "tr" : { @@ -16495,13 +15352,6 @@ }, "Media playback live activity" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Эфир активности воспроизведения медиа" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16576,8 +15426,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Media playback live activity" + "state" : "translated", + "value" : "Эфир активности воспроизведения медиа" } }, "tr" : { @@ -16602,13 +15452,6 @@ }, "Media Source" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Источник медиа" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16683,8 +15526,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Media Source" + "state" : "translated", + "value" : "Источник медиа" } }, "tr" : { @@ -16824,13 +15667,6 @@ }, "Memory Usage" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Память" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -16842,19 +15678,18 @@ "state" : "translated", "value" : "Uso de Memória" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Память" + } } } }, "Menubar icon" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Значок в строке меню" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -16929,8 +15764,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Menubar icon" + "state" : "translated", + "value" : "Значок в строке меню" } }, "tr" : { @@ -16973,20 +15808,6 @@ "Mic %@" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Микро %@" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Микро" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17061,8 +15882,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Mic %@" + "state" : "translated", + "value" : "Микро %@" } }, "tr" : { @@ -17194,13 +16015,6 @@ }, "Minimum hover duration" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Минимальная длительность наведения курсора" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17275,8 +16089,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Minimum hover duration" + "state" : "translated", + "value" : "Минимальная длительность наведения курсора" } }, "tr" : { @@ -17300,6 +16114,7 @@ } }, "Minutes" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -17311,13 +16126,6 @@ }, "Mirror" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зеркало" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17392,8 +16200,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Mirror" + "state" : "translated", + "value" : "Зеркало" } }, "tr" : { @@ -17430,13 +16238,6 @@ }, "Mirror shape" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Форма зеркала" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17511,8 +16312,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Mirror shape" + "state" : "translated", + "value" : "Форма зеркала" } }, "tr" : { @@ -17606,13 +16407,6 @@ }, "More" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Больше" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17687,8 +16481,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "More" + "state" : "translated", + "value" : "Больше" } }, "tr" : { @@ -17747,13 +16541,6 @@ }, "Music Source" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Источник музыки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17828,8 +16615,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Music Source" + "state" : "translated", + "value" : "Источник музыки" } }, "tr" : { @@ -17854,13 +16641,6 @@ }, "muted" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "без звука" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -17935,8 +16715,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "muted" + "state" : "translated", + "value" : "без звука" } }, "tr" : { @@ -17979,13 +16759,6 @@ }, "Name" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Название" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18060,8 +16833,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Name" + "state" : "translated", + "value" : "Название" } }, "tr" : { @@ -18102,13 +16875,6 @@ }, "Need Restart" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Требуется перезапуск" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18183,8 +16949,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Need Restart" + "state" : "translated", + "value" : "Требуется перезапуск" } }, "tr" : { @@ -18213,13 +16979,6 @@ }, "Network Download" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сеть" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -18231,6 +16990,12 @@ "state" : "translated", "value" : "Download de Rede" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сеть" + } } } }, @@ -18252,13 +17017,6 @@ }, "Never hide" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Никогда не скрывать" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18333,8 +17091,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Never hide" + "state" : "translated", + "value" : "Никогда не скрывать" } }, "tr" : { @@ -18459,13 +17217,6 @@ }, "No custom animation available" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет пользовательских анимаций" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18540,8 +17291,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No custom animation available" + "state" : "translated", + "value" : "Нет пользовательских анимаций" } }, "tr" : { @@ -18566,13 +17317,6 @@ }, "No custom visualizer" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет пользовательских визуализаций" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18647,8 +17391,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No custom visualizer" + "state" : "translated", + "value" : "Нет пользовательских визуализаций" } }, "tr" : { @@ -18685,13 +17429,6 @@ }, "No events today" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сегодня нет событий" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18766,8 +17503,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No events today" + "state" : "translated", + "value" : "Сегодня нет событий" } }, "tr" : { @@ -18793,31 +17530,23 @@ "No excludes" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Исключений нет" - } - }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "No excludes" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исключений нет" + } } } }, "No extension installed" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Расширения не установлены" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -18892,8 +17621,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No extension installed" + "state" : "translated", + "value" : "Расширения не установлены" } }, "tr" : { @@ -19036,13 +17765,6 @@ }, "Non-notch display height" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Высота для дисплеев без выреза" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19117,8 +17839,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Non-notch display height" + "state" : "translated", + "value" : "Высота для дисплеев без выреза" } }, "tr" : { @@ -19165,20 +17887,6 @@ }, "Not Now" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не сейчас" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не заряжается" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19253,8 +17961,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Not Now" + "state" : "translated", + "value" : "Не сейчас" } }, "tr" : { @@ -19289,13 +17997,6 @@ }, "Notch behavior" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поведение выреза" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19370,8 +18071,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Notch behavior" + "state" : "translated", + "value" : "Поведение выреза" } }, "tr" : { @@ -19396,13 +18097,6 @@ }, "Notch display height" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Высота выреза" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19477,8 +18171,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Notch display height" + "state" : "translated", + "value" : "Высота выреза" } }, "tr" : { @@ -19508,20 +18202,6 @@ }, "Notch Height" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Высота выреза" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функция динамического острова" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19596,8 +18276,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Notch Height" + "state" : "translated", + "value" : "Высота выреза" } }, "tr" : { @@ -19652,25 +18332,20 @@ "comment" : "A label describing the slider that allows the user to set a reminder lead time.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ничего не воспроизводится" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Notificar antes" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ничего не воспроизводится" + } } } }, - "Now Playing" : { - "comment" : "Localized name for each media controller type.", - "isCommentAutoGenerated" : true - }, "Number of lines kept in the scrollback buffer. Higher values use more memory." : { }, @@ -19680,34 +18355,6 @@ }, "OK" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "ОК" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключён" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Количество строк, сохраняемых в буфере обратной прокрутки. Большие значения используют больше памяти." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сейчас воспроизводится" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19782,8 +18429,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "OK" + "state" : "translated", + "value" : "ОК" } }, "tr" : { @@ -19813,20 +18460,6 @@ }, "One or more files failed to load" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не удалось загрузить один или несколько файлов" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Включён" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -19901,8 +18534,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "One or more files failed to load" + "state" : "translated", + "value" : "Не удалось загрузить один или несколько файлов" } }, "tr" : { @@ -19928,18 +18561,17 @@ "Only app icon" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Только значок приложения" - } - }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Only app icon" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Только значок приложения" + } } } }, @@ -19970,18 +18602,17 @@ "Only download icon" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Только значок загрузки" - } - }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Only download icon" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Только значок загрузки" + } } } }, @@ -20032,18 +18663,17 @@ }, "Opens the AI assistant panel for file analysis and conversation. Default is Cmd+Shift+A. Only works when screen assistant feature is enabled." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Открывает прозрачное окно предварительного просмотра с имитаций данных, отражающих текущую конфигурацию виджета экрана блокировки." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abre o painel do assistente de IA para análise e conversação de arquivos. O padrão é Cmd+Shift+A. Só funciona quando o recurso de assistente de tela está ativado." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открывает прозрачное окно предварительного просмотра с имитаций данных, отражающих текущую конфигурацию виджета экрана блокировки." + } } } }, @@ -20080,25 +18710,17 @@ "comment" : "Section title for the list of available audio output devices in the media output selector.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Использование Option в качестве Meta клавиши отправляет Esc+клавишу вместо специальных символов macOS. Отправка сообщений о действиях мыши передаёт события мыши приложениям терминала, например vim или tmux." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Использовать Option в качестве Meta клавиши" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dispositivo de Saída" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Использование Option в качестве Meta клавиши отправляет Esc+клавишу вместо специальных символов macOS. Отправка сообщений о действиях мыши передаёт события мыши приложениям терминала, например vim или tmux." + } } } }, @@ -20126,26 +18748,21 @@ } } }, - "Panel" : { - "comment" : "Display name for the \"Panel\" clipboard display mode.", - "isCommentAutoGenerated" : true - }, "Panel mode shows clipboard in a floating window near the notch." : { "comment" : "A description of the \"Panel\" clipboard display mode.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Панель" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O modo painel mostra a área de transferência em uma janela flutuante perto do notch." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Панель" + } } } }, @@ -20186,18 +18803,17 @@ }, "Percentage" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Процент" - } - }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Percentage" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Процент" + } } } }, @@ -20279,18 +18895,17 @@ }, "Pin items from history to save them here" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Остров с закруглёнными углами, похожий на динамический остров iPhone" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fixe itens do histórico para salvá-los aqui" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Остров с закруглёнными углами, похожий на динамический остров iPhone" + } } } }, @@ -20348,13 +18963,6 @@ }, "Plugged In" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Подключено" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -20429,8 +19037,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Plugged In" + "state" : "translated", + "value" : "Подключено" } }, "tr" : { @@ -20461,18 +19069,17 @@ "comment" : "A description of the \"Popover mode\" in the Clipboard Settings view.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Всплывающее окно" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O modo popover mostra a área de transferência como um menu suspenso anexado ao botão da área de transferência." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всплывающее окно" + } } } }, @@ -20592,18 +19199,17 @@ "comment" : "A label describing the zoom control for the animation preview.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Предпросмотр виджетов экрана блокировки" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Zoom de visualização" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предпросмотр виджетов экрана блокировки" + } } } }, @@ -20638,25 +19244,17 @@ "Progress bar" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Индикатор прогресса" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Прогресс" - } - }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Progress bar" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Индикатор прогресса" + } } } }, @@ -20672,13 +19270,6 @@ }, "Progressbar style" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль индикатора прогресса" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -20753,8 +19344,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Progressbar style" + "state" : "translated", + "value" : "Стиль индикатора прогресса" } }, "tr" : { @@ -20777,6 +19368,10 @@ } } }, + "Provider" : { + "comment" : "A label for selecting a third-party display integration provider.", + "isCommentAutoGenerated" : true + }, "Quick Actions" : { "localizations" : { "fr" : { @@ -20802,30 +19397,22 @@ "comment" : "A picker that lets the user select a quick share service to use when sharing files from the shelf.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Быстрая отправка" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Serviço de Quick Share" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Быстрая отправка" + } } } }, "Quit" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Закрыть" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -20900,8 +19487,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Quit" + "state" : "translated", + "value" : "Закрыть" } }, "tr" : { @@ -20926,13 +19513,6 @@ }, "Quit app" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Закрыть приложение" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -21007,8 +19587,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Quit app" + "state" : "translated", + "value" : "Закрыть приложение" } }, "tr" : { @@ -21034,13 +19614,6 @@ "Quit boring.notch" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Закрыть boring.notch" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -21052,6 +19625,12 @@ "state" : "translated", "value" : "Quitter boring.notch" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть boring.notch" + } } } }, @@ -21063,18 +19642,17 @@ "comment" : "A button that re-authorizes an app when pressed.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ч " - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Autorizar Novamente" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ч " + } } } }, @@ -21212,20 +19790,6 @@ }, "Release name" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Название релиза" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Обновить статус обнаружения" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -21300,8 +19864,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Release name" + "state" : "translated", + "value" : "Название релиза" } }, "tr" : { @@ -21326,13 +19890,6 @@ }, "Remember last tab" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Запомнить последнюю вкладку" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -21407,8 +19964,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remember last tab" + "state" : "translated", + "value" : "Запомнить последнюю вкладку" } }, "tr" : { @@ -21448,13 +20005,6 @@ }, "Reminders" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Виджет напоминаний" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -21466,6 +20016,12 @@ "state" : "translated", "value" : "Lembretes" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Виджет напоминаний" + } } } }, @@ -21518,13 +20074,6 @@ }, "Request Access" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Удалить выбранное" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -21536,6 +20085,12 @@ "state" : "translated", "value" : "Requer Acesso" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить выбранное" + } } } }, @@ -21545,18 +20100,17 @@ }, "Requires Accessibility permission so Dynamic Island can intercept the hardware volume keys." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Требуется сторонний клиент с включенным плагином API." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Requer permissão de Acessibilidade para que a Dynamic Island possa interceptar as teclas de volume do hardware." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется сторонний клиент с включенным плагином API." + } } } }, @@ -21592,6 +20146,10 @@ } } }, + "Reset Context" : { + "comment" : "A button label that resets the conversation context.", + "isCommentAutoGenerated" : true + }, "Reset Live Activities" : { "comment" : "A menu item that resets live activities for a particular app.", "isCommentAutoGenerated" : true, @@ -21683,13 +20241,6 @@ "Restart" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перезапустить" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -21764,8 +20315,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Restart" + "state" : "translated", + "value" : "Перезапустить" } }, "tr" : { @@ -21820,32 +20371,17 @@ "comment" : "The text for a button that restores default settings.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перезапускает процессы оболочки. Любые несохраненные данные в терминале будут потеряны." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перезапустить оболочку" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перезапустить оболочку" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перезапускает процессы оболочки. Любые несохраненные данные в терминале будут потеряны." + } } } }, @@ -21907,53 +20443,40 @@ }, "Right" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Форматированный текст" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Direita" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Форматированный текст" + } } } }, - "Ring" : { - "comment" : "Localized name for the \"Ring\" timer progress style.", - "isCommentAutoGenerated" : true - }, "Rotation:" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Кольцо" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rotation:" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кольцо" + } } } }, "Running" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Запущено" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -22028,8 +20551,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Running" + "state" : "translated", + "value" : "Запущено" } }, "tr" : { @@ -22261,20 +20784,6 @@ }, "Search clipboard..." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Строк обратной прокрутки" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Обратная прокрутка" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -22286,6 +20795,12 @@ "state" : "translated", "value" : "Pesquisar Área de Transferência" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Строк обратной прокрутки" + } } } }, @@ -22360,13 +20875,6 @@ "seconds" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "секунды" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -22378,6 +20886,12 @@ "state" : "translated", "value" : "secondes" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "секунды" + } } } }, @@ -22460,13 +20974,6 @@ }, "Select the music source you want to use. You can change this later in the app settings." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выберите желаемый источник музыки. Вы сможете изменить его позже в настройках приложения." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -22541,8 +21048,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Select the music source you want to use. You can change this later in the app settings." + "state" : "translated", + "value" : "Выберите желаемый источник музыки. Вы сможете изменить его позже в настройках приложения." } }, "tr" : { @@ -22567,13 +21074,6 @@ }, "selected" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "выбрано" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -22648,8 +21148,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "selected" + "state" : "translated", + "value" : "выбрано" } }, "tr" : { @@ -22692,13 +21192,6 @@ }, "Selected animation" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выбранная анимация" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -22773,8 +21266,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Selected animation" + "state" : "translated", + "value" : "Выбранная анимация" } }, "tr" : { @@ -22808,26 +21301,21 @@ } } }, - "Separate Tab" : { - "comment" : "Name of the clipboard display mode option that opens the clipboard in a separate tab.", - "isCommentAutoGenerated" : true - }, "Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : { "comment" : "A description of the \"Separate Tab\" clipboard display mode.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отдельная вкладка" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O modo Abas Separadas integra Itens Copiados e Notas em uma única visualização. Se ambos estiverem ativados, o Notes aparecerá à direita e a Área de Transferência à esquerda." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отдельная вкладка" + } } } }, @@ -22835,13 +21323,6 @@ "comment" : "A row displaying the total downloaded and uploaded bytes for the network session.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сессия · ↓ %1$@ · ↑ %2$@" - } - }, - "en" : { "stringUnit" : { "state" : "new", @@ -22859,6 +21340,12 @@ "state" : "translated", "value" : "Sessão · ↓ %1$@ · ↑ %2$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сессия · ↓ %1$@ · ↑ %2$@" + } } } }, @@ -22880,13 +21367,6 @@ }, "Settings" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настройки" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -22961,8 +21441,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings" + "state" : "translated", + "value" : "Настройки" } }, "tr" : { @@ -22999,13 +21479,6 @@ }, "Shelf" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Панель файлов" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23080,8 +21553,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shelf" + "state" : "translated", + "value" : "Панель файлов" } }, "tr" : { @@ -23113,27 +21586,6 @@ }, "Shortcuts" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сочетания клавиш" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Путь командной оболочки" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Оболочка" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23208,8 +21660,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shortcuts" + "state" : "translated", + "value" : "Сочетания клавиш" } }, "tr" : { @@ -23235,13 +21687,6 @@ "Show battery indicator" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать индикатор заряда батареи" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23316,8 +21761,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show battery indicator" + "state" : "translated", + "value" : "Показать индикатор заряда батареи" } }, "tr" : { @@ -23343,13 +21788,6 @@ "Show charging indicator" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать индикатор зарядки" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -23361,6 +21799,12 @@ "state" : "translated", "value" : "Afficher l’indicateur de charge" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать индикатор зарядки" + } } } }, @@ -23393,13 +21837,6 @@ "Show cool face animation while inactivity" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать прикольную анимацию лица при бездействия" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23474,8 +21911,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show cool face animation while inactivity" + "state" : "translated", + "value" : "Показать прикольную анимацию лица при бездействия" } }, "tr" : { @@ -23511,13 +21948,6 @@ "Show download progress" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать прогресс загрузки" - } - }, - "en" : { "stringUnit" : { "state" : "translated", @@ -23535,6 +21965,12 @@ "state" : "translated", "value" : "Mostrar o progresso do download" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать прогресс загрузки" + } } } }, @@ -23553,39 +21989,17 @@ }, "Show floating pause/stop controls" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать вкладки расширений" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать события в течение следующих" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать события из всех календарей" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать событие после его начала" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar controles flutuantes de pausa/parada" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать вкладки расширений" + } } } }, @@ -23603,13 +22017,6 @@ }, "Show on a specific display" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать на определённом дисплее" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23684,8 +22091,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show on a specific display" + "state" : "translated", + "value" : "Показать на определённом дисплее" } }, "tr" : { @@ -23710,13 +22117,6 @@ }, "Show on all displays" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать на всех дисплеях" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23791,8 +22191,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show on all displays" + "state" : "translated", + "value" : "Показать на всех дисплеях" } }, "tr" : { @@ -23850,13 +22250,6 @@ "Show shuffle and repeat buttons" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать кнопки перемешивания и повтора" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -23931,8 +22324,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show shuffle and repeat buttons" + "state" : "translated", + "value" : "Показать кнопки перемешивания и повтора" } }, "tr" : { @@ -23999,30 +22392,8 @@ } } }, - "Shows screen assistant as a dropdown attached to the AI button" : { - "comment" : "Description of the \"popover\" display mode for the screen assistant.", - "isCommentAutoGenerated" : true - }, - "Shows screen assistant in a floating panel near the notch" : { - "comment" : "Description of the \"panel\" display mode for the screen assistant.", - "isCommentAutoGenerated" : true - }, "Shows the model's reasoning process before the final answer" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывает экранный помощник во всплывающей панели рядом с вырезом" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывает экранный помощник в выпадающем списке, прикреплённом к кнопке ИИ" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -24034,6 +22405,12 @@ "state" : "translated", "value" : "Mostra o processo de raciocínio do modelo antes da resposta final" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывает экранный помощник во всплывающей панели рядом с вырезом" + } } } }, @@ -24053,18 +22430,17 @@ "Size & Scale" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Простой и минималистичный интерфейс с самым необходимым для повседневных задач." - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille et échelle" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Простой и минималистичный интерфейс с самым необходимым для повседневных задач." + } } } }, @@ -24090,26 +22466,8 @@ } } }, - "Skip forward or backward by ten seconds" : { - "comment" : "Description of the \"Skip forward or backward by ten seconds\" music skip behavior option.", - "isCommentAutoGenerated" : true - }, "Slider color" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цвет ползунка" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Промотать вперёд или назад на 10 секунд" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -24184,8 +22542,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Slider color" + "state" : "translated", + "value" : "Цвет ползунка" } }, "tr" : { @@ -24244,13 +22602,6 @@ }, "Sneak Peek shows the media title and artist under the notch for a few seconds." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "В режиме предварительного просмотра под вырезом в течение нескольких секунд отображается название медиа и исполнителя" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -24325,8 +22676,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." + "state" : "translated", + "value" : "В режиме предварительного просмотра под вырезом в течение нескольких секунд отображается название медиа и исполнителя" } }, "tr" : { @@ -24351,13 +22702,6 @@ }, "Sneak Peek Style" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стиль предварительного просмотра" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -24432,8 +22776,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Sneak Peek Style" + "state" : "translated", + "value" : "Стиль предварительного просмотра" } }, "tr" : { @@ -24471,13 +22815,6 @@ }, "Software updates" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Обновления ПО" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -24552,8 +22889,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Software updates" + "state" : "translated", + "value" : "Обновления ПО" } }, "tr" : { @@ -24588,13 +22925,6 @@ }, "Speed" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скорость" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -24669,8 +22999,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Speed" + "state" : "translated", + "value" : "Скорость" } }, "tr" : { @@ -24704,26 +23034,8 @@ } } }, - "Spotify" : { - "comment" : "Name of a media controller type for Spotify.", - "isCommentAutoGenerated" : true - }, "Square" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Квадрат" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spotify" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -24798,8 +23110,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Square" + "state" : "translated", + "value" : "Квадрат" } }, "tr" : { @@ -24834,41 +23146,9 @@ }, "Standard notch media controls are hidden. Re-enable the toggle above to restore them." : { - }, - "Standard previous/next track controls" : { - "comment" : "Description of a music skip behavior option that uses standard previous/next track controls.", - "isCommentAutoGenerated" : true }, "Start" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стандартные элементы управления предыдущим/следующим треком" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стандартные элементы управления медиа скрыты. Включите переключатель выше заново, чтобы их восстановить." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стандартные элементы управления медиа в вырезе скрыты, когда этот переключатель отключен." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стандартный вырез" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -24880,6 +23160,12 @@ "state" : "translated", "value" : "Começar " } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стандартные элементы управления предыдущим/следующим треком" + } } } }, @@ -25105,6 +23391,10 @@ } } }, + "Status" : { + "comment" : "A label displayed next to the status indicator in the \"Third-party DDC provider\" section of the External Display Integrations settings.", + "isCommentAutoGenerated" : true + }, "Status & Actions" : { "localizations" : { "fr" : { @@ -25131,20 +23421,6 @@ }, "Stop" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Держите дела под контролем с календарём, таймером и мониторингом батареи." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Статус:" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -25156,6 +23432,12 @@ "state" : "translated", "value" : "Parar" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Держите дела под контролем с календарём, таймером и мониторингом батареи." + } } } }, @@ -25193,13 +23475,6 @@ }, "Stopped" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Остановлено" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -25274,8 +23549,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Stopped" + "state" : "translated", + "value" : "Остановлено" } }, "tr" : { @@ -25304,18 +23579,17 @@ }, "Style" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Студент" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Estilo" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Студент" + } } } }, @@ -25334,13 +23608,6 @@ "Support Us" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поддержать нас" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -25415,8 +23682,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Support Us" + "state" : "translated", + "value" : "Поддержать нас" } }, "tr" : { @@ -25506,13 +23773,6 @@ }, "System features" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Системные функции" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -25587,8 +23847,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "System features" + "state" : "translated", + "value" : "Системные функции" } }, "tr" : { @@ -25610,6 +23870,9 @@ } } } + }, + "System Monospaced" : { + }, "System Performance Monitor" : { "extractionState" : "stale", @@ -25628,13 +23891,6 @@ }, "Take area screenshot" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вкладка" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -25646,6 +23902,12 @@ "state" : "translated", "value" : "Captura de tela da área" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вкладка" + } } } }, @@ -25686,53 +23948,17 @@ }, "These controls sit beside the notch while a timer runs. They require the timer name to stay hidden for spacing." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Эти элементы управления дублируют вкладку «Экран блокировки», позволяя настроить панель медиа с акцентом на параметры воспроизведения." - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Динамический остров" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Текст" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Непрозрачность терминала" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Терминал отключён" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Терминал" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Esses controles ficam ao lado do notch enquanto um cronômetro é executado. Eles exigem que o nome do temporizador permaneça oculto para o espaçamento." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эти элементы управления дублируют вкладку «Экран блокировки», позволяя настроить панель медиа с акцентом на параметры воспроизведения." + } } } }, @@ -25775,18 +24001,17 @@ }, "This permanently removes every saved note and its attachments." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Это необязательный параметр. Вы можете изменить его в любое время в строке меню." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso remove permanentemente todas as notas salvas e seus anexos." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это необязательный параметр. Вы можете изменить его в любое время в строке меню." + } } } }, @@ -25815,13 +24040,6 @@ }, "Time to Full Charge: %lld min" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Время до полной зарядки: %lld минут" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -25896,8 +24114,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Time to Full Charge: %lld min" + "state" : "translated", + "value" : "Время до полной зарядки: %lld минут" } }, "tr" : { @@ -26106,13 +24324,6 @@ }, "Toggle Notch Open:" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переключить открытие выреза:" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -26187,8 +24398,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Toggle Notch Open:" + "state" : "translated", + "value" : "Переключить открытие выреза:" } }, "tr" : { @@ -26213,13 +24424,6 @@ }, "Toggle Sneak Peek:" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переключить предварительный просмотр:" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -26294,8 +24498,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Toggle Sneak Peek:" + "state" : "translated", + "value" : "Переключить предварительный просмотр:" } }, "tr" : { @@ -26353,25 +24557,20 @@ }, "shouldTranslate" : false }, - "Track Skip" : { - "comment" : "Display name for the \"Track Skip\" music skip behavior.", - "isCommentAutoGenerated" : true - }, "Transform" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пропустить" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Transformez" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропустить" + } } } }, @@ -26425,18 +24624,17 @@ "comment" : "A description of the feature that allows users to rearrange the order of media buttons.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключите 'Показывать событие всю его длительность', чтобы использовать опцию отображения времени с начала события." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ative os controles personalizáveis para reorganizar os botões de mídia." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отключите 'Показывать событие всю его длительность', чтобы использовать опцию отображения времени с начала события." + } } } }, @@ -26456,13 +24654,6 @@ "comment" : "A footer text explaining the gesture control functionality.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сделайте свайп двумя пальцами вверх, чтобы свернуть вырез и вниз - чтобы его развернуть, когда функция **Развернуть вырез при наведении курсора\" отключена" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -26537,8 +24728,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" + "state" : "translated", + "value" : "Сделайте свайп двумя пальцами вверх, чтобы свернуть вырез и вниз - чтобы его развернуть, когда функция **Развернуть вырез при наведении курсора\" отключена" } }, "tr" : { @@ -26580,13 +24771,6 @@ "Uninstall" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Удалить" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -26661,8 +24845,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Uninstall" + "state" : "translated", + "value" : "Удалить" } }, "tr" : { @@ -26691,20 +24875,6 @@ }, "Unlock advanced features and improve your experience. Upgrade now for more customizations!" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Откройте для себя расширенные возможности и улучшите свой пользовательский опыт. Обновите подписку прямо сейчас, чтобы получить больше настроек!" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неизвестно" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -26779,8 +24949,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" + "state" : "translated", + "value" : "Откройте для себя расширенные возможности и улучшите свой пользовательский опыт. Обновите подписку прямо сейчас, чтобы получить больше настроек!" } }, "tr" : { @@ -26805,13 +24975,6 @@ }, "unmuted" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "незаглушенный" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -26886,8 +25049,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "unmuted" + "state" : "translated", + "value" : "незаглушенный" } }, "tr" : { @@ -26916,18 +25079,17 @@ }, "Update interval" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отключено" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Intervalo de atualização" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отключено" + } } } }, @@ -26950,13 +25112,6 @@ }, "Upgrade to Pro" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Обновиться до Pro" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -27031,8 +25186,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Upgrade to Pro" + "state" : "translated", + "value" : "Обновиться до Pro" } }, "tr" : { @@ -27082,13 +25237,6 @@ "comment" : "A label displayed above the CPU usage statistics.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ссылка" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -27100,6 +25248,12 @@ "state" : "translated", "value" : "Uso" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ссылка" + } } } }, @@ -27137,20 +25291,6 @@ }, "Use music visualizer spectrogram" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Использовать визуализацию музыки (спектрограмму)" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Использовать инструменты разработчика (DevTools)" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -27225,8 +25365,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use music visualizer spectrogram" + "state" : "translated", + "value" : "Использовать визуализацию музыки (спектрограмму)" } }, "tr" : { @@ -27279,13 +25419,6 @@ "comment" : "A label describing the percentage of memory that is currently in use.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Использовать без инструментов разработчика (DevTools)" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -27297,6 +25430,12 @@ "state" : "translated", "value" : "Usado" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Использовать без инструментов разработчика (DevTools)" + } } } }, @@ -27371,13 +25510,6 @@ }, "Version" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Версия" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -27452,8 +25584,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Version" + "state" : "translated", + "value" : "Версия" } }, "tr" : { @@ -27478,13 +25610,6 @@ }, "Version info" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Информация о версии" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -27559,8 +25684,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Version info" + "state" : "translated", + "value" : "Информация о версии" } }, "tr" : { @@ -27589,13 +25714,6 @@ "Vertical:" : { "extractionState" : "stale", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вертикальный отступ" - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -27607,19 +25725,18 @@ "state" : "translated", "value" : "Vertical:" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вертикальный отступ" + } } }, "shouldTranslate" : false }, "View on GitHub: th-ch/youtube-music" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Посмотреть на GitHub: th-ch/youtube-music" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -27694,8 +25811,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "View on GitHub: th-ch/youtube-music" + "state" : "translated", + "value" : "Посмотреть на GitHub: th-ch/youtube-music" } }, "tr" : { @@ -27766,18 +25883,17 @@ "comment" : "A label and its accompanying text displayed in a lock screen panel.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "З " - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tempo" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "З " + } } } }, @@ -27806,13 +25922,6 @@ "Weather: %@ %@" : { "comment" : "Weather description and temperature", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Погода: %1$@ %2$@" - } - }, - "en" : { "stringUnit" : { "state" : "new", @@ -27824,19 +25933,18 @@ "state" : "translated", "value" : "Tempo: %1$@ %2$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Погода: %1$@ %2$@" + } } } }, "Weather: %@ %@ in %@" : { "comment" : "Weather description, temperature, and location", "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Погода: %1$@ %2$@ в %3$@" - } - }, - "en" : { "stringUnit" : { "state" : "new", @@ -27848,18 +25956,17 @@ "state" : "translated", "value" : "Tempo: %1$@ %2$@ in %3$@" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Погода: %1$@ %2$@ в %3$@" + } } } }, "Welcome" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добро пожаловать" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -27934,8 +26041,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Welcome" + "state" : "translated", + "value" : "Добро пожаловать" } }, "tr" : { @@ -27960,13 +26067,6 @@ }, "What's New" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Что нового" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -28041,8 +26141,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "What's New" + "state" : "translated", + "value" : "Что нового" } }, "tr" : { @@ -28070,41 +26170,38 @@ }, "When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Когда параметр «Жирный текст как яркие цвета» отключён, он отображается более толстым начертанием шрифта вместо использования ярких ANSI-цветов." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando desativado, todos os atalhos de teclado estarão inativos. Você ainda pode usar os controles da interface do usuário." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Когда параметр «Жирный текст как яркие цвета» отключён, он отображается более толстым начертанием шрифта вместо использования ярких ANSI-цветов." + } } } }, - "When enabled, Atoll listens for OSD notifications from BetterDisplay and displays them using your selected HUD style above. This works alongside the existing media key interception — BetterDisplay handles external display controls while Atoll provides the visual feedback." : { - "comment" : "A footer description for the \"BetterDisplay Integration\" section in the settings view.", - "isCommentAutoGenerated" : true + "When disabled, the notch music player stays visible with placeholder metadata even when playback is inactive." : { + }, "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "При включении, Atoll будет получать экранные уведомления от BetterDisplay и отображать их в выбранном выше стиле панели. Это работает вместе с существующим перехватом мультимедийных клавиш — BetterDisplay управляет внешними дисплеями, а Atoll обеспечивает визуальную индикацию." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando ativado, o Foco aparece brevemente (ligado/desligado) e depois fecha em vez de permanecer visível." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "При включении, Atoll будет получать экранные уведомления от BetterDisplay и отображать их в выбранном выше стиле панели. Это работает вместе с существующим перехватом мультимедийных клавиш — BetterDisplay управляет внешними дисплеями, а Atoll обеспечивает визуальную индикацию." + } } } }, @@ -28124,18 +26221,17 @@ }, "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "При включении, вырез на экране сдвигается вверх и скрывается на внешних (не имеющих выреза) дисплеях, пока вы не наведете на него курсор." - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando ativada, a guia Estatísticas exibirá gráficos de desempenho do sistema em tempo real. Esse recurso requer permissões do sistema e pode usar bateria adicional." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "При включении, вырез на экране сдвигается вверх и скрывается на внешних (не имеющих выреза) дисплеях, пока вы не наведете на него курсор." + } } } }, @@ -28146,18 +26242,17 @@ "comment" : "A label describing the number of times the extension has updated its widget.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Белый" - } - }, - "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizações dos Widgets" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Белый" + } } } }, @@ -28206,13 +26301,6 @@ "comment" : "A label for the write section of the disk overview.", "isCommentAutoGenerated" : true, "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Работает со всеми медиа приложениями, включая браузеры, определяя, что воспроизводится. Заметка: функция может быть удалена в будущих версиях macOS." - } - }, - "fr" : { "stringUnit" : { "state" : "translated", @@ -28224,6 +26312,12 @@ "state" : "translated", "value" : "Escrever" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Работает со всеми медиа приложениями, включая браузеры, определяя, что воспроизводится. Заметка: функция может быть удалена в будущих версиях macOS." + } } } }, @@ -28245,13 +26339,6 @@ }, "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Теперь Вы можете пользоваться приложением. Если хотите внести дополнительные изменения, Вы всегда можете сделать это в настройках." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -28326,8 +26413,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." + "state" : "translated", + "value" : "Теперь Вы можете пользоваться приложением. Если хотите внести дополнительные изменения, Вы всегда можете сделать это в настройках." } }, "tr" : { @@ -28352,13 +26439,6 @@ }, "You're All Set!" : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Всё готово!" - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -28433,8 +26513,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You're All Set!" + "state" : "translated", + "value" : "Всё готово!" } }, "tr" : { @@ -28481,33 +26561,8 @@ "comment" : "A footer note explaining the app's funding.", "isCommentAutoGenerated" : true }, - "Youtube Music" : { - "comment" : "Name for the \"Youtube Music\" media controller type in settings.", - "isCommentAutoGenerated" : true - }, "YouTube Music requires this third-party app to be installed: " : { "localizations" : { - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Для работы YouTube Music необходимо установить следующее стороннее приложение: " - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "YouTube Music" - } - }, - - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша поддержка финансирует обучение разработке программного обеспечения для учащихся 9-12 классов." - } - }, - "ar" : { "stringUnit" : { "state" : "needs_review", @@ -28582,8 +26637,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "YouTube Music requires this third-party app to be installed: " + "state" : "translated", + "value" : "Для работы YouTube Music необходимо установить следующее стороннее приложение: " } }, "tr" : { @@ -28608,4 +26663,4 @@ } }, "version" : "1.2" -} +} \ No newline at end of file diff --git a/DynamicIsland/components/AnimatedFace.swift b/DynamicIsland/components/AnimatedFace.swift deleted file mode 100644 index 66d4b65..0000000 --- a/DynamicIsland/components/AnimatedFace.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors - * - * Originally from boring.notch project - * Modified and adapted for Atoll (DynamicIsland) - * See NOTICE for details. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import SwiftUI - -struct MinimalFaceFeatures: View { - @State private var isBlinking = false - @State var height:CGFloat = 20; - @State var width:CGFloat = 30; - - var body: some View { - VStack(spacing: 4) { // Adjusted spacing to fit within 30x30 - // Eyes - HStack(spacing: 4) { // Adjusted spacing to fit within 30x30 - Eye(isBlinking: $isBlinking) - Eye(isBlinking: $isBlinking) - } - - // Nose and mouth combined - VStack(spacing: 2) { // Adjusted spacing to fit within 30x30 - // Nose - RoundedRectangle(cornerRadius: 2) - .fill(Color.white) - .frame(width: 3, height: 4) - - // Mouth (happy) - GeometryReader { geometry in - Path { path in - let width = geometry.size.width - let height = geometry.size.height - path.move(to: CGPoint(x: 0, y: height / 2)) - path.addQuadCurve(to: CGPoint(x: width, y: height / 2), control: CGPoint(x: width / 2, y: height)) - } - .stroke(Color.white, lineWidth: 2) - } - .frame(width: 14, height: 10) - } - } - .frame(width: self.width, height: self.height) // Maximum size of face - .onAppear { - startBlinking() - } - } - - func startBlinking() { - Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in - withAnimation(.spring(duration: 0.2)) { - isBlinking = true - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - withAnimation(.spring(duration: 0.2)) { - isBlinking = false - } - } - } - } -} - -struct Eye: View { - @Binding var isBlinking: Bool - - var body: some View { - RoundedRectangle(cornerRadius: 10) - .fill(Color.white) - .frame(width: 4, height: isBlinking ? 1 : 4) - .frame(maxWidth: 15, maxHeight: 15) // Adjusted max size - .animation(.easeInOut(duration: 0.1), value: isBlinking) - } -} - -struct MinimalFaceFeatures_Previews: PreviewProvider { - static var previews: some View { - ZStack { - Color.black - MinimalFaceFeatures() - } - .previewLayout(.fixed(width: 60, height: 60)) // Adjusted preview size for better visibility - } -} diff --git a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift index 7211240..11c62dc 100644 --- a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift +++ b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift @@ -261,7 +261,16 @@ struct StandaloneCalendarView: View { @Default(.hideCompletedReminders) private var hideCompletedReminders private let calendar = Calendar.current - private let weekdaySymbols = Calendar.current.shortWeekdaySymbols + + private var weekdaySymbols: [String] { + let symbols = calendar.veryShortStandaloneWeekdaySymbols + guard !symbols.isEmpty else { return symbols } + + let firstWeekdayIndex = max(0, min(symbols.count - 1, calendar.firstWeekday - 1)) + var ordered = Array(symbols[firstWeekdayIndex...]) + ordered.append(contentsOf: symbols[.. Bool { + guard let data = try? JSONEncoder().encode(descriptor), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let value = json["allowRemoteRequests"] as? Bool else { + return false + } + return value + } } } diff --git a/DynamicIsland/components/IdleAnimationView.swift b/DynamicIsland/components/IdleAnimationView.swift index 935c714..41015a9 100644 --- a/DynamicIsland/components/IdleAnimationView.swift +++ b/DynamicIsland/components/IdleAnimationView.swift @@ -31,8 +31,8 @@ struct IdleAnimationView: View { AnimationContentView(animation: animation) .id("\(animation.id)-\(overrides[animation.id.uuidString]?.hashValue ?? 0)") // Force recreation when override changes } else { - // Fallback to original face if nothing selected - MinimalFaceFeatures(height: 20, width: 30) + // No animation selected + EmptyView() } } } @@ -85,20 +85,6 @@ private struct AnimationContentView: View { .padding(.bottom, config.paddingBottom) .frame(width: config.expandWithAnimation ? nil : 30, height: 20) .clipped() - - case .builtInFace: - // Apply transforms to built-in face too! - MinimalFaceFeatures(height: 20, width: 30) - .frame( - width: config.cropWidth * config.scale, - height: config.cropHeight * config.scale - ) - .offset(x: config.offsetX, y: config.offsetY) - .rotationEffect(.degrees(config.rotation)) - .opacity(config.opacity) - .padding(.bottom, config.paddingBottom) - .frame(width: config.expandWithAnimation ? nil : 30, height: 20) - .clipped() } } } diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index 0c8f7f2..2b32047 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -824,7 +824,7 @@ struct LockScreenMusicPanel: View { private func albumArtImage(size: CGFloat, cornerRadius: CGFloat) -> some View { Image(nsImage: musicManager.albumArt) .resizable() - .aspectRatio(contentMode: .fill) + .aspectRatio(contentMode: .fit) .frame(width: size, height: size) .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) } diff --git a/DynamicIsland/components/MinimalisticMusicView.swift b/DynamicIsland/components/MinimalisticMusicView.swift index 6129bd7..43b8a13 100644 --- a/DynamicIsland/components/MinimalisticMusicView.swift +++ b/DynamicIsland/components/MinimalisticMusicView.swift @@ -92,7 +92,7 @@ struct MinimalisticMusicView: View { .background( Image(nsImage: musicManager.albumArt) .resizable() - .aspectRatio(contentMode: .fill) + .aspectRatio(contentMode: .fit) ) .clipped() .clipShape(RoundedRectangle(cornerRadius: 18)) // Dramatically increased corner radius for minimalistic mode diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index fd20d3b..c306ce3 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -1058,7 +1058,7 @@ struct MinimalisticAlbumArtView: View { .background( Image(nsImage: musicManager.albumArt) .resizable() - .aspectRatio(contentMode: .fill) + .aspectRatio(contentMode: .fit) ) .clipped() .clipShape(RoundedRectangle(cornerRadius: 12)) @@ -1077,7 +1077,7 @@ struct MinimalisticAlbumArtView: View { .background( Image(nsImage: musicManager.albumArt) .resizable() - .aspectRatio(contentMode: .fill) + .aspectRatio(contentMode: .fit) ) .clipped() .clipShape(RoundedRectangle(cornerRadius: 12)) diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 7b3d0fa..37deed7 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -36,8 +36,6 @@ struct MusicPlayerView: View { .padding(.all, 5) MusicControlsView() .frame(maxWidth: .infinity, alignment: .leading) - .drawingGroup() - .compositingGroup() } .frame(maxWidth: .infinity, alignment: .leading) } @@ -58,8 +56,13 @@ struct AlbumArtView: View { } private var albumArtBackground: some View { - Image(nsImage: musicManager.albumArt) - .resizable() + Color.clear + .aspectRatio(1, contentMode: .fit) + .background( + Image(nsImage: musicManager.albumArt) + .resizable() + .aspectRatio(contentMode: .fit) + ) .clipped() .clipShape( RoundedRectangle( @@ -67,7 +70,6 @@ struct AlbumArtView: View { ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed) ) - .aspectRatio(1, contentMode: .fit) .scaleEffect(x: 1.3, y: 1.4) .rotationEffect(.degrees(92)) .blur(radius: 40) @@ -102,9 +104,13 @@ struct AlbumArtView: View { } private var albumArtImage: some View { - Image(nsImage: musicManager.albumArt) - .resizable() + Color.clear .aspectRatio(1, contentMode: .fit) + .overlay { + Image(nsImage: musicManager.albumArt) + .resizable() + .aspectRatio(contentMode: .fit) + } .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) .clipped() .clipShape( @@ -529,11 +535,12 @@ struct NotchHomeView: View { @ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared @ObservedObject private var musicManager = MusicManager.shared @Default(.showStandardMediaControls) private var showStandardMediaControls + @Default(.autoHideInactiveNotchMediaPlayer) private var autoHideInactiveNotchMediaPlayer let albumArtNamespace: Namespace.ID /// Whether the music player should actively display (enabled AND has real content). private var shouldShowMusicPlayer: Bool { - showStandardMediaControls && musicManager.hasActiveSession + showStandardMediaControls && (!autoHideInactiveNotchMediaPlayer || musicManager.hasActiveSession) } var body: some View { diff --git a/DynamicIsland/components/Settings/AnimationEditorView.swift b/DynamicIsland/components/Settings/AnimationEditorView.swift index 1f6ebf1..9480841 100644 --- a/DynamicIsland/components/Settings/AnimationEditorView.swift +++ b/DynamicIsland/components/Settings/AnimationEditorView.swift @@ -170,16 +170,11 @@ struct AnimationEditorView: View { .frame(width: 30 * previewScale, height: 20 * previewScale) Group { - if let existing = existingAnimation, case .builtInFace = existing.source { - MinimalFaceFeatures(height: 20, width: 30) - .scaleEffect(previewScale) - } else { - LottieView(state: LUStateData( - type: .loadedFrom(sourceURL), - speed: speed, - loopMode: loopMode.lottieLoopMode - )) - } + LottieView(state: LUStateData( + type: .loadedFrom(sourceURL), + speed: speed, + loopMode: loopMode.lottieLoopMode + )) } .frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale) .offset(x: offsetX * previewScale, y: offsetY * previewScale) diff --git a/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift b/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift index be45da7..fa587bc 100644 --- a/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift +++ b/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift @@ -87,12 +87,6 @@ struct IdleAnimationsSettingsSection: View { print("🔧 [Edit] Lottie URL: \(url)") editorSourceURL = url editorIsRemote = true - - case .builtInFace: - print("🔧 [Edit] Built-in face - using dummy URL") - // Use a dummy URL for built-in face (editor only needs URL for preview) - editorSourceURL = URL(string: "builtin://face")! - editorIsRemote = false } // Show editor with slight delay to ensure state is set @@ -118,7 +112,7 @@ struct IdleAnimationsSettingsSection: View { } .frame(height: 140) } else { - Text("Enable \"Show cool face animation while inactivity\" to customize animations") + Text("Enable \"Idle Animation\" to customize animations") .font(.caption) .foregroundStyle(.secondary) .padding(.vertical, 8) @@ -388,10 +382,6 @@ struct AnimationPreview: View { loopMode: .loop )) .frame(width: 60, height: 40) - - case .builtInFace: - MinimalFaceFeatures(height: 40, width: 60) - .scaleEffect(2.0) // Scale up the face for preview } } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index a751434..ae7a5ec 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -422,8 +422,23 @@ struct SettingsView: View { @ViewBuilder private func sidebarIcon(for tab: SettingsTab) -> some View { RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(tab.tint) + .fill( + LinearGradient( + colors: [ + tab.tint.opacity(1), + tab.tint.opacity(0.7) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) .frame(width: 26, height: 26) + .overlay { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .strokeBorder(Color.white.opacity(0.2), lineWidth: 0.7) + .blendMode(.plusLighter) + } + .shadow(color: tab.tint.opacity(0.35), radius: 2, x: 0, y: 1) .overlay { Image(systemName: tab.systemImage) .font(.system(size: 13, weight: .semibold)) @@ -739,13 +754,16 @@ struct SettingsView: View { SettingsSearchEntry(tab: .hudAndOSD, title: "Keyboard Backlight OSD", keywords: ["keyboard", "backlight", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Keyboard Backlight OSD")), SettingsSearchEntry(tab: .hudAndOSD, title: "Material", keywords: ["material", "frosted", "liquid", "glass", "solid", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Material")), SettingsSearchEntry(tab: .hudAndOSD, title: "Icon & Progress Color", keywords: ["color", "icon", "white", "black", "gray", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Icon & Progress Color")), - SettingsSearchEntry(tab: .hudAndOSD, title: "BetterDisplay integration", keywords: ["betterdisplay", "external", "display", "brightness", "integration", "third party"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "BetterDisplay integration")), + SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC app integration", keywords: ["ddc", "third party", "external", "display", "betterdisplay", "lunar"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC app integration")), + SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC provider", keywords: ["provider", "betterdisplay", "lunar", "integration", "refresh detection"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC provider")), + SettingsSearchEntry(tab: .hudAndOSD, title: "Enable external volume control listener", keywords: ["external volume", "ddc volume", "betterdisplay volume", "lunar volume", "disable native volume"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Enable external volume control listener")), // Media SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")), SettingsSearchEntry(tab: .media, title: "Skip buttons", keywords: ["skip", "controls", "±10"], highlightID: SettingsTab.media.highlightID(for: "Skip buttons")), SettingsSearchEntry(tab: .media, title: "Sneak Peek Style", keywords: ["sneak peek", "preview"], highlightID: SettingsTab.media.highlightID(for: "Sneak Peek Style")), SettingsSearchEntry(tab: .media, title: "Enable lyrics", keywords: ["lyrics", "song text"], highlightID: SettingsTab.media.highlightID(for: "Enable lyrics")), + SettingsSearchEntry(tab: .media, title: "Auto-hide inactive notch media player", keywords: ["auto hide", "inactive", "placeholder", "notch media"], highlightID: SettingsTab.media.highlightID(for: "Auto-hide inactive notch media player")), SettingsSearchEntry(tab: .media, title: "Show Change Media Output control", keywords: ["airplay", "route picker", "media output"], highlightID: SettingsTab.media.highlightID(for: "Show Change Media Output control")), SettingsSearchEntry(tab: .media, title: "Enable album art parallax", keywords: ["parallax", "lock screen", "album art"], highlightID: SettingsTab.media.highlightID(for: "Enable album art parallax")), SettingsSearchEntry(tab: .media, title: "Enable album art parallax effect", keywords: ["parallax", "parallax effect", "album art"], highlightID: SettingsTab.media.highlightID(for: "Enable album art parallax effect")), @@ -789,7 +807,7 @@ struct SettingsView: View { SettingsSearchEntry(tab: .appearance, title: "Slider color", keywords: ["slider", "accent"], highlightID: SettingsTab.appearance.highlightID(for: "Slider color")), SettingsSearchEntry(tab: .appearance, title: "Enable Dynamic mirror", keywords: ["mirror", "reflection"], highlightID: SettingsTab.appearance.highlightID(for: "Enable Dynamic mirror")), SettingsSearchEntry(tab: .appearance, title: "Mirror shape", keywords: ["mirror shape", "circle", "rectangle"], highlightID: SettingsTab.appearance.highlightID(for: "Mirror shape")), - SettingsSearchEntry(tab: .appearance, title: "Show cool face animation while inactivity", keywords: ["face animation", "idle"], highlightID: SettingsTab.appearance.highlightID(for: "Show cool face animation while inactivity")), + SettingsSearchEntry(tab: .appearance, title: "Idle Animation", keywords: ["face animation", "idle", "cool face"], highlightID: SettingsTab.appearance.highlightID(for: "Idle Animation")), SettingsSearchEntry(tab: .appearance, title: "App icon", keywords: ["app icon", "custom icon"], highlightID: SettingsTab.appearance.highlightID(for: "App icon")), // Lock Screen @@ -1494,7 +1512,7 @@ private struct HUDAndOSDSettingsView: View { @Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.verticalHUDShowValue) var verticalHUDShowValue @Default(.verticalHUDInteractive) var verticalHUDInteractive @Default(.verticalHUDHeight) var verticalHUDHeight @@ -1730,7 +1748,7 @@ private struct HUDAndOSDSettingsView: View { } case .vertical: Form { - if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration { + if !accessibilityPermission.isAuthorized && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission is needed to intercept system controls for the Vertical HUD.", @@ -1746,13 +1764,13 @@ private struct HUDAndOSDSettingsView: View { } } - if accessibilityPermission.isAuthorized || enableBetterDisplayIntegration { + if accessibilityPermission.isAuthorized || enableThirdPartyDDCIntegration { Section { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) - .disabled(enableBetterDisplayIntegration) - .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active — BetterDisplay uses Cmd+Brightness keys for its own controls." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -1847,7 +1865,7 @@ private struct HUDAndOSDSettingsView: View { case .circular: Form { - if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration { + if !accessibilityPermission.isAuthorized && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission is needed to intercept system controls for the Circular HUD.", @@ -1863,13 +1881,13 @@ private struct HUDAndOSDSettingsView: View { } } - if accessibilityPermission.isAuthorized || enableBetterDisplayIntegration { + if accessibilityPermission.isAuthorized || enableThirdPartyDDCIntegration { Section { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) - .disabled(enableBetterDisplayIntegration) - .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active — BetterDisplay uses Cmd+Brightness keys for its own controls." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -1909,8 +1927,8 @@ private struct HUDAndOSDSettingsView: View { } } - // BetterDisplay Integration (shared across all HUD variants) - BetterDisplayIntegrationSection() + // Third-party display integrations (shared across all HUD variants) + ExternalDisplayIntegrationsSection() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(paneBackgroundColor) @@ -1924,93 +1942,135 @@ private struct HUDAndOSDSettingsView: View { } } -// MARK: - BetterDisplay Integration Settings Section +// MARK: - External Display Integrations Settings Section -private struct BetterDisplayIntegrationSection: View { - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration +private struct ExternalDisplayIntegrationsSection: View { + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration + @Default(.thirdPartyDDCProvider) var thirdPartyDDCProvider + @Default(.enableExternalVolumeControlListener) var enableExternalVolumeControlListener @ObservedObject private var betterDisplayManager = BetterDisplayManager.shared + @ObservedObject private var lunarManager = LunarManager.shared private func highlightID(_ title: String) -> String { SettingsTab.hudAndOSD.highlightID(for: title) } - private var statusText: String { - if betterDisplayManager.isRunning { - return "Running" - } else if betterDisplayManager.isDetected { - return "Not running" - } else { + private var providerStatusText: String { + switch thirdPartyDDCProvider { + case .betterDisplay: + if betterDisplayManager.isRunning { return "Running" } + if betterDisplayManager.isDetected { return "Not running" } + return "Not detected" + case .lunar: + if lunarManager.isConnected { return "Connected" } + if lunarManager.isRunning { return "Running" } + if lunarManager.isDetected { return "Not running" } return "Not detected" } } - private var statusColor: Color { - if betterDisplayManager.isRunning { - return .green - } else if betterDisplayManager.isDetected { - return .orange - } else { + private var providerStatusColor: Color { + switch thirdPartyDDCProvider { + case .betterDisplay: + if betterDisplayManager.isRunning { return .green } + if betterDisplayManager.isDetected { return .orange } return .secondary + case .lunar: + if lunarManager.isConnected { return .green } + if lunarManager.isRunning { return .orange } + if lunarManager.isDetected { return .orange } + return .secondary + } + } + + private var providerStatusDescription: String { + switch thirdPartyDDCProvider { + case .betterDisplay: + if !betterDisplayManager.isDetected { + return "Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness (and optional volume) through Atoll's HUD." + } + if !betterDisplayManager.isRunning { + return "BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration." + } + return "BetterDisplay OSD events will be routed through Atoll's active HUD style. Brightness is always routed; volume is routed when external volume control listener is enabled below. Make sure BetterDisplay's OSD integration is enabled in Settings › Application › Integration." + case .lunar: + if !lunarManager.isDetected { + return "Install [Lunar](https://lunar.fyi) to control external display brightness, contrast, and optional volume through Atoll's HUD via DDC." + } + if !lunarManager.isRunning { + return "Lunar is installed but not currently running. Launch Lunar to enable integration." + } + if lunarManager.isConnected { + return "Connected to Lunar's DDC socket. Brightness and contrast adjustments are shown through Atoll's HUD; volume follows when external volume control listener is enabled below." + } + return "Lunar is running but the socket connection is not yet established. It will connect automatically." + } + } + + private func refreshDetectionStatus() { + switch thirdPartyDDCProvider { + case .betterDisplay: + betterDisplayManager.refreshDetectionStatus() + case .lunar: + lunarManager.refreshDetectionStatus() } } var body: some View { Form { Section { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("BetterDisplay") - .font(.system(size: 13, weight: .medium)) - Text(statusText) + Toggle("Enable third-party DDC app integration", isOn: $enableThirdPartyDDCIntegration) + .settingsHighlight(id: highlightID("Third-party DDC app integration")) + + if enableThirdPartyDDCIntegration { + Picker("Provider", selection: $thirdPartyDDCProvider) { + ForEach(ThirdPartyDDCProvider.allCases) { provider in + Text(provider.displayName).tag(provider) + } + } + .settingsHighlight(id: highlightID("Third-party DDC provider")) + + Toggle("Enable external volume control listener", isOn: $enableExternalVolumeControlListener) + .settingsHighlight(id: highlightID("Enable external volume control listener")) + + Text( + enableExternalVolumeControlListener + ? "Atoll's built-in volume key interception is disabled while external volume listening is on. Volume HUD/OSD will follow \(thirdPartyDDCProvider.displayName) payloads." + : "Atoll keeps native volume key interception. External provider volume payloads are ignored while this is off." + ) + .font(.caption) + .foregroundStyle(.secondary) + + HStack { + Text("Status") + Spacer() + Text(providerStatusText) .font(.caption) - .foregroundStyle(statusColor) + .foregroundStyle(providerStatusColor) } - Spacer() + Text(providerStatusDescription) + .font(.caption) + .foregroundStyle(.secondary) - Toggle("", isOn: $enableBetterDisplayIntegration) - .toggleStyle(.switch) - .labelsHidden() - .disabled(!betterDisplayManager.isDetected) - } - .settingsHighlight(id: highlightID("BetterDisplay integration")) - - if !betterDisplayManager.isDetected { - Text("Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD.") - .font(.caption) - .foregroundStyle(.secondary) - } else if !betterDisplayManager.isRunning { - Text("BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration.") - .font(.caption) - .foregroundStyle(.secondary) - } else if enableBetterDisplayIntegration { - Text("BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration.") - .font(.caption) - .foregroundStyle(.secondary) - } else { - Text("Enable to route BetterDisplay's brightness and volume changes through Atoll's HUD instead of the system OSD.") - .font(.caption) - .foregroundStyle(.secondary) - } - - if betterDisplayManager.isDetected { Button { - betterDisplayManager.refreshDetectionStatus() + refreshDetectionStatus() } label: { Label("Refresh detection", systemImage: "arrow.clockwise") .font(.caption) } .buttonStyle(.link) - } - } header: { - HStack(spacing: 6) { - Image(systemName: "display.2") - Text("BetterDisplay Integration") + } else { + Text("Enable to route BetterDisplay or Lunar display adjustments through Atoll's active HUD style.") + .font(.caption) + .foregroundStyle(.secondary) } } footer: { - Text("When enabled, Atoll listens for OSD notifications from BetterDisplay and displays them using your selected HUD style above. This works alongside the existing media key interception — BetterDisplay handles external display controls while Atoll provides the visual feedback.") - .foregroundStyle(.secondary) - .font(.caption) + if enableThirdPartyDDCIntegration { + Text("Atoll always listens to selected-provider brightness events, and listens to provider volume events only when external volume listener is enabled.") + .foregroundStyle(.secondary) + .font(.caption) + } } } } @@ -2157,7 +2217,7 @@ struct HUD: View { @Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.systemHUDSensitivity) var systemHUDSensitivity @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared @@ -2176,7 +2236,7 @@ struct HUD: View { var body: some View { Form { - if !hasAccessibilityPermission && !enableBetterDisplayIntegration { + if !hasAccessibilityPermission && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission lets Dynamic Island replace the native volume, brightness, and keyboard HUDs.", @@ -2190,13 +2250,13 @@ struct HUD: View { - if enableSystemHUD && !Defaults[.enableCustomOSD] && (hasAccessibilityPermission || enableBetterDisplayIntegration) { + if enableSystemHUD && !Defaults[.enableCustomOSD] && (hasAccessibilityPermission || enableThirdPartyDDCIntegration) { Section { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) - .disabled(enableBetterDisplayIntegration) - .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active \u{2014} BetterDisplay uses Cmd+Brightness keys for its own controls." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -2312,6 +2372,7 @@ struct Media: View { @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode @Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled @Default(.showStandardMediaControls) private var showStandardMediaControls + @Default(.autoHideInactiveNotchMediaPlayer) private var autoHideInactiveNotchMediaPlayer private func highlightID(_ title: String) -> String { SettingsTab.media.highlightID(for: title) @@ -2359,6 +2420,10 @@ struct Media: View { .disabled(enableMinimalisticUI) .settingsHighlight(id: highlightID("Show media controls in Dynamic Island")) + Defaults.Toggle("Auto-hide inactive notch media player", key: .autoHideInactiveNotchMediaPlayer) + .disabled(enableMinimalisticUI || !showStandardMediaControls) + .settingsHighlight(id: highlightID("Auto-hide inactive notch media player")) + if enableMinimalisticUI { Text("Disable Minimalistic UI to configure the standard notch media controls.") .font(.caption) @@ -2367,6 +2432,10 @@ struct Media: View { Text("Standard notch media controls are hidden. Re-enable the toggle above to restore them.") .font(.caption) .foregroundStyle(.secondary) + } else if !autoHideInactiveNotchMediaPlayer { + Text("When disabled, the notch music player stays visible with placeholder metadata even when playback is inactive.") + .font(.caption) + .foregroundStyle(.secondary) } } header: { Text("Dynamic Island Visibility") @@ -3129,6 +3198,15 @@ struct Shelf: View { @Default(.autoRemoveShelfItems) var autoRemoveShelfItems @StateObject private var quickShareService = QuickShareService.shared @ObservedObject private var fullDiskAccessPermission = FullDiskAccessPermissionStore.shared + @ObservedObject private var shelfFolderAccessPermission = ShelfFolderAccessPermissionStore.shared + + private var hasDocumentsAndDownloadsAccess: Bool { + shelfFolderAccessPermission.hasDocumentsAndDownloadsAccess + } + + private var canEnableShelf: Bool { + fullDiskAccessPermission.isAuthorized || hasDocumentsAndDownloadsAccess + } private var selectedProvider: QuickShareProvider? { quickShareService.availableProviders.first(where: { $0.id == quickShareProvider }) @@ -3144,18 +3222,33 @@ struct Shelf: View { var body: some View { Form { - if !fullDiskAccessPermission.isAuthorized { + if !canEnableShelf || !fullDiskAccessPermission.isAuthorized { Section { - SettingsPermissionCallout( - title: "Full Disk Access required", - message: "Grant Full Disk Access so the Shelf can index and move files outside the app sandbox.", - icon: "externaldrive.fill", - iconColor: .purple, - requestButtonTitle: "Request Full Disk Access", - openSettingsButtonTitle: "Open Privacy & Security", - requestAction: { fullDiskAccessPermission.requestAccessPrompt() }, - openSettingsAction: { fullDiskAccessPermission.openSystemSettings() } - ) + if !canEnableShelf { + SettingsPermissionCallout( + title: "Additional folder access required", + message: "Enable Full Disk Access, or grant access to both Documents and Downloads folders to use Shelf.", + icon: "folder.badge.questionmark", + iconColor: .orange, + requestButtonTitle: "Request Folder Access", + openSettingsButtonTitle: "Open Privacy & Security", + requestAction: { shelfFolderAccessPermission.requestAccessPrompt() }, + openSettingsAction: { shelfFolderAccessPermission.openSystemSettings() } + ) + } + + if !fullDiskAccessPermission.isAuthorized { + SettingsPermissionCallout( + title: "Full Disk Access for global mode", + message: "Without Full Disk Access, Shelf can only read files from Documents and Downloads. Grant Full Disk Access to make Shelf work globally.", + icon: "externaldrive.fill", + iconColor: .purple, + requestButtonTitle: "Request Full Disk Access", + openSettingsButtonTitle: "Open Privacy & Security", + requestAction: { fullDiskAccessPermission.requestAccessPrompt() }, + openSettingsAction: { fullDiskAccessPermission.openSystemSettings() } + ) + } } header: { Text("Permissions") } @@ -3163,7 +3256,7 @@ struct Shelf: View { Section { Defaults.Toggle("Enable shelf", key: .dynamicShelf) - .disabled(!fullDiskAccessPermission.isAuthorized) + .disabled(!canEnableShelf) .settingsHighlight(id: highlightID("Enable shelf")) Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault) @@ -3250,6 +3343,7 @@ struct Shelf: View { .navigationTitle("Shelf") .onAppear { fullDiskAccessPermission.refreshStatus() + shelfFolderAccessPermission.refreshStatus() } } } @@ -3857,8 +3951,8 @@ struct Appearance: View { .tag(MirrorShapeEnum.rectangle) } .settingsHighlight(id: highlightID("Mirror shape")) - Defaults.Toggle("Show cool face animation while inactivity", key: .showNotHumanFace) - .settingsHighlight(id: highlightID("Show cool face animation while inactivity")) + Defaults.Toggle("Idle Animation", key: .showNotHumanFace) + .settingsHighlight(id: highlightID("Idle Animation")) } header: { HStack { Text("Additional features") @@ -6908,7 +7002,7 @@ struct CustomOSDSettings: View { @Default(.enableOSDVolume) var enableOSDVolume @Default(.enableOSDBrightness) var enableOSDBrightness @Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.osdMaterial) var osdMaterial @Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode @Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant @@ -6951,7 +7045,7 @@ struct CustomOSDSettings: View { var body: some View { Form { - if !hasAccessibilityPermission && !enableBetterDisplayIntegration { + if !hasAccessibilityPermission && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission is needed to intercept system controls for the Custom OSD.", @@ -6963,7 +7057,7 @@ struct CustomOSDSettings: View { } } - if hasAccessibilityPermission || enableBetterDisplayIntegration { + if hasAccessibilityPermission || enableThirdPartyDDCIntegration { Section { Toggle("Volume OSD", isOn: $enableOSDVolume) .settingsHighlight(id: highlightID("Volume OSD")) @@ -6971,8 +7065,8 @@ struct CustomOSDSettings: View { .settingsHighlight(id: highlightID("Brightness OSD")) Toggle("Keyboard Backlight OSD", isOn: $enableOSDKeyboardBacklight) .settingsHighlight(id: highlightID("Keyboard Backlight OSD")) - .disabled(enableBetterDisplayIntegration) - .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active \u{2014} BetterDisplay uses Cmd+Brightness keys for its own controls." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -7209,6 +7303,7 @@ struct TerminalSettings: View { @ObservedObject var terminalManager = TerminalManager.shared @Default(.enableTerminalFeature) var enableTerminalFeature @Default(.terminalShellPath) var terminalShellPath + @Default(.terminalFontFamily) var terminalFontFamily @Default(.terminalFontSize) var terminalFontSize @Default(.terminalOpacity) var terminalOpacity @Default(.terminalMaxHeightFraction) var terminalMaxHeightFraction @@ -7229,6 +7324,21 @@ struct TerminalSettings: View { "\(Int(terminalMaxHeightFraction * 100))% of screen" } + /// All monospaced font families available on the system. + private var monospacedFontFamilies: [String] { + NSFontManager.shared.availableFontFamilies.filter { family in + guard let font = NSFont(name: family, size: 12) else { return false } + return font.isFixedPitch + || font.fontDescriptor.symbolicTraits.contains(.monoSpace) + } + .sorted() + } + + /// Display name for the font picker — shows "System Monospaced" when no custom font is set. + private var fontDisplayName: String { + terminalFontFamily.isEmpty ? "System Monospaced" : terminalFontFamily + } + private var cursorStyleBinding: Binding { Binding( get: { TerminalCursorStyleOption(rawValue: terminalCursorStyle) ?? .blinkBlock }, @@ -7279,6 +7389,20 @@ struct TerminalSettings: View { // MARK: Appearance Section { + Picker("Font family", selection: $terminalFontFamily) { + Text("System Monospaced").tag("") + Divider() + ForEach(monospacedFontFamilies, id: \.self) { family in + Text(family) + .font(.custom(family, size: 13)) + .tag(family) + } + } + .onChange(of: terminalFontFamily) { _, newValue in + terminalManager.applyFontFamily(newValue) + } + .settingsHighlight(id: highlightID("Font family")) + VStack(alignment: .leading, spacing: 8) { HStack { Text("Font size") diff --git a/DynamicIsland/helpers/FullDiskAccessPermissionStore.swift b/DynamicIsland/helpers/FullDiskAccessPermissionStore.swift index a891f1e..adab59d 100644 --- a/DynamicIsland/helpers/FullDiskAccessPermissionStore.swift +++ b/DynamicIsland/helpers/FullDiskAccessPermissionStore.swift @@ -53,6 +53,53 @@ enum FullDiskAccessAuthorization { } } +enum ShelfFolderAccessAuthorization { + private static var documentsDirectoryURL: URL? { + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + } + + private static var downloadsDirectoryURL: URL? { + FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + } + + static func hasDocumentsAccess() -> Bool { + canReadDirectory(at: documentsDirectoryURL) + } + + static func hasDownloadsAccess() -> Bool { + canReadDirectory(at: downloadsDirectoryURL) + } + + static func hasDocumentsAndDownloadsAccess() -> Bool { + hasDocumentsAccess() && hasDownloadsAccess() + } + + static func requestAccessProbe() { + if let documentsDirectoryURL { + _ = try? FileManager.default.contentsOfDirectory(at: documentsDirectoryURL, includingPropertiesForKeys: nil) + } + + if let downloadsDirectoryURL { + _ = try? FileManager.default.contentsOfDirectory(at: downloadsDirectoryURL, includingPropertiesForKeys: nil) + } + } + + private static func canReadDirectory(at url: URL?) -> Bool { + guard let url else { return false } + + do { + _ = try FileManager.default.contentsOfDirectory( + at: url, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + return true + } catch { + return false + } + } +} + @MainActor final class FullDiskAccessPermissionStore: ObservableObject { static let shared = FullDiskAccessPermissionStore() @@ -128,3 +175,83 @@ final class FullDiskAccessPermissionStore: ObservableObject { isAuthorized = newValue } } + +@MainActor +final class ShelfFolderAccessPermissionStore: ObservableObject { + static let shared = ShelfFolderAccessPermissionStore() + + @Published private(set) var hasDocumentsAccess: Bool = ShelfFolderAccessAuthorization.hasDocumentsAccess() + @Published private(set) var hasDownloadsAccess: Bool = ShelfFolderAccessAuthorization.hasDownloadsAccess() + + var hasDocumentsAndDownloadsAccess: Bool { + hasDocumentsAccess && hasDownloadsAccess + } + + private var pollingTask: Task? + + private init() {} + + deinit { + pollingTask?.cancel() + } + + func refreshStatus() { + updateStatus( + documents: ShelfFolderAccessAuthorization.hasDocumentsAccess(), + downloads: ShelfFolderAccessAuthorization.hasDownloadsAccess() + ) + } + + func requestAccessPrompt() { + ShelfFolderAccessAuthorization.requestAccessProbe() + beginPollingForStatusChanges() + } + + func openSystemSettings() { +#if os(macOS) + let urls = [ + "x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders", + "x-apple.systempreferences:com.apple.preference.security" + ] + + for candidate in urls { + guard let url = URL(string: candidate) else { continue } + if NSWorkspace.shared.open(url) { + return + } + } +#endif + } + + private func beginPollingForStatusChanges() { + pollingTask?.cancel() + pollingTask = Task { [weak self] in + guard let self else { return } + + for _ in 0..<40 { + try? await Task.sleep(nanoseconds: 500_000_000) + + let docs = ShelfFolderAccessAuthorization.hasDocumentsAccess() + let downloads = ShelfFolderAccessAuthorization.hasDownloadsAccess() + + await MainActor.run { + self.updateStatus(documents: docs, downloads: downloads) + } + + if docs && downloads { + break + } + } + } + } + + private func updateStatus(documents: Bool, downloads: Bool) { + if hasDocumentsAccess != documents { + hasDocumentsAccess = documents + } + + if hasDownloadsAccess != downloads { + hasDownloadsAccess = downloads + } + } +} diff --git a/DynamicIsland/managers/BetterDisplayManager.swift b/DynamicIsland/managers/BetterDisplayManager.swift index 6b66b2b..c421570 100644 --- a/DynamicIsland/managers/BetterDisplayManager.swift +++ b/DynamicIsland/managers/BetterDisplayManager.swift @@ -115,6 +115,14 @@ final class BetterDisplayManager: ObservableObject { private var cancellables = Set() private weak var coordinator: DynamicIslandViewCoordinator? + private var isBetterDisplayIntegrationEnabled: Bool { + Defaults[.enableThirdPartyDDCIntegration] && Defaults[.thirdPartyDDCProvider] == .betterDisplay + } + + private var isExternalVolumeListenerEnabled: Bool { + isBetterDisplayIntegrationEnabled && Defaults[.enableExternalVolumeControlListener] + } + private init() { isDetected = Self.checkInstallation() isRunning = Self.checkRunning() @@ -128,11 +136,7 @@ final class BetterDisplayManager: ObservableObject { /// Configure with the view coordinator for HUD dispatch. func configure(coordinator: DynamicIslandViewCoordinator) { self.coordinator = coordinator - - // Start listening if integration is enabled and BetterDisplay is running - if Defaults[.enableBetterDisplayIntegration] && isRunning { - startListening() - } + refreshListeningState() } /// Refresh detection status (e.g. after app install/uninstall). @@ -141,11 +145,13 @@ final class BetterDisplayManager: ObservableObject { let wasRunning = isRunning isDetected = Self.checkInstallation() isRunning = Self.checkRunning() - if wasRunning && !isRunning { - stopListening() - } else if !wasRunning && isRunning && Defaults[.enableBetterDisplayIntegration] { - startListening() + if wasDetected != isDetected { + NSLog("📺 BetterDisplay detection changed: detected=\(isDetected)") } + if wasRunning != isRunning { + NSLog("📺 BetterDisplay running state changed: running=\(isRunning)") + } + refreshListeningState() } // MARK: - Detection @@ -199,7 +205,7 @@ final class BetterDisplayManager: ObservableObject { // MARK: - OSD Handling private func handleOSDNotification(_ notification: Notification) { - guard Defaults[.enableBetterDisplayIntegration], isRunning else { return } + guard isBetterDisplayIntegrationEnabled, isRunning else { return } guard let notificationString = notification.object as? String else { NSLog("⚠️ BetterDisplay OSD: unexpected notification format") @@ -213,6 +219,16 @@ final class BetterDisplayManager: ObservableObject { BetterDisplayOSDNotification.self, from: Data(notificationString.utf8) ) + let displayIDText = osd.displayID.map { String($0) } ?? "nil" + let targetText = osd.controlTarget ?? "nil" + let iconIDText = osd.systemIconID.map { String($0) } ?? "nil" + let valueText = osd.value.map { String($0) } ?? "nil" + let maxValueText = osd.maxValue.map { String($0) } ?? "nil" + let symbolText = osd.customSymbol ?? "nil" + let textValue = osd.text ?? "nil" + NSLog( + "📺 BetterDisplay decoded payload: displayID=\(displayIDText) target=\(targetText) iconID=\(iconIDText) value=\(valueText) maxValue=\(maxValueText) customSymbol=\(symbolText) text=\(textValue)" + ) routeOSDToHUD(osd) } catch { NSLog("⚠️ BetterDisplay OSD decode error: \(error.localizedDescription)") @@ -224,9 +240,20 @@ final class BetterDisplayManager: ObservableObject { let category = classifyControlTarget(osd.controlTarget, systemIconID: osd.systemIconID) let normalizedValue = normalizeValue(osd.value, maxValue: osd.maxValue) let targetScreen = resolveScreen(for: osd.displayID) - let isExternalDisplay = isExternal(displayID: osd.displayID) + let isExternalDisplay = isExternal(displayID: osd.displayID, resolvedScreen: targetScreen) + let inferredMute = osd.controlTarget == "mute" || osd.systemIconID == 4 + let hasVolumeData = category == .volume && osd.value != nil + let externalVolumeListenerEnabled = isExternalVolumeListenerEnabled + let targetText = osd.controlTarget ?? "nil" + let displayIDText = osd.displayID.map { String($0) } ?? "nil" + let resolvedScreenText = targetScreen?.localizedName ?? "nil" + let valueText = osd.value.map { String($0) } ?? "nil" + let maxValueText = osd.maxValue.map { String($0) } ?? "nil" + let normalizedText = String(format: "%.3f", normalizedValue) - NSLog("📺 BetterDisplay OSD: target=\(osd.controlTarget ?? "nil") displayID=\(osd.displayID.map(String.init) ?? "nil") resolvedScreen=\(targetScreen?.localizedName ?? "nil") isExternal=\(isExternalDisplay) value=\(osd.value ?? -1)") + NSLog( + "📺 BetterDisplay routed payload: category=\(categoryName(category)) target=\(targetText) displayID=\(displayIDText) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternalDisplay) rawValue=\(valueText) maxValue=\(maxValueText) normalized=\(normalizedText) hasVolumeData=\(hasVolumeData) inferredMute=\(inferredMute) externalVolumeListener=\(externalVolumeListenerEnabled)" + ) switch category { case .brightness: @@ -234,6 +261,10 @@ final class BetterDisplayManager: ObservableObject { dispatchBrightnessHUD(value: normalizedValue, customSymbol: icon, onScreen: targetScreen) case .volume: + guard externalVolumeListenerEnabled else { + NSLog("📺 BetterDisplay volume payload ignored because external volume listener is disabled") + return + } let isMuted = osd.controlTarget == "mute" || osd.systemIconID == 4 dispatchVolumeHUD(value: normalizedValue, isMuted: isMuted, onScreen: targetScreen) @@ -266,7 +297,8 @@ final class BetterDisplayManager: ObservableObject { status: true, type: .volume, value: value, - icon: "" + icon: "", + onScreen: targetScreen ) } } @@ -289,7 +321,8 @@ final class BetterDisplayManager: ObservableObject { status: true, type: .brightness, value: value, - icon: icon + icon: icon, + onScreen: targetScreen ) } } @@ -302,28 +335,50 @@ final class BetterDisplayManager: ObservableObject { NSLog("📺 BetterDisplay resolveScreen: displayID is nil, falling back to all screens") return nil } + + if let directDisplayID = UInt32(exactly: displayID), + let matchedScreen = NSScreen.screens.first(where: { screenNumber(for: $0) == directDisplayID }) { + return matchedScreen + } + + // Fallback for payloads that use 1-based display indexes (1, 2, ...) + if displayID > 0 { + let index = displayID - 1 + if NSScreen.screens.indices.contains(index) { + let fallback = NSScreen.screens[index] + NSLog("📺 BetterDisplay resolveScreen: displayID=\(displayID) resolved via index fallback to '\(fallback.localizedName)'") + return fallback + } + } + let target = UInt32(displayID) let availableScreens = NSScreen.screens.map { screen -> (String, UInt32) in let num = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0 return (screen.localizedName, num) } NSLog("📺 BetterDisplay resolveScreen: looking for displayID=\(displayID) (UInt32=\(target)) among screens: \(availableScreens)") - let matched = NSScreen.screens.first { screen in - guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { - return false - } - return screenNumber.uint32Value == target - } - if matched == nil { - NSLog("📺 BetterDisplay resolveScreen: no match found for displayID=\(displayID), HUD will show on all screens") - } - return matched + NSLog("📺 BetterDisplay resolveScreen: no match found for displayID=\(displayID), HUD will show on all screens") + return nil } /// Whether the given displayID refers to an external (non-built-in) display. - private func isExternal(displayID: Int?) -> Bool { - guard let displayID else { return false } - return CGDisplayIsBuiltin(UInt32(displayID)) == 0 + private func isExternal(displayID: Int?, resolvedScreen: NSScreen?) -> Bool { + if let resolvedScreen, + let resolvedDisplayID = screenNumber(for: resolvedScreen) { + return CGDisplayIsBuiltin(resolvedDisplayID) == 0 + } + guard let displayID, + let displayIDValue = UInt32(exactly: displayID) else { + return false + } + return CGDisplayIsBuiltin(displayIDValue) == 0 + } + + private func screenNumber(for screen: NSScreen) -> UInt32? { + guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { + return nil + } + return screenNumber.uint32Value } private func classifyControlTarget(_ target: String?, systemIconID: Int?) -> BetterDisplayControlCategory { @@ -341,6 +396,17 @@ final class BetterDisplayManager: ObservableObject { } } + private func categoryName(_ category: BetterDisplayControlCategory) -> String { + switch category { + case .brightness: + return "brightness" + case .volume: + return "volume" + case .other: + return "other" + } + } + /// Normalize a BetterDisplay value (0...maxValue) to 0...1 range. private func normalizeValue(_ value: Double?, maxValue: Double?) -> CGFloat { guard let value else { return 0 } @@ -464,7 +530,7 @@ final class BetterDisplayManager: ObservableObject { Task { @MainActor in NSLog("🔴 BetterDisplay terminated (workspace notification)") self?.isRunning = false - self?.stopListening() + self?.refreshListeningState() } } } @@ -483,9 +549,7 @@ final class BetterDisplayManager: ObservableObject { NSLog("🟢 BetterDisplay launched notification received") self?.isDetected = true self?.isRunning = true - if Defaults[.enableBetterDisplayIntegration] { - self?.startListening() - } + self?.refreshListeningState() } } @@ -497,23 +561,34 @@ final class BetterDisplayManager: ObservableObject { Task { @MainActor in NSLog("🔴 BetterDisplay terminated notification received") self?.isRunning = false - self?.stopListening() + self?.refreshListeningState() } } } // MARK: - Settings Observer + private func refreshListeningState() { + if isBetterDisplayIntegrationEnabled && isRunning { + startListening() + } else { + stopListening() + } + } + private func setupSettingsObserver() { - Defaults.publisher(.enableBetterDisplayIntegration, options: []) - .sink { [weak self] change in + Defaults.publisher(.enableThirdPartyDDCIntegration, options: []) + .sink { [weak self] _ in Task { @MainActor in - guard let self else { return } - if change.newValue && self.isRunning { - self.startListening() - } else { - self.stopListening() - } + self?.refreshListeningState() + } + } + .store(in: &cancellables) + + Defaults.publisher(.thirdPartyDDCProvider, options: []) + .sink { [weak self] _ in + Task { @MainActor in + self?.refreshListeningState() } } .store(in: &cancellables) diff --git a/DynamicIsland/managers/Extensions/ExtensionLiveActivityManager.swift b/DynamicIsland/managers/Extensions/ExtensionLiveActivityManager.swift index 8f43953..4c833b8 100644 --- a/DynamicIsland/managers/Extensions/ExtensionLiveActivityManager.swift +++ b/DynamicIsland/managers/Extensions/ExtensionLiveActivityManager.swift @@ -128,6 +128,7 @@ final class ExtensionLiveActivityManager: ObservableObject { if previousCount != activeActivities.count { Logger.log("Dismissed extension live activity \(activityID) from \(bundleIdentifier)", category: .extensions) ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: activityID) + ExtensionRPCServer.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: activityID) logDiagnostics("Removed live activity \(activityID) for \(bundleIdentifier); remaining: \(activeActivities.count)") broadcastSnapshot() } @@ -140,7 +141,10 @@ final class ExtensionLiveActivityManager: ObservableObject { withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) { activeActivities.removeAll { $0.bundleIdentifier == bundleIdentifier } } - ids.forEach { ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) } + ids.forEach { + ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) + ExtensionRPCServer.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) + } if !ids.isEmpty { logDiagnostics("Removed all live activities for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))") broadcastSnapshot() diff --git a/DynamicIsland/managers/Extensions/ExtensionLockScreenWidgetManager.swift b/DynamicIsland/managers/Extensions/ExtensionLockScreenWidgetManager.swift index 372669e..7cbdcab 100644 --- a/DynamicIsland/managers/Extensions/ExtensionLockScreenWidgetManager.swift +++ b/DynamicIsland/managers/Extensions/ExtensionLockScreenWidgetManager.swift @@ -118,6 +118,7 @@ final class ExtensionLockScreenWidgetManager: ObservableObject { if previousCount != activeWidgets.count { Logger.log("Dismissed extension widget \(widgetID) from \(bundleIdentifier)", category: .extensions) ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: widgetID) + ExtensionRPCServer.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: widgetID) logDiagnostics("Removed lock screen widget \(widgetID) for \(bundleIdentifier); remaining: \(activeWidgets.count)") broadcastSnapshot() } @@ -128,7 +129,10 @@ final class ExtensionLockScreenWidgetManager: ObservableObject { .filter { $0.bundleIdentifier == bundleIdentifier } .map { $0.descriptor.id } activeWidgets.removeAll { $0.bundleIdentifier == bundleIdentifier } - ids.forEach { ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: $0) } + ids.forEach { + ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: $0) + ExtensionRPCServer.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: $0) + } if !ids.isEmpty { logDiagnostics("Removed all lock screen widgets for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))") broadcastSnapshot() diff --git a/DynamicIsland/managers/Extensions/ExtensionNotchExperienceManager.swift b/DynamicIsland/managers/Extensions/ExtensionNotchExperienceManager.swift index f4a7117..28d1cf7 100644 --- a/DynamicIsland/managers/Extensions/ExtensionNotchExperienceManager.swift +++ b/DynamicIsland/managers/Extensions/ExtensionNotchExperienceManager.swift @@ -137,6 +137,7 @@ final class ExtensionNotchExperienceManager: ObservableObject { guard previousCount != activeExperiences.count else { return } Logger.log("Dismissed notch experience \(experienceID) from \(bundleIdentifier)", category: .extensions) ExtensionXPCServiceHost.shared.notifyNotchExperienceDismiss(bundleIdentifier: bundleIdentifier, experienceID: experienceID) + ExtensionRPCServer.shared.notifyNotchExperienceDismiss(bundleIdentifier: bundleIdentifier, experienceID: experienceID) logDiagnostics("Removed notch experience \(experienceID) for \(bundleIdentifier); remaining: \(activeExperiences.count)") broadcastSnapshot() } @@ -148,7 +149,10 @@ final class ExtensionNotchExperienceManager: ObservableObject { withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) { activeExperiences.removeAll { $0.bundleIdentifier == bundleIdentifier } } - ids.forEach { ExtensionXPCServiceHost.shared.notifyNotchExperienceDismiss(bundleIdentifier: bundleIdentifier, experienceID: $0) } + ids.forEach { + ExtensionXPCServiceHost.shared.notifyNotchExperienceDismiss(bundleIdentifier: bundleIdentifier, experienceID: $0) + ExtensionRPCServer.shared.notifyNotchExperienceDismiss(bundleIdentifier: bundleIdentifier, experienceID: $0) + } if !ids.isEmpty { logDiagnostics("Removed all notch experiences for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))") broadcastSnapshot() diff --git a/DynamicIsland/managers/IdleAnimationManager.swift b/DynamicIsland/managers/IdleAnimationManager.swift index 3bb02cc..ed1d4f6 100644 --- a/DynamicIsland/managers/IdleAnimationManager.swift +++ b/DynamicIsland/managers/IdleAnimationManager.swift @@ -39,19 +39,10 @@ class IdleAnimationManager { // MARK: - Initialization - /// Load bundled animations from the LottieAnimations folder and built-in face + /// Load bundled animations from the LottieAnimations folder func initializeDefaultAnimations() { var animations: [CustomIdleAnimation] = [] - // Add built-in face as first option - let builtInFace = CustomIdleAnimation( - name: "Classic Face", - source: .builtInFace, - speed: 1.0, - isBuiltIn: true - ) - animations.append(builtInFace) - // Load bundled Lottie files if let bundledAnimations = loadBundledAnimations() { animations.append(contentsOf: bundledAnimations) @@ -60,14 +51,21 @@ class IdleAnimationManager { // Get existing animations var existing = Defaults[.customIdleAnimations] + // Remove any legacy builtInFace entries that may be stored from older versions + existing.removeAll { animation in + if case .lottieFile(let url) = animation.source, url.absoluteString == "builtin://face" { + return true + } + return false + } + if existing.isEmpty { // First launch - set everything Defaults[.customIdleAnimations] = animations - Defaults[.selectedIdleAnimation] = builtInFace + Defaults[.selectedIdleAnimation] = animations.first print("✅ [IdleAnimationManager] First launch: Initialized with \(animations.count) animations") } else { // Subsequent launch - ensure all bundled animations are present - let existingIDs = Set(existing.map { $0.id }) let existingNames = Set(existing.filter { $0.isBuiltIn }.map { $0.name }) // Add any missing bundled animations @@ -79,7 +77,23 @@ class IdleAnimationManager { } Defaults[.customIdleAnimations] = existing - print("✅ [IdleAnimationManager] Subsequent launch: \(existing.count) total animations (\(animations.count - 1) bundled + built-in face)") + + // If current selection was the old built-in face (no longer valid), select first available + if let selected = Defaults[.selectedIdleAnimation] { + let isValid: Bool + switch selected.source { + case .lottieFile(let url): + isValid = url.absoluteString != "builtin://face" + case .lottieURL: + isValid = true + } + if !isValid { + Defaults[.selectedIdleAnimation] = existing.first + print("🔄 [IdleAnimationManager] Migrated selection from legacy face to: \(existing.first?.name ?? "nil")") + } + } + + print("✅ [IdleAnimationManager] Subsequent launch: \(existing.count) total animations") } } diff --git a/DynamicIsland/managers/LunarManager.swift b/DynamicIsland/managers/LunarManager.swift new file mode 100644 index 0000000..e2e00fb --- /dev/null +++ b/DynamicIsland/managers/LunarManager.swift @@ -0,0 +1,574 @@ +/* + * Atoll (DynamicIsland) + * Copyright (C) 2024-2026 Atoll Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@preconcurrency import Foundation +import AppKit +import Combine +import Defaults +import Network + +// MARK: - Lunar Display Data Model + +/// Matches the JSON structure streamed by Lunar's socket listener. +struct LunarDisplayData: Codable { + var brightness: Float? + var contrast: Float? + var volume: Float? + var mute: Bool? + var nits: Float? + var display: Int +} + +// MARK: - Lunar Control Category + +enum LunarControlCategory { + case brightness + case contrast + case volume +} + +// MARK: - Lunar Manager + +/// Manages integration with the Lunar app (fyi.lunar.Lunar). +/// +/// Responsibilities: +/// - Detect whether Lunar is installed and running +/// - Observe app launch / termination to connect / disconnect automatically +/// - Connect to Lunar's TCP socket on localhost:23803 and listen for DDC changes +/// - Route brightness / contrast / volume events to Atoll's HUD pipeline +@MainActor +final class LunarManager: ObservableObject { + static let shared = LunarManager() + + /// Bundle identifier of the Lunar app. + nonisolated static let bundleID = "fyi.lunar.Lunar" + + /// UserDefaults suite used by Lunar to store its API key. + private static let lunarDomain = "fyi.lunar.Lunar" + private static let apiKeyKey = "apiKey" + + // MARK: Published state + + /// Whether Lunar is currently detected (installed) on this machine. + @Published private(set) var isDetected: Bool = false + + /// Whether Lunar is currently running. + @Published private(set) var isRunning: Bool = false + + /// Whether the TCP socket connection to Lunar is active. + @Published private(set) var isConnected: Bool = false + + // MARK: Private + + private var connection: NWConnection? + private var workspaceLaunchObserver: NSObjectProtocol? + private var workspaceTermObserver: NSObjectProtocol? + private var cancellables = Set() + private weak var coordinator: DynamicIslandViewCoordinator? + + /// Reconnection back-off state. + private var reconnectTask: Task? + private var reconnectAttempts: Int = 0 + private static let maxReconnectAttempts = 5 + private static let baseReconnectDelay: UInt64 = 2_000_000_000 // 2 seconds + + private var isLunarIntegrationEnabled: Bool { + Defaults[.enableThirdPartyDDCIntegration] && Defaults[.thirdPartyDDCProvider] == .lunar + } + + private var isExternalVolumeListenerEnabled: Bool { + isLunarIntegrationEnabled && Defaults[.enableExternalVolumeControlListener] + } + + private init() { + isDetected = Self.checkInstallation() + isRunning = Self.checkRunning() + setupWorkspaceObservers() + setupSettingsObserver() + } + + // MARK: - Public API + + /// Configure with the view coordinator for HUD dispatch. + func configure(coordinator: DynamicIslandViewCoordinator) { + self.coordinator = coordinator + refreshConnectionState() + } + + /// Called when Atoll is about to quit so Lunar's native OSD is restored. + func appWillTerminate() { + setLunarHideOSD(false) + } + + /// Refresh detection status (e.g. after manual check from Settings). + func refreshDetectionStatus() { + let wasRunning = isRunning + isDetected = Self.checkInstallation() + isRunning = Self.checkRunning() + if wasRunning != isRunning { + NSLog("🌙 Lunar running state changed: running=\(isRunning)") + } + refreshConnectionState() + } + + // MARK: - Detection + + /// Check if Lunar is installed by looking for its bundle ID. + nonisolated static func checkInstallation() -> Bool { + if NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) { + return true + } + if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + return FileManager.default.fileExists(atPath: url.path) + } + return false + } + + /// Check if Lunar is currently running. + nonisolated static func checkRunning() -> Bool { + NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) + } + + // MARK: - Socket Connection + + private func connectToLunar() { + guard connection == nil else { return } + guard let apiKey = Self.retrieveAPIKey() else { + NSLog("⚠️ Lunar: failed to retrieve API key from UserDefaults suite '\(Self.lunarDomain)'") + return + } + + setLunarHideOSD(true) + + let host = NWEndpoint.Host("localhost") + let port = NWEndpoint.Port(23803) + let conn = NWConnection(host: host, port: port, using: .tcp) + + conn.stateUpdateHandler = { [weak self] state in + Task { @MainActor in + self?.handleConnectionState(state, apiKey: apiKey) + } + } + + connection = conn + conn.start(queue: DispatchQueue.global(qos: .utility)) + NSLog("🌙 Lunar: connecting to socket on \(host):\(port)…") + } + + private func disconnect() { + reconnectTask?.cancel() + reconnectTask = nil + reconnectAttempts = 0 + + if let conn = connection { + conn.stateUpdateHandler = nil + conn.cancel() + connection = nil + } + isConnected = false + setLunarHideOSD(false) + NSLog("⏹ Lunar: socket disconnected") + } + + private func handleConnectionState(_ state: NWConnection.State, apiKey: String) { + switch state { + case .ready: + isConnected = true + reconnectAttempts = 0 + NSLog("🌙 Lunar: connected to socket") + sendListenCommand(apiKey: apiKey) + receiveMessages() + + case .failed(let error): + NSLog("🌙 Lunar: connection failed — \(error.localizedDescription)") + isConnected = false + connection?.cancel() + connection = nil + scheduleReconnect() + + case .cancelled: + isConnected = false + + case .waiting(let error): + NSLog("🌙 Lunar: connection waiting — \(error.localizedDescription)") + + default: + break + } + } + + /// Send Lunar's listen command with the API key. + private func sendListenCommand(apiKey: String) { + guard let conn = connection else { return } + + let separator = String(UnicodeScalar(0x01)) + let message = [apiKey, "listen", "--json", "--only-user-adjustments"] + .joined(separator: separator) + + guard let data = message.data(using: .utf8) else { + NSLog("⚠️ Lunar: failed to encode listen command") + return + } + + conn.send(content: data, completion: .contentProcessed { error in + if let error { + NSLog("⚠️ Lunar: failed to send listen command — \(error.localizedDescription)") + } else { + NSLog("🌙 Lunar: listen command sent") + } + }) + } + + /// Receive JSON messages from Lunar's socket in a loop. + private func receiveMessages() { + guard let conn = connection else { return } + + conn.receive(minimumIncompleteLength: 1, maximumLength: 4096) { [weak self] data, _, isComplete, error in + if let data, !data.isEmpty { + Task { @MainActor in + self?.handleReceivedData(data) + } + } + + if isComplete { + NSLog("🌙 Lunar: stream ended") + Task { @MainActor in + self?.isConnected = false + self?.connection?.cancel() + self?.connection = nil + self?.scheduleReconnect() + } + } else if let error { + NSLog("⚠️ Lunar: receive error — \(error.localizedDescription)") + Task { @MainActor in + self?.isConnected = false + self?.connection?.cancel() + self?.connection = nil + self?.scheduleReconnect() + } + } else { + // Continue reading + Task { @MainActor in + self?.receiveMessages() + } + } + } + } + + private func handleReceivedData(_ data: Data) { + guard isLunarIntegrationEnabled, isRunning else { return } + + do { + let displayData = try JSONDecoder().decode(LunarDisplayData.self, from: data) + logDecodedPayload(displayData, source: "socket") + routeToHUD(displayData) + } catch { + // Lunar may send multi-line JSON or partial data; try splitting by newlines + let rawString = String(data: data, encoding: .utf8) ?? "" + let lines = rawString.components(separatedBy: .newlines).filter { !$0.isEmpty } + for line in lines { + guard let lineData = line.data(using: .utf8) else { continue } + if let displayData = try? JSONDecoder().decode(LunarDisplayData.self, from: lineData) { + logDecodedPayload(displayData, source: "socket-line") + routeToHUD(displayData) + } + } + } + } + + // MARK: - HUD Routing + + private func routeToHUD(_ data: LunarDisplayData) { + let targetScreen = resolveScreen(for: data.display) + let isExternal = isExternalDisplay(data.display, resolvedScreen: targetScreen) + let hasVolumeData = data.volume != nil || data.mute != nil + let externalVolumeListenerEnabled = isExternalVolumeListenerEnabled + let resolvedScreenText = targetScreen?.localizedName ?? "nil" + let brightnessText = data.brightness.map { String($0) } ?? "nil" + let contrastText = data.contrast.map { String($0) } ?? "nil" + let volumeText = data.volume.map { String($0) } ?? "nil" + let muteText = data.mute.map { String($0) } ?? "nil" + + NSLog( + "🌙 Lunar routed payload: display=\(data.display) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternal) brightness=\(brightnessText) contrast=\(contrastText) volume=\(volumeText) mute=\(muteText) hasVolumeData=\(hasVolumeData) externalVolumeListener=\(externalVolumeListenerEnabled)" + ) + + // Nits-only events are informational (no user-facing HUD needed). + let hasActionableChange = data.brightness != nil || data.contrast != nil || data.volume != nil || data.mute != nil + guard hasActionableChange else { return } + + // Brightness — values arrive in 0…1 range from Lunar + if let brightness = data.brightness { + let value = CGFloat(brightness) + let icon = isExternal ? "display" : "" + dispatchBrightnessHUD(value: value, icon: icon, onScreen: targetScreen) + } + + // Contrast — show as brightness-style HUD with a contrast icon + if let contrast = data.contrast { + let value = CGFloat(contrast) + dispatchBrightnessHUD(value: value, icon: "circle.righthalf.filled", onScreen: targetScreen) + } + + // Volume + if externalVolumeListenerEnabled { + if let volume = data.volume { + let value = CGFloat(volume) + let isMuted = data.mute ?? false + dispatchVolumeHUD(value: value, isMuted: isMuted, onScreen: targetScreen) + } else if let mute = data.mute { + // Mute toggle without a volume value + dispatchVolumeHUD(value: mute ? 0 : 1, isMuted: mute, onScreen: targetScreen) + } + } else if hasVolumeData { + NSLog("🌙 Lunar volume payload ignored because external volume listener is disabled") + } + } + + // MARK: - HUD Dispatch (mirrors BetterDisplayManager / SystemChangesObserver logic) + + private func dispatchVolumeHUD(value: CGFloat, isMuted: Bool, onScreen targetScreen: NSScreen? = nil) { + if HUDSuppressionCoordinator.shared.shouldSuppressVolumeHUD { return } + + if Defaults[.enableCircularHUD] { + CircularHUDWindowManager.shared.show(type: .volume, value: value, onScreen: targetScreen) + return + } + if Defaults[.enableVerticalHUD] { + VerticalHUDWindowManager.shared.show(type: .volume, value: value, icon: "", onScreen: targetScreen) + return + } + if Defaults[.enableCustomOSD] && Defaults[.enableOSDVolume] { + CustomOSDWindowManager.shared.showVolume(value: value, onScreen: targetScreen) + } + if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] { + coordinator?.toggleSneakPeek( + status: true, + type: .volume, + value: value, + icon: "", + onScreen: targetScreen + ) + } + } + + private func dispatchBrightnessHUD(value: CGFloat, icon: String = "", onScreen targetScreen: NSScreen? = nil) { + if Defaults[.enableCircularHUD] { + CircularHUDWindowManager.shared.show(type: .brightness, value: value, icon: icon, onScreen: targetScreen) + return + } + if Defaults[.enableVerticalHUD] { + VerticalHUDWindowManager.shared.show(type: .brightness, value: value, icon: icon, onScreen: targetScreen) + return + } + if Defaults[.enableCustomOSD] && Defaults[.enableOSDBrightness] { + CustomOSDWindowManager.shared.showBrightness(value: value, icon: icon, onScreen: targetScreen) + } + if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] { + coordinator?.toggleSneakPeek( + status: true, + type: .brightness, + value: value, + icon: icon, + onScreen: targetScreen + ) + } + } + + // MARK: - Helpers + + /// Retrieve the Lunar API key from its UserDefaults suite. + nonisolated private static func retrieveAPIKey() -> String? { + UserDefaults(suiteName: lunarDomain)?.string(forKey: apiKeyKey) + } + + /// Resolve a Lunar display ID (CGDirectDisplayID) to the matching NSScreen. + private func resolveScreen(for displayID: Int) -> NSScreen? { + if let directDisplayID = UInt32(exactly: displayID), + let matchedScreen = NSScreen.screens.first(where: { screenNumber(for: $0) == directDisplayID }) { + return matchedScreen + } + + // Fallback for integrations that send 1-based display indexes (e.g. 1, 2) + if displayID > 0 { + let index = displayID - 1 + if NSScreen.screens.indices.contains(index) { + let fallback = NSScreen.screens[index] + NSLog("🌙 Lunar: display=\(displayID) resolved via index fallback to '\(fallback.localizedName)'") + return fallback + } + } + + NSLog("🌙 Lunar: unable to resolve display=\(displayID), HUD will show on all screens") + return nil + } + + /// Whether the given displayID refers to an external (non-built-in) display. + private func isExternalDisplay(_ displayID: Int, resolvedScreen: NSScreen?) -> Bool { + if let resolvedScreen, + let resolvedDisplayID = screenNumber(for: resolvedScreen) { + return CGDisplayIsBuiltin(resolvedDisplayID) == 0 + } + if let displayIDValue = UInt32(exactly: displayID) { + return CGDisplayIsBuiltin(displayIDValue) == 0 + } + return false + } + + private func screenNumber(for screen: NSScreen) -> UInt32? { + guard let num = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { + return nil + } + return num.uint32Value + } + + private func logDecodedPayload(_ data: LunarDisplayData, source: String) { + let hasVolumeData = data.volume != nil || data.mute != nil + let brightnessText = data.brightness.map { String($0) } ?? "nil" + let contrastText = data.contrast.map { String($0) } ?? "nil" + let volumeText = data.volume.map { String($0) } ?? "nil" + let muteText = data.mute.map { String($0) } ?? "nil" + NSLog( + "🌙 Lunar decoded payload (\(source)): display=\(data.display) brightness=\(brightnessText) contrast=\(contrastText) volume=\(volumeText) mute=\(muteText) hasVolumeData=\(hasVolumeData)" + ) + } + + // MARK: - Reconnection + + private func scheduleReconnect() { + guard isLunarIntegrationEnabled, isRunning else { return } + guard reconnectAttempts < Self.maxReconnectAttempts else { + NSLog("🌙 Lunar: max reconnect attempts reached, giving up") + return + } + + reconnectTask?.cancel() + let attempt = reconnectAttempts + reconnectAttempts += 1 + + let delay = Self.baseReconnectDelay * UInt64(1 << min(attempt, 3)) // exponential back-off, max 16s + + reconnectTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: delay) + guard !Task.isCancelled, self.isRunning, self.isLunarIntegrationEnabled else { return } + NSLog("🌙 Lunar: reconnect attempt \(attempt + 1)/\(Self.maxReconnectAttempts)") + self.connectToLunar() + } + } + + // MARK: - Workspace Observers (detect Lunar launch / quit) + + private func setupWorkspaceObservers() { + let lunarBundleID = Self.bundleID + + workspaceLaunchObserver = NSWorkspace.shared.notificationCenter.addObserver( + forName: NSWorkspace.didLaunchApplicationNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, + app.bundleIdentifier == lunarBundleID + else { return } + Task { @MainActor in + NSLog("🟢 Lunar launched (pid: \(app.processIdentifier))") + self?.isDetected = true + self?.isRunning = true + self?.reconnectAttempts = 0 + if self?.isLunarIntegrationEnabled == true { + // Give Lunar a moment to start its socket server + try? await Task.sleep(nanoseconds: 1_500_000_000) + self?.connectToLunar() + } + } + } + + workspaceTermObserver = NSWorkspace.shared.notificationCenter.addObserver( + forName: NSWorkspace.didTerminateApplicationNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, + app.bundleIdentifier == lunarBundleID + else { return } + Task { @MainActor in + NSLog("🔴 Lunar terminated (pid: \(app.processIdentifier))") + self?.isRunning = false + self?.disconnect() + } + } + } + + // MARK: - Settings Observer + + private func refreshConnectionState() { + if isLunarIntegrationEnabled && isRunning { + connectToLunar() + } else { + disconnect() + } + } + + private func setupSettingsObserver() { + Defaults.publisher(.enableThirdPartyDDCIntegration, options: []) + .sink { [weak self] _ in + Task { @MainActor in + self?.refreshConnectionState() + } + } + .store(in: &cancellables) + + Defaults.publisher(.thirdPartyDDCProvider, options: []) + .sink { [weak self] _ in + Task { @MainActor in + self?.refreshConnectionState() + } + } + .store(in: &cancellables) + } + + deinit { + // Restore Lunar's native OSD synchronously on teardown + LunarManager.writeLunarDefault(hideOSD: false) + connection?.cancel() + reconnectTask?.cancel() + if let workspaceLaunchObserver { + NSWorkspace.shared.notificationCenter.removeObserver(workspaceLaunchObserver) + } + if let workspaceTermObserver { + NSWorkspace.shared.notificationCenter.removeObserver(workspaceTermObserver) + } + cancellables.removeAll() + } + + // MARK: - Lunar OSD Suppression + + /// Tell Lunar to hide (or show) its own native OSD by writing to its UserDefaults. + private func setLunarHideOSD(_ hide: Bool) { + Self.writeLunarDefault(hideOSD: hide) + } + + /// Synchronous, nonisolated helper so it can also be called from `deinit`. + nonisolated private static func writeLunarDefault(hideOSD hide: Bool) { + guard let defaults = UserDefaults(suiteName: lunarDomain) else { + NSLog("⚠️ Lunar: unable to open UserDefaults suite '\(lunarDomain)' to set hideOSD") + return + } + defaults.set(hide, forKey: "hideOSD") + NSLog("🌙 Lunar: hideOSD set to \(hide)") + } +} diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index c5edc0f..e1bfd36 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -630,15 +630,19 @@ class MusicManager: ObservableObject { func togglePlay() { guard let controller = activeController else { return } + let targetState = !isPlaying Task { await MainActor.run { - let newState = !isPlaying - pendingOptimisticPlayState = newState - applyPlayState(newState, animation: .smooth(duration: 0.18)) + pendingOptimisticPlayState = targetState + applyPlayState(targetState, animation: .smooth(duration: 0.18)) } - await controller.togglePlay() + if targetState { + await controller.play() + } else { + await controller.pause() + } } } diff --git a/DynamicIsland/managers/ScreenAssistantManager.swift b/DynamicIsland/managers/ScreenAssistantManager.swift index da0a0cd..9bf0c2c 100644 --- a/DynamicIsland/managers/ScreenAssistantManager.swift +++ b/DynamicIsland/managers/ScreenAssistantManager.swift @@ -662,12 +662,14 @@ class ScreenAssistantManager: NSObject, ObservableObject { return } - let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + var task: URLSessionDataTask? + task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in DispatchQueue.main.async { guard let self = self else { return } + guard let currentTask = task else { return } // Ensure this callback belongs to the current in-flight request - guard self.activeRequest === task else { return } + guard self.activeRequest === currentTask else { return } self.isLoading = false self.activeRequest = nil @@ -677,7 +679,7 @@ class ScreenAssistantManager: NSObject, ObservableObject { } activeRequest = task - task.resume() + task?.resume() } private func performOpenAIRequest(url: URL, requestBody: [String: Any], apiKey: String) { @@ -696,12 +698,14 @@ class ScreenAssistantManager: NSObject, ObservableObject { return } - let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + var task: URLSessionDataTask? + task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in DispatchQueue.main.async { guard let self = self else { return } + guard let currentTask = task else { return } // Ensure this callback belongs to the current in-flight request - guard self.activeRequest === task else { return } + guard self.activeRequest === currentTask else { return } self.isLoading = false self.activeRequest = nil @@ -711,7 +715,7 @@ class ScreenAssistantManager: NSObject, ObservableObject { } activeRequest = task - task.resume() + task?.resume() } private func performClaudeRequest(url: URL, requestBody: [String: Any], apiKey: String) { @@ -731,12 +735,14 @@ class ScreenAssistantManager: NSObject, ObservableObject { return } - let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + var task: URLSessionDataTask? + task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in DispatchQueue.main.async { guard let self = self else { return } + guard let currentTask = task else { return } // Ensure this callback belongs to the current in-flight request - guard self.activeRequest === task else { return } + guard self.activeRequest === currentTask else { return } self.isLoading = false self.activeRequest = nil @@ -746,7 +752,7 @@ class ScreenAssistantManager: NSObject, ObservableObject { } activeRequest = task - task.resume() + task?.resume() } // MARK: - Response Handlers diff --git a/DynamicIsland/managers/StatsManager.swift b/DynamicIsland/managers/StatsManager.swift index 8bcef43..7abe47e 100644 --- a/DynamicIsland/managers/StatsManager.swift +++ b/DynamicIsland/managers/StatsManager.swift @@ -414,14 +414,20 @@ class StatsManager: ObservableObject { private var delayedStopTimer: Timer? private var delayedStartTimer: Timer? private let maxHistoryPoints = 30 + /// Cached host port to avoid leaking Mach send rights. + /// Every call to `mach_host_self()` acquires a new send right that must be + /// explicitly deallocated; caching it once prevents port exhaustion over time. + private let hostPort: mach_port_t = mach_host_self() private let totalPhysicalMemory: UInt64 = { var stats = host_basic_info() var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + let initHostPort = mach_host_self() let result = withUnsafeMutablePointer(to: &stats) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { - host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count) + host_info(initHostPort, HOST_BASIC_INFO, $0, &count) } } + mach_port_deallocate(mach_task_self_, initHostPort) if result == KERN_SUCCESS { return UInt64(stats.max_mem) } @@ -786,7 +792,7 @@ class StatsManager: ObservableObject { let result = withUnsafeMutablePointer(to: &info) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { - host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &count) + host_statistics(hostPort, HOST_CPU_LOAD_INFO, $0, &count) } } @@ -858,7 +864,7 @@ class StatsManager: ObservableObject { let vmResult = withUnsafeMutablePointer(to: &vmStatistics) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { - host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &size) + host_statistics64(hostPort, HOST_VM_INFO64, $0, &size) } } @@ -978,7 +984,7 @@ class StatsManager: ObservableObject { var cpuInfo: processor_info_array_t? var numCpuInfo: mach_msg_type_number_t = 0 var numCPUsU: natural_t = 0 - let result = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuInfo, &numCpuInfo) + let result = host_processor_info(hostPort, PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuInfo, &numCpuInfo) guard result == KERN_SUCCESS, let cpuInfo else { return cpuCoreUsage } diff --git a/DynamicIsland/managers/SystemHUDManager.swift b/DynamicIsland/managers/SystemHUDManager.swift index efc7893..b7243e6 100644 --- a/DynamicIsland/managers/SystemHUDManager.swift +++ b/DynamicIsland/managers/SystemHUDManager.swift @@ -181,8 +181,22 @@ class SystemHUDManager { ) }.store(in: &cancellables) - // When BetterDisplay integration toggled, restart observer to update Cmd+Brightness key interception - Defaults.publisher(.enableBetterDisplayIntegration, options: []).sink { [weak self] _ in + // Restart observer when third-party DDC integration state changes. + Defaults.publisher(.enableThirdPartyDDCIntegration, options: []).sink { [weak self] _ in + guard let self = self, self.isSetupComplete else { return } + Task { @MainActor in + await self.startSystemObserver() + } + }.store(in: &cancellables) + + Defaults.publisher(.thirdPartyDDCProvider, options: []).sink { [weak self] _ in + guard let self = self, self.isSetupComplete else { return } + Task { @MainActor in + await self.startSystemObserver() + } + }.store(in: &cancellables) + + Defaults.publisher(.enableExternalVolumeControlListener, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete else { return } Task { @MainActor in await self.startSystemObserver() @@ -220,7 +234,7 @@ class SystemHUDManager { } } - /// Resolves the effective control flags, applying BetterDisplay overrides. + /// Resolves the effective control flags, applying third-party DDC overrides. private func resolvedControlFlags() -> (volume: Bool, brightness: Bool, backlight: Bool) { var volumeEnabled: Bool var brightnessEnabled: Bool @@ -240,11 +254,16 @@ class SystemHUDManager { keyboardBacklightEnabled = Defaults[.enableKeyboardBacklightHUD] } - // When BetterDisplay integration is on, stop intercepting brightness and - // Cmd+Brightness keys so BetterDisplay receives them and sends OSD notifications. - if Defaults[.enableBetterDisplayIntegration] { + // When third-party DDC integration is on, stop intercepting brightness and + // Cmd+Brightness keys so the external app receives them and sends DDC/OSD events. + if Defaults[.enableThirdPartyDDCIntegration] { brightnessEnabled = false keyboardBacklightEnabled = false + + // Optional: route volume exclusively from the selected external provider. + if Defaults[.enableExternalVolumeControlListener] { + volumeEnabled = false + } } return (volumeEnabled, brightnessEnabled, keyboardBacklightEnabled) @@ -273,7 +292,7 @@ class SystemHUDManager { // Force disable system HUD to ensure no duplicates SystemOSDManager.disableSystemHUD() - print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), BD: \(Defaults[.enableBetterDisplayIntegration]))") + print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), ThirdPartyDDC: \(Defaults[.enableThirdPartyDDCIntegration]), Provider: \(Defaults[.thirdPartyDDCProvider].displayName), ExternalVolumeListener: \(Defaults[.enableExternalVolumeControlListener]))") isSystemOperationInProgress = false } diff --git a/DynamicIsland/managers/TerminalManager.swift b/DynamicIsland/managers/TerminalManager.swift index 212c81b..de0aad7 100644 --- a/DynamicIsland/managers/TerminalManager.swift +++ b/DynamicIsland/managers/TerminalManager.swift @@ -181,15 +181,28 @@ class TerminalManager: ObservableObject { terminalTitle = title } + // MARK: - Font Resolution + + /// Resolves the terminal font from the user's chosen family and size. + /// + /// - If `family` is empty, returns the system monospaced font. + /// - Otherwise tries `NSFont(name:size:)` and falls back to system monospaced + /// when the name is invalid or the font is not installed. + private func resolveFont(family: String, size: CGFloat) -> NSFont { + if !family.isEmpty, let custom = NSFont(name: family, size: size) { + return custom + } + return NSFont.monospacedSystemFont(ofSize: size, weight: .regular) + } + // MARK: - Settings Application /// Applies all persisted settings to a terminal view at creation time. private func applyAllSettings(to view: LocalProcessTerminalView) { // Font let fontSize = CGFloat(Defaults[.terminalFontSize]) - if let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) as NSFont? { - view.font = font - } + let fontFamily = Defaults[.terminalFontFamily] + view.font = resolveFont(family: fontFamily, size: fontSize) // Opacity view.layer?.opacity = Float(Defaults[.terminalOpacity]) @@ -220,9 +233,15 @@ class TerminalManager: ObservableObject { /// Updates font size on the live terminal view. func applyFontSize(_ size: Double) { guard let view = terminalView else { return } - if let font = NSFont.monospacedSystemFont(ofSize: CGFloat(size), weight: .regular) as NSFont? { - view.font = font - } + let fontFamily = Defaults[.terminalFontFamily] + view.font = resolveFont(family: fontFamily, size: CGFloat(size)) + } + + /// Updates font family on the live terminal view. + func applyFontFamily(_ family: String) { + guard let view = terminalView else { return } + let fontSize = CGFloat(Defaults[.terminalFontSize]) + view.font = resolveFont(family: family, size: fontSize) } /// Updates opacity on the live terminal view. diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 347335c..8ae20b6 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -128,13 +128,11 @@ enum AnimationLoopMode: String, Codable, CaseIterable { enum AnimationSource: Codable, Hashable, Equatable { case lottieFile(URL) // Local file (in app support or bundle) case lottieURL(URL) // Remote URL - case builtInFace // Original MinimalFaceFeatures var displayType: String { switch self { case .lottieFile: return "Local" case .lottieURL: return "Remote" - case .builtInFace: return "Built-in" } } } @@ -316,6 +314,22 @@ enum ColorPickerDisplayMode: String, CaseIterable, Codable, Defaults.Serializabl } } +enum ThirdPartyDDCProvider: String, CaseIterable, Codable, Defaults.Serializable, Identifiable { + case betterDisplay + case lunar + + var id: String { rawValue } + + var displayName: String { + switch self { + case .betterDisplay: + return "BetterDisplay" + case .lunar: + return "Lunar" + } + } +} + enum HideNotchOption: String, Defaults.Serializable { case always case nowPlayingOnly @@ -774,6 +788,7 @@ extension Defaults.Keys { static let musicSkipBehavior = Key("musicSkipBehavior", default: .track) static let musicControlWindowEnabled = Key("musicControlWindowEnabled", default: false) static let showStandardMediaControls = Key("showStandardMediaControls", default: true) + static let autoHideInactiveNotchMediaPlayer = Key("autoHideInactiveNotchMediaPlayer", default: true) static let cachedMusicLiveActivityPreference = Key("cachedMusicLiveActivityPreference", default: nil) static let cachedLockScreenMediaWidgetPreference = Key("cachedLockScreenMediaWidgetPreference", default: nil) static let cachedMusicControlWindowPreference = Key("cachedMusicControlWindowPreference", default: nil) @@ -915,6 +930,7 @@ extension Defaults.Keys { // MARK: Terminal Feature static let enableTerminalFeature = Key("enableTerminalFeature", default: false) static let terminalShellPath = Key("terminalShellPath", default: "/bin/zsh") + static let terminalFontFamily = Key("terminalFontFamily", default: "") static let terminalFontSize = Key("terminalFontSize", default: 12.0) static let terminalOpacity = Key("terminalOpacity", default: 1.0) static let terminalMaxHeightFraction = Key("terminalMaxHeightFraction", default: 0.4) @@ -1027,8 +1043,15 @@ extension Defaults.Keys { static let circularHUDStrokeWidth = Key("circularHUDStrokeWidth", default: 4) static let circularHUDUseAccentColor = Key("circularHUDUseAccentColor", default: true) - // MARK: BetterDisplay Integration + // MARK: Third-Party DDC Integration + static let enableThirdPartyDDCIntegration = Key("enableThirdPartyDDCIntegration", default: false) + static let thirdPartyDDCProvider = Key("thirdPartyDDCProvider", default: .betterDisplay) + static let enableExternalVolumeControlListener = Key("enableExternalVolumeControlListener", default: false) + static let didMigrateThirdPartyDDCIntegration = Key("didMigrateThirdPartyDDCIntegration", default: false) + + // Legacy keys retained for migration/backward compatibility static let enableBetterDisplayIntegration = Key("enableBetterDisplayIntegration", default: false) + static let enableLunarIntegration = Key("enableLunarIntegration", default: false) static let hasSeenOSDAlphaWarning = Key("hasSeenOSDAlphaWarning", default: false) static let enableOSDVolume = Key("enableOSDVolume", default: true) @@ -1152,6 +1175,29 @@ extension Defaults.Keys { Defaults[.didMigrateMusicControlSlots] = true } + static func migrateThirdPartyDDCIntegration() { + if Defaults[.didMigrateThirdPartyDDCIntegration] == false { + let legacyBetterDisplayEnabled = Defaults[.enableBetterDisplayIntegration] + let legacyLunarEnabled = Defaults[.enableLunarIntegration] + + if legacyBetterDisplayEnabled || legacyLunarEnabled { + Defaults[.enableThirdPartyDDCIntegration] = true + Defaults[.thirdPartyDDCProvider] = (legacyLunarEnabled && !legacyBetterDisplayEnabled) ? .lunar : .betterDisplay + } + + Defaults[.didMigrateThirdPartyDDCIntegration] = true + } + + syncLegacyThirdPartyDDCKeys() + } + + static func syncLegacyThirdPartyDDCKeys() { + let isIntegrationEnabled = Defaults[.enableThirdPartyDDCIntegration] + let selectedProvider = Defaults[.thirdPartyDDCProvider] + Defaults[.enableBetterDisplayIntegration] = isIntegrationEnabled && selectedProvider == .betterDisplay + Defaults[.enableLunarIntegration] = isIntegrationEnabled && selectedProvider == .lunar + } + private static func normalizeMusicAuxControls() { guard Defaults[.musicAuxLeftControl] == Defaults[.musicAuxRightControl] else { return } diff --git a/DynamicIsland/services/Extensions/AtollDistributedNotifications.swift b/DynamicIsland/services/Extensions/AtollDistributedNotifications.swift new file mode 100644 index 0000000..fda5463 --- /dev/null +++ b/DynamicIsland/services/Extensions/AtollDistributedNotifications.swift @@ -0,0 +1,20 @@ +/* + * Atoll (DynamicIsland) + * Copyright (C) 2024-2026 Atoll Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +import Foundation + +enum AtollDistributedNotifications { + static let didBecomeActive = Notification.Name("com.ebullioscopic.Atoll.lifecycle.didBecomeActive") + static let didBecomeIdle = Notification.Name("com.ebullioscopic.Atoll.lifecycle.didBecomeIdle") + + enum UserInfoKey { + static let sourcePID = "sourcePID" + } +} diff --git a/DynamicIsland/services/Extensions/ExtensionRPCMessages.swift b/DynamicIsland/services/Extensions/ExtensionRPCMessages.swift new file mode 100644 index 0000000..8a97145 --- /dev/null +++ b/DynamicIsland/services/Extensions/ExtensionRPCMessages.swift @@ -0,0 +1,184 @@ +/* + * Atoll (DynamicIsland) + * Copyright (C) 2024-2026 Atoll Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation + +// MARK: - JSON-RPC 2.0 Message Types + +/// Incoming JSON-RPC 2.0 request from a client. +struct RPCRequest: Codable { + let jsonrpc: String + let method: String + let params: RPCParams? + let id: String +} + +/// Outgoing JSON-RPC 2.0 success response. +struct RPCSuccessResponse: Codable { + let jsonrpc: String + let result: [String: RPCValue] + let id: String + + init(result: [String: RPCValue], id: String) { + self.jsonrpc = "2.0" + self.result = result + self.id = id + } +} + +/// Outgoing JSON-RPC 2.0 error response. +struct RPCErrorResponse: Codable { + let jsonrpc: String + let error: RPCErrorObject + let id: String? + + init(error: RPCErrorObject, id: String?) { + self.jsonrpc = "2.0" + self.error = error + self.id = id + } +} + +/// JSON-RPC error object. +struct RPCErrorObject: Codable { + let code: Int + let message: String + let data: String? + + init(code: Int, message: String, data: String? = nil) { + self.code = code + self.message = message + self.data = data + } +} + +/// Outgoing JSON-RPC 2.0 notification (server → client, no id). +struct RPCNotification: Codable { + let jsonrpc: String + let method: String + let params: [String: RPCValue] + + init(method: String, params: [String: RPCValue]) { + self.jsonrpc = "2.0" + self.method = method + self.params = params + } +} + +// MARK: - Standard Error Codes + +enum RPCErrorCode { + static let parseError = -32700 + static let invalidRequest = -32600 + static let methodNotFound = -32601 + static let invalidParams = -32602 + static let internalError = -32603 + // Application-specific codes + static let unauthorized = -32001 + static let featureDisabled = -32002 + static let capacityExceeded = -32003 + static let descriptorInvalid = -32004 + static let unsupported = -32005 +} + +// MARK: - Flexible JSON Value + +/// Type-erased JSON value for RPC params and results. +enum RPCValue: Codable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) + case null + case object([String: RPCValue]) + case array([RPCValue]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let b = try? container.decode(Bool.self) { + self = .bool(b) + } else if let i = try? container.decode(Int.self) { + self = .int(i) + } else if let d = try? container.decode(Double.self) { + self = .double(d) + } else if let s = try? container.decode(String.self) { + self = .string(s) + } else if let obj = try? container.decode([String: RPCValue].self) { + self = .object(obj) + } else if let arr = try? container.decode([RPCValue].self) { + self = .array(arr) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON type") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let s): try container.encode(s) + case .int(let i): try container.encode(i) + case .double(let d): try container.encode(d) + case .bool(let b): try container.encode(b) + case .null: try container.encodeNil() + case .object(let o): try container.encode(o) + case .array(let a): try container.encode(a) + } + } + + var stringValue: String? { + if case .string(let s) = self { return s } + return nil + } + var boolValue: Bool? { + if case .bool(let b) = self { return b } + return nil + } + var objectValue: [String: RPCValue]? { + if case .object(let o) = self { return o } + return nil + } +} + +// MARK: - Flexible Params + +/// JSON-RPC params can be an object or individual values. +struct RPCParams: Codable { + let values: [String: RPCValue] + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.values = try container.decode([String: RPCValue].self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(values) + } + + subscript(key: String) -> RPCValue? { + values[key] + } + + /// Extract a JSON sub-object as raw Data for use with JSONDecoder. + func jsonData(for key: String) -> Data? { + guard let value = values[key] else { return nil } + return try? JSONEncoder().encode(value) + } +} diff --git a/DynamicIsland/services/Extensions/ExtensionRPCServer.swift b/DynamicIsland/services/Extensions/ExtensionRPCServer.swift new file mode 100644 index 0000000..13a9dd2 --- /dev/null +++ b/DynamicIsland/services/Extensions/ExtensionRPCServer.swift @@ -0,0 +1,294 @@ +/* + * Atoll (DynamicIsland) + * Copyright (C) 2024-2026 Atoll Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation +import Network +import Defaults + +/// WebSocket server for Atoll RPC. +/// Uses Apple's Network.framework (`NWListener`) — no external dependencies. +/// Listens on localhost:9020 for JSON-RPC 2.0 requests over WebSocket. +@MainActor +final class ExtensionRPCServer { + static let shared = ExtensionRPCServer() + + private var listener: NWListener? + private var connections: [UUID: RPCClientConnection] = [:] + private let port: UInt16 = 9020 + private let queue = DispatchQueue(label: "com.ebullioscopic.Atoll.rpc.server", qos: .userInitiated) + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + private init() {} + + // MARK: - Lifecycle + + func start() { + guard listener == nil else { + logDiagnostics("RPC server already running") + return + } + + let params = NWParameters(tls: nil) + let wsOptions = NWProtocolWebSocket.Options() + wsOptions.autoReplyPing = true + params.defaultProtocolStack.applicationProtocols.insert(wsOptions, at: 0) + + do { + listener = try NWListener(using: params, on: NWEndpoint.Port(integerLiteral: port)) + } catch { + Logger.log("Failed to create RPC listener: \(error.localizedDescription)", category: .extensions) + return + } + + listener?.stateUpdateHandler = { [weak self] state in + Task { @MainActor in + self?.handleListenerState(state) + } + } + + listener?.newConnectionHandler = { [weak self] connection in + Task { @MainActor in + self?.handleNewConnection(connection) + } + } + + listener?.start(queue: queue) + } + + func stop() { + listener?.cancel() + listener = nil + for (_, conn) in connections { + conn.connection.cancel() + } + connections.removeAll() + logDiagnostics("RPC server stopped") + } + + // MARK: - Client Notifications + + func notifyActivityDismiss(bundleIdentifier: String, activityID: String) { + sendNotification( + to: bundleIdentifier, + method: "atoll.activityDidDismiss", + params: [ + "bundleIdentifier": .string(bundleIdentifier), + "activityID": .string(activityID) + ] + ) + } + + func notifyWidgetDismiss(bundleIdentifier: String, widgetID: String) { + sendNotification( + to: bundleIdentifier, + method: "atoll.widgetDidDismiss", + params: [ + "bundleIdentifier": .string(bundleIdentifier), + "widgetID": .string(widgetID) + ] + ) + } + + func notifyNotchExperienceDismiss(bundleIdentifier: String, experienceID: String) { + sendNotification( + to: bundleIdentifier, + method: "atoll.notchExperienceDidDismiss", + params: [ + "bundleIdentifier": .string(bundleIdentifier), + "experienceID": .string(experienceID) + ] + ) + } + + func notifyAuthorizationChange(bundleIdentifier: String, isAuthorized: Bool) { + sendNotification( + to: bundleIdentifier, + method: "atoll.authorizationDidChange", + params: [ + "bundleIdentifier": .string(bundleIdentifier), + "isAuthorized": .bool(isAuthorized) + ] + ) + } + + // MARK: - Connection Handling + + private func handleListenerState(_ state: NWListener.State) { + switch state { + case .ready: + Logger.log("Started Atoll RPC WebSocket server on port \(port)", category: .extensions) + case .failed(let error): + Logger.log("RPC server failed: \(error.localizedDescription)", category: .extensions) + // Attempt restart after delay + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + self?.listener = nil + self?.start() + } + case .cancelled: + logDiagnostics("RPC server listener cancelled") + default: + break + } + } + + private func handleNewConnection(_ nwConnection: NWConnection) { + let connID = UUID() + let clientConn = RPCClientConnection( + id: connID, + connection: nwConnection, + bundleIdentifier: nil + ) + connections[connID] = clientConn + + nwConnection.stateUpdateHandler = { [weak self] state in + Task { @MainActor in + self?.handleConnectionState(connID: connID, state: state) + } + } + + nwConnection.start(queue: queue) + receiveMessage(connID: connID) + logDiagnostics("RPC client connected (id: \(connID.uuidString.prefix(8)))") + } + + private func handleConnectionState(connID: UUID, state: NWConnection.State) { + switch state { + case .failed, .cancelled: + connections.removeValue(forKey: connID) + logDiagnostics("RPC client disconnected (id: \(connID.uuidString.prefix(8)))") + default: + break + } + } + + private func receiveMessage(connID: UUID) { + guard let clientConn = connections[connID] else { return } + let connection = clientConn.connection + + connection.receiveMessage { [weak self] content, context, isComplete, error in + guard let self else { return } + + if let error { + Task { @MainActor in + self.logDiagnostics("RPC receive error for \(connID.uuidString.prefix(8)): \(error.localizedDescription)") + self.connections.removeValue(forKey: connID) + } + return + } + + if let data = content, !data.isEmpty { + Task { @MainActor in + self.processMessage(data: data, connID: connID) + } + } + + // Continue receiving + Task { @MainActor in + self.receiveMessage(connID: connID) + } + } + } + + private func processMessage(data: Data, connID: UUID) { + guard var clientConn = connections[connID] else { return } + + // Parse JSON-RPC request + guard let request = try? decoder.decode(RPCRequest.self, from: data) else { + let errorResponse = RPCErrorResponse( + error: RPCErrorObject(code: RPCErrorCode.parseError, message: "Invalid JSON-RPC request"), + id: nil + ) + sendResponse(errorResponse, to: connID) + return + } + + // Resolve bundle identifier from first authorization request + if clientConn.bundleIdentifier == nil, + let params = request.params, + let bi = params["bundleIdentifier"]?.stringValue { + clientConn.bundleIdentifier = bi + connections[connID] = clientConn + } + + let service = ExtensionRPCService( + bundleIdentifier: clientConn.bundleIdentifier ?? "unknown", + server: self + ) + + let responseData = service.handleRequest(request) + sendRawData(responseData, to: connID) + } + + // MARK: - Send Helpers + + private func sendResponse(_ response: Codable, to connID: UUID) { + guard let data = try? encoder.encode(response) else { return } + sendRawData(data, to: connID) + } + + private func sendRawData(_ data: Data, to connID: UUID) { + guard let clientConn = connections[connID] else { return } + + let metadata = NWProtocolWebSocket.Metadata(opcode: .text) + let context = NWConnection.ContentContext(identifier: "rpc-response", metadata: [metadata]) + + clientConn.connection.send( + content: data, + contentContext: context, + isComplete: true, + completion: .contentProcessed { error in + if let error { + Task { @MainActor in + self.logDiagnostics("RPC send error: \(error.localizedDescription)") + } + } + } + ) + } + + private func sendNotification(to bundleIdentifier: String, method: String, params: [String: RPCValue]) { + let notification = RPCNotification(method: method, params: params) + guard let data = try? encoder.encode(notification) else { return } + + for (_, clientConn) in connections where clientConn.bundleIdentifier == bundleIdentifier { + let metadata = NWProtocolWebSocket.Metadata(opcode: .text) + let context = NWConnection.ContentContext(identifier: "rpc-notification", metadata: [metadata]) + + clientConn.connection.send( + content: data, + contentContext: context, + isComplete: true, + completion: .contentProcessed { _ in } + ) + } + } + + private func logDiagnostics(_ message: String) { + guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return } + Logger.log(message, category: .extensions) + } +} + +// MARK: - Client Connection + +struct RPCClientConnection { + let id: UUID + let connection: NWConnection + var bundleIdentifier: String? +} diff --git a/DynamicIsland/services/Extensions/ExtensionRPCService.swift b/DynamicIsland/services/Extensions/ExtensionRPCService.swift new file mode 100644 index 0000000..383011e --- /dev/null +++ b/DynamicIsland/services/Extensions/ExtensionRPCService.swift @@ -0,0 +1,454 @@ +/* + * Atoll (DynamicIsland) + * Copyright (C) 2024-2026 Atoll Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation +import Defaults +import AtollExtensionKit + +/// Handles JSON-RPC method calls for a single WebSocket connection. +/// Mirrors the functionality of `ExtensionXPCService` but uses JSON-RPC transport. +@MainActor +final class ExtensionRPCService { + let bundleIdentifier: String + private weak var server: ExtensionRPCServer? + + private let liveActivityManager = ExtensionLiveActivityManager.shared + private let widgetManager = ExtensionLockScreenWidgetManager.shared + private let notchManager = ExtensionNotchExperienceManager.shared + private let authorizationManager = ExtensionAuthorizationManager.shared + + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + // Keys whose values represent Swift enums that use {"type":...} in client format. + // These need to be transformed to Swift Codable format: {"caseName": {params}}. + private static let enumKeys: Set = [ + "leadingIcon", "trailingContent", "progressIndicator", + "badgeIcon", "leadingContent", "icon", "tint" + ] + + // Enum-typed fields that can appear inside arrays (like content elements) + private static let contentElementTypeFields: Set = [ + "text", "icon", "progress", "graph", "gauge", "spacer", "divider", "webView" + ] + + init(bundleIdentifier: String, server: ExtensionRPCServer) { + self.bundleIdentifier = bundleIdentifier + self.server = server + } + + // MARK: - Method Routing + + func handleRequest(_ request: RPCRequest) -> Data { + let result: Codable + + switch request.method { + case "atoll.getVersion": + result = handleGetVersion(id: request.id) + + case "atoll.requestAuthorization": + result = handleRequestAuthorization(params: request.params, id: request.id) + + case "atoll.checkAuthorization": + result = handleCheckAuthorization(params: request.params, id: request.id) + + case "atoll.presentLiveActivity": + result = handlePresentLiveActivity(params: request.params, id: request.id) + + case "atoll.updateLiveActivity": + result = handleUpdateLiveActivity(params: request.params, id: request.id) + + case "atoll.dismissLiveActivity": + result = handleDismissLiveActivity(params: request.params, id: request.id) + + case "atoll.presentLockScreenWidget": + result = handlePresentLockScreenWidget(params: request.params, id: request.id) + + case "atoll.updateLockScreenWidget": + result = handleUpdateLockScreenWidget(params: request.params, id: request.id) + + case "atoll.dismissLockScreenWidget": + result = handleDismissLockScreenWidget(params: request.params, id: request.id) + + case "atoll.presentNotchExperience": + result = handlePresentNotchExperience(params: request.params, id: request.id) + + case "atoll.updateNotchExperience": + result = handleUpdateNotchExperience(params: request.params, id: request.id) + + case "atoll.dismissNotchExperience": + result = handleDismissNotchExperience(params: request.params, id: request.id) + + default: + result = RPCErrorResponse( + error: RPCErrorObject(code: RPCErrorCode.methodNotFound, message: "Method not found: \(request.method)"), + id: request.id + ) + } + + return (try? encoder.encode(result)) ?? Data() + } + + // MARK: - Version + + private func handleGetVersion(id: String) -> RPCSuccessResponse { + RPCSuccessResponse( + result: ["version": .string(AtollExtensionKitVersion)], + id: id + ) + } + + // MARK: - Authorization + + private func handleRequestAuthorization(params: RPCParams?, id: String) -> Codable { + guard Defaults[.enableThirdPartyExtensions] else { + return RPCErrorResponse( + error: RPCErrorObject(code: RPCErrorCode.featureDisabled, message: "Extensions are disabled"), + id: id + ) + } + + let bi = params?["bundleIdentifier"]?.stringValue ?? bundleIdentifier + let entry = authorizationManager.ensureEntryExists(bundleIdentifier: bi, appName: bi) + + if entry.isAuthorized { + return RPCSuccessResponse(result: ["authorized": .bool(true)], id: id) + } + + // Auto-authorize for now (user can revoke in settings) + authorizationManager.authorize(bundleIdentifier: bi, appName: bi) + logDiagnostics("Authorized RPC client \(bi)") + + return RPCSuccessResponse(result: ["authorized": .bool(true)], id: id) + } + + private func handleCheckAuthorization(params: RPCParams?, id: String) -> Codable { + let bi = params?["bundleIdentifier"]?.stringValue ?? bundleIdentifier + + guard Defaults[.enableThirdPartyExtensions] else { + return RPCSuccessResponse(result: ["authorized": .bool(false)], id: id) + } + + let entry = authorizationManager.authorizationEntry(for: bi) + let authorized = entry?.isAuthorized ?? false + + return RPCSuccessResponse(result: ["authorized": .bool(authorized)], id: id) + } + + // MARK: - Live Activities + + private func handlePresentLiveActivity(params: RPCParams?, id: String) -> Codable { + guard let descriptorData = params?.jsonData(for: "descriptor") else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing descriptor", id: id) + } + + do { + let transformed = Self.transformClientJSON(descriptorData) + let descriptor = try decoder.decode(AtollLiveActivityDescriptor.self, from: transformed) + try ExtensionDescriptorValidator.validate(descriptor) + try liveActivityManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier) + logDiagnostics("RPC: Presented live activity \(descriptor.id) for \(descriptor.bundleIdentifier)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } catch let error as ExtensionValidationError { + return errorResponse(from: error, id: id) + } catch { + logDiagnostics("RPC: Decode error for presentLiveActivity: \(error)") + logDiagnostics("RPC: Raw payload: \(String(data: descriptorData, encoding: .utf8) ?? "")") + return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id) + } + } + + private func handleUpdateLiveActivity(params: RPCParams?, id: String) -> Codable { + guard let descriptorData = params?.jsonData(for: "descriptor") else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing descriptor", id: id) + } + + do { + let transformed = Self.transformClientJSON(descriptorData) + let descriptor = try decoder.decode(AtollLiveActivityDescriptor.self, from: transformed) + try liveActivityManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier) + logDiagnostics("RPC: Updated live activity \(descriptor.id) for \(descriptor.bundleIdentifier)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } catch let error as ExtensionValidationError { + return errorResponse(from: error, id: id) + } catch { + logDiagnostics("RPC: Decode error for updateLiveActivity: \(error)") + return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id) + } + } + + private func handleDismissLiveActivity(params: RPCParams?, id: String) -> Codable { + guard let activityID = params?["activityID"]?.stringValue else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing activityID", id: id) + } + let bi = params?["bundleIdentifier"]?.stringValue ?? bundleIdentifier + liveActivityManager.dismiss(activityID: activityID, bundleIdentifier: bi) + logDiagnostics("RPC: Dismissed live activity \(activityID) for \(bi)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } + + // MARK: - Lock Screen Widgets + + private func handlePresentLockScreenWidget(params: RPCParams?, id: String) -> Codable { + guard let descriptorData = params?.jsonData(for: "descriptor") else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing descriptor", id: id) + } + + do { + let transformed = Self.transformClientJSON(descriptorData) + let descriptor = try decoder.decode(AtollLockScreenWidgetDescriptor.self, from: transformed) + try ExtensionDescriptorValidator.validate(descriptor) + try widgetManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier) + logDiagnostics("RPC: Presented widget \(descriptor.id) for \(descriptor.bundleIdentifier)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } catch let error as ExtensionValidationError { + return errorResponse(from: error, id: id) + } catch { + logDiagnostics("RPC: Decode error for presentWidget: \(error)") + logDiagnostics("RPC: Raw payload: \(String(data: descriptorData, encoding: .utf8) ?? "")") + return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id) + } + } + + private func handleUpdateLockScreenWidget(params: RPCParams?, id: String) -> Codable { + guard let descriptorData = params?.jsonData(for: "descriptor") else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing descriptor", id: id) + } + + do { + let transformed = Self.transformClientJSON(descriptorData) + let descriptor = try decoder.decode(AtollLockScreenWidgetDescriptor.self, from: transformed) + try widgetManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier) + logDiagnostics("RPC: Updated widget \(descriptor.id) for \(descriptor.bundleIdentifier)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } catch let error as ExtensionValidationError { + return errorResponse(from: error, id: id) + } catch { + logDiagnostics("RPC: Decode error for updateWidget: \(error)") + return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id) + } + } + + private func handleDismissLockScreenWidget(params: RPCParams?, id: String) -> Codable { + guard let widgetID = params?["widgetID"]?.stringValue else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing widgetID", id: id) + } + let bi = params?["bundleIdentifier"]?.stringValue ?? bundleIdentifier + widgetManager.dismiss(widgetID: widgetID, bundleIdentifier: bi) + logDiagnostics("RPC: Dismissed widget \(widgetID) for \(bi)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } + + // MARK: - Notch Experiences + + private func handlePresentNotchExperience(params: RPCParams?, id: String) -> Codable { + guard let descriptorData = params?.jsonData(for: "descriptor") else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing descriptor", id: id) + } + + do { + let transformed = Self.transformClientJSON(descriptorData) + let descriptor = try decoder.decode(AtollNotchExperienceDescriptor.self, from: transformed) + try ExtensionDescriptorValidator.validate(descriptor) + try notchManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier) + logDiagnostics("RPC: Presented notch experience \(descriptor.id) for \(descriptor.bundleIdentifier)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } catch let error as ExtensionValidationError { + return errorResponse(from: error, id: id) + } catch { + logDiagnostics("RPC: Decode error for presentNotchExperience: \(error)") + logDiagnostics("RPC: Raw payload: \(String(data: descriptorData, encoding: .utf8) ?? "")") + return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id) + } + } + + private func handleUpdateNotchExperience(params: RPCParams?, id: String) -> Codable { + guard let descriptorData = params?.jsonData(for: "descriptor") else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing descriptor", id: id) + } + + do { + let transformed = Self.transformClientJSON(descriptorData) + let descriptor = try decoder.decode(AtollNotchExperienceDescriptor.self, from: transformed) + try notchManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier) + logDiagnostics("RPC: Updated notch experience \(descriptor.id) for \(descriptor.bundleIdentifier)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } catch let error as ExtensionValidationError { + return errorResponse(from: error, id: id) + } catch { + logDiagnostics("RPC: Decode error for updateNotchExperience: \(error)") + return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id) + } + } + + private func handleDismissNotchExperience(params: RPCParams?, id: String) -> Codable { + guard let experienceID = params?["experienceID"]?.stringValue else { + return errorResponse(code: RPCErrorCode.invalidParams, message: "Missing experienceID", id: id) + } + let bi = params?["bundleIdentifier"]?.stringValue ?? bundleIdentifier + notchManager.dismiss(experienceID: experienceID, bundleIdentifier: bi) + logDiagnostics("RPC: Dismissed notch experience \(experienceID) for \(bi)") + return RPCSuccessResponse(result: ["success": .bool(true)], id: id) + } + + // MARK: - Helpers + + private func errorResponse(code: Int, message: String, id: String) -> RPCErrorResponse { + RPCErrorResponse( + error: RPCErrorObject(code: code, message: message), + id: id + ) + } + + private func errorResponse(from error: ExtensionValidationError, id: String) -> RPCErrorResponse { + let code: Int + switch error { + case .featureDisabled: code = RPCErrorCode.featureDisabled + case .unauthorized: code = RPCErrorCode.unauthorized + case .invalidDescriptor: code = RPCErrorCode.descriptorInvalid + case .exceedsCapacity: code = RPCErrorCode.capacityExceeded + case .unsupportedContent: code = RPCErrorCode.unsupported + case .rateLimited: code = RPCErrorCode.internalError + case .duplicateIdentifier: code = RPCErrorCode.descriptorInvalid + } + return RPCErrorResponse( + error: RPCErrorObject(code: code, message: error.localizedDescription ?? "Unknown error"), + id: id + ) + } + + private func logDiagnostics(_ message: String) { + guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return } + Logger.log(message, category: .extensions) + } + + // MARK: - Client JSON Transformation + + /// Converts client wire format to Swift Codable format for enum types. + /// + /// Client sends: `{ "type": "symbol", "name": "timer", "size": 16 }` + /// Swift expects: `{ "symbol": { "name": "timer", "size": 16 } }` + /// + /// For enums with unnamed parameters (e.g., `case text(String, font:, color:)`): + /// Client sends: `{ "type": "text", "text": "LIVE", "font": {...} }` + /// Swift expects: `{ "text": { "_0": "LIVE", "font": {...} } }` + static func transformClientJSON(_ data: Data) -> Data { + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return data + } + let transformed = transformObject(json) + return (try? JSONSerialization.data(withJSONObject: transformed)) ?? data + } + + private static func transformObject(_ obj: [String: Any]) -> [String: Any] { + var result: [String: Any] = [:] + + for (key, value) in obj { + if let dict = value as? [String: Any] { + if enumKeys.contains(key), let typeName = dict["type"] as? String { + // Transform: { "type": "symbol", ... } → { "symbol": { ... } } + result[key] = transformEnumValue(dict, typeName: typeName) + } else { + result[key] = transformObject(dict) + } + } else if let arr = value as? [[String: Any]] { + // Arrays like "content", "elements", "sections" + result[key] = arr.map { item in + if let typeName = item["type"] as? String, + (key == "content" || key == "elements") { + return transformEnumValue(item, typeName: typeName) + } + return transformObject(item) + } + } else { + result[key] = value + } + } + + return result + } + + /// Transforms `{ "type": "caseName", ...params }` → `{ "caseName": { ...params } }`. + /// Handles special cases where Swift enum cases have unnamed first parameters: + /// - `text` / `marquee`: `text` field → `_0` + /// - `icon`: `icon` field → `_0` (AtollIconDescriptor) + /// - `progress`: `indicator` field → `_0` (AtollProgressIndicator) + /// - `webView`: `content` field → `_0` (AtollWidgetWebContentDescriptor) + /// - `animation`: `data` field → `_0` (Data) + private static func transformEnumValue(_ dict: [String: Any], typeName: String) -> [String: Any] { + var inner: [String: Any] = [:] + + for (k, v) in dict where k != "type" { + if let nestedDict = v as? [String: Any] { + // Check if this nested dict is an enum-typed field + if (enumKeys.contains(k) || k == "indicator"), + let nestedType = nestedDict["type"] as? String { + inner[k] = transformEnumValue(nestedDict, typeName: nestedType) + } else { + inner[k] = transformObject(nestedDict) + } + } else if let nestedArr = v as? [[String: Any]] { + inner[k] = nestedArr.map { item in + if let t = item["type"] as? String { + return transformEnumValue(item, typeName: t) + } + return transformObject(item) + } + } else { + inner[k] = v + } + } + + // Handle unnamed first parameter for specific enum cases. + // Swift's auto-synthesized Codable encodes unnamed associated values as `_0`, `_1`, etc. + switch typeName { + case "text": + // AtollTrailingContent.text(String, ...) AND AtollWidgetContentElement.text(String, ...) + if let textValue = inner.removeValue(forKey: "text") { + inner["_0"] = textValue + } + case "marquee": + // AtollTrailingContent.marquee(String, ...) + if let textValue = inner.removeValue(forKey: "text") { + inner["_0"] = textValue + } + case "icon": + // AtollWidgetContentElement.icon(AtollIconDescriptor, tint:) + if let iconValue = inner.removeValue(forKey: "icon") { + inner["_0"] = iconValue + } + case "progress": + // AtollWidgetContentElement.progress(AtollProgressIndicator, value:, color:) + if let indicatorValue = inner.removeValue(forKey: "indicator") { + inner["_0"] = indicatorValue + } + case "webView": + // AtollWidgetContentElement.webView(AtollWidgetWebContentDescriptor) + if let contentValue = inner.removeValue(forKey: "content") { + inner["_0"] = contentValue + } + case "animation": + // AtollTrailingContent.animation(data:, size:) — data is named but let's be safe + break + default: + break + } + + return [typeName: inner] + } +}