mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 00:04:44 +08:00
Move feature enable switches to the top of Settings pages (#1144)
* Promote feature enable controls in Settings * Reuse the shared feature toggle label * Keep Search Scopes available when Applications is off * Restore search highlight on launcher settings lists
This commit is contained in:
@@ -55,6 +55,7 @@ for (const m of catalog.matchAll(/\.init\(\s*\.(\w+),\s*"((?:[^"\\]|\\.)*)"/g))
|
||||
const quoted = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const marked =
|
||||
source.includes(`SettingsRowTitle(.${anchor}, "${title}")`) ||
|
||||
new RegExp(`SettingsFeatureToggleLabel\\(\\s*anchor: \\.${anchor}, title: "${quoted}"`).test(source) ||
|
||||
// A `SettingsRow` renders the pill from its own title.
|
||||
new RegExp(`SettingsRow\\(\\s*title: "${quoted}",[\\s\\S]*?anchor: \\.${anchor}`).test(source) ||
|
||||
// A feature pane's master switch, rendered by `FeatureSwitchSection`.
|
||||
|
||||
@@ -3,6 +3,51 @@ import SwiftUI
|
||||
|
||||
// The few pieces more than one Settings pane or editor needs; everything else stays feature-owned.
|
||||
|
||||
/// The Settings sidebar tile, shared with the matching feature switches.
|
||||
struct SettingsTabIcon: View {
|
||||
let systemImage: String
|
||||
let tint: Color
|
||||
var size = Theme.Size.settingsSidebarGlyph + Theme.Spacing.xs * 2
|
||||
|
||||
var body: some View {
|
||||
let scale = size / (Theme.Size.settingsSidebarGlyph + Theme.Spacing.xs * 2)
|
||||
Image(systemName: systemImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(
|
||||
width: Theme.Size.settingsSidebarGlyph * scale,
|
||||
height: Theme.Size.settingsSidebarGlyph * scale
|
||||
)
|
||||
.foregroundStyle(tint)
|
||||
.padding(Theme.Spacing.xs * scale)
|
||||
.background(
|
||||
tint.opacity(0.1),
|
||||
in: RoundedRectangle(
|
||||
cornerRadius: Theme.Radius.thumbnail * scale, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsFeatureToggleLabel: View {
|
||||
let anchor: SettingsAnchor
|
||||
let title: String
|
||||
let subtitle: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Theme.Spacing.lg) {
|
||||
SettingsTabIcon(
|
||||
systemImage: anchor.tab.systemImage, tint: .accentColor,
|
||||
size: Theme.Size.settingsRowIcon * 1.5)
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xxs) {
|
||||
SettingsRowTitle(anchor, title)
|
||||
.fontWeight(.semibold)
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Not `LabeledContent`: its selectable text field eats the taps a `ShortcutRecorder` needs.
|
||||
struct SettingsRow<Icon: View, Trailing: View>: View {
|
||||
let title: String
|
||||
@@ -166,18 +211,33 @@ struct FeatureSwitchSection: View {
|
||||
var enableSubtitle: String?
|
||||
@Binding var isEnabled: Bool
|
||||
@Binding var showsInLauncher: Bool
|
||||
var showsIcon = false
|
||||
var showsHeader = true
|
||||
|
||||
var body: some View {
|
||||
if showsHeader {
|
||||
section
|
||||
} else {
|
||||
section.settingsAnchor(anchor)
|
||||
}
|
||||
}
|
||||
|
||||
private var section: some View {
|
||||
Section {
|
||||
Toggle(isOn: $isEnabled) {
|
||||
SettingsRowTitle(anchor, enableTitle)
|
||||
if let enableSubtitle { Text(enableSubtitle) }
|
||||
if showsIcon, let enableSubtitle {
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: anchor, title: enableTitle, subtitle: enableSubtitle)
|
||||
} else {
|
||||
SettingsRowTitle(anchor, enableTitle)
|
||||
if let enableSubtitle { Text(enableSubtitle) }
|
||||
}
|
||||
}
|
||||
Toggle("Show in launcher", isOn: $showsInLauncher)
|
||||
// The switch above stays live so the feature can always be turned back on.
|
||||
.settingsEnabled(isEnabled)
|
||||
} header: {
|
||||
SettingsSectionHeader(anchor)
|
||||
if showsHeader { SettingsSectionHeader(anchor) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ struct AISettingsView: View {
|
||||
return Form {
|
||||
Section {
|
||||
Toggle(isOn: $appSettings.aiEnabled) {
|
||||
SettingsRowTitle(.aiAI, "Enable AI")
|
||||
Text("Nothing is loaded or sent while it is off.")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .aiAI, title: "Enable AI",
|
||||
subtitle: "Nothing is loaded or sent while it is off.")
|
||||
}
|
||||
SettingsRow(
|
||||
title: "Providers", subtitle: providerSummary, anchor: .aiProviders
|
||||
|
||||
@@ -20,12 +20,13 @@ struct AppleShortcutsSettingsView: View {
|
||||
return Form {
|
||||
Section {
|
||||
Toggle(isOn: $settings.appleShortcutsEnabled) {
|
||||
SettingsRowTitle(.appleShortcutsAppleShortcuts, "Enable Apple Shortcuts")
|
||||
Text("Run your shortcuts from the launcher.")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .appleShortcutsAppleShortcuts,
|
||||
title: "Enable Apple Shortcuts",
|
||||
subtitle: "Run your shortcuts from the launcher.")
|
||||
}
|
||||
} header: {
|
||||
SettingsSectionHeader(.appleShortcutsAppleShortcuts)
|
||||
}
|
||||
.settingsAnchor(.appleShortcutsAppleShortcuts)
|
||||
|
||||
if settings.appleShortcutsEnabled {
|
||||
library
|
||||
|
||||
@@ -15,7 +15,9 @@ struct CalendarSettingsView: View {
|
||||
"Reads \(core.calendarCoordinator.span.possessivePhrase) events for join links. "
|
||||
+ "Nothing leaves this Mac.",
|
||||
isEnabled: enabledBinding,
|
||||
showsInLauncher: $settings.calendarShowInLauncher)
|
||||
showsInLauncher: $settings.calendarShowInLauncher,
|
||||
showsIcon: true,
|
||||
showsHeader: false)
|
||||
|
||||
Section {
|
||||
Picker(selection: $settings.calendarLauncherLimit) {
|
||||
|
||||
@@ -11,11 +11,12 @@ struct ClipboardSettingsView: View {
|
||||
return Form {
|
||||
Section {
|
||||
Toggle(isOn: $settings.clipboardEnabled) {
|
||||
SettingsRowTitle(.clipboardClipboard, "Enable Clipboard History")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .clipboardClipboard, title: "Enable Clipboard History",
|
||||
subtitle: "Keep copied items ready to reuse.")
|
||||
}
|
||||
} header: {
|
||||
SettingsSectionHeader(.clipboardClipboard)
|
||||
}
|
||||
.settingsAnchor(.clipboardClipboard)
|
||||
|
||||
FeatureCommandsSection(owner: .clipboard, anchor: .clipboardCommands)
|
||||
.settingsEnabled(settings.clipboardEnabled)
|
||||
|
||||
@@ -11,6 +11,8 @@ struct CommandsSettingsView: View {
|
||||
var body: some View {
|
||||
@Bindable var settings = settings
|
||||
return Form {
|
||||
LauncherCategorySwitchSection(kind: .command, anchor: .commandsCommands)
|
||||
|
||||
LauncherItemsSection(
|
||||
kind: .command,
|
||||
anchor: .commandsCommands,
|
||||
|
||||
@@ -28,7 +28,8 @@ struct ExtensionsSettingsView: View {
|
||||
isEnabled: Binding(
|
||||
get: { settings.extensionsEnabled },
|
||||
set: { core.extensionCoordinator.setExtensionsEnabled($0) }),
|
||||
showsInLauncher: $settings.extensionsShowInLauncher)
|
||||
showsInLauncher: $settings.extensionsShowInLauncher,
|
||||
showsIcon: true)
|
||||
|
||||
Group {
|
||||
install
|
||||
|
||||
@@ -8,12 +8,12 @@ struct FileSearchSettingsView: View {
|
||||
return Form {
|
||||
Section {
|
||||
Toggle(isOn: $settings.fileSearchEnabled) {
|
||||
SettingsRowTitle(.fileSearchFileSearch, "Enable File Search")
|
||||
Text("Uses the Spotlight index, only when you search.")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .fileSearchFileSearch, title: "Enable File Search",
|
||||
subtitle: "Uses the Spotlight index, only when you search.")
|
||||
}
|
||||
} header: {
|
||||
SettingsSectionHeader(.fileSearchFileSearch)
|
||||
}
|
||||
.settingsAnchor(.fileSearchFileSearch)
|
||||
|
||||
FeatureCommandsSection(owner: .fileSearch, anchor: .fileSearchCommands)
|
||||
.settingsEnabled(settings.fileSearchEnabled)
|
||||
|
||||
@@ -3,7 +3,9 @@ import SwiftUI
|
||||
struct ApplicationsSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
// Scopes first: they decide what gets indexed, so they read before the results.
|
||||
LauncherCategorySwitchSection(
|
||||
kind: .application, anchor: .applicationsApplications)
|
||||
|
||||
SearchScopesSection()
|
||||
|
||||
LauncherItemsSection(
|
||||
@@ -15,4 +17,5 @@ struct ApplicationsSettingsView: View {
|
||||
.settingsScrollTarget(.applications)
|
||||
.releasesFocusOnOutsideClick()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import SwiftUI
|
||||
|
||||
/// One category's Settings sections; never filters by visibility, so hidden rows stay listed.
|
||||
/// A category's master switch stays available while its list is disabled.
|
||||
struct LauncherCategorySwitchSection: View {
|
||||
let kind: AppEntry.Kind
|
||||
let anchor: SettingsAnchor
|
||||
|
||||
@Environment(VisibilityStore.self) private var visibility
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
isOn: Binding(
|
||||
get: { visibility.isKindEnabled(kind) },
|
||||
set: { visibility.setKindEnabled($0, for: kind) }
|
||||
)
|
||||
) {
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: anchor, title: "Enable \(anchor.title)",
|
||||
subtitle: "Off hides all of them and stops their shortcuts.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One category's Settings list; never filters by visibility, so hidden rows stay listed.
|
||||
struct LauncherItemsSection: View {
|
||||
let kind: AppEntry.Kind
|
||||
let anchor: SettingsAnchor
|
||||
@@ -19,29 +42,15 @@ struct LauncherItemsSection: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
Toggle(isOn: enabledBinding) {
|
||||
SettingsRowTitle(anchor, "Enable \(anchor.title)")
|
||||
Text("Off hides all of them and stops their shortcuts.")
|
||||
}
|
||||
} header: {
|
||||
SettingsSectionHeader(anchor)
|
||||
}
|
||||
|
||||
Section {
|
||||
SettingsFilterField(prompt: searchPrompt, query: $query)
|
||||
LauncherItemsList(
|
||||
entries: entries, query: query, isEnabled: visibility.isKindEnabled(kind))
|
||||
} header: {
|
||||
SettingsSectionHeader(anchor)
|
||||
}
|
||||
.settingsEnabled(visibility.isKindEnabled(kind))
|
||||
}
|
||||
|
||||
private var enabledBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { visibility.isKindEnabled(kind) },
|
||||
set: { visibility.setKindEnabled($0, for: kind) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The rows under a filter field, or what to say when there are none; shared by item panes.
|
||||
|
||||
@@ -4,6 +4,9 @@ import SwiftUI
|
||||
struct SystemSettingsSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
LauncherCategorySwitchSection(
|
||||
kind: .systemSettings, anchor: .systemSettingsSystemSettings)
|
||||
|
||||
LauncherItemsSection(
|
||||
kind: .systemSettings,
|
||||
anchor: .systemSettingsSystemSettings,
|
||||
|
||||
@@ -8,20 +8,25 @@ struct NotesSettingsView: View {
|
||||
return Form {
|
||||
Section {
|
||||
Toggle(isOn: $settings.notesEnabled) {
|
||||
SettingsRowTitle(.notesNotes, "Enable Notes")
|
||||
Text("Plain Markdown in a floating editor.")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .notesNotes, title: "Enable Notes",
|
||||
subtitle: "Plain Markdown in a floating editor.")
|
||||
}
|
||||
}
|
||||
.settingsAnchor(.notesNotes)
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $settings.notesRendersMarkdown) {
|
||||
SettingsRowTitle(.notesNotes, "Render Markdown")
|
||||
SettingsRowTitle(.notesOptions, "Render Markdown")
|
||||
Text("Formats as you type.")
|
||||
}
|
||||
.settingsEnabled(settings.notesEnabled)
|
||||
Toggle(isOn: $settings.notesShowsFormattingBar) {
|
||||
SettingsRowTitle(.notesNotes, "Show Formatting Bar")
|
||||
SettingsRowTitle(.notesOptions, "Show Formatting Bar")
|
||||
}
|
||||
.settingsEnabled(settings.notesEnabled && settings.notesRendersMarkdown)
|
||||
} header: {
|
||||
SettingsSectionHeader(.notesNotes)
|
||||
SettingsSectionHeader(.notesOptions)
|
||||
}
|
||||
|
||||
FeatureCommandsSection(owner: .notes, anchor: .notesCommands)
|
||||
|
||||
@@ -20,8 +20,9 @@ struct QuickActionsSettingsView: View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(isOn: enabledBinding) {
|
||||
SettingsRowTitle(.quickActionsQuickActions, "Enable Quick Actions")
|
||||
Text("Act on selected text. Nothing is read until you press a shortcut.")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .quickActionsQuickActions, title: "Enable Quick Actions",
|
||||
subtitle: "Act on selected text. Nothing is read until you press a shortcut.")
|
||||
}
|
||||
if appSettings.quickActionsEnabled, !isTrusted {
|
||||
// Every shortcut fails without it; better said here than found one press later.
|
||||
|
||||
@@ -15,8 +15,11 @@ struct QuicklinksSettingsView: View {
|
||||
FeatureSwitchSection(
|
||||
anchor: .quicklinksQuicklinks,
|
||||
enableTitle: "Enable quicklinks",
|
||||
enableSubtitle: "Open saved links and searches from the launcher.",
|
||||
isEnabled: $settings.quicklinksEnabled,
|
||||
showsInLauncher: $settings.quicklinksShowInLauncher)
|
||||
showsInLauncher: $settings.quicklinksShowInLauncher,
|
||||
showsIcon: true,
|
||||
showsHeader: false)
|
||||
|
||||
Group {
|
||||
if !store.isAvailable { storageNotice }
|
||||
|
||||
@@ -10,12 +10,12 @@ struct NavigationSettingsView: View {
|
||||
return Form {
|
||||
Section {
|
||||
Toggle(isOn: $settings.navigationEnabled) {
|
||||
SettingsRowTitle(.navigationNavigation, "Enable navigation")
|
||||
Text("Switch windows and search menu bar items.")
|
||||
SettingsFeatureToggleLabel(
|
||||
anchor: .navigationNavigation, title: "Enable navigation",
|
||||
subtitle: "Switch windows and search menu bar items.")
|
||||
}
|
||||
} header: {
|
||||
SettingsSectionHeader(.navigationNavigation)
|
||||
}
|
||||
.settingsAnchor(.navigationNavigation)
|
||||
|
||||
// No "show in launcher" switch: the per-command checkboxes below already are one.
|
||||
FeatureCommandsSection(
|
||||
|
||||
@@ -57,6 +57,7 @@ extension SettingsAnchor {
|
||||
static let fileSearchIgnorePatterns = Self(tab: .fileSearch, title: "Ignore Patterns")
|
||||
|
||||
static let notesNotes = Self(tab: .notes, title: "Notes")
|
||||
static let notesOptions = Self(tab: .notes, title: "Options")
|
||||
static let notesCommands = Self(tab: .notes, title: "Commands")
|
||||
|
||||
static let snippetsSnippets = Self(tab: .snippets, title: "Snippets")
|
||||
|
||||
@@ -176,12 +176,12 @@ enum SettingsSearchCatalog {
|
||||
|
||||
private static let applications: [SettingsSearchEntry] = [
|
||||
.init(pane: .applications, keywords: ["apps", "index", "launcher"]),
|
||||
.init(
|
||||
group: .applicationsSearchScopes, "Search Scopes",
|
||||
keywords: ["folders", "indexed", "locations", "add folder"]),
|
||||
.init(
|
||||
.applicationsApplications, "Enable Applications",
|
||||
keywords: ["hide apps", "visibility"]),
|
||||
.init(
|
||||
group: .applicationsSearchScopes, "Search Scopes",
|
||||
keywords: ["folders", "indexed", "locations", "add folder"]),
|
||||
.init(
|
||||
group: .applicationsApplications, "Aliases and shortcuts",
|
||||
keywords: ["alias", "hotkey", "per app", "hide"])
|
||||
@@ -349,10 +349,10 @@ enum SettingsSearchCatalog {
|
||||
.notesNotes, "Enable Notes",
|
||||
keywords: ["markdown", "scratchpad"]),
|
||||
.init(
|
||||
.notesNotes, "Render Markdown",
|
||||
.notesOptions, "Render Markdown",
|
||||
keywords: ["markdown", "formatting", "preview", "raw", "source"]),
|
||||
.init(
|
||||
.notesNotes, "Show Formatting Bar",
|
||||
.notesOptions, "Show Formatting Bar",
|
||||
keywords: ["toolbar", "format bar", "buttons", "bold", "heading", "markdown"]),
|
||||
.init(
|
||||
group: .notesCommands, "Notes commands",
|
||||
|
||||
@@ -117,21 +117,3 @@ private struct CenteredLabelStyle: LabelStyle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph on a tinted tile, so every row's icon reads at one weight whatever its symbol's shape.
|
||||
private struct SettingsTabIcon: View {
|
||||
let systemImage: String
|
||||
let tint: Color
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: systemImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: Theme.Size.settingsSidebarGlyph, height: Theme.Size.settingsSidebarGlyph)
|
||||
.foregroundStyle(tint)
|
||||
.padding(Theme.Spacing.xs)
|
||||
.background(
|
||||
tint.opacity(0.1),
|
||||
in: RoundedRectangle(cornerRadius: Theme.Radius.thumbnail, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ struct SnippetsSettingsView: View {
|
||||
isEnabled: Binding(
|
||||
get: { settings.snippetsEnabled },
|
||||
set: { core.snippetCoordinator.setSnippetsEnabled($0) }),
|
||||
showsInLauncher: $settings.snippetsShowInLauncher)
|
||||
showsInLauncher: $settings.snippetsShowInLauncher,
|
||||
showsIcon: true,
|
||||
showsHeader: false)
|
||||
|
||||
if settings.snippetsEnabled, core.snippetListener.status == .needsAccessibility {
|
||||
Section {
|
||||
|
||||
@@ -3,6 +3,9 @@ import SwiftUI
|
||||
struct SystemActionsSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
LauncherCategorySwitchSection(
|
||||
kind: .systemAction, anchor: .systemActionsSystemActions)
|
||||
|
||||
LauncherItemsSection(
|
||||
kind: .systemAction,
|
||||
anchor: .systemActionsSystemActions,
|
||||
|
||||
@@ -15,7 +15,9 @@ struct WindowManagementSettingsView: View {
|
||||
enableTitle: "Enable window management",
|
||||
enableSubtitle: "Moves the last window you used. Needs Accessibility.",
|
||||
isEnabled: $settings.windowManagementEnabled,
|
||||
showsInLauncher: $settings.windowManagementShowInLauncher)
|
||||
showsInLauncher: $settings.windowManagementShowInLauncher,
|
||||
showsIcon: true,
|
||||
showsHeader: false)
|
||||
|
||||
Group {
|
||||
options
|
||||
|
||||
@@ -11,9 +11,10 @@ earliest scope wins).
|
||||
`LauncherList.rows`, in that order.
|
||||
- **A category's switch is a master switch, not a list filter.** `VisibilityStore.isKindEnabled` gates
|
||||
`orderedResults` *and* `HotKeyManager.perform`, so `Enable Applications` off stops the per-app chords
|
||||
as well as the rows — the guard sits in the one dispatch funnel, the way each feature switch already
|
||||
guards its own. The per-item checkbox beside it is the narrow tool: it hides one row and leaves that
|
||||
row's shortcut firing, and **Hide from Search** in the ⌘K menu ticks that same checkbox off for the
|
||||
as well as the rows. Its Settings switch stays available while the application
|
||||
list disables beneath it — the guard sits in the one dispatch funnel, the way each feature switch
|
||||
already guards its own. The per-item checkbox beside it is the narrow tool: it hides one row and
|
||||
leaves that row's shortcut firing, and **Hide from Search** in the ⌘K menu ticks that checkbox off for the
|
||||
kinds whose pane can tick it back on. A new category must be wired into
|
||||
`VisibilityStore.allowsHotKey`, or its chords keep running while its pane reads off.
|
||||
- **One command, one pane, one switch.** `SettingsTab.ownedCommands` is the whole table of which pane
|
||||
|
||||
@@ -660,6 +660,8 @@ system-drawn and a pane reads exactly as macOS System Settings does.
|
||||
titlebar they were tuned for. Never hand-draw a header band; a main surface takes the system's
|
||||
material, not `glassEffect`.
|
||||
- `SettingsComponents.swift` holds only what more than one pane or editor needs: **`SettingsRow`**,
|
||||
**`SettingsTabIcon`** (the sidebar tile reused by feature switches),
|
||||
**`SettingsFeatureToggleLabel`** (the icon, title and subtitle of a feature's master switch),
|
||||
**`FeatureSwitchSection`** (a feature's master switch plus its launcher-visibility companion),
|
||||
**`SettingsFilterField`** (the filter row above a long list), **`launcherVisibilityHelp()`**, and the
|
||||
Settings editor header, fields and surface. `ModalActionButtonStyle.swift` keeps every borderless surface's actions on one
|
||||
|
||||
Reference in New Issue
Block a user