From d2b6a158db0a1e1b5b9b816fa3a0adf5bfa54977 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 20 Apr 2026 21:21:18 +0200 Subject: [PATCH] Enable app icon selection on macOS --- Yattee/AppDelegate.swift | 5 +++ .../Settings/SettingsManager+General.swift | 44 ++++++++++++++++++- Yattee/Core/Settings/SettingsTypes.swift | 2 - Yattee/Core/SettingsManager.swift | 2 - .../Settings/AppearanceSettingsView.swift | 10 +++-- 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Yattee/AppDelegate.swift b/Yattee/AppDelegate.swift index fcb189c7..d8563f32 100644 --- a/Yattee/AppDelegate.swift +++ b/Yattee/AppDelegate.swift @@ -78,6 +78,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { LoggingService.shared.logCloudKit("Requesting remote notification registration...") NSApplication.shared.registerForRemoteNotifications() + + if let rawValue = UserDefaults.standard.string(forKey: "appIcon"), + let icon = AppIcon(rawValue: rawValue) { + SettingsManager.applyMacAppIcon(icon) + } } func application(_ application: NSApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { diff --git a/Yattee/Core/Settings/SettingsManager+General.swift b/Yattee/Core/Settings/SettingsManager+General.swift index ea62c21c..29165d27 100644 --- a/Yattee/Core/Settings/SettingsManager+General.swift +++ b/Yattee/Core/Settings/SettingsManager+General.swift @@ -8,6 +8,8 @@ import Foundation #if os(iOS) import UIKit +#elseif os(macOS) +import AppKit #endif extension SettingsManager { @@ -35,9 +37,8 @@ extension SettingsManager { } } - // MARK: - App Icon Settings (iOS only) + // MARK: - App Icon Settings - #if os(iOS) var appIcon: AppIcon { get { if let cached = _appIcon { return cached } @@ -53,14 +54,53 @@ extension SettingsManager { // Apply the icon change Task { @MainActor in + #if os(iOS) do { try await UIApplication.shared.setAlternateIconName(newValue.alternateIconName) } catch { LoggingService.shared.error("Failed to set alternate icon: \(error)", category: .general) } + #elseif os(macOS) + SettingsManager.applyMacAppIcon(newValue) + #endif } } } + + #if os(macOS) + static func applyMacAppIcon(_ icon: AppIcon) { + if icon == .default { + NSApp.applicationIconImage = nil + } else if let source = NSImage(named: icon.previewImageName) { + NSApp.applicationIconImage = makeMacIconImage(from: source) + } + } + + /// Paints the source image onto a 1024×1024 canvas with the standard macOS + /// squircle mask and transparent padding so the Dock renders it like a + /// native app icon. + private static func makeMacIconImage(from source: NSImage) -> NSImage { + let canvasSize: CGFloat = 1024 + // macOS icon grid: artwork occupies ~824×824 centered in a 1024 canvas, + // with a ~185pt corner radius on the masked rect. + let artworkSize: CGFloat = 824 + let cornerRadius: CGFloat = 185 + let inset = (canvasSize - artworkSize) / 2 + let artworkRect = NSRect(x: inset, y: inset, width: artworkSize, height: artworkSize) + + let image = NSImage(size: NSSize(width: canvasSize, height: canvasSize)) + image.lockFocus() + let path = NSBezierPath(roundedRect: artworkRect, xRadius: cornerRadius, yRadius: cornerRadius) + path.addClip() + source.draw(in: artworkRect, + from: .zero, + operation: .sourceOver, + fraction: 1.0, + respectFlipped: true, + hints: [.interpolation: NSImageInterpolation.high.rawValue]) + image.unlockFocus() + return image + } #endif /// Whether to show a checkmark badge on fully watched video thumbnails. diff --git a/Yattee/Core/Settings/SettingsTypes.swift b/Yattee/Core/Settings/SettingsTypes.swift index fa384724..97fb6f36 100644 --- a/Yattee/Core/Settings/SettingsTypes.swift +++ b/Yattee/Core/Settings/SettingsTypes.swift @@ -52,7 +52,6 @@ enum AccentColor: String, CaseIterable, Codable { } } -#if os(iOS) enum AppIcon: String, CaseIterable, Codable { case `default` case classic @@ -89,7 +88,6 @@ enum AppIcon: String, CaseIterable, Codable { } } } -#endif // MARK: - Video Quality diff --git a/Yattee/Core/SettingsManager.swift b/Yattee/Core/SettingsManager.swift index a096a188..baf15d90 100644 --- a/Yattee/Core/SettingsManager.swift +++ b/Yattee/Core/SettingsManager.swift @@ -203,9 +203,7 @@ final class SettingsManager { // Appearance settings var _listStyle: VideoListStyle? - #if os(iOS) var _appIcon: AppIcon? - #endif // Video Swipe Actions #if !os(tvOS) diff --git a/Yattee/Views/Settings/AppearanceSettingsView.swift b/Yattee/Views/Settings/AppearanceSettingsView.swift index 4a9c5677..412a9cd0 100644 --- a/Yattee/Views/Settings/AppearanceSettingsView.swift +++ b/Yattee/Views/Settings/AppearanceSettingsView.swift @@ -18,8 +18,8 @@ struct AppearanceSettingsView: View { ThemeSection(settings: settings) #endif - // App icon section (iOS only) - #if os(iOS) + // App icon section + #if !os(tvOS) AppIconSection(settings: settings) #endif @@ -66,9 +66,9 @@ private struct ThemeSection: View { } } -// MARK: - App Icon Section (iOS only) +// MARK: - App Icon Section -#if os(iOS) +#if !os(tvOS) private struct AppIconSection: View { @Bindable var settings: SettingsManager @@ -131,7 +131,9 @@ private struct AppIconPickerView: View { } } .navigationTitle(String(localized: "settings.appearance.appIcon.header")) + #if os(iOS) .navigationBarTitleDisplayMode(.inline) + #endif } } #endif