Update AppIcon setting

This commit is contained in:
Hariharan Mudaliar 2026-01-28 22:45:53 +05:30
commit 8b633b3ff5
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
5 changed files with 243 additions and 33 deletions

View file

@ -351,6 +351,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Initialize idle animations (load bundled + built-in face) // Initialize idle animations (load bundled + built-in face)
idleAnimationManager.initializeDefaultAnimations() idleAnimationManager.initializeDefaultAnimations()
applySelectedAppIcon()
// Setup SystemHUD Manager // Setup SystemHUD Manager
SystemHUDManager.shared.setup(coordinator: coordinator) SystemHUDManager.shared.setup(coordinator: coordinator)

View file

@ -2061,6 +2061,9 @@
} }
} }
} }
},
"Add icon" : {
}, },
"Add manually" : { "Add manually" : {
"extractionState" : "stale", "extractionState" : "stale",
@ -7503,6 +7506,9 @@
"Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Use the width sliders below to narrow the media and timer widgets without exceeding their default size. Changes apply instantly while the widgets are visible." : { "Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Use the width sliders below to narrow the media and timer widgets without exceeding their default size. Changes apply instantly while the widgets are visible." : {
"comment" : "A footer text describing the functionality of the lock screen positioning controls.", "comment" : "A footer text describing the functionality of the lock screen positioning controls.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Drop a PNG, JPEG, TIFF, or ICNS file to add it to your icon library." : {
}, },
"Drop files here" : { "Drop files here" : {
"localizations" : { "localizations" : {
@ -16039,6 +16045,9 @@
}, },
"Remove from shelf after dragging" : { "Remove from shelf after dragging" : {
},
"Remove selected" : {
}, },
"Request Access" : { "Request Access" : {
"localizations" : { "localizations" : {

View file

@ -677,6 +677,7 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .appearance, title: "Enable Dynamic mirror", keywords: ["mirror", "reflection"], highlightID: SettingsTab.appearance.highlightID(for: "Enable Dynamic mirror")), 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: "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: "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: "App icon", keywords: ["app icon", "custom icon"], highlightID: SettingsTab.appearance.highlightID(for: "App icon")),
// Lock Screen // Lock Screen
SettingsSearchEntry(tab: .lockScreen, title: "Enable lock screen live activity", keywords: ["lock screen", "live activity"], highlightID: SettingsTab.lockScreen.highlightID(for: "Enable lock screen live activity")), SettingsSearchEntry(tab: .lockScreen, title: "Enable lock screen live activity", keywords: ["lock screen", "live activity"], highlightID: SettingsTab.lockScreen.highlightID(for: "Enable lock screen live activity")),
@ -2827,6 +2828,8 @@ struct Appearance: View {
@Default(.useMusicVisualizer) var useMusicVisualizer @Default(.useMusicVisualizer) var useMusicVisualizer
@Default(.customVisualizers) var customVisualizers @Default(.customVisualizers) var customVisualizers
@Default(.selectedVisualizer) var selectedVisualizer @Default(.selectedVisualizer) var selectedVisualizer
@Default(.customAppIcons) private var customAppIcons
@Default(.selectedAppIconID) private var selectedAppIconID
@Default(.openNotchWidth) var openNotchWidth @Default(.openNotchWidth) var openNotchWidth
@Default(.enableMinimalisticUI) var enableMinimalisticUI @Default(.enableMinimalisticUI) var enableMinimalisticUI
@Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode
@ -2838,10 +2841,12 @@ struct Appearance: View {
@Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass @Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass
@Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget @Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget @Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
let icons: [String] = ["logo2"]
@State private var selectedIcon: String = "logo2"
@State private var selectedListVisualizer: CustomVisualizer? = nil @State private var selectedListVisualizer: CustomVisualizer? = nil
@State private var isIconImporterPresented = false
@State private var isIconDropTarget = false
@State private var iconImportError: String?
@State private var isPresented: Bool = false @State private var isPresented: Bool = false
@State private var name: String = "" @State private var name: String = ""
@State private var url: String = "" @State private var url: String = ""
@ -3185,55 +3190,205 @@ struct Appearance: View {
IdleAnimationsSettingsSection() IdleAnimationsSettingsSection()
Section { Section {
HStack { VStack(alignment: .leading, spacing: 12) {
ForEach(icons, id: \.self) { icon in let columns = [GridItem(.adaptive(minimum: 90), spacing: 12)]
Spacer() LazyVGrid(columns: columns, spacing: 12) {
VStack { appIconCard(
Image(icon) title: "Default",
.resizable() image: defaultAppIconImage(),
.frame(width: 80, height: 80) isSelected: selectedAppIconID == nil
.background( ) {
RoundedRectangle(cornerRadius: 20, style: .circular) selectedAppIconID = nil
.strokeBorder( applySelectedAppIcon()
icon == selectedIcon ? Color.accentColor : .clear, }
lineWidth: 2.5
)
)
Text("Default") ForEach(customAppIcons) { icon in
.fontWeight(.medium) appIconCard(
.font(.caption) title: icon.name,
.foregroundStyle(icon == selectedIcon ? .white : .secondary) image: customIconImage(for: icon),
.padding(.horizontal, 10) isSelected: selectedAppIconID == icon.id.uuidString
.padding(.vertical, 3) ) {
.background( selectedAppIconID = icon.id.uuidString
Capsule() applySelectedAppIcon()
.fill(icon == selectedIcon ? Color.accentColor : .clear) }
) .contextMenu {
} Button("Remove") {
.onTapGesture { removeCustomIcon(icon)
withAnimation { }
selectedIcon = icon
} }
NSApp.applicationIconImage = NSImage(named: icon)
} }
Spacer() }
.padding(8)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color.secondary.opacity(isIconDropTarget ? 0.18 : 0.1))
)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.accentColor.opacity(isIconDropTarget ? 0.8 : 0), lineWidth: 2)
)
.onDrop(of: [UTType.fileURL], isTargeted: $isIconDropTarget) { providers in
handleIconDrop(providers)
}
HStack(spacing: 8) {
Button("Add icon") {
iconImportError = nil
isIconImporterPresented = true
}
.buttonStyle(.borderedProminent)
Button("Remove selected") {
if let id = selectedAppIconID,
let icon = customAppIcons.first(where: { $0.id.uuidString == id }) {
removeCustomIcon(icon)
}
}
.buttonStyle(.bordered)
.disabled(selectedAppIconID == nil)
}
if let iconImportError {
Text(iconImportError)
.font(.caption)
.foregroundStyle(.secondary)
} else {
Text("Drop a PNG, JPEG, TIFF, or ICNS file to add it to your icon library.")
.font(.caption)
.foregroundStyle(.secondary)
} }
} }
.disabled(true) .settingsHighlight(id: highlightID("App icon"))
} header: { } header: {
HStack { HStack {
Text("App icon") Text("App icon")
customBadge(text: "Coming soon")
} }
} }
} }
.onAppear(perform: enforceLockScreenGlassConsistency) .onAppear(perform: enforceLockScreenGlassConsistency)
.onChange(of: lockScreenGlassStyle) { _, _ in enforceLockScreenGlassConsistency() } .onChange(of: lockScreenGlassStyle) { _, _ in enforceLockScreenGlassConsistency() }
.onChange(of: lockScreenGlassCustomizationMode) { _, _ in enforceLockScreenGlassConsistency() } .onChange(of: lockScreenGlassCustomizationMode) { _, _ in enforceLockScreenGlassConsistency() }
.fileImporter(
isPresented: $isIconImporterPresented,
allowedContentTypes: [.png, .jpeg, .tiff, .icns, .image]
) { result in
switch result {
case .success(let url):
importCustomIcon(from: url)
case .failure:
iconImportError = "Icon import was canceled or failed."
}
}
.navigationTitle("Appearance") .navigationTitle("Appearance")
} }
private func defaultAppIconImage() -> NSImage? {
let fallbackName = Bundle.main.iconFileName ?? "AppIcon"
return NSImage(named: fallbackName)
}
private func customIconImage(for icon: CustomAppIcon) -> NSImage? {
NSImage(contentsOf: icon.fileURL)
}
private func appIconCard(title: String, image: NSImage?, isSelected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
VStack(spacing: 6) {
Group {
if let image {
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
Image(systemName: "app.dashed")
.font(.system(size: 28, weight: .medium))
.foregroundStyle(.secondary)
}
}
.frame(width: 64, height: 64)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.black.opacity(0.08))
)
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(isSelected ? Color.accentColor : .clear, lineWidth: 2)
)
Text(title)
.font(.caption)
.lineLimit(1)
.foregroundStyle(isSelected ? .white : .secondary)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(
Capsule().fill(isSelected ? Color.accentColor : .clear)
)
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.plain)
}
private func handleIconDrop(_ providers: [NSItemProvider]) -> Bool {
let matching = providers.first { $0.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) }
guard let provider = matching else { return false }
provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, _ in
let url: URL?
if let directURL = item as? URL {
url = directURL
} else if let data = item as? Data {
url = URL(dataRepresentation: data, relativeTo: nil)
} else {
url = nil
}
guard let url else { return }
Task { @MainActor in importCustomIcon(from: url) }
}
return true
}
private func importCustomIcon(from url: URL) {
guard let image = NSImage(contentsOf: url) else {
iconImportError = "That file could not be loaded as an image."
return
}
let name = url.deletingPathExtension().lastPathComponent
let ext = url.pathExtension.isEmpty ? "png" : url.pathExtension
let id = UUID()
let fileName = "custom-icon-\(id.uuidString).\(ext)"
let destination = CustomAppIcon.iconDirectory.appendingPathComponent(fileName)
do {
let data = try Data(contentsOf: url)
try data.write(to: destination, options: [.atomic])
} catch {
iconImportError = "Unable to save the icon file."
return
}
let newIcon = CustomAppIcon(id: id, name: name.isEmpty ? "Custom Icon" : name, fileName: fileName)
if !customAppIcons.contains(newIcon) {
customAppIcons.append(newIcon)
}
selectedAppIconID = newIcon.id.uuidString
NSApp.applicationIconImage = image
iconImportError = nil
}
private func removeCustomIcon(_ icon: CustomAppIcon) {
if let index = customAppIcons.firstIndex(of: icon) {
customAppIcons.remove(at: index)
}
if FileManager.default.fileExists(atPath: icon.fileURL.path) {
try? FileManager.default.removeItem(at: icon.fileURL)
}
if selectedAppIconID == icon.id.uuidString {
selectedAppIconID = nil
applySelectedAppIcon()
}
}
func checkVideoInput() -> Bool { func checkVideoInput() -> Bool {
if let _ = AVCaptureDevice.default(for: .video) { if let _ = AVCaptureDevice.default(for: .video) {
return true return true

View file

@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import Defaults
struct AppIcons { struct AppIcons {
@ -58,3 +59,18 @@ func AppIconAsNSImage(for bundleID: String) -> NSImage? {
return nil return nil
} }
func applySelectedAppIcon() {
let customIcons = Defaults[.customAppIcons]
if let selectedID = Defaults[.selectedAppIconID],
let icon = customIcons.first(where: { $0.id.uuidString == selectedID }),
let image = NSImage(contentsOf: icon.fileURL) {
NSApp.applicationIconImage = image
return
}
let fallbackName = Bundle.main.iconFileName ?? "AppIcon"
if let image = NSImage(named: fallbackName) {
NSApp.applicationIconImage = image
}
}

View file

@ -27,6 +27,32 @@ struct CustomVisualizer: Codable, Hashable, Equatable, Defaults.Serializable {
var speed: CGFloat = 1.0 var speed: CGFloat = 1.0
} }
struct CustomAppIcon: Codable, Hashable, Equatable, Defaults.Serializable, Identifiable {
let id: UUID
var name: String
var fileName: String
var addedAt: Date
init(id: UUID = UUID(), name: String, fileName: String, addedAt: Date = .now) {
self.id = id
self.name = name
self.fileName = fileName
self.addedAt = addedAt
}
static let iconDirectory: URL = {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let dir = base.appendingPathComponent("DynamicIsland", isDirectory: true)
.appendingPathComponent("AppIcons", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}()
var fileURL: URL {
Self.iconDirectory.appendingPathComponent(fileName)
}
}
// MARK: - Custom Idle Animation Models // MARK: - Custom Idle Animation Models
struct CustomIdleAnimation: Codable, Hashable, Equatable, Defaults.Serializable, Identifiable { struct CustomIdleAnimation: Codable, Hashable, Equatable, Defaults.Serializable, Identifiable {
let id: UUID let id: UUID
@ -656,6 +682,8 @@ extension Defaults.Keys {
static let useMusicVisualizer = Key<Bool>("useMusicVisualizer", default: true) static let useMusicVisualizer = Key<Bool>("useMusicVisualizer", default: true)
static let customVisualizers = Key<[CustomVisualizer]>("customVisualizers", default: []) static let customVisualizers = Key<[CustomVisualizer]>("customVisualizers", default: [])
static let selectedVisualizer = Key<CustomVisualizer?>("selectedVisualizer", default: nil) static let selectedVisualizer = Key<CustomVisualizer?>("selectedVisualizer", default: nil)
static let customAppIcons = Key<[CustomAppIcon]>("customAppIcons", default: [])
static let selectedAppIconID = Key<String?>("selectedAppIconID", default: nil)
// MARK: Gestures // MARK: Gestures
static let enableGestures = Key<Bool>("enableGestures", default: true) static let enableGestures = Key<Bool>("enableGestures", default: true)