mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update AppIcon setting
This commit is contained in:
parent
1bfce0c19a
commit
8b633b3ff5
5 changed files with 243 additions and 33 deletions
|
|
@ -352,6 +352,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
// Initialize idle animations (load bundled + built-in face)
|
||||
idleAnimationManager.initializeDefaultAnimations()
|
||||
|
||||
applySelectedAppIcon()
|
||||
|
||||
// Setup SystemHUD Manager
|
||||
SystemHUDManager.shared.setup(coordinator: coordinator)
|
||||
|
||||
|
|
|
|||
|
|
@ -2061,6 +2061,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Add icon" : {
|
||||
|
||||
},
|
||||
"Add manually" : {
|
||||
"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." : {
|
||||
"comment" : "A footer text describing the functionality of the lock screen positioning controls.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Drop a PNG, JPEG, TIFF, or ICNS file to add it to your icon library." : {
|
||||
|
||||
},
|
||||
"Drop files here" : {
|
||||
"localizations" : {
|
||||
|
|
@ -16039,6 +16045,9 @@
|
|||
},
|
||||
"Remove from shelf after dragging" : {
|
||||
|
||||
},
|
||||
"Remove selected" : {
|
||||
|
||||
},
|
||||
"Request Access" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -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: "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: "App icon", keywords: ["app icon", "custom icon"], highlightID: SettingsTab.appearance.highlightID(for: "App icon")),
|
||||
|
||||
// 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")),
|
||||
|
|
@ -2827,6 +2828,8 @@ struct Appearance: View {
|
|||
@Default(.useMusicVisualizer) var useMusicVisualizer
|
||||
@Default(.customVisualizers) var customVisualizers
|
||||
@Default(.selectedVisualizer) var selectedVisualizer
|
||||
@Default(.customAppIcons) private var customAppIcons
|
||||
@Default(.selectedAppIconID) private var selectedAppIconID
|
||||
@Default(.openNotchWidth) var openNotchWidth
|
||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||
@Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode
|
||||
|
|
@ -2838,10 +2841,12 @@ struct Appearance: View {
|
|||
@Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass
|
||||
@Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
|
||||
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
|
||||
let icons: [String] = ["logo2"]
|
||||
@State private var selectedIcon: String = "logo2"
|
||||
@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 name: String = ""
|
||||
@State private var url: String = ""
|
||||
|
|
@ -3185,55 +3190,205 @@ struct Appearance: View {
|
|||
IdleAnimationsSettingsSection()
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
ForEach(icons, id: \.self) { icon in
|
||||
Spacer()
|
||||
VStack {
|
||||
Image(icon)
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||
.strokeBorder(
|
||||
icon == selectedIcon ? Color.accentColor : .clear,
|
||||
lineWidth: 2.5
|
||||
)
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
let columns = [GridItem(.adaptive(minimum: 90), spacing: 12)]
|
||||
LazyVGrid(columns: columns, spacing: 12) {
|
||||
appIconCard(
|
||||
title: "Default",
|
||||
image: defaultAppIconImage(),
|
||||
isSelected: selectedAppIconID == nil
|
||||
) {
|
||||
selectedAppIconID = nil
|
||||
applySelectedAppIcon()
|
||||
}
|
||||
|
||||
Text("Default")
|
||||
.fontWeight(.medium)
|
||||
.font(.caption)
|
||||
.foregroundStyle(icon == selectedIcon ? .white : .secondary)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 3)
|
||||
ForEach(customAppIcons) { icon in
|
||||
appIconCard(
|
||||
title: icon.name,
|
||||
image: customIconImage(for: icon),
|
||||
isSelected: selectedAppIconID == icon.id.uuidString
|
||||
) {
|
||||
selectedAppIconID = icon.id.uuidString
|
||||
applySelectedAppIcon()
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Remove") {
|
||||
removeCustomIcon(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(icon == selectedIcon ? Color.accentColor : .clear)
|
||||
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)
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
selectedIcon = icon
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Add icon") {
|
||||
iconImportError = nil
|
||||
isIconImporterPresented = true
|
||||
}
|
||||
NSApp.applicationIconImage = NSImage(named: icon)
|
||||
}
|
||||
Spacer()
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button("Remove selected") {
|
||||
if let id = selectedAppIconID,
|
||||
let icon = customAppIcons.first(where: { $0.id.uuidString == id }) {
|
||||
removeCustomIcon(icon)
|
||||
}
|
||||
}
|
||||
.disabled(true)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
.settingsHighlight(id: highlightID("App icon"))
|
||||
} header: {
|
||||
HStack {
|
||||
Text("App icon")
|
||||
customBadge(text: "Coming soon")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear(perform: enforceLockScreenGlassConsistency)
|
||||
.onChange(of: lockScreenGlassStyle) { _, _ 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")
|
||||
}
|
||||
|
||||
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 {
|
||||
if let _ = AVCaptureDevice.default(for: .video) {
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import Defaults
|
||||
|
||||
struct AppIcons {
|
||||
|
||||
|
|
@ -58,3 +59,18 @@ func AppIconAsNSImage(for bundleID: String) -> NSImage? {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,32 @@ struct CustomVisualizer: Codable, Hashable, Equatable, Defaults.Serializable {
|
|||
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
|
||||
struct CustomIdleAnimation: Codable, Hashable, Equatable, Defaults.Serializable, Identifiable {
|
||||
let id: UUID
|
||||
|
|
@ -656,6 +682,8 @@ extension Defaults.Keys {
|
|||
static let useMusicVisualizer = Key<Bool>("useMusicVisualizer", default: true)
|
||||
static let customVisualizers = Key<[CustomVisualizer]>("customVisualizers", default: [])
|
||||
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
|
||||
static let enableGestures = Key<Bool>("enableGestures", default: true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue