diff --git a/App/Sources/App/KeyboardCowboy.swift b/App/Sources/App/KeyboardCowboy.swift index 66f2fb30..fb4f0134 100644 --- a/App/Sources/App/KeyboardCowboy.swift +++ b/App/Sources/App/KeyboardCowboy.swift @@ -192,19 +192,19 @@ struct KeyboardCowboy: App { .environmentObject(OpenPanelController()) .matchedGeometryEffect(id: "content-window", in: namespace) case .loading: - AppLoadingView() - .frame(width: 480, height: 360) + AppLoadingView(namespace: namespace) + .frame(width: 560, height: 380) .matchedGeometryEffect(id: "content-window", in: namespace) case .noConfiguration: - EmptyConfigurationView { + EmptyConfigurationView(namespace) { contentStore.handle($0) } .matchedGeometryEffect(id: "content-window", in: namespace) - .frame(width: 480, height: 360) + .frame(width: 560, height: 380) .animation(.none, value: contentStore.state) } } - .animation(.spring(), value: contentStore.state) + .animation(.spring(response: 0.3, dampingFraction: 0.65, blendDuration: 0.2), value: contentStore.state) } .windowResizability(.contentSize) .windowStyle(.hiddenTitleBar) diff --git a/App/Sources/Controllers/IconCache.swift b/App/Sources/Controllers/IconCache.swift index fbb07035..e6b01dbf 100644 --- a/App/Sources/Controllers/IconCache.swift +++ b/App/Sources/Controllers/IconCache.swift @@ -55,7 +55,7 @@ final class IconCache { // MARK: Private methods private func load(_ identifier: String) async throws -> NSImage? { - let url = try applicationCacheDirectory().appending(component: "\(identifier).tiff") + let url = try applicationCacheDirectory().appending(component: identifier) if FileManager.default.fileExists(atPath: url.path()) { return NSImage(contentsOf: url) @@ -64,7 +64,7 @@ final class IconCache { } private func save(_ image: NSImage, identifier: String) async throws { - let url = try applicationCacheDirectory().appending(component: "\(identifier).tiff") + let url = try applicationCacheDirectory().appending(component: identifier) guard let tiff = image.tiffRepresentation else { throw IconCacheError.unableToObtainTiffRepresentation @@ -100,5 +100,5 @@ final class IconCache { } private extension CGSize { - var suffix: String { "\(width)x\(height)" } + var suffix: String { "\(Int(width))x\(Int(height))" } } diff --git a/App/Sources/Engines/CommandEngine.swift b/App/Sources/Engines/CommandEngine.swift index e1bf689d..f0f4bd42 100644 --- a/App/Sources/Engines/CommandEngine.swift +++ b/App/Sources/Engines/CommandEngine.swift @@ -120,7 +120,7 @@ final class CommandEngine { } } - private func run(_ command: Command) async throws { + func run(_ command: Command) async throws { if command.notification { await MainActor.run { lastExecutedCommand = command diff --git a/App/Sources/Engines/KeyboardEngine.swift b/App/Sources/Engines/KeyboardEngine.swift index 3050d3b0..6878077a 100644 --- a/App/Sources/Engines/KeyboardEngine.swift +++ b/App/Sources/Engines/KeyboardEngine.swift @@ -3,13 +3,14 @@ import MachPort import CoreGraphics import KeyCodes +enum KeyboardEngineError: Error { + case failedToResolveMachPortController + case failedToResolveKey(String) + case failedToCreateKeyCode(Int) + case failedToCreateEvent +} + final class KeyboardEngine { - enum KeyboardEngineError: Error { - case failedToResolveMachPortController - case failedToResolveKey(String) - case failedToCreateKeyCode(Int) - case failedToCreateEvent - } var machPort: MachPortEventController? let store: KeyCodesStore diff --git a/App/Sources/Engines/Scripting/Plugins/AppleScriptPlugin.swift b/App/Sources/Engines/Scripting/Plugins/AppleScriptPlugin.swift index 0789a92d..85d7f085 100644 --- a/App/Sources/Engines/Scripting/Plugins/AppleScriptPlugin.swift +++ b/App/Sources/Engines/Scripting/Plugins/AppleScriptPlugin.swift @@ -1,13 +1,14 @@ import Combine import Cocoa +enum AppleScriptPluginError: Error { + case failedToCreateInlineScript + case failedToCreateScriptAtURL(URL) + case compileFailed(Error) + case executionFailed(Error) +} + final class AppleScriptPlugin { - enum AppleScriptPluginError: Error { - case failedToCreateInlineScript - case failedToCreateScriptAtURL(URL) - case compileFailed(Error) - case executionFailed(Error) - } private let bundleIdentifier = Bundle.main.bundleIdentifier! private let queue = DispatchQueue(label: "ApplicationPlugin") diff --git a/App/Sources/Models/Commands/KeyboardCommand.swift b/App/Sources/Models/Commands/KeyboardCommand.swift index 0e39e3df..22a3055c 100644 --- a/App/Sources/Models/Commands/KeyboardCommand.swift +++ b/App/Sources/Models/Commands/KeyboardCommand.swift @@ -16,7 +16,7 @@ public struct KeyboardCommand: Identifiable, Codable, Hashable, Sendable { public init(id: String = UUID().uuidString, name: String = "", keyboardShortcut: KeyShortcut, - notification: Bool) { + notification: Bool = false) { self.id = id self.name = name self.keyboardShortcuts = [keyboardShortcut] diff --git a/App/Sources/Models/Commands/ScriptCommand.swift b/App/Sources/Models/Commands/ScriptCommand.swift index 901ba722..9581c3cf 100644 --- a/App/Sources/Models/Commands/ScriptCommand.swift +++ b/App/Sources/Models/Commands/ScriptCommand.swift @@ -9,8 +9,8 @@ public enum ScriptCommand: Identifiable, Codable, Hashable, Sendable { case shellScript = "sh" } - case appleScript(id: String, isEnabled: Bool, name: String?, source: Source) - case shell(id: String, isEnabled: Bool, name: String?, source: Source) + case appleScript(id: String = UUID().uuidString, isEnabled: Bool = true, name: String?, source: Source) + case shell(id: String = UUID().uuidString, isEnabled: Bool = true, name: String?, source: Source) public enum CodingKeys: String, CodingKey { case appleScript diff --git a/App/Sources/Models/Commands/TypeCommand.swift b/App/Sources/Models/Commands/TypeCommand.swift index 02e88879..59e0f0e2 100644 --- a/App/Sources/Models/Commands/TypeCommand.swift +++ b/App/Sources/Models/Commands/TypeCommand.swift @@ -10,7 +10,7 @@ public struct TypeCommand: Identifiable, Codable, Hashable, Sendable { public init(id: String = UUID().uuidString, name: String, input: String, - notification: Bool) { + notification: Bool = false) { self.id = id self.name = name self.input = input diff --git a/App/Sources/Models/KeyboardCowboyConfiguration.swift b/App/Sources/Models/KeyboardCowboyConfiguration.swift index c1bcfc80..431ee8b6 100644 --- a/App/Sources/Models/KeyboardCowboyConfiguration.swift +++ b/App/Sources/Models/KeyboardCowboyConfiguration.swift @@ -1,3 +1,4 @@ +import Cocoa import Foundation struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable { @@ -16,7 +17,33 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable { } static func `default`() -> KeyboardCowboyConfiguration { - KeyboardCowboyConfiguration( + let editorWorkflow: Workflow + + if !NSWorkspace.shared.urlsForApplications(withBundleIdentifier: "com.apple.dt.Xcode").isEmpty { + editorWorkflow = Workflow( + name: "Switch to Xcode", + trigger: .keyboardShortcuts([.init(key: "E", modifiers: [.function])]), + commands: [ + .application( + .init(application: .xcode()) + ) + ] + ) + } else { + editorWorkflow = Workflow( + name: "Switch to TextEdit", + trigger: .keyboardShortcuts([.init(key: "E", modifiers: [.function])]), + commands: [ + .application( + .init(application: .init(bundleIdentifier: "com.apple.TextEdit", + bundleName: "TextEdit", + path: "/System/Applications/TextEdit.app")) + ) + ] + ) + } + + return KeyboardCowboyConfiguration( name: "Default Configuration", groups: [ WorkflowGroup(symbol: "autostartstop", name: "Automation", color: "#EB5545"), @@ -30,6 +57,29 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable { ) ] ), + editorWorkflow, + Workflow( + name: "Switch to Terminal", + trigger: .keyboardShortcuts([.init(key: "T", modifiers: [.function])]), + commands: [ + .application( + .init(application: .init( + bundleIdentifier: "com.apple.Terminal", + bundleName: "Terminal", + path: "/System/Applications/Utilities/Terminal.app")) + ) + ] + ), + + Workflow( + name: "Switch to Safari", + trigger: .keyboardShortcuts([.init(key: "S", modifiers: [.function])]), + commands: [ + .application( + .init(application: .safari()) + ) + ] + ), Workflow( name: "Open System Settings", trigger: .keyboardShortcuts([.init(key: ",", modifiers: [.function])]), @@ -40,16 +90,60 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable { ] ), ]), - WorkflowGroup(symbol: "applescript", name: "AppleScripts", color: "#F9D64A"), + WorkflowGroup(symbol: "applescript", name: "AppleScripts", color: "#F9D64A", + workflows: [ + Workflow(name: "Open a specific note", + commands: [ + .script(.appleScript(name: "Show note", source: .inline(""" + tell application "Notes" + show note "awesome note" + end tell + """))) + ]) + ]), WorkflowGroup(symbol: "folder", name: "Files & Folders", color: "#6BD35F", workflows: [ Workflow(name: "Open Home folder", trigger: .keyboardShortcuts([.init(key: "H", modifiers: [.function])]), commands: [ - .open(.init(path: "~/")) - ]) + .open(.init(path: ("~/" as NSString).expandingTildeInPath)) + ]), + Workflow(name: "Open Documents folder", + trigger: .keyboardShortcuts([]), + commands: [ + .open(.init(path: ("~/Documents" as NSString).expandingTildeInPath)) + ]), + Workflow(name: "Open Downloads folder", + trigger: .keyboardShortcuts([]), + commands: [ + .open(.init(path: ("~/Downloads" as NSString).expandingTildeInPath)) + ]), + ]), + WorkflowGroup(symbol: "app.connected.to.app.below.fill", + name: "Rebinding", + color: "#3984F7", + workflows: [ + Workflow(name: "Vim bindings H to ←", + trigger: .keyboardShortcuts([.init(key: "H", modifiers: [.option])]), + commands: [ + .keyboard(.init(keyboardShortcut: .init(key: "←"))) + ], isEnabled: false), + Workflow(name: "Vim bindings J to ↓", + trigger: .keyboardShortcuts([.init(key: "J", modifiers: [.option])]), + commands: [ + .keyboard(.init(keyboardShortcut: .init(key: "↓"))) + ], isEnabled: false), + Workflow(name: "Vim bindings K to ↑", + trigger: .keyboardShortcuts([.init(key: "K", modifiers: [.option])]), + commands: [ + .keyboard(.init(keyboardShortcut: .init(key: "↑"))) + ], isEnabled: false), + Workflow(name: "Vim bindings L to →", + trigger: .keyboardShortcuts([.init(key: "L", modifiers: [.option])]), + commands: [ + .keyboard(.init(keyboardShortcut: .init(key: "→"))) + ], isEnabled: false) ]), - WorkflowGroup(symbol: "app.connected.to.app.below.fill", name: "Rebinding", color: "#3984F7"), WorkflowGroup(symbol: "flowchart", name: "Shortcuts", color: "#B263EA"), WorkflowGroup(symbol: "terminal", name: "ShellScripts", color: "#5D5FDE"), WorkflowGroup(symbol: "laptopcomputer", name: "System", color: "#A78F6D"), @@ -67,8 +161,28 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable { .init(key: "G"), ]), commands: [.open(.init(path: "https://www.github.com"))]), - + Workflow(name: "Open imdb.com", + trigger: .keyboardShortcuts([ + .init(key: "⇥", modifiers: [.function]), + .init(key: "I"), + ]), + commands: [.open(.init(path: "https://www.imdb.com"))]), ]), + WorkflowGroup(name: "Mail", color:"#3984F7", + rule: Rule.init(bundleIdentifiers: ["com.apple.mail"]), + workflows: [ + Workflow(name: "Type mail signature", + trigger: .keyboardShortcuts([.init(key: "S", modifiers: [.function, .command])]), + commands: [ + .type(.init(name: "Signature", input: """ +Stay hungry, stay awesome! +-------------------------- +xoxo +\(NSFullUserName()) +""" + )) + ]) + ]) ]) } } diff --git a/App/Sources/Reducers/DetailCommandActionReducer.swift b/App/Sources/Reducers/DetailCommandActionReducer.swift index 8e5d2240..3bb75a4e 100644 --- a/App/Sources/Reducers/DetailCommandActionReducer.swift +++ b/App/Sources/Reducers/DetailCommandActionReducer.swift @@ -3,7 +3,7 @@ import Cocoa final class DetailCommandActionReducer { static func reduce(_ action: CommandView.Action, - keyboardCowboyEngine: KeyboardCowboyEngine, + commandEngine: CommandEngine, workflow: inout Workflow) { guard var command: Command = workflow.commands.first(where: { $0.id == action.commandId }) else { fatalError("Unable to find command.") @@ -14,7 +14,29 @@ final class DetailCommandActionReducer { command.isEnabled = newValue workflow.updateOrAddCommand(command) case .run(_, _): - break + let runCommand = command + Task { + do { + try await commandEngine.run(runCommand) + } catch let error as KeyboardEngineError { + let alert = await NSAlert(error: error) + await alert.runModal() + } catch let error as AppleScriptPluginError { + let alert: NSAlert + switch error { + case .failedToCreateInlineScript: + alert = await NSAlert(error: error) + case .failedToCreateScriptAtURL: + alert = await NSAlert(error: error) + case .compileFailed(let error): + alert = await NSAlert(error: error) + case .executionFailed(let error): + alert = await NSAlert(error: error) + } + + await alert.runModal() + } + } case .remove(_, let commandId): workflow.commands.removeAll(where: { $0.id == commandId }) case .modify(let kind): @@ -126,7 +148,7 @@ final class DetailCommandActionReducer { let execution = workflow.execution Task { let path = (source as NSString).expandingTildeInPath - await keyboardCowboyEngine.run([.open(.init(path: path, notification: false))], execution: execution) + try await commandEngine.run(.open(.init(path: path))) } case .reveal(let path): NSWorkspace.shared.selectFile(path, inFileViewerRootedAtPath: "") diff --git a/App/Sources/Reducers/DetailViewActionReducer.swift b/App/Sources/Reducers/DetailViewActionReducer.swift index 9c7a7860..beacd15c 100644 --- a/App/Sources/Reducers/DetailViewActionReducer.swift +++ b/App/Sources/Reducers/DetailViewActionReducer.swift @@ -24,10 +24,7 @@ final class DetailViewActionReducer { case .updateKeyboardShortcuts(_, let keyboardShortcuts): workflow.trigger = .keyboardShortcuts(keyboardShortcuts) case .commandView(_, let action): - DetailCommandActionReducer.reduce( - action, - keyboardCowboyEngine: keyboardCowboyEngine, - workflow: &workflow) + DetailCommandActionReducer.reduce(action, commandEngine: commandEngine, workflow: &workflow) case .moveCommand(_, let fromOffsets, let toOffset): workflow.commands.move(fromOffsets: fromOffsets, toOffset: toOffset) result = .animated diff --git a/App/Sources/Views/AppLoadingView.swift b/App/Sources/Views/AppLoadingView.swift index cbdef152..26588148 100644 --- a/App/Sources/Views/AppLoadingView.swift +++ b/App/Sources/Views/AppLoadingView.swift @@ -1,13 +1,19 @@ import SwiftUI struct AppLoadingView: View { + private let namespace: Namespace.ID @State var done: Bool = false + init(namespace: Namespace.ID) { + self.namespace = namespace + } + var body: some View { VStack { KeyboardCowboyAsset.applicationIcon.swiftUIImage .resizable() .frame(width: 64, height: 64) + .matchedGeometryEffect(id: "initial-item", in: namespace) Text("Loading ...") } .padding() @@ -17,7 +23,8 @@ struct AppLoadingView: View { } struct AppLoadingView_Previews: PreviewProvider { + @Namespace static var namespace static var previews: some View { - AppLoadingView() + AppLoadingView(namespace: namespace) } } diff --git a/App/Sources/Views/ContentImageView.swift b/App/Sources/Views/ContentImageView.swift index a7674f78..a71a2eac 100644 --- a/App/Sources/Views/ContentImageView.swift +++ b/App/Sources/Views/ContentImageView.swift @@ -56,6 +56,7 @@ struct ContentImageView: View { Text(">_") .font(Font.system(.caption, design: .monospaced)) } + .frame(width: size, height: size) case .path: Image(nsImage: NSWorkspace.shared.icon(forFile: "/System/Applications/Utilities/Script Editor.app")) .resizable() diff --git a/App/Sources/Views/ContentImagesView.swift b/App/Sources/Views/ContentImagesView.swift index 64bf2bef..1f53daca 100644 --- a/App/Sources/Views/ContentImagesView.swift +++ b/App/Sources/Views/ContentImagesView.swift @@ -13,22 +13,21 @@ struct ContentImagesView: View { Image(systemName: "app.dashed") .resizable() .aspectRatio(contentMode: .fit) - .fixedSize() .frame(width: 24, height: 24) - Image(systemName: "plus") .resizable() .aspectRatio(contentMode: .fit) .fixedSize() - .frame(width: 8, height: 8) + .shadow(radius: 1, y: 1) } + .foregroundColor(.white) .compositingGroup() - .shadow(radius: 1, y: 1) .opacity(0.5) + .frame(width: size, height: size) } else { ZStack { ForEach(images) { image in - ContentImageView(image: image, size: size) + ContentImageView(image: image, size: size - 2) .rotationEffect(.degrees(-(isHovered ? -20 * image.offset : 3.75 * image.offset))) .offset(.init(width: -(image.offset * (isHovered ? -8 : 1.25)), height: image.offset * (isHovered ? 1.25 : 1.25))) diff --git a/App/Sources/Views/ContentItemView.swift b/App/Sources/Views/ContentItemView.swift index 23a6587e..d7e0f06b 100644 --- a/App/Sources/Views/ContentItemView.swift +++ b/App/Sources/Views/ContentItemView.swift @@ -25,11 +25,7 @@ struct ContentItemView: View { var body: some View { HStack { ContentImagesView(images: workflow.wrappedValue.images, size: 32) - .background(Color.black.opacity(0.2).cornerRadius(8, antialiased: false)) - .overlay(alignment: .bottomTrailing, content: { - ContentImagesView(images: workflow.wrappedValue.overlayImages, size: 16) - .opacity(workflow.wrappedValue.overlayImages.isEmpty ? 0 : 1) - }) + .background(Color.black.opacity(0.3).cornerRadius(8, antialiased: false)) .overlay(alignment: .topTrailing, content: { Text("\(workflow.wrappedValue.badge)") .aspectRatio(1, contentMode: .fill) diff --git a/App/Sources/Views/EditableKeyboardShortcutsView.swift b/App/Sources/Views/EditableKeyboardShortcutsView.swift index 7c21ccc2..3ec283a6 100644 --- a/App/Sources/Views/EditableKeyboardShortcutsView.swift +++ b/App/Sources/Views/EditableKeyboardShortcutsView.swift @@ -66,7 +66,6 @@ struct EditableKeyboardShortcutsView: View { .id(keyboardShortcut.id) } .focused($isFocused) - .focusSection() .onChange(of: isFocused, perform: { newValue in guard newValue else { return } @@ -104,6 +103,7 @@ struct EditableKeyboardShortcutsView: View { } } } + .focusSection() .padding(4) } Spacer() diff --git a/App/Sources/Views/EmptyConfigurationView.swift b/App/Sources/Views/EmptyConfigurationView.swift index c4bf0c18..ed6a470f 100644 --- a/App/Sources/Views/EmptyConfigurationView.swift +++ b/App/Sources/Views/EmptyConfigurationView.swift @@ -7,61 +7,153 @@ struct EmptyConfigurationView: View { } @State var done: Bool = false + @State var selected: Action = .initial private let colors = SplashColors(primaryColor: Color(.systemGreen), secondaryColor: Color(.systemBlue), backgroundColor: Color(.sRGB, red: 0.03, green: 0.11, blue: 0.25, opacity: 1.0)) + private let namespace: Namespace.ID private let onAction: (Action) -> Void + private let model = KeyboardCowboyConfiguration.default() - init(onAction: @escaping (Action) -> Void) { + init(_ namespace: Namespace.ID, onAction: @escaping (Action) -> Void) { + self.namespace = namespace self.onAction = onAction } var body: some View { - VStack(spacing: 16) { - HStack(spacing: 16) { - KeyboardCowboyAsset.applicationIcon.swiftUIImage - .resizable() - .frame(width: 64, height: 64) - Text("Welcome to Keyboard Cowboy!") - .font(.title) - .frame(maxWidth: .infinity, alignment: .leading) + VStack(spacing: 0) { + Text("Choose your configuration") + .font(.title) + .frame(maxWidth: .infinity, alignment: .center) + .padding(32) + + Divider() + + HStack(spacing: 48) { + Button(action: { + selected = .initial + }, label: { + LazyVGrid(columns: [ + .init(.adaptive(minimum: 24)), + .init(.adaptive(minimum: 24)), + .init(.adaptive(minimum: 24)), + .init(.adaptive(minimum: 24)), + .init(.adaptive(minimum: 24)), + ], spacing: 12) { + ForEach(model.groups) { group in + GroupIconView(color: group.color, + icon: nil, + symbol: group.symbol) + .frame(width: 24, height: 24) + .shadow(color: .black.opacity(0.3), radius: 1, y: 2) + } + } + }) + .buttonStyle(EmptyConfigurationButtonStyle(title: "Default", + subtitle: "Recommended", + action: .initial, + selected: $selected)) + + Button(action: { + selected = .empty + }, label: { + GroupIconView(color: "#000", + icon: nil, + symbol: "app.dashed") + .frame(width: 30, height: 30) + .shadow(color: .black.opacity(0.3), radius: 1, y: 2) + + }) + .buttonStyle(EmptyConfigurationButtonStyle(title: "Empty", + subtitle: " ", + action: .empty, + selected: $selected)) + + + } + .padding(16) + .frame(maxWidth: .infinity) + .background(.black.opacity(0.5)) + + Divider() Text("To get started, you can either select an empty configuration or choose from our prefilled configurations with default groups.\n\nSimply tap on the option you prefer and you'll be ready to go!") .font(.title3) - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 8) + .padding(.horizontal, 64) - Spacer() - Divider() - HStack { - VStack { - Button("Empty Configuration", action: { - onAction(.empty) - }) - .buttonStyle(.gradientStyle(config: .init(nsColor: .systemGray, hoverEffect: false))) - Text(" ") - .font(.footnote) - } - - VStack { - Button("Default Configuration", action: { - onAction(.initial) - }) - .buttonStyle(.gradientStyle(config: .init(nsColor: .systemGreen, hoverEffect: false))) - Text("Recommended") - .font(.footnote) - } - } + Button(action: { + onAction(selected) + }, label: { + Text("Confirm") + }) + .buttonStyle(.gradientStyle(config: .init(nsColor: .systemGreen, hoverEffect: false))) + .padding(.vertical) + .matchedGeometryEffect(id: "initial-item", in: namespace) } - .padding() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .background(SplashView(colors: colors, done: $done)) } } +struct EmptyConfigurationButtonStyle: ButtonStyle { + let title: String + let subtitle: String + + var action: EmptyConfigurationView.Action + @Binding var selected: EmptyConfigurationView.Action + + func makeBody(configuration: Configuration) -> some View { + VStack { + Text(title) + configuration.label + .buttonStyle(.plain) + .frame(width: 188, height: 80) + .clipped() + .background( + EmptyConfigurationBackgroundView(action: action, + selected: $selected) + ) + Text(subtitle) + .font(.footnote) + } + } +} + +struct EmptyConfigurationBackgroundView: View { + @State var action: EmptyConfigurationView.Action + @Binding var selected: EmptyConfigurationView.Action + + var body: some View { + let color: NSColor = action == .initial ? .systemGreen : .systemGray + let selected = action == selected + ZStack { + RoundedRectangle(cornerRadius: 8) + .stroke(Color(.systemGreen).opacity(0.5), lineWidth: 6) + .opacity(selected ? 1 : 0) + LinearGradient(stops: [ + .init(color: Color(color), location: 0.0), + .init(color: Color(color.blended(withFraction: 0.3, of: .black)!), location: 0.025), + .init(color: Color(color.blended(withFraction: 0.5, of: .black)!), location: 1.0), + ], startPoint: .top, endPoint: .bottom) + .cornerRadius(8) + } + .shadow( + color: selected + ? Color(.systemGreen) + : Color(.sRGBLinear, white: 0, opacity: 0.33), + radius: 4) + .animation(.default, value: selected) + } +} + struct EmptyConfigurationView_Previews: PreviewProvider { + @Namespace static var namespace static var previews: some View { - EmptyConfigurationView { _ in } + EmptyConfigurationView(namespace) { _ in } + .previewLayout(.sizeThatFits) } } diff --git a/App/Sources/Views/GroupsListView.swift b/App/Sources/Views/GroupsListView.swift index f78c0c74..a2d87118 100644 --- a/App/Sources/Views/GroupsListView.swift +++ b/App/Sources/Views/GroupsListView.swift @@ -103,16 +103,10 @@ struct GroupsListView: View { } } .padding(8) - .onReceive(selectionManager.$selections, perform: { newValue in confirmDelete = nil debounceSelectionManager.process(.init(groups: newValue)) }) - .onAppear { - if let firstSelection = selectionManager.selections.first { - proxy.scrollTo(firstSelection) - } - } } } } diff --git a/App/Sources/Views/IconView.swift b/App/Sources/Views/IconView.swift index 110c8d0f..56488e4a 100644 --- a/App/Sources/Views/IconView.swift +++ b/App/Sources/Views/IconView.swift @@ -30,14 +30,16 @@ struct IconView: View { if let image = publisher.image { Image(nsImage: image) .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size.width, height: size.height) + .fixedSize() } else { Rectangle() .fill(.clear) + .frame(width: size.width, height: size.height) + .fixedSize() } } - .aspectRatio(contentMode: .fit) - .fixedSize() - .frame(width: size.width, height: size.height) .onAppear { publisher.load(at: icon.path, bundleIdentifier: icon.bundleIdentifier, of: size) }