diff --git a/OverflowBar/Onboarding/OnboardingView.swift b/OverflowBar/Onboarding/OnboardingView.swift index 1dab2c7..7601941 100644 --- a/OverflowBar/Onboarding/OnboardingView.swift +++ b/OverflowBar/Onboarding/OnboardingView.swift @@ -36,26 +36,28 @@ struct OnboardingView: View { } var body: some View { - ZStack { - background - VStack(spacing: 0) { - progressHeader - .padding(.horizontal, 36) - .padding(.top, 24) + VStack(spacing: 0) { + progressHeader + .padding(.horizontal, 28) + .padding(.top, 22) + .padding(.bottom, 12) - ZStack { - stepContent - .id(step) - .transition(stepTransition) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - - navigation - .padding(.horizontal, 36) - .padding(.bottom, 28) + ScrollView { + stepContent + .id(step) + .transition(stepTransition) + .frame(maxWidth: .infinity) + .padding(.horizontal, 22) + .padding(.vertical, 8) } + .scrollIndicators(.hidden) + .frame(maxWidth: .infinity, maxHeight: .infinity) + + navigation + .padding(.horizontal, 28) + .padding(.vertical, 18) } + .background(Color(nsColor: .windowBackgroundColor).ignoresSafeArea()) .frame(minWidth: 680, minHeight: 500) .animation(reduceMotion ? .easeOut(duration: 0.12) : .spring(response: 0.42, dampingFraction: 0.86), value: step) .task { @@ -80,36 +82,16 @@ struct OnboardingView: View { } } - private var background: some View { - ZStack { - Color(nsColor: .windowBackgroundColor) - LinearGradient( - colors: [Color.accentColor.opacity(0.14), .clear, Color.cyan.opacity(0.08)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - Circle() - .fill(Color.accentColor.opacity(0.09)) - .frame(width: 360, height: 360) - .blur(radius: 70) - .offset(x: 300, y: -230) - } - .ignoresSafeArea() - } - private var progressHeader: some View { - HStack(spacing: 12) { - HStack(spacing: 8) { - ForEach(Step.allCases, id: \.rawValue) { item in - Capsule() - .fill(item.rawValue <= step.rawValue ? Color.accentColor : Color.secondary.opacity(0.2)) - .frame(width: item == step ? 28 : 8, height: 8) - } + VStack(alignment: .leading, spacing: 7) { + HStack { + Text(step.title).font(.headline) + Spacer() + Text("Step \(step.rawValue + 1) of \(Step.allCases.count)") + .font(.caption) + .foregroundStyle(.secondary) } - Spacer() - Text("\(step.rawValue + 1) of \(Step.allCases.count) · \(step.title)") - .font(.caption.weight(.medium)) - .foregroundStyle(.secondary) + ProgressView(value: Double(step.rawValue + 1), total: Double(Step.allCases.count)) } } @@ -124,95 +106,84 @@ struct OnboardingView: View { } private var welcomeStep: some View { - VStack(spacing: 22) { - overflowLogo - VStack(spacing: 9) { - Text("Make room for what matters") - .font(.system(size: 34, weight: .semibold, design: .rounded)) - Text("OverflowBar keeps a clean menu bar and brings hidden controls back in one fluid row.") + VStack(spacing: 18) { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .interpolation(.high) + .scaledToFit() + .frame(width: 92, height: 92) + .accessibilityLabel("OverflowBar app icon") + + VStack(spacing: 8) { + Text("Welcome to OverflowBar") + .font(.largeTitle.weight(.semibold)) + Text("Keep a tidy menu bar and bring selected status items back in a compact second row.") .font(.title3) .foregroundStyle(.secondary) .multilineTextAlignment(.center) - .frame(maxWidth: 540) - } - HStack(spacing: 12) { - featureChip("menubar.rectangle", "Clean menu bar") - featureChip("hand.tap", "Click-through controls") - featureChip("macbook", "Notch-aware") + .frame(maxWidth: 520) } } - .padding(36) + .frame(maxWidth: .infinity) + .padding(.vertical, 34) } private var permissionsStep: some View { - VStack(spacing: 22) { - VStack(spacing: 8) { - Text("Two permissions, clearly explained") - .font(.system(size: 28, weight: .semibold, design: .rounded)) - Text("OverflowBar only uses these permissions to find, display, and activate your menu bar controls.") + Form { + Section("Accessibility") { + Text("Finds menu bar controls and activates their normal click action.") + .font(.callout) .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 560) - } - - VStack(spacing: 12) { - PermissionCard( - icon: "accessibility", + permissionStatusRow( title: "Accessibility", - detail: "Finds menu bar controls and triggers their normal click action.", + symbol: "accessibility", + detail: permissions.accessibilityGranted ? "Allowed" : "Not Allowed", isGranted: permissions.accessibilityGranted, request: permissions.requestAccessibility, openSettings: permissions.openAccessibilitySettings ) - PermissionCard( - icon: "rectangle.inset.filled.and.person.filled", + } + Section("Screen Recording") { + Text("Captures only the small icon regions used in the overflow row.") + .font(.callout) + .foregroundStyle(.secondary) + permissionStatusRow( title: "Screen Recording", - detail: "Captures only the small icon regions used in the overflow row.", + symbol: "rectangle.inset.filled.and.person.filled", + detail: permissions.screenRecordingGranted ? "Allowed" : "Not Allowed", isGranted: permissions.screenRecordingGranted, request: permissions.requestScreenRecording, - openSettings: permissions.openScreenRecordingSettings, + openSettings: nil, requestTitle: "Open Settings" ) } - .frame(maxWidth: 610) } - .padding(32) + .formStyle(.grouped) + .frame(maxWidth: 640) + .padding(.vertical, 18) } private var customizeStep: some View { - VStack(spacing: 18) { - VStack(spacing: 7) { - Text("Make OverflowBar yours") - .font(.system(size: 28, weight: .semibold, design: .rounded)) - Text("Choose what moves into the overflow row. You can change this anytime.") - .foregroundStyle(.secondary) - } - - VStack(spacing: 0) { + Form { + Section("Options") { Toggle("Open OverflowBar at Login", isOn: Binding( get: { launchAtLogin.isEnabled }, set: { launchAtLogin.setEnabled($0) } )) - .padding(.horizontal, 18) - .padding(.vertical, 13) - - Divider().padding(.leading, 18) - Toggle("Hide selected icons from the original menu bar", isOn: $hideSelectedIcons) - .padding(.horizontal, 18) - .padding(.vertical, 13) } - .background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 16, style: .continuous)) - .frame(maxWidth: 610) - - VStack(spacing: 10) { + Section("Menu Bar Items") { HStack { - Text("Menu Bar Items").font(.headline) + Text("Choose what moves into the overflow row.") + .foregroundStyle(.secondary) Spacer() Button("Refresh") { store.refresh() } Button("Select All") { store.selectAll(true) } .disabled(store.items.isEmpty) } + if let message = store.iconCaptureMessage { + Text(message).font(.caption).foregroundStyle(.secondary) + } if store.items.isEmpty { ContentUnavailableView( "No Items Found", @@ -221,56 +192,41 @@ struct OnboardingView: View { ? "Try Refresh after your menu bar apps have opened." : "Allow Accessibility first, then return here to scan.") ) - .frame(height: 122) } else { - ScrollView { - LazyVGrid(columns: [.init(.flexible()), .init(.flexible())], spacing: 8) { - ForEach(store.items) { item in - itemChoice(item) - } - } - } - .frame(maxHeight: 142) + ForEach(store.items) { item in itemChoice(item) } } } - .frame(maxWidth: 610) } - .padding(.horizontal, 34) - .padding(.vertical, 22) + .formStyle(.grouped) + .frame(maxWidth: 640) + .padding(.vertical, 8) } private var readyStep: some View { - VStack(spacing: 24) { - ZStack { - Circle().fill(Color.green.opacity(0.14)).frame(width: 104, height: 104) - Image(systemName: "checkmark") - .font(.system(size: 42, weight: .bold)) - .foregroundStyle(.green) - .symbolEffect(.bounce, value: step) - } - VStack(spacing: 8) { - Text("OverflowBar is ready") - .font(.system(size: 32, weight: .semibold, design: .rounded)) - Text("Use the chevron at the right side of your menu bar to reveal your new overflow row.") - .font(.title3) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 520) - } - HStack(spacing: 16) { + VStack(spacing: 18) { + Label("OverflowBar is ready", systemImage: "checkmark.circle.fill") + .font(.title2.weight(.semibold)) + .foregroundStyle(.green) + Text("Use the chevron at the right side of your menu bar to reveal your overflow row.") + .font(.title3) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: 520) + VStack(alignment: .leading, spacing: 8) { statusSummary("Accessibility", granted: permissions.accessibilityGranted) statusSummary("Screen Recording", granted: permissions.screenRecordingGranted) - statusSummary("Selected", value: "\(store.selectedItems.count)") + Label("\(store.selectedItems.count) selected items", systemImage: "menubar.rectangle") } + .padding(.top, 6) } - .padding(36) + .frame(maxWidth: .infinity) + .padding(.vertical, 32) } private var navigation: some View { HStack { if step != .welcome { Button("Back") { move(to: step.rawValue - 1) } - .buttonStyle(.borderless) } Spacer() if step == .permissions && (!permissions.accessibilityGranted || !permissions.screenRecordingGranted) { @@ -288,31 +244,31 @@ struct OnboardingView: View { } } - private var overflowLogo: some View { - ZStack { - RoundedRectangle(cornerRadius: 28, style: .continuous) - .fill(LinearGradient(colors: [.accentColor, .cyan], startPoint: .topLeading, endPoint: .bottomTrailing)) - .frame(width: 112, height: 112) - .shadow(color: Color.accentColor.opacity(0.28), radius: 28, y: 14) - VStack(spacing: 7) { - HStack(spacing: 7) { - ForEach(0..<4) { _ in Circle().fill(.white.opacity(0.92)).frame(width: 10, height: 10) } + private func permissionStatusRow( + title: String, + symbol: String, + detail: String, + isGranted: Bool, + request: @escaping () -> Void, + openSettings: (() -> Void)?, + requestTitle: String = "Allow" + ) -> some View { + HStack(spacing: 12) { + Label(title, systemImage: symbol) + Spacer() + if isGranted { + Label(detail, systemImage: "checkmark.circle.fill") + .foregroundStyle(.green) + } else { + Button(requestTitle, action: request) + .buttonStyle(.borderedProminent) + if let openSettings { + Button("Open Settings", action: openSettings) } - Image(systemName: "chevron.down") - .font(.system(size: 28, weight: .bold)) - .foregroundStyle(.white) } } } - private func featureChip(_ icon: String, _ title: String) -> some View { - Label(title, systemImage: icon) - .font(.subheadline.weight(.medium)) - .padding(.horizontal, 14) - .padding(.vertical, 9) - .background(.quaternary.opacity(0.55), in: Capsule()) - } - private func itemChoice(_ item: MenuBarItem) -> some View { Toggle(isOn: Binding( get: { item.isSelected }, @@ -324,27 +280,20 @@ struct OnboardingView: View { else { Image(systemName: item.fallbackSymbolName) } } .frame(width: 22, height: 18) - Text(item.title.isEmpty ? item.ownerName : item.title).lineLimit(1) + Text(item.title.isEmpty ? item.ownerName : item.title) + .lineLimit(1) } } .toggleStyle(.checkbox) - .padding(.horizontal, 12) - .padding(.vertical, 9) - .background(.quaternary.opacity(0.38), in: RoundedRectangle(cornerRadius: 11, style: .continuous)) } + @ViewBuilder private func statusSummary(_ title: String, granted: Bool) -> some View { - statusSummary(title, value: granted ? "Ready" : "Later", positive: granted) - } - - private func statusSummary(_ title: String, value: String, positive: Bool = true) -> some View { - VStack(spacing: 4) { - Text(value).font(.headline).foregroundStyle(positive ? .primary : .secondary) - Text(title).font(.caption).foregroundStyle(.secondary) + if granted { + Label(title, systemImage: "checkmark.circle.fill").foregroundStyle(.green) + } else { + Label(title, systemImage: "exclamationmark.circle").foregroundStyle(.secondary) } - .frame(width: 132) - .padding(.vertical, 12) - .background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 14, style: .continuous)) } private func move(to rawValue: Int) { @@ -360,47 +309,3 @@ struct OnboardingView: View { ) } } - -private struct PermissionCard: View { - let icon: String - let title: String - let detail: String - let isGranted: Bool - let request: () -> Void - let openSettings: () -> Void - var requestTitle = "Allow" - - var body: some View { - HStack(spacing: 14) { - Image(systemName: icon) - .font(.system(size: 20, weight: .semibold)) - .foregroundStyle(isGranted ? .green : Color.accentColor) - .frame(width: 42, height: 42) - .background((isGranted ? Color.green : Color.accentColor).opacity(0.12), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - VStack(alignment: .leading, spacing: 3) { - HStack(spacing: 7) { - Text(title).font(.headline) - if isGranted { - Label("Allowed", systemImage: "checkmark.circle.fill") - .font(.caption.weight(.medium)) - .foregroundStyle(.green) - } - } - Text(detail).font(.subheadline).foregroundStyle(.secondary) - } - Spacer(minLength: 12) - if isGranted { - Image(systemName: "checkmark.circle.fill") - .font(.title2) - .foregroundStyle(.green) - } else { - Button(requestTitle, action: request).buttonStyle(.borderedProminent) - Button(action: openSettings) { Image(systemName: "gear") } - .buttonStyle(.bordered) - .help("Open System Settings") - } - } - .padding(15) - .background(.quaternary.opacity(0.48), in: RoundedRectangle(cornerRadius: 17, style: .continuous)) - } -} diff --git a/OverflowBar/Settings/SettingsView.swift b/OverflowBar/Settings/SettingsView.swift index e6c57f9..32cadf8 100644 --- a/OverflowBar/Settings/SettingsView.swift +++ b/OverflowBar/Settings/SettingsView.swift @@ -9,72 +9,99 @@ struct SettingsView: View { @AppStorage("hoverRevealEnabled") private var hoverRevealEnabled = true var body: some View { + TabView { + generalSettings + .tabItem { Label("General", systemImage: "gearshape") } + menuBarSettings + .tabItem { Label("Menu Bar", systemImage: "menubar.rectangle") } + permissionsSettings + .tabItem { Label("Permissions", systemImage: "hand.raised") } + } + .frame(minWidth: 620, minHeight: 460) + .onAppear { permissions.refresh(); launchAtLogin.refresh(); store.refresh() } + .alert("OverflowBar", isPresented: Binding(get: { store.lastActivationError != nil }, set: { if !$0 { store.lastActivationError = nil } })) { + Button("OK", role: .cancel) { store.lastActivationError = nil } + } message: { + Text(store.lastActivationError ?? "") + } + } + + private var generalSettings: some View { Form { - Section("Updates") { - HStack { - Text("Version") - Spacer() - Text(appVersion) - .foregroundStyle(.secondary) - } - Button("Check for Updates…", action: checkForUpdates) - Text("OverflowBar checks periodically and asks before installing updates.") - .font(.caption) - .foregroundStyle(.secondary) - } Section("General") { Toggle("Open OverflowBar at Login", isOn: Binding( get: { launchAtLogin.isEnabled }, set: { launchAtLogin.setEnabled($0) } )) Toggle("Reveal when the pointer reaches the menu bar", isOn: $hoverRevealEnabled) - Button("Run Welcome Setup Again", action: showOnboarding) - Button("Quit OverflowBar", role: .destructive) { NSApp.terminate(nil) } if let error = launchAtLogin.errorMessage { - Text(error).font(.caption).foregroundStyle(.red) + Text(error).foregroundStyle(.red) } } - Section("Permissions") { - permissionRow("Accessibility", granted: permissions.accessibilityGranted, open: permissions.openAccessibilitySettings, request: permissions.requestAccessibility) - permissionRow("Screen Recording", granted: permissions.screenRecordingGranted, open: permissions.openScreenRecordingSettings, request: permissions.requestScreenRecording) - Button("Refresh Permission Status") { permissions.refresh() } + Section("Updates") { + LabeledContent("Version", value: appVersion) + Button("Check for Updates…", action: checkForUpdates) + Text("OverflowBar checks periodically and asks before installing updates.") + .font(.caption) + .foregroundStyle(.secondary) } + Section("Advanced") { + Button("Run Welcome Setup Again", action: showOnboarding) + Button("Quit OverflowBar", role: .destructive) { NSApp.terminate(nil) } + } + } + .formStyle(.grouped) + } + + private var menuBarSettings: some View { + Form { Section("Menu Bar Layout") { - Toggle("Hide selected original icons", isOn: Binding(get: { store.layoutManagementEnabled }, set: { store.setLayoutManagementEnabled($0) })) - Text("OverflowBar keeps the original menu-bar order while the hidden layout is applied. Items are moved only after you enable this setting; visible status windows use a direct, low-latency click path.") + Toggle("Hide selected original icons", isOn: Binding( + get: { store.layoutManagementEnabled }, + set: { store.setLayoutManagementEnabled($0) } + )) + Text("OverflowBar keeps the original menu-bar order while the hidden layout is applied. Items move only after you enable this setting.") .font(.caption) .foregroundStyle(.secondary) if #available(macOS 27.0, *) { - Text("macOS 27 uses a unified menu-bar host. OverflowBar uses Accessibility per-item controls when available; items that macOS does not expose individually remain under the system's native overflow control.") + Text("macOS 27 uses a unified menu-bar host. Items the system does not expose individually remain under the native overflow control.") .font(.caption) .foregroundStyle(.secondary) } HStack { - Button("Apply Hidden Layout") { store.applyLayout() }.disabled(!store.layoutManagementEnabled || store.selectedItems.isEmpty) - Button("Restore All Managed Icons") { store.restoreLayout() }.disabled(store.selectedItems.isEmpty) - // Safe Reset must remain available when persisted IDs no - // longer match a live window. That is precisely the - // state left by older diagnostic builds. + Button("Apply Hidden Layout") { store.applyLayout() } + .disabled(!store.layoutManagementEnabled || store.selectedItems.isEmpty) + Button("Restore All Managed Icons") { store.restoreLayout() } + .disabled(store.selectedItems.isEmpty) Button("Safe Reset", role: .destructive) { store.restoreAllAndDisable() } } if let message = store.layoutOperationMessage { Text(message).font(.caption).foregroundStyle(.secondary) } } - Section { + Section("Menu Bar Items") { HStack { - Text("Menu Bar Items") + Text("Choose the items to manage") + .foregroundStyle(.secondary) Spacer() - Button("Refresh Menu Bar Items") { store.refresh() } - Button("Select All") { store.selectAll(true) }.disabled(store.items.isEmpty) - Button("Deselect All") { store.selectAll(false) }.disabled(store.items.isEmpty) + Button("Refresh") { store.refresh() } + Button("Select All") { store.selectAll(true) } + .disabled(store.items.isEmpty) + Button("Deselect All") { store.selectAll(false) } + .disabled(store.items.isEmpty) } if let message = store.iconCaptureMessage { Text(message).font(.caption).foregroundStyle(.secondary) } - if !permissions.accessibilityGranted { Text("Enable Accessibility to scan and activate menu bar items.").foregroundStyle(.secondary) } + if !permissions.accessibilityGranted { + Text("Enable Accessibility to scan and activate menu bar items.") + .foregroundStyle(.secondary) + } ForEach(store.items) { item in - Toggle(isOn: Binding(get: { item.isSelected }, set: { store.setSelected(item, selected: $0) })) { + Toggle(isOn: Binding( + get: { item.isSelected }, + set: { store.setSelected(item, selected: $0) } + )) { HStack { icon(for: item) VStack(alignment: .leading) { @@ -98,13 +125,24 @@ struct SettingsView: View { } .disabled(item.isAlwaysVisibleSystemItem) } - if permissions.accessibilityGranted && store.items.isEmpty { Text("No accessible right-side menu bar items were found. Some apps do not expose status items to Accessibility.").foregroundStyle(.secondary) } + if permissions.accessibilityGranted && store.items.isEmpty { + Text("No accessible right-side menu bar items were found. Some apps do not expose status items to Accessibility.") + .foregroundStyle(.secondary) + } + } + } + .formStyle(.grouped) + } + + private var permissionsSettings: some View { + Form { + Section("Permissions") { + permissionRow("Accessibility", granted: permissions.accessibilityGranted, open: permissions.openAccessibilitySettings, request: permissions.requestAccessibility) + permissionRow("Screen Recording", granted: permissions.screenRecordingGranted, open: permissions.openScreenRecordingSettings, request: permissions.requestScreenRecording) + Button("Refresh Permission Status") { permissions.refresh() } } } .formStyle(.grouped) - .frame(minWidth: 600, minHeight: 420) - .onAppear { permissions.refresh(); launchAtLogin.refresh(); store.refresh() } - .alert("OverflowBar", isPresented: Binding(get: { store.lastActivationError != nil }, set: { if !$0 { store.lastActivationError = nil } })) { Button("OK", role: .cancel) { store.lastActivationError = nil } } message: { Text(store.lastActivationError ?? "") } } private var appVersion: String { @@ -113,9 +151,13 @@ struct SettingsView: View { @ViewBuilder private func permissionRow(_ name: String, granted: Bool, open: @escaping () -> Void, request: @escaping () -> Void) -> some View { HStack { - Label("\(name): \(granted ? "Granted" : "Not Granted")", systemImage: granted ? "checkmark.circle.fill" : "exclamationmark.triangle.fill").foregroundStyle(granted ? .green : .orange) + Label("\(name): \(granted ? "Granted" : "Not Granted")", systemImage: granted ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") + .foregroundStyle(granted ? .green : .orange) Spacer() - if !granted { Button("Open \(name) Settings", action: open); Button("Request", action: request) } + if !granted { + Button("Open \(name) Settings", action: open) + Button("Request", action: request) + } } } @@ -127,7 +169,8 @@ struct SettingsView: View { .scaledToFit() .foregroundStyle(.primary) .frame(width: 24, height: 20) + } else { + Image(systemName: "menubar.rectangle").frame(width: 24) } - else { Image(systemName: "menubar.rectangle").frame(width: 24) } } } diff --git a/OverflowBarIcon.icon/Assets/Image 2.svg b/OverflowBarIcon.icon/Assets/Image 2.svg index 13708ee..010825f 100644 --- a/OverflowBarIcon.icon/Assets/Image 2.svg +++ b/OverflowBarIcon.icon/Assets/Image 2.svg @@ -1,11 +1,4 @@ - - - - - - - + + diff --git a/OverflowBarIcon.icon/Assets/Image.svg b/OverflowBarIcon.icon/Assets/Image.svg index 3f2c228..ff884d1 100644 --- a/OverflowBarIcon.icon/Assets/Image.svg +++ b/OverflowBarIcon.icon/Assets/Image.svg @@ -1,12 +1,4 @@ - - - - - - - - + + diff --git a/OverflowBarIcon.icon/icon.json b/OverflowBarIcon.icon/icon.json index 672489f..d32fec5 100644 --- a/OverflowBarIcon.icon/icon.json +++ b/OverflowBarIcon.icon/icon.json @@ -1,8 +1,8 @@ { "fill" : { "linear-gradient" : [ - "extended-gray:1.00000,1.00000", - "srgb:0.00000,0.94118,1.00000,1.00000" + "extended-gray:0.98000,1.00000", + "extended-gray:0.98000,1.00000" ], "orientation" : { "start" : { @@ -93,11 +93,11 @@ ], "shadow" : { "kind" : "neutral", - "opacity" : 0.5 + "opacity" : 0.25 }, "translucency" : { - "enabled" : true, - "value" : 0.5 + "enabled" : false, + "value" : 0 } } ], @@ -107,4 +107,4 @@ ], "squares" : "shared" } -} \ No newline at end of file +} diff --git a/assets/overflowbar-icon.png b/assets/overflowbar-icon.png index d180d0c..f6c784a 100644 Binary files a/assets/overflowbar-icon.png and b/assets/overflowbar-icon.png differ