Release 8.0.1 dark glass themes
41 files modified
7 files added
| New file |
| | |
| | | # CODEGRAPH |
| | | |
| | | ## Stable Modules |
| | | |
| | | - App entry and overlay state: `src/Apptag/ContentView.swift` |
| | | - Reads user defaults for AppGrid display, theme, tag navigation, usage tips, and hotkeys. |
| | | - Owns the AppGrid full-window background rendering. |
| | | - Reuses the last complete AppLibrary snapshot before showing startup loading UI. |
| | | - Calls `AppGridCollectionView` for AppKit grid rendering. |
| | | |
| | | - App library snapshot assembly: `src/Apptag/AppLibraryController.swift` |
| | | - Scans apps, reconciles tags, runs SmartStart when needed, and assembles AppGrid/Quick Search snapshots. |
| | | - Keeps the most recent in-process `AppLibrarySnapshot` so newly-created overlay views can render immediately while background refresh catches up. |
| | | |
| | | - AppGrid collection renderer: `src/Apptag/AppGridCollectionView.swift` |
| | | - AppKit `NSCollectionView` implementation for grouped app layout, drag/drop, app ordering, bubbles, and usage tips overlay. |
| | | - Should not own full-screen AppGrid theme gradients; it receives theme only for derived glass readability. |
| | | |
| | | - AppGrid theme model: `src/Apptag/AppGridTheme.swift` |
| | | - Central source for theme IDs, localization keys, preview swatches, full-screen background gradients, derived glass tone, and edit-mode contrast tokens. |
| | | - Persistent key: `appGridThemeID`. |
| | | |
| | | - Settings surface: `src/Apptag/PreferencesView.swift` |
| | | - Custom SwiftUI settings tabs. |
| | | - Theme tab writes only `appGridThemeID`; it must not mutate tags, app ordering, or category data. |
| | | - Can be opened with an initial target tab from AppKit/overlay notifications. |
| | | |
| | | - Defaults and migrations: `src/Apptag/AppDefaults.swift` |
| | | - Registers first-run defaults. |
| | | - Migrates legacy `useDarkAppGrid=true` to `appGridThemeID=deepBlue` when no new theme key exists. |
| | | |
| | | - Localization: `src/Apptag/Localization/*.json` |
| | | - 29 language JSON files. |
| | | - New user-visible settings keys must be present in all files. |
| | | |
| | | ## QA Entrypoints |
| | | |
| | | - `src/Scripts/theme_settings_qa.sh` |
| | | - Verifies 8 theme cases, Theme tab, legacy dark-grid UI removal, AppGrid-only rendering boundary, edit-mode default-theme override, migration path, and 29-language keys. |
| | | - `src/Scripts/appgrid_startup_loading_qa.sh` |
| | | - Verifies AppGrid startup loading uses last-snapshot reuse and delayed spinner instead of immediately exposing empty-state loading. |
| | | - `src/Scripts/macos14_availability_typecheck_qa.sh` |
| | | - Typechecks for macOS 14.0 compatibility. |
| | | - `src/Scripts/macos14_build_metadata_qa.sh` |
| | | - Verifies build metadata and deployment target. |
| | | - `src/Scripts/usage_tips_qa.sh` |
| | | - Verifies usage tips overlay and 29-language coverage. |
| | | - `src/Scripts/tag_navigation_hover_scroll_qa.sh` |
| | | - Verifies tag hover scroll semantics remain guarded. |
| | | - `src/Scripts/tag_double_click_preferences_qa.sh` |
| | | - Verifies double-clicking a tag navigation item opens Preferences on the Tags tab while preserving single-click activation, hover scroll, and long-press reorder wiring. |
| | | |
| | | ## Protected Behavior |
| | | |
| | | - Theme changes are visual preferences and must not change tag data, app ordering, notes, SmartStart/category scheme, quick search, drag/drop behavior, or import/export data. |
| | | - AppGrid startup and repeated overlay creation must not immediately show a spinner just because a new `ContentView` starts with `allApps.isEmpty`; it should first reuse the last complete in-process snapshot and only show loading after a short delay if no snapshot is available. |
| | | - Editing mode uses a temporary runtime theme override: regardless of the stored `appGridThemeID`, editing renders as the default light glass AppGrid and restores the stored theme when edit mode exits. |
| | | - Individual AppGrid containers must remain a consistent translucent glass surface. They may use a derived light/dark glass tone for readability, but must not receive per-theme internal gradients. |
| | | - Edit mode controls must use the runtime rendered theme, not the stored theme, so editing stays visually identical to the default light AppGrid. |
| | | - Deep Blue and Black use dark glass. Bright Pink, Purple, Green, Blue, and Colorful themes use light glass to keep the theme bright and readable. |
| | | - The default theme preserves the original light AppGrid background. |
| | | - Legacy users with `useDarkAppGrid=true` must land on the `deepBlue` theme. |
| | | - Tag navigation single-click must keep immediate scroll behavior. Hover must keep guarded auto-scroll. Long-press must keep tag reorder behavior. Double-click may open Preferences on the Tags tab but must not replace those existing behaviors. |
| | | |
| | | Last updated: 2026-06-24, tag double-click opens Tags settings. |
| | |
| | | static let launchAtLogin = true |
| | | static let showUncommonAppBubbles = false |
| | | static let hideUsageTips = false |
| | | static let appGridThemeID = AppGridTheme.defaultLight.rawValue |
| | | static let useAppKitTagNavigation = true |
| | | |
| | | static func register() { |
| | | migrateAppGridThemePreferenceIfNeeded() |
| | | UserDefaults.standard.register(defaults: [ |
| | | "tagFontSize": tagFontSize, |
| | | "iconSize": iconSize, |
| | |
| | | "launchAtLogin": launchAtLogin, |
| | | "showUncommonAppBubbles": showUncommonAppBubbles, |
| | | "hideUsageTips": hideUsageTips, |
| | | AppGridTheme.storageKey: appGridThemeID, |
| | | "useAppKitTagNavigation": useAppKitTagNavigation, |
| | | "skipTagRemovalDropConfirm": false, |
| | | "skipUncategorizedDropConfirm": false, |
| | |
| | | return UserDefaults.standard.persistentDomain(forName: domain)?[key] != nil |
| | | } |
| | | |
| | | private static func migrateAppGridThemePreferenceIfNeeded() { |
| | | let defaults = UserDefaults.standard |
| | | guard !hasStoredValue(for: AppGridTheme.storageKey), |
| | | hasStoredValue(for: "useDarkAppGrid"), |
| | | defaults.bool(forKey: "useDarkAppGrid") |
| | | else { return } |
| | | defaults.set(AppGridTheme.deepBlue.rawValue, forKey: AppGridTheme.storageKey) |
| | | } |
| | | |
| | | private static func removeShortcutCustomizationDefaults() { |
| | | let defaults = UserDefaults.standard |
| | | [ |
| | |
| | | let displayMode: String |
| | | let iconSize: CGFloat |
| | | let showNames: Bool |
| | | let appGridTheme: AppGridTheme |
| | | let bubbleDisabled: Bool |
| | | let showUncommonAppBubbles: Bool |
| | | let highlightedGroupName: String? |
| | |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: showNames, |
| | | appGridTheme: appGridTheme, |
| | | bubbleDisabled: bubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | highlightedGroupName: highlightedGroupName, |
| | |
| | | var displayMode = AppDefaults.displayMode |
| | | var iconSize: CGFloat = AppDefaults.iconSize |
| | | var showNames = true |
| | | var appGridTheme = AppGridTheme.fallback |
| | | private var externalBubbleDisabled = false |
| | | private var scrollBubbleDisabled = false |
| | | var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | |
| | | displayMode: String, |
| | | iconSize: CGFloat, |
| | | showNames: Bool, |
| | | appGridTheme: AppGridTheme, |
| | | bubbleDisabled: Bool, |
| | | showUncommonAppBubbles: Bool, |
| | | highlightedGroupName: String?, |
| | |
| | | self.displayMode = displayMode |
| | | self.iconSize = iconSize |
| | | self.showNames = showNames |
| | | self.appGridTheme = appGridTheme |
| | | self.externalBubbleDisabled = bubbleDisabled |
| | | self.showUncommonAppBubbles = showUncommonAppBubbles |
| | | self.highlightedGroupName = highlightedGroupName |
| | |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: showNames, |
| | | appGridTheme: appGridTheme, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | bottomContentPadding: self.bottomContentPadding, |
| | | contentRevision: contentRevision |
| | |
| | | displayMode: String, |
| | | iconSize: CGFloat, |
| | | showNames: Bool, |
| | | appGridTheme: AppGridTheme, |
| | | showUncommonAppBubbles: Bool, |
| | | bottomContentPadding: CGFloat, |
| | | contentRevision: Int |
| | |
| | | displayMode, |
| | | "\(Int(iconSize.rounded()))", |
| | | showNames ? "names" : "nonames", |
| | | "theme=\(appGridTheme.rawValue)", |
| | | showUncommonAppBubbles ? "uncommon" : "allbubbles", |
| | | "bottom=\(Int(bottomContentPadding.rounded()))", |
| | | colorPart, |
| | |
| | | |
| | | func applyCoordinatorUpdate() { |
| | | guard let coordinator else { return } |
| | | applyAppearance() |
| | | if coordinator.needsReload { |
| | | coordinator.needsReload = false |
| | | collectionView.reloadData() |
| | |
| | | lastReportedBoundsOrigin = origin |
| | | } |
| | | return didScroll |
| | | } |
| | | |
| | | private func applyAppearance() { |
| | | let usesDarkGlass = coordinator?.appGridTheme.usesDarkGlass == true |
| | | layer?.backgroundColor = NSColor.clear.cgColor |
| | | collectionView.appearance = usesDarkGlass ? NSAppearance(named: .darkAqua) : nil |
| | | } |
| | | } |
| | | |
| | |
| | | x += 5 + spacing |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | private final class AppGridContainerCollectionLayout: NSCollectionViewLayout { |
| | |
| | | if displayStyle.usesCardSurface { |
| | | let rect = bounds.insetBy(dx: 0.5, dy: 0.5) |
| | | let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14) |
| | | cardSurfaceColor().setFill() |
| | | path.fill() |
| | | if coordinator.isColoredContainerMode || isColorlessActive { |
| | | tagColor.withAlphaComponent(0.30).setFill() |
| | | if coordinator.appGridTheme.usesDarkGlass { |
| | | drawDarkCardSurface( |
| | | path: path, |
| | | tagColor: tagColor, |
| | | isActive: isHovered || isNavigationHighlighted, |
| | | isTinted: coordinator.isColoredContainerMode || isColorlessActive |
| | | ) |
| | | } else { |
| | | cardSurfaceColor().setFill() |
| | | path.fill() |
| | | if coordinator.isColoredContainerMode || isColorlessActive { |
| | | tagColor.withAlphaComponent(0.30).setFill() |
| | | path.fill() |
| | | } |
| | | NSColor.labelColor.withAlphaComponent(0.08).setStroke() |
| | | path.lineWidth = 1 |
| | | path.stroke() |
| | | } |
| | | NSColor.labelColor.withAlphaComponent(0.08).setStroke() |
| | | path.lineWidth = 1 |
| | | path.stroke() |
| | | } |
| | | |
| | | drawHeader(title: group.name, displayStyle: displayStyle) |
| | |
| | | && (isHovered || isNavigationHighlighted) |
| | | let shouldShadow = coordinator.displayStyle.usesCardSurface |
| | | && ((coordinator.isColoredContainerMode && (isHovered || isNavigationHighlighted)) || isColorlessActive) |
| | | if coordinator.appGridTheme.usesDarkGlass && coordinator.displayStyle.usesCardSurface { |
| | | let tagColor = TagColor.nsColor(for: coordinator.tagColors[group?.name ?? ""] ?? 0) |
| | | let active = isHovered || isNavigationHighlighted |
| | | layer?.shadowColor = active |
| | | ? tagColor.withAlphaComponent(0.82).cgColor |
| | | : NSColor.black.withAlphaComponent(0.72).cgColor |
| | | layer?.shadowOpacity = active ? 0.50 : 0.28 |
| | | layer?.shadowRadius = active ? 24 : 14 |
| | | layer?.shadowOffset = NSSize(width: 0, height: active ? -8 : -4) |
| | | return |
| | | } |
| | | layer?.shadowColor = NSColor.black.cgColor |
| | | layer?.shadowOpacity = shouldShadow ? 0.22 : 0 |
| | | layer?.shadowRadius = shouldShadow ? 8 : 0 |
| | |
| | | return NSColor.white.withAlphaComponent(0.62) |
| | | } |
| | | |
| | | private func drawDarkCardSurface( |
| | | path: NSBezierPath, |
| | | tagColor: NSColor, |
| | | isActive: Bool, |
| | | isTinted: Bool |
| | | ) { |
| | | NSGraphicsContext.saveGraphicsState() |
| | | if isActive { |
| | | let glow = NSShadow() |
| | | glow.shadowColor = tagColor.withAlphaComponent(0.36) |
| | | glow.shadowBlurRadius = 28 |
| | | glow.shadowOffset = NSSize(width: 0, height: -8) |
| | | glow.set() |
| | | } |
| | | NSColor(calibratedRed: 0.12, green: 0.18, blue: 0.25, alpha: 0.58).setFill() |
| | | path.fill() |
| | | NSGraphicsContext.restoreGraphicsState() |
| | | |
| | | if isTinted { |
| | | tagColor.withAlphaComponent(isActive ? 0.18 : 0.10).setFill() |
| | | path.fill() |
| | | } |
| | | |
| | | NSColor.white.withAlphaComponent(isActive ? 0.08 : 0.045).setFill() |
| | | path.fill() |
| | | |
| | | let strokeColor = isActive |
| | | ? tagColor.withAlphaComponent(0.72) |
| | | : NSColor.white.withAlphaComponent(0.18) |
| | | strokeColor.setStroke() |
| | | path.lineWidth = isActive ? 1.4 : 1.0 |
| | | path.stroke() |
| | | } |
| | | |
| | | private func drawHeader(title: String, displayStyle: AppGridCollectionDisplayMode) { |
| | | let horizontalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0 |
| | | let verticalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0 |
| | |
| | | width: max(1, bounds.width - horizontalInset * 2), |
| | | height: AppGridCollectionMetrics.headerHeight |
| | | ) |
| | | let isDarkGrid = coordinator?.appGridTheme.usesDarkGlass == true |
| | | let headerTextColor = isDarkGrid |
| | | ? NSColor.white.withAlphaComponent(0.74) |
| | | : NSColor.secondaryLabelColor |
| | | let attributes: [NSAttributedString.Key: Any] = [ |
| | | .font: NSFont.systemFont(ofSize: 18, weight: .semibold), |
| | | .foregroundColor: NSColor.secondaryLabelColor, |
| | | .foregroundColor: headerTextColor, |
| | | .paragraphStyle: centeredParagraph(lineBreak: .byTruncatingMiddle) |
| | | ] |
| | | let titleSize = title.size(withAttributes: attributes) |
| | |
| | | height: headerRect.height - 6 |
| | | ) |
| | | let lineY = headerRect.midY |
| | | NSColor.secondaryLabelColor.withAlphaComponent(0.25).setStroke() |
| | | (isDarkGrid ? NSColor.white : NSColor.secondaryLabelColor) |
| | | .withAlphaComponent(isDarkGrid ? 0.16 : 0.25) |
| | | .setStroke() |
| | | let leftLine = NSBezierPath() |
| | | leftLine.move(to: NSPoint(x: headerRect.minX, y: lineY)) |
| | | leftLine.line(to: NSPoint(x: max(headerRect.minX, titleRect.minX - 2), y: lineY)) |
| New file |
| | |
| | | import SwiftUI |
| | | import AppKit |
| | | |
| | | enum AppGridTheme: String, CaseIterable, Identifiable { |
| | | case defaultLight |
| | | case deepBlue |
| | | case black |
| | | case pink |
| | | case purple |
| | | case green |
| | | case blue |
| | | case colorful |
| | | |
| | | static let storageKey = "appGridThemeID" |
| | | static let fallback = AppGridTheme.defaultLight |
| | | |
| | | var id: String { rawValue } |
| | | |
| | | init(storedID: String) { |
| | | self = AppGridTheme(rawValue: storedID) ?? .fallback |
| | | } |
| | | |
| | | var titleKey: String { |
| | | switch self { |
| | | case .defaultLight: return "theme.default" |
| | | case .deepBlue: return "theme.deepBlue" |
| | | case .black: return "theme.black" |
| | | case .pink: return "theme.pink" |
| | | case .purple: return "theme.purple" |
| | | case .green: return "theme.green" |
| | | case .blue: return "theme.blue" |
| | | case .colorful: return "theme.colorful" |
| | | } |
| | | } |
| | | |
| | | var isDefaultLight: Bool { |
| | | self == .defaultLight |
| | | } |
| | | |
| | | var usesVisualEffectBackdrop: Bool { |
| | | self != .black |
| | | } |
| | | |
| | | var isPureBlackBackground: Bool { |
| | | self == .black |
| | | } |
| | | |
| | | var usesDarkGlass: Bool { |
| | | switch self { |
| | | case .deepBlue, .black: |
| | | return true |
| | | case .defaultLight, .pink, .purple, .green, .blue, .colorful: |
| | | return false |
| | | } |
| | | } |
| | | |
| | | var material: NSVisualEffectView.Material { |
| | | usesDarkGlass ? .underWindowBackground : .hudWindow |
| | | } |
| | | |
| | | var backgroundDimmingOpacity: Double { |
| | | switch self { |
| | | case .deepBlue: |
| | | return 0.20 |
| | | case .defaultLight, .black, .pink, .purple, .green, .blue, .colorful: |
| | | return 0 |
| | | } |
| | | } |
| | | |
| | | var editPrimaryTextColor: Color { |
| | | usesDarkGlass ? Color.white : Color(red: 0.08, green: 0.10, blue: 0.13) |
| | | } |
| | | |
| | | var editSecondaryTextColor: Color { |
| | | usesDarkGlass |
| | | ? Color.white.opacity(0.78) |
| | | : Color(red: 0.10, green: 0.13, blue: 0.17).opacity(0.78) |
| | | } |
| | | |
| | | var editTertiaryTextColor: Color { |
| | | usesDarkGlass |
| | | ? Color.white.opacity(0.58) |
| | | : Color(red: 0.10, green: 0.13, blue: 0.17).opacity(0.56) |
| | | } |
| | | |
| | | var editDividerColor: Color { |
| | | usesDarkGlass ? Color.white.opacity(0.22) : Color.black.opacity(0.16) |
| | | } |
| | | |
| | | var editToolbarSurfaceColor: Color { |
| | | if isPureBlackBackground { |
| | | return Color.black.opacity(0.82) |
| | | } |
| | | return usesDarkGlass ? Color.black.opacity(0.54) : Color.white.opacity(0.56) |
| | | } |
| | | |
| | | var editControlSurfaceColor: Color { |
| | | usesDarkGlass ? Color.white.opacity(0.18) : Color.white.opacity(0.72) |
| | | } |
| | | |
| | | var editControlStrokeColor: Color { |
| | | usesDarkGlass ? Color.white.opacity(0.38) : Color.black.opacity(0.18) |
| | | } |
| | | |
| | | var editInactiveIndicatorColor: Color { |
| | | usesDarkGlass ? Color.white.opacity(0.42) : Color.black.opacity(0.36) |
| | | } |
| | | |
| | | var editDisabledTextColor: Color { |
| | | usesDarkGlass |
| | | ? Color.white.opacity(0.70) |
| | | : Color(red: 0.10, green: 0.13, blue: 0.17).opacity(0.58) |
| | | } |
| | | |
| | | var editDisabledSurfaceColor: Color { |
| | | usesDarkGlass ? Color.white.opacity(0.10) : Color.black.opacity(0.07) |
| | | } |
| | | |
| | | var editButtonShadowColor: Color { |
| | | usesDarkGlass ? Color.black.opacity(0.28) : Color.black.opacity(0.10) |
| | | } |
| | | |
| | | var editAccentColor: Color { |
| | | switch self { |
| | | case .defaultLight: |
| | | return Color.accentColor |
| | | case .deepBlue: |
| | | return Color(red: 0.26, green: 0.68, blue: 1.00) |
| | | case .black: |
| | | return Color(red: 0.10, green: 0.52, blue: 1.00) |
| | | case .pink: |
| | | return Color(red: 0.92, green: 0.20, blue: 0.58) |
| | | case .purple: |
| | | return Color(red: 0.50, green: 0.24, blue: 0.92) |
| | | case .green: |
| | | return Color(red: 0.12, green: 0.62, blue: 0.24) |
| | | case .blue: |
| | | return Color(red: 0.08, green: 0.42, blue: 0.96) |
| | | case .colorful: |
| | | return Color(red: 0.00, green: 0.58, blue: 0.92) |
| | | } |
| | | } |
| | | |
| | | var editConfirmForegroundColor: Color { |
| | | Color.white |
| | | } |
| | | |
| | | var backgroundBaseColors: [Color] { |
| | | switch self { |
| | | case .defaultLight: |
| | | return [ |
| | | Color.white.opacity(0.16), |
| | | Color.white.opacity(0.08) |
| | | ] |
| | | case .deepBlue: |
| | | return [ |
| | | Color(red: 0.02, green: 0.04, blue: 0.09).opacity(0.96), |
| | | Color(red: 0.04, green: 0.09, blue: 0.18).opacity(0.94), |
| | | Color(red: 0.02, green: 0.20, blue: 0.22).opacity(0.88) |
| | | ] |
| | | case .black: |
| | | return [ |
| | | Color.black, |
| | | Color.black |
| | | ] |
| | | case .pink: |
| | | return [ |
| | | Color(red: 1.00, green: 0.86, blue: 0.94), |
| | | Color(red: 1.00, green: 0.57, blue: 0.78), |
| | | Color(red: 1.00, green: 0.82, blue: 0.66) |
| | | ] |
| | | case .purple: |
| | | return [ |
| | | Color(red: 0.83, green: 0.75, blue: 1.00), |
| | | Color(red: 0.58, green: 0.34, blue: 0.95), |
| | | Color(red: 0.96, green: 0.72, blue: 1.00) |
| | | ] |
| | | case .green: |
| | | return [ |
| | | Color(red: 0.80, green: 0.96, blue: 0.58), |
| | | Color(red: 0.22, green: 0.78, blue: 0.36), |
| | | Color(red: 0.66, green: 0.92, blue: 0.44) |
| | | ] |
| | | case .blue: |
| | | return [ |
| | | Color(red: 0.70, green: 0.91, blue: 1.00), |
| | | Color(red: 0.22, green: 0.58, blue: 1.00), |
| | | Color(red: 0.45, green: 0.96, blue: 1.00) |
| | | ] |
| | | case .colorful: |
| | | return [ |
| | | Color(red: 0.10, green: 0.78, blue: 1.00), |
| | | Color(red: 0.34, green: 0.96, blue: 0.94), |
| | | Color(red: 0.73, green: 0.78, blue: 1.00), |
| | | Color(red: 1.00, green: 0.58, blue: 0.94) |
| | | ] |
| | | } |
| | | } |
| | | |
| | | var backgroundAccentColors: [Color] { |
| | | switch self { |
| | | case .defaultLight: |
| | | return [Color.clear, Color.clear] |
| | | case .deepBlue: |
| | | return [ |
| | | Color(red: 0.48, green: 0.35, blue: 0.88).opacity(0.18), |
| | | Color.clear, |
| | | Color(red: 0.00, green: 0.78, blue: 0.86).opacity(0.16) |
| | | ] |
| | | case .black: |
| | | return [ |
| | | Color.clear, |
| | | Color.clear |
| | | ] |
| | | case .pink: |
| | | return [ |
| | | Color.white.opacity(0.28), |
| | | Color(red: 1.00, green: 0.38, blue: 0.74).opacity(0.24), |
| | | Color(red: 1.00, green: 0.72, blue: 0.42).opacity(0.18) |
| | | ] |
| | | case .purple: |
| | | return [ |
| | | Color.white.opacity(0.20), |
| | | Color(red: 0.43, green: 0.22, blue: 0.92).opacity(0.28), |
| | | Color(red: 1.00, green: 0.52, blue: 0.92).opacity(0.18) |
| | | ] |
| | | case .green: |
| | | return [ |
| | | Color.white.opacity(0.22), |
| | | Color(red: 0.10, green: 0.62, blue: 0.22).opacity(0.20), |
| | | Color(red: 0.70, green: 1.00, blue: 0.54).opacity(0.22) |
| | | ] |
| | | case .blue: |
| | | return [ |
| | | Color.white.opacity(0.20), |
| | | Color(red: 0.00, green: 0.40, blue: 1.00).opacity(0.24), |
| | | Color(red: 0.00, green: 0.90, blue: 1.00).opacity(0.18) |
| | | ] |
| | | case .colorful: |
| | | return [ |
| | | Color.white.opacity(0.16), |
| | | Color(red: 0.00, green: 0.78, blue: 1.00).opacity(0.22), |
| | | Color(red: 1.00, green: 0.45, blue: 0.95).opacity(0.22), |
| | | Color(red: 0.35, green: 1.00, blue: 0.92).opacity(0.18) |
| | | ] |
| | | } |
| | | } |
| | | |
| | | var previewColors: [Color] { |
| | | switch self { |
| | | case .defaultLight: |
| | | return [Color.white, Color(red: 0.88, green: 0.90, blue: 0.92)] |
| | | case .deepBlue: |
| | | return [Color(red: 0.03, green: 0.08, blue: 0.16), Color(red: 0.02, green: 0.24, blue: 0.28)] |
| | | case .black: |
| | | return [Color.black, Color.black] |
| | | case .pink: |
| | | return [Color(red: 1.00, green: 0.86, blue: 0.94), Color(red: 1.00, green: 0.57, blue: 0.78)] |
| | | case .purple: |
| | | return [Color(red: 0.83, green: 0.75, blue: 1.00), Color(red: 0.58, green: 0.34, blue: 0.95)] |
| | | case .green: |
| | | return [Color(red: 0.80, green: 0.96, blue: 0.58), Color(red: 0.22, green: 0.78, blue: 0.36)] |
| | | case .blue: |
| | | return [Color(red: 0.70, green: 0.91, blue: 1.00), Color(red: 0.22, green: 0.58, blue: 1.00)] |
| | | case .colorful: |
| | | return [ |
| | | Color(red: 0.10, green: 0.78, blue: 1.00), |
| | | Color(red: 0.34, green: 0.96, blue: 0.94), |
| | | Color(red: 0.75, green: 0.82, blue: 1.00), |
| | | Color(red: 1.00, green: 0.56, blue: 0.94) |
| | | ] |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | enum AppLibraryController { |
| | | private static let snapshotCacheLock = NSLock() |
| | | private static var cachedSnapshot: AppLibrarySnapshot? |
| | | |
| | | static func lastSnapshot() -> AppLibrarySnapshot? { |
| | | snapshotCacheLock.lock() |
| | | defer { snapshotCacheLock.unlock() } |
| | | return cachedSnapshot |
| | | } |
| | | |
| | | static func refresh(useCache: Bool = true) -> AppLibraryRefreshResult { |
| | | let scannedApps = AppIndexer.scan(useCache: useCache) |
| | | let reconciledStore = TagEditor.reconcileScannedApps(scannedApps) |
| | |
| | | apps: scannedApps, |
| | | store: reconciledStore |
| | | ) |
| | | let snapshot = makeSnapshot(scannedApps: scannedApps, store: smartStartResult.store) |
| | | updateLastSnapshot(snapshot) |
| | | return AppLibraryRefreshResult( |
| | | snapshot: makeSnapshot(scannedApps: scannedApps, store: smartStartResult.store), |
| | | snapshot: snapshot, |
| | | smartStartResult: smartStartResult |
| | | ) |
| | | } |
| | |
| | | scannedApps: [AppInfo] |
| | | ) -> AppLibrarySmartStartApplyResult { |
| | | let result = SmartStartService.applySuggestion(draft) |
| | | let snapshot = makeSnapshot(scannedApps: scannedApps, store: result.store) |
| | | updateLastSnapshot(snapshot) |
| | | return AppLibrarySmartStartApplyResult( |
| | | snapshot: makeSnapshot(scannedApps: scannedApps, store: result.store), |
| | | snapshot: snapshot, |
| | | summary: result.summary |
| | | ) |
| | | } |
| | |
| | | static func applySystemInitialScheme() -> AppLibrarySystemSchemeApplyResult { |
| | | let scannedApps = AppIndexer.scan(useCache: false) |
| | | let result = SmartStartService.applySystemInitialScheme(apps: scannedApps) |
| | | let snapshot = makeSnapshot(scannedApps: scannedApps, store: result.store) |
| | | updateLastSnapshot(snapshot) |
| | | return AppLibrarySystemSchemeApplyResult( |
| | | snapshot: makeSnapshot(scannedApps: scannedApps, store: result.store), |
| | | snapshot: snapshot, |
| | | summary: result.summary |
| | | ) |
| | | } |
| | |
| | | static func resetToUncategorized() -> AppLibraryUncategorizedResetResult { |
| | | let scannedApps = AppIndexer.scan(useCache: false) |
| | | let store = TagDatabase.resetAppTagAssignmentsToUncategorized() |
| | | let snapshot = makeSnapshot(scannedApps: scannedApps, store: store) |
| | | updateLastSnapshot(snapshot) |
| | | return AppLibraryUncategorizedResetResult( |
| | | snapshot: makeSnapshot(scannedApps: scannedApps, store: store) |
| | | snapshot: snapshot |
| | | ) |
| | | } |
| | | |
| | | private static func updateLastSnapshot(_ snapshot: AppLibrarySnapshot) { |
| | | snapshotCacheLock.lock() |
| | | cachedSnapshot = snapshot |
| | | snapshotCacheLock.unlock() |
| | | } |
| | | |
| | | private static func makeSnapshot( |
| | | scannedApps: [AppInfo], |
| | | store: TagDatabase.Store |
| | |
| | | forName: .tagLauncherOpenPreferencesRequested, |
| | | object: nil, |
| | | queue: .main |
| | | ) { [weak self] _ in |
| | | self?.openPreferences() |
| | | ) { [weak self] notification in |
| | | self?.openPreferences(targetTab: Self.preferencesTabTarget(from: notification)) |
| | | } |
| | | } |
| | | |
| | | private static func preferencesTabTarget(from notification: Notification) -> String? { |
| | | notification.userInfo?[SettingsTabTarget.userInfoKey] as? String |
| | | } |
| | | |
| | | private func observeExternalActivationRequests() { |
| | |
| | | } |
| | | |
| | | @objc private func openPreferences(_ sender: Any? = nil) { |
| | | openPreferences(targetTab: nil) |
| | | } |
| | | |
| | | private func openPreferences(targetTab: String?) { |
| | | dismissQuickSearchIfNeeded() |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | if overlayAvoidsSpaceSwitch { |
| | |
| | | |
| | | if let settingsWindow { |
| | | prepareSettingsWindow(settingsWindow) |
| | | requestPreferencesTabSelection(targetTab) |
| | | return |
| | | } |
| | | |
| | |
| | | defer: false |
| | | ) |
| | | window.title = tr("menu.preferences").replacingOccurrences(of: "…", with: "") |
| | | window.contentView = NSHostingView(rootView: PreferencesView()) |
| | | window.contentView = NSHostingView(rootView: PreferencesView(initialTabRawValue: targetTab)) |
| | | window.isReleasedWhenClosed = false |
| | | settingsWindow = window |
| | | prepareSettingsWindow(window) |
| | | } |
| | | |
| | | private func requestPreferencesTabSelection(_ targetTab: String?) { |
| | | guard let targetTab else { return } |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherPreferencesTabRequested, |
| | | object: nil, |
| | | userInfo: [SettingsTabTarget.userInfoKey: targetTab] |
| | | ) |
| | | } |
| | | |
| | | private func dismissQuickSearchIfNeeded() { |
| | | guard isQuickSearchOpen else { return } |
| | | isQuickSearchOpen = false |
| | |
| | | static let tagLauncherAppNoteEditingChanged = Notification.Name("TagLauncherAppNoteEditingChanged") |
| | | static let tagLauncherDataDidChange = Notification.Name("TagLauncherDataDidChange") |
| | | static let tagLauncherOpenPreferencesRequested = Notification.Name("TagLauncherOpenPreferencesRequested") |
| | | static let tagLauncherPreferencesTabRequested = Notification.Name("TagLauncherPreferencesTabRequested") |
| | | static let tagLauncherOverlayDidShow = Notification.Name("TagLauncherOverlayDidShow") |
| | | static let tagLauncherOverlayDidHide = Notification.Name("TagLauncherOverlayDidHide") |
| | | static let tagLauncherModalInteractionChanged = Notification.Name("TagLauncherModalInteractionChanged") |
| | | } |
| | | |
| | | enum SettingsTabTarget { |
| | | static let userInfoKey = "tab" |
| | | static let tags = "tags" |
| | | } |
| | | |
| | | // MARK: - Edit Phase |
| | |
| | | @State private var smartStartNotice: SmartStartNotice? = nil |
| | | @State private var pendingSmartStartDraft: SmartCategorizationDraft? = nil |
| | | @State private var refreshInProgress = false |
| | | @State private var loadingSpinnerVisible = false |
| | | @State private var loadingSpinnerToken = 0 |
| | | @State private var refreshAgainAfterCurrent = false |
| | | @State private var refreshAgainForceLayout = false |
| | | @State private var refreshAgainUseCache = true |
| | |
| | | @AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips |
| | | @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID |
| | | @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation |
| | | @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false |
| | | @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false |
| | |
| | | private let editSidebarHorizontalInset: CGFloat = 12 |
| | | private let floatingControlsTrailingInset: CGFloat = 20 |
| | | private let floatingControlsReservedWidth: CGFloat = 120 |
| | | private let loadingSpinnerDelay: TimeInterval = 0.25 |
| | | private var appBubbleDisabled: Bool { |
| | | usageTipsHovered |
| | | || appGridInteraction.appDragModeActive |
| | |
| | | private let tagNavigationHoverScrollDelay: TimeInterval = 0.14 |
| | | private let tagNavigationHoverScrollInterval: TimeInterval = 0.22 |
| | | private var floatingButtonSurfaceColor: Color { |
| | | colorScheme == .dark |
| | | return colorScheme == .dark |
| | | ? Color.white.opacity(0.10) |
| | | : Color.white.opacity(0.78) |
| | | } |
| | | |
| | | private var appGridTheme: AppGridTheme { |
| | | AppGridTheme(storedID: appGridThemeID) |
| | | } |
| | | |
| | | private var renderedAppGridTheme: AppGridTheme { |
| | | editPhase == .none ? appGridTheme : .defaultLight |
| | | } |
| | | |
| | | private var isSideLayout: Bool { |
| | |
| | | var body: some View { |
| | | ZStack { |
| | | if shouldRenderAppGridBehindQuickSearch { |
| | | VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(false) |
| | | appGridBackground |
| | | } |
| | | |
| | | if shouldRenderAppGridBehindQuickSearch { |
| | |
| | | } |
| | | } |
| | | |
| | | private var appGridBackground: some View { |
| | | let theme = renderedAppGridTheme |
| | | return ZStack { |
| | | if theme.usesVisualEffectBackdrop { |
| | | VisualEffectView( |
| | | material: theme.material, |
| | | blendingMode: .behindWindow |
| | | ) |
| | | } |
| | | if theme.isPureBlackBackground { |
| | | Color.black |
| | | } |
| | | if !theme.isDefaultLight { |
| | | LinearGradient( |
| | | colors: theme.backgroundBaseColors, |
| | | startPoint: .topLeading, |
| | | endPoint: .bottomTrailing |
| | | ) |
| | | LinearGradient( |
| | | colors: theme.backgroundAccentColors, |
| | | startPoint: .topTrailing, |
| | | endPoint: .bottomLeading |
| | | ) |
| | | if theme.backgroundDimmingOpacity > 0 { |
| | | Rectangle() |
| | | .fill(Color.black.opacity(theme.backgroundDimmingOpacity)) |
| | | } |
| | | } |
| | | } |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(false) |
| | | } |
| | | |
| | | private var floatingActionButtons: some View { |
| | | HStack(spacing: 8) { |
| | | floatingOverlayButton(systemImage: "pencil.line") { |
| | |
| | | onActivate: { tagID in |
| | | activateTagNavigation(tagID) |
| | | }, |
| | | onDoubleActivate: { _ in |
| | | openTagSettingsFromNavigation() |
| | | }, |
| | | onHoverChange: { tagID, active in |
| | | handleTagNavigationHover(tagID, active: active) |
| | | }, |
| | |
| | | onAppDrop: { path, source, targetTag, copy in |
| | | dropAppOnTagNavigation(path: path, sourceTag: source, targetTag: targetTag, copy: copy) |
| | | } |
| | | ) |
| | | } |
| | | |
| | | private func openTagSettingsFromNavigation() { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherOpenPreferencesRequested, |
| | | object: nil, |
| | | userInfo: [SettingsTabTarget.userInfoKey: SettingsTabTarget.tags] |
| | | ) |
| | | } |
| | | |
| | |
| | | Group { |
| | | if allApps.isEmpty { |
| | | Spacer() |
| | | ProgressView().scaleEffect(0.8) |
| | | if loadingSpinnerVisible { |
| | | ProgressView().scaleEffect(0.8) |
| | | } |
| | | Spacer() |
| | | } else { |
| | | AppGridCollectionView( |
| | |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: !hideAppNames, |
| | | appGridTheme: renderedAppGridTheme, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | highlightedGroupName: appGridHighlightedGroupName, |
| | |
| | | } label: { |
| | | Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right") |
| | | .font(.system(size: 12)) |
| | | .foregroundStyle(renderedAppGridTheme.editPrimaryTextColor) |
| | | .padding(.horizontal, 8) |
| | | .frame(height: 28) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 7, style: .continuous) |
| | | .fill(renderedAppGridTheme.editControlSurfaceColor) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 7, style: .continuous) |
| | | .stroke(renderedAppGridTheme.editControlStrokeColor, lineWidth: 1) |
| | | ) |
| | | } |
| | | .buttonStyle(.bordered) |
| | | .buttonStyle(.plain) |
| | | .shadow(color: renderedAppGridTheme.editButtonShadowColor, radius: 10, y: 4) |
| | | Spacer() |
| | | Text(tr("edit.tags")).font(.headline) |
| | | Text(tr("edit.tags")) |
| | | .font(.headline) |
| | | .foregroundStyle(renderedAppGridTheme.editPrimaryTextColor) |
| | | Spacer() |
| | | } |
| | | .padding(.horizontal, 24) |
| | | .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20) |
| | | .padding(.bottom, 12) |
| | | .background(renderedAppGridTheme.editToolbarSurfaceColor) |
| | | |
| | | Divider().opacity(0.3) |
| | | Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1) |
| | | |
| | | TagEditorView( |
| | | tagColors: $tagColors, |
| | |
| | | private var editAppsView: some View { |
| | | VStack(spacing: 0) { |
| | | EditAppsHeaderView( |
| | | theme: renderedAppGridTheme, |
| | | operation: editTagOperation, |
| | | hintText: editModeHintText, |
| | | confirmTitle: editConfirmTitle, |
| | |
| | | onConfirm: confirmAssign |
| | | ) |
| | | |
| | | Divider().opacity(0.3) |
| | | Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1) |
| | | |
| | | HStack(spacing: 0) { |
| | | VStack(alignment: .leading, spacing: 4) { |
| | | EditAppsSidebarIntroView( |
| | | theme: renderedAppGridTheme, |
| | | width: editSidebarWidth, |
| | | horizontalInset: editSidebarHorizontalInset |
| | | ) |
| | |
| | | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) |
| | | } |
| | | .frame(width: editSidebarWidth, alignment: .topLeading) |
| | | Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1) |
| | | Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(width: 1) |
| | | |
| | | if allApps.isEmpty { |
| | | Spacer(); ProgressView().scaleEffect(0.8); Spacer() |
| | | Spacer() |
| | | if loadingSpinnerVisible { |
| | | ProgressView().scaleEffect(0.8) |
| | | } |
| | | Spacer() |
| | | } else { |
| | | editAppsGrid |
| | | } |
| | |
| | | private func editFlatGroup(_ group: TagGroup) -> some View { |
| | | VStack(alignment: .leading, spacing: 0) { |
| | | HStack(spacing: 0) { |
| | | Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1) |
| | | Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1) |
| | | Text(group.name) |
| | | .font(.system(size: tagFontSize, weight: .semibold)) |
| | | .foregroundStyle(.secondary) |
| | | .foregroundStyle(renderedAppGridTheme.editSecondaryTextColor) |
| | | .padding(.horizontal, 10) |
| | | Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1) |
| | | Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1) |
| | | } |
| | | .padding(.bottom, 6) |
| | | |
| | |
| | | colorIndex: colorIndex, |
| | | operation: editTagOperation, |
| | | isSelected: isSelected, |
| | | isRemovableCandidate: isRemovableCandidate |
| | | isRemovableCandidate: isRemovableCandidate, |
| | | theme: renderedAppGridTheme |
| | | ) { |
| | | if isSelected { |
| | | selectedTagNames.remove(tagName) |
| | |
| | | return EditableAppSelectionItem( |
| | | app: app, |
| | | iconSize: iconSize, |
| | | isSelected: isSelected |
| | | isSelected: isSelected, |
| | | theme: renderedAppGridTheme |
| | | ) { |
| | | toggleEditableAppSelection(app) |
| | | } |
| | |
| | | // Each overlay show creates a fresh ContentView with empty `allApps`. The indexer |
| | | // cache may still be warm from a prior overlay/settings scan, so path-signature |
| | | // alone must not skip the first hydrate or the grid spinner never clears. |
| | | guard allApps.isEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return } |
| | | let wasEmpty = allApps.isEmpty |
| | | hydrateFromLastAppLibrarySnapshotIfNeeded() |
| | | guard wasEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return } |
| | | refreshApps() |
| | | } |
| | | |
| | | private func refreshAppsForQuickSearch() { |
| | | hydrateFromLastAppLibrarySnapshotIfNeeded() |
| | | guard allApps.isEmpty |
| | | || quickSearchDocuments.isEmpty |
| | | || AppIndexer.shouldRefreshForSearchPathChanges() |
| | | else { return } |
| | | refreshApps() |
| | | } |
| | | |
| | | private func hydrateFromLastAppLibrarySnapshotIfNeeded() { |
| | | guard allApps.isEmpty, let snapshot = AppLibraryController.lastSnapshot() else { return } |
| | | applyAppLibrarySnapshot(snapshot) |
| | | } |
| | | |
| | | private func refreshNotchHeight() { |
| | |
| | | } |
| | | |
| | | refreshInProgress = true |
| | | scheduleLoadingSpinnerIfNeeded() |
| | | DispatchQueue.global(qos: .userInitiated).async { |
| | | let result = AppLibraryController.refresh(useCache: useCache) |
| | | DispatchQueue.main.async { |
| | |
| | | finishDropRefreshAfterMinimumDuration() |
| | | } |
| | | refreshInProgress = false |
| | | hideLoadingSpinner() |
| | | if refreshAgainAfterCurrent { |
| | | let shouldForceLayout = refreshAgainForceLayout |
| | | let shouldUseCache = refreshAgainUseCache |
| | |
| | | } |
| | | } |
| | | |
| | | private func scheduleLoadingSpinnerIfNeeded() { |
| | | loadingSpinnerToken &+= 1 |
| | | let token = loadingSpinnerToken |
| | | loadingSpinnerVisible = false |
| | | guard allApps.isEmpty else { return } |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + loadingSpinnerDelay) { |
| | | guard token == loadingSpinnerToken, |
| | | refreshInProgress, |
| | | allApps.isEmpty |
| | | else { return } |
| | | loadingSpinnerVisible = true |
| | | } |
| | | } |
| | | |
| | | private func hideLoadingSpinner() { |
| | | loadingSpinnerToken &+= 1 |
| | | loadingSpinnerVisible = false |
| | | } |
| | | |
| | | private func applyAppLibrarySnapshot(_ snapshot: AppLibrarySnapshot) { |
| | | allApps = snapshot.apps |
| | | quickSearchDocuments = snapshot.quickSearchDocuments |
| | |
| | | import AppKit |
| | | |
| | | struct EditAppsHeaderView: View { |
| | | let theme: AppGridTheme |
| | | let operation: EditTagOperation |
| | | let hintText: String |
| | | let confirmTitle: String |
| | |
| | | Button(action: onExit) { |
| | | Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right") |
| | | .font(.system(size: 12)) |
| | | .foregroundStyle(theme.editPrimaryTextColor) |
| | | .padding(.horizontal, 8) |
| | | .frame(height: 28) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 7, style: .continuous) |
| | | .fill(theme.editControlSurfaceColor) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 7, style: .continuous) |
| | | .stroke(theme.editControlStrokeColor, lineWidth: 1) |
| | | ) |
| | | } |
| | | .buttonStyle(.bordered) |
| | | .buttonStyle(.plain) |
| | | .help(tr("edit.exit")) |
| | | .shadow(color: theme.editButtonShadowColor, radius: 10, y: 4) |
| | | |
| | | Spacer(minLength: 20) |
| | | |
| | | VStack(alignment: .trailing, spacing: 5) { |
| | | HStack(alignment: .center, spacing: 14) { |
| | | EditOperationPicker( |
| | | theme: theme, |
| | | operation: operation, |
| | | onSelect: onSelectOperation |
| | | ) |
| | | |
| | | EditConfirmButton( |
| | | theme: theme, |
| | | title: confirmTitle, |
| | | isDisabled: isConfirmDisabled, |
| | | action: onConfirm |
| | |
| | | |
| | | Text(hintText) |
| | | .font(.system(size: 11, weight: .medium)) |
| | | .foregroundStyle(.secondary) |
| | | .foregroundStyle(theme.editSecondaryTextColor) |
| | | .lineLimit(2) |
| | | .multilineTextAlignment(.trailing) |
| | | .frame(maxWidth: 460, alignment: .trailing) |
| | |
| | | .padding(.horizontal, 24) |
| | | .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20) |
| | | .padding(.bottom, 10) |
| | | .background(theme.editToolbarSurfaceColor) |
| | | } |
| | | } |
| | | |
| | | private struct EditOperationPicker: View { |
| | | let theme: AppGridTheme |
| | | let operation: EditTagOperation |
| | | let onSelect: (EditTagOperation) -> Void |
| | | |
| | |
| | | HStack(spacing: 10) { |
| | | Text(tr("edit.operationLabel")) |
| | | .font(.system(size: 12, weight: .medium)) |
| | | .foregroundStyle(.secondary) |
| | | .foregroundStyle(theme.editSecondaryTextColor) |
| | | |
| | | HStack(spacing: 14) { |
| | | EditOperationModeButton( |
| | | theme: theme, |
| | | operation: operation, |
| | | mode: .add, |
| | | titleKey: "edit.modeAdd", |
| | | onSelect: onSelect |
| | | ) |
| | | EditOperationModeButton( |
| | | theme: theme, |
| | | operation: operation, |
| | | mode: .remove, |
| | | titleKey: "edit.modeRemove", |
| | |
| | | ) |
| | | } |
| | | } |
| | | .padding(.horizontal, 10) |
| | | .frame(height: 34) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .fill(theme.editControlSurfaceColor) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .stroke(theme.editControlStrokeColor, lineWidth: 1) |
| | | ) |
| | | .shadow(color: theme.editButtonShadowColor, radius: 10, y: 4) |
| | | } |
| | | } |
| | | |
| | | private struct EditOperationModeButton: View { |
| | | let theme: AppGridTheme |
| | | let operation: EditTagOperation |
| | | let mode: EditTagOperation |
| | | let titleKey: String |
| | |
| | | ZStack { |
| | | Circle() |
| | | .strokeBorder( |
| | | isActive ? Color.accentColor : Color.secondary.opacity(0.58), |
| | | isActive ? theme.editAccentColor : theme.editInactiveIndicatorColor, |
| | | lineWidth: 1.6 |
| | | ) |
| | | .frame(width: 13, height: 13) |
| | | if isActive { |
| | | Circle() |
| | | .fill(Color.accentColor) |
| | | .fill(theme.editAccentColor) |
| | | .frame(width: 7, height: 7) |
| | | } |
| | | } |
| | | |
| | | Text(tr(titleKey)) |
| | | .font(.system(size: 12, weight: .semibold)) |
| | | .foregroundStyle(isActive ? Color.primary : Color.secondary) |
| | | .foregroundStyle(isActive ? theme.editPrimaryTextColor : theme.editSecondaryTextColor) |
| | | .lineLimit(1) |
| | | } |
| | | .padding(.vertical, 4) |
| | | .padding(.horizontal, 2) |
| | | .contentShape(Rectangle()) |
| | | } |
| | | .buttonStyle(.plain) |
| | |
| | | } |
| | | |
| | | private struct EditConfirmButton: View { |
| | | let theme: AppGridTheme |
| | | let title: String |
| | | let isDisabled: Bool |
| | | let action: () -> Void |
| | |
| | | Button(action: action) { |
| | | Text(title) |
| | | .font(.system(size: 13, weight: .semibold)) |
| | | .foregroundStyle(Color.white.opacity(isDisabled ? 0.76 : 1.0)) |
| | | .foregroundStyle(isDisabled ? theme.editDisabledTextColor : theme.editConfirmForegroundColor) |
| | | .lineLimit(1) |
| | | .minimumScaleFactor(0.82) |
| | | .frame(minWidth: 116) |
| | |
| | | .padding(.horizontal, 8) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .fill(Color.accentColor.opacity(isDisabled ? 0.40 : 1.0)) |
| | | .fill(isDisabled ? theme.editDisabledSurfaceColor : theme.editAccentColor) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .stroke(Color.accentColor.opacity(isDisabled ? 0.18 : 0.24), lineWidth: 1) |
| | | .stroke(isDisabled ? theme.editControlStrokeColor : theme.editAccentColor.opacity(0.72), lineWidth: 1) |
| | | ) |
| | | } |
| | | .buttonStyle(.plain) |
| | | .disabled(isDisabled) |
| | | .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) |
| | | .shadow(color: isDisabled ? Color.clear : theme.editButtonShadowColor, radius: 12, y: 5) |
| | | } |
| | | } |
| | | |
| | | struct EditAppsSidebarIntroView: View { |
| | | let theme: AppGridTheme |
| | | let width: CGFloat |
| | | let horizontalInset: CGFloat |
| | | |
| | |
| | | VStack(alignment: .leading, spacing: 6) { |
| | | Text(tr("edit.selectTags")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .foregroundStyle(theme.editSecondaryTextColor) |
| | | .lineLimit(2) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | .multilineTextAlignment(.leading) |
| | |
| | | |
| | | Text(tr("edit.dragHint")) |
| | | .font(.caption2) |
| | | .foregroundStyle(.tertiary) |
| | | .foregroundStyle(theme.editTertiaryTextColor) |
| | | .lineLimit(nil) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | .multilineTextAlignment(.leading) |
| | |
| | | let operation: EditTagOperation |
| | | let isSelected: Bool |
| | | let isRemovableCandidate: Bool |
| | | let theme: AppGridTheme |
| | | let onToggle: () -> Void |
| | | |
| | | private var isEnabled: Bool { |
| | |
| | | var body: some View { |
| | | HStack(spacing: 6) { |
| | | Circle() |
| | | .fill(isSelected ? Color.accentColor : Color.secondary.opacity(isEnabled ? 0.3 : 0.18)) |
| | | .fill(isSelected ? theme.editAccentColor : theme.editInactiveIndicatorColor.opacity(isEnabled ? 1.0 : 0.56)) |
| | | .frame(width: 16, height: 16) |
| | | .overlay( |
| | | isSelected |
| | |
| | | |
| | | Text(displayName) |
| | | .font(.system(size: 13, weight: isUncommon ? .semibold : .medium)) |
| | | .foregroundStyle(isEnabled ? Color.primary : Color.secondary.opacity(0.66)) |
| | | .foregroundStyle(isEnabled ? theme.editPrimaryTextColor : theme.editDisabledTextColor) |
| | | .lineLimit(1) |
| | | .truncationMode(.tail) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | |
| | | if isUncommon { |
| | | Image(systemName: "questionmark.bubble.fill") |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(isEnabled ? Color.secondary : Color.secondary.opacity(0.55)) |
| | | .foregroundStyle(isEnabled ? theme.editSecondaryTextColor : theme.editDisabledTextColor) |
| | | .frame(width: 16, height: 16) |
| | | } |
| | | if showsPendingRemoval { |
| | | Image(systemName: "trash.fill") |
| | | .font(.system(size: 11, weight: .bold)) |
| | | .foregroundStyle(.red) |
| | | .foregroundStyle(Color.red.opacity(theme.usesDarkGlass ? 0.96 : 0.84)) |
| | | .frame(width: 16, height: 16) |
| | | .transition(.scale(scale: 0.85).combined(with: .opacity)) |
| | | } else if operation == .remove { |
| | |
| | | .fill( |
| | | Color( |
| | | nsColor: TagColor.nsColor(for: colorIndex) |
| | | .withAlphaComponent(isEnabled ? (isUncommon ? 0.22 : 0.3) : 0.12) |
| | | .withAlphaComponent(isEnabled ? (isUncommon ? 0.26 : 0.34) : 0.16) |
| | | ) |
| | | ) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 6) |
| | | .stroke(theme.editControlStrokeColor.opacity(isSelected ? 1.0 : 0.44), lineWidth: isSelected ? 1.1 : 0.6) |
| | | ) |
| | | .contentShape(RoundedRectangle(cornerRadius: 6)) |
| | | .onTapGesture { |
| | |
| | | let app: AppInfo |
| | | let iconSize: CGFloat |
| | | let isSelected: Bool |
| | | let theme: AppGridTheme |
| | | let onToggle: () -> Void |
| | | |
| | | var body: some View { |
| | |
| | | .frame(width: iconSize, height: iconSize) |
| | | |
| | | Circle() |
| | | .fill(isSelected ? Color.accentColor : Color.secondary.opacity(0.3)) |
| | | .fill(isSelected ? theme.editAccentColor : theme.editInactiveIndicatorColor) |
| | | .frame(width: 20, height: 20) |
| | | .overlay( |
| | | isSelected |
| | |
| | | |
| | | Text(app.displayName) |
| | | .font(.system(size: 11, weight: .medium)) |
| | | .foregroundStyle(theme.editPrimaryTextColor) |
| | | .lineLimit(1) |
| | | .truncationMode(.tail) |
| | | .frame(maxWidth: iconSize + 20) |
| | |
| | | .padding(.horizontal, 4) |
| | | .contentShape(RoundedRectangle(cornerRadius: 10)) |
| | | .opacity(isSelected ? 1.0 : 0.65) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 10, style: .continuous) |
| | | .fill(isSelected ? theme.editControlSurfaceColor : Color.clear) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 10, style: .continuous) |
| | | .stroke(isSelected ? theme.editControlStrokeColor : Color.clear, lineWidth: 1) |
| | | ) |
| | | } |
| | | .buttonStyle(.plain) |
| | | } |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>APPL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>7.9.4</string> |
| | | <string>8.0.1</string> |
| | | <key>CFBundleVersion</key> |
| | | <string>20260619.2221</string> |
| | | <string>20260624.2021</string> |
| | | <key>LSApplicationCategoryType</key> |
| | | <string>public.app-category.utilities</string> |
| | | <key>LSMinimumSystemVersion</key> |
| | |
| | | "settings.languagePicker": "لغة التطبيق:", |
| | | "settings.languageDesc": "اختر لغة القوائم والإعدادات.", |
| | | "settings.general": "عام", |
| | | "settings.theme": "السمة", |
| | | "settings.themeDesc": "اختر سمة خلفية شبكة التطبيقات.", |
| | | "settings.themeScopeDesc": "السمات تغيّر خلفية شبكة التطبيقات فقط. الحاويات تبقى بسطح زجاجي موحّد.", |
| | | "settings.tags": "الوسوم", |
| | | "settings.data": "البيانات", |
| | | "settings.about": "عن التطبيق", |
| | |
| | | "usageTips.tip7.title": "تلميحة 7 - ترتيب التطبيقات", |
| | | "usageTips.tip7.detail": "اضغط مطوّل على التطبيق, اسحبه لمكان داخل الحاوية.", |
| | | "usageTips.tip8.title": "تلميحة 8 - تعديل الملاحظات", |
| | | "usageTips.tip8.detail": "انقر يمين على التطبيق, اكتب ملاحظة." |
| | | "usageTips.tip8.detail": "انقر يمين على التطبيق, اكتب ملاحظة.", |
| | | "theme.default": "الافتراضية", |
| | | "theme.deepBlue": "أزرق عميق", |
| | | "theme.black": "أسود", |
| | | "theme.pink": "وردي", |
| | | "theme.purple": "بنفسجي", |
| | | "theme.green": "أخضر", |
| | | "theme.blue": "أزرق", |
| | | "theme.colorful": "ملوّن" |
| | | } |
| | |
| | | "settings.languagePicker": "لغة التطبيق:", |
| | | "settings.languageDesc": "اختر اللغة المستخدمة في القوائم والإعدادات.", |
| | | "settings.general": "عام", |
| | | "settings.theme": "السمة", |
| | | "settings.themeDesc": "اختر سمة خلفية شبكة التطبيقات.", |
| | | "settings.themeScopeDesc": "تغيّر السمات خلفية شبكة التطبيقات فقط. تبقى الحاويات بسطح زجاجي موحّد.", |
| | | "settings.tags": "الوسوم", |
| | | "settings.data": "البيانات", |
| | | "settings.about": "حول", |
| | |
| | | "usageTips.tip7.title": "النصيحة 7 - ترتيب التطبيقات", |
| | | "usageTips.tip7.detail": "اضغط مطولاً على تطبيق, اسحبه إلى موضع داخل الحاوية.", |
| | | "usageTips.tip8.title": "النصيحة 8 - تحرير الملاحظات", |
| | | "usageTips.tip8.detail": "انقر بزر الماوس الأيمن على تطبيق, أدخل ملاحظة." |
| | | "usageTips.tip8.detail": "انقر بزر الماوس الأيمن على تطبيق, أدخل ملاحظة.", |
| | | "theme.default": "الافتراضية", |
| | | "theme.deepBlue": "أزرق عميق", |
| | | "theme.black": "أسود", |
| | | "theme.pink": "وردي", |
| | | "theme.purple": "بنفسجي", |
| | | "theme.green": "أخضر", |
| | | "theme.blue": "أزرق", |
| | | "theme.colorful": "ملوّن" |
| | | } |
| | |
| | | "settings.languagePicker": "Jazyk aplikace:", |
| | | "settings.languageDesc": "Vyberte jazyk používaný v nabídkách a nastaveních.", |
| | | "settings.general": "Obecné", |
| | | "settings.theme": "Motiv", |
| | | "settings.themeDesc": "Vyberte motiv pozadí mřížky aplikací.", |
| | | "settings.themeScopeDesc": "Motivy mění jen pozadí mřížky aplikací. Kontejnery si ponechají jednotný skleněný povrch.", |
| | | "settings.tags": "Štítky", |
| | | "settings.data": "Data", |
| | | "settings.about": "O aplikaci", |
| | |
| | | "usageTips.tip7.title": "Tip 7 - Seřadit aplikace", |
| | | "usageTips.tip7.detail": "Podržte aplikaci, přetáhněte ji na místo v kontejneru.", |
| | | "usageTips.tip8.title": "Tip 8 - Upravit poznámky", |
| | | "usageTips.tip8.detail": "Klikněte na aplikaci pravým tlačítkem, zadejte poznámku." |
| | | "usageTips.tip8.detail": "Klikněte na aplikaci pravým tlačítkem, zadejte poznámku.", |
| | | "theme.default": "Výchozí", |
| | | "theme.deepBlue": "Tmavě modrý", |
| | | "theme.black": "Černý", |
| | | "theme.pink": "Růžový", |
| | | "theme.purple": "Fialový", |
| | | "theme.green": "Zelený", |
| | | "theme.blue": "Modrý", |
| | | "theme.colorful": "Barevný" |
| | | } |
| | |
| | | "settings.languagePicker": "Appsprog:", |
| | | "settings.languageDesc": "Vælg sproget til menuer og indstillinger.", |
| | | "settings.general": "Generelt", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Vælg baggrundstema for appgitteret.", |
| | | "settings.themeScopeDesc": "Temaer ændrer kun baggrunden i appgitteret. Containere beholder en ensartet glasflade.", |
| | | "settings.tags": "Tags", |
| | | "settings.data": "Data", |
| | | "settings.about": "Om", |
| | |
| | | "usageTips.tip7.title": "Tip 7 - Sortér apps", |
| | | "usageTips.tip7.detail": "Hold en app nede, flyt den i containeren.", |
| | | "usageTips.tip8.title": "Tip 8 - Rediger noter", |
| | | "usageTips.tip8.detail": "Højreklik på en app, indtast en note." |
| | | "usageTips.tip8.detail": "Højreklik på en app, indtast en note.", |
| | | "theme.default": "Standard", |
| | | "theme.deepBlue": "Dyb blå", |
| | | "theme.black": "Sort", |
| | | "theme.pink": "Pink", |
| | | "theme.purple": "Lilla", |
| | | "theme.green": "Grøn", |
| | | "theme.blue": "Blå", |
| | | "theme.colorful": "Farverig" |
| | | } |
| | |
| | | "settings.languagePicker": "App-Sprache:", |
| | | "settings.languageDesc": "Wähle die Sprache für Menüs und Einstellungen.", |
| | | "settings.general": "Allgemein", |
| | | "settings.theme": "Design", |
| | | "settings.themeDesc": "Wähle das Hintergrunddesign für das App-Raster.", |
| | | "settings.themeScopeDesc": "Designs ändern nur den Hintergrund des App-Rasters. Container behalten eine einheitliche Glasfläche.", |
| | | "settings.tags": "Tags", |
| | | "settings.data": "Daten", |
| | | "settings.about": "Über", |
| | |
| | | "usageTips.tip7.title": "Tipp 7 - Apps sortieren", |
| | | "usageTips.tip7.detail": "App gedrückt halten, zur Position im Container ziehen.", |
| | | "usageTips.tip8.title": "Tipp 8 - Notizen bearbeiten", |
| | | "usageTips.tip8.detail": "Rechtsklick auf App, Notiz eingeben." |
| | | "usageTips.tip8.detail": "Rechtsklick auf App, Notiz eingeben.", |
| | | "theme.default": "Standard", |
| | | "theme.deepBlue": "Tiefblau", |
| | | "theme.black": "Schwarz", |
| | | "theme.pink": "Pink", |
| | | "theme.purple": "Violett", |
| | | "theme.green": "Grün", |
| | | "theme.blue": "Blau", |
| | | "theme.colorful": "Bunt" |
| | | } |
| | |
| | | "settings.languagePicker": "App language:", |
| | | "settings.languageDesc": "Choose the language used by menus and settings.", |
| | | "settings.general": "General", |
| | | "settings.theme": "Theme", |
| | | "settings.themeDesc": "Choose the App Grid background theme.", |
| | | "settings.themeScopeDesc": "Themes only change the App Grid background. Containers keep one consistent glass surface.", |
| | | "settings.tags": "Tags", |
| | | "settings.data": "Data", |
| | | "settings.about": "About", |
| | |
| | | "usageTips.tip7.title": "Tip 7 - Reorder apps", |
| | | "usageTips.tip7.detail": "Long-press an app, then drag it to a spot in the container.", |
| | | "usageTips.tip8.title": "Tip 8 - Edit notes", |
| | | "usageTips.tip8.detail": "Right-click an app, enter a note." |
| | | "usageTips.tip8.detail": "Right-click an app, enter a note.", |
| | | "theme.default": "Default", |
| | | "theme.deepBlue": "Deep Blue", |
| | | "theme.black": "Black", |
| | | "theme.pink": "Pink", |
| | | "theme.purple": "Purple", |
| | | "theme.green": "Green", |
| | | "theme.blue": "Blue", |
| | | "theme.colorful": "Colorful" |
| | | } |
| | |
| | | "settings.languagePicker": "Idioma de la app:", |
| | | "settings.languageDesc": "Elige el idioma usado en los menús y ajustes.", |
| | | "settings.general": "General", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Elige el tema de fondo de la cuadrícula de apps.", |
| | | "settings.themeScopeDesc": "Los temas solo cambian el fondo de la cuadrícula de apps. Los contenedores mantienen una superficie de cristal uniforme.", |
| | | "settings.tags": "Etiquetas", |
| | | "settings.data": "Datos", |
| | | "settings.about": "Acerca de", |
| | |
| | | "usageTips.tip7.title": "Consejo 7 - Ordenar apps", |
| | | "usageTips.tip7.detail": "Mantén pulsada una app, arrástrala a una posición del contenedor.", |
| | | "usageTips.tip8.title": "Consejo 8 - Editar notas", |
| | | "usageTips.tip8.detail": "Clic derecho en una app, escriba una nota." |
| | | "usageTips.tip8.detail": "Clic derecho en una app, escriba una nota.", |
| | | "theme.default": "Predeterminado", |
| | | "theme.deepBlue": "Azul profundo", |
| | | "theme.black": "Negro", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Morado", |
| | | "theme.green": "Verde", |
| | | "theme.blue": "Azul", |
| | | "theme.colorful": "Colorido" |
| | | } |
| | |
| | | "settings.languagePicker": "Langue de l'app :", |
| | | "settings.languageDesc": "Choisissez la langue utilisée par les menus et les réglages.", |
| | | "settings.general": "Général", |
| | | "settings.theme": "Thème", |
| | | "settings.themeDesc": "Choisissez le thème d’arrière-plan de la grille d’apps.", |
| | | "settings.themeScopeDesc": "Les thèmes ne modifient que l’arrière-plan de la grille d’apps. Les conteneurs gardent une surface vitrée uniforme.", |
| | | "settings.tags": "Étiquettes", |
| | | "settings.data": "Données", |
| | | "settings.about": "À propos", |
| | |
| | | "usageTips.tip7.title": "Astuce 7 - Trier les apps", |
| | | "usageTips.tip7.detail": "Appuyez longuement sur une app, déposez-la dans le conteneur.", |
| | | "usageTips.tip8.title": "Astuce 8 - Modifier les notes", |
| | | "usageTips.tip8.detail": "Clic droit sur une app, saisissez une note." |
| | | "usageTips.tip8.detail": "Clic droit sur une app, saisissez une note.", |
| | | "theme.default": "Par défaut", |
| | | "theme.deepBlue": "Bleu profond", |
| | | "theme.black": "Noir", |
| | | "theme.pink": "Rose", |
| | | "theme.purple": "Violet", |
| | | "theme.green": "Vert", |
| | | "theme.blue": "Bleu", |
| | | "theme.colorful": "Coloré" |
| | | } |
| | |
| | | "settings.languagePicker": "Bahasa aplikasi:", |
| | | "settings.languageDesc": "Pilih bahasa yang digunakan untuk menu dan pengaturan.", |
| | | "settings.general": "Umum", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Pilih tema latar belakang grid app.", |
| | | "settings.themeScopeDesc": "Tema hanya mengubah latar belakang grid app. Kontainer tetap memakai permukaan kaca yang konsisten.", |
| | | "settings.tags": "Tag", |
| | | "settings.data": "Data", |
| | | "settings.about": "Tentang", |
| | |
| | | "usageTips.tip7.title": "Tips 7 - Urutkan app", |
| | | "usageTips.tip7.detail": "Tekan lama app, pindahkan di dalam kontainer.", |
| | | "usageTips.tip8.title": "Tips 8 - Edit catatan", |
| | | "usageTips.tip8.detail": "Klik kanan app, masukkan catatan." |
| | | "usageTips.tip8.detail": "Klik kanan app, masukkan catatan.", |
| | | "theme.default": "Default", |
| | | "theme.deepBlue": "Biru Tua", |
| | | "theme.black": "Hitam", |
| | | "theme.pink": "Merah Muda", |
| | | "theme.purple": "Ungu", |
| | | "theme.green": "Hijau", |
| | | "theme.blue": "Biru", |
| | | "theme.colorful": "Berwarna" |
| | | } |
| | |
| | | "settings.languagePicker": "Lingua app:", |
| | | "settings.languageDesc": "Scegli la lingua usata da menu e impostazioni.", |
| | | "settings.general": "Generali", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Scegli il tema di sfondo della griglia delle app.", |
| | | "settings.themeScopeDesc": "I temi cambiano solo lo sfondo della griglia delle app. I contenitori mantengono una superficie in vetro uniforme.", |
| | | "settings.tags": "Tag", |
| | | "settings.data": "Dati", |
| | | "settings.about": "Informazioni", |
| | |
| | | "usageTips.tip7.title": "Suggerimento 7 - Riordinare app", |
| | | "usageTips.tip7.detail": "Tieni premuta un’app, spostala nel contenitore.", |
| | | "usageTips.tip8.title": "Suggerimento 8 - Modificare note", |
| | | "usageTips.tip8.detail": "Fai clic destro su un’app, inserisci una nota." |
| | | "usageTips.tip8.detail": "Fai clic destro su un’app, inserisci una nota.", |
| | | "theme.default": "Predefinito", |
| | | "theme.deepBlue": "Blu profondo", |
| | | "theme.black": "Nero", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Viola", |
| | | "theme.green": "Verde", |
| | | "theme.blue": "Blu", |
| | | "theme.colorful": "Colorato" |
| | | } |
| | |
| | | "settings.languagePicker": "アプリの言語:", |
| | | "settings.languageDesc": "メニューと設定で使う言語を選択します。", |
| | | "settings.general": "一般", |
| | | "settings.theme": "テーマ", |
| | | "settings.themeDesc": "アプリグリッドの背景テーマを選びます。", |
| | | "settings.themeScopeDesc": "テーマはアプリグリッドの背景だけを変更します。コンテナは統一されたガラス面のままです。", |
| | | "settings.tags": "タグ", |
| | | "settings.data": "データ", |
| | | "settings.about": "概要", |
| | |
| | | "usageTips.tip7.title": "ヒント7 - アプリを並べ替え", |
| | | "usageTips.tip7.detail": "アプリを長押し, コンテナ内の目的位置へ移動します。", |
| | | "usageTips.tip8.title": "ヒント8 - メモを編集", |
| | | "usageTips.tip8.detail": "アプリを右クリック, メモを入力します。" |
| | | "usageTips.tip8.detail": "アプリを右クリック, メモを入力します。", |
| | | "theme.default": "デフォルト", |
| | | "theme.deepBlue": "ディープブルー", |
| | | "theme.black": "ブラック", |
| | | "theme.pink": "ピンク", |
| | | "theme.purple": "パープル", |
| | | "theme.green": "グリーン", |
| | | "theme.blue": "ブルー", |
| | | "theme.colorful": "カラフル" |
| | | } |
| | |
| | | "settings.languagePicker": "앱 언어:", |
| | | "settings.languageDesc": "메뉴와 설정에서 사용할 언어를 선택합니다.", |
| | | "settings.general": "일반", |
| | | "settings.theme": "테마", |
| | | "settings.themeDesc": "앱 그리드 배경 테마를 선택합니다.", |
| | | "settings.themeScopeDesc": "테마는 앱 그리드 배경만 바꿉니다. 컨테이너는 일관된 유리 표면을 유지합니다.", |
| | | "settings.tags": "태그", |
| | | "settings.data": "데이터", |
| | | "settings.about": "정보", |
| | |
| | | "usageTips.tip7.title": "팁 7 - 앱 정렬", |
| | | "usageTips.tip7.detail": "앱을 길게 누르기, 컨테이너 안에서 이동.", |
| | | "usageTips.tip8.title": "팁 8 - 메모 편집", |
| | | "usageTips.tip8.detail": "앱을 오른쪽 클릭, 메모 입력." |
| | | "usageTips.tip8.detail": "앱을 오른쪽 클릭, 메모 입력.", |
| | | "theme.default": "기본", |
| | | "theme.deepBlue": "딥 블루", |
| | | "theme.black": "검정", |
| | | "theme.pink": "분홍", |
| | | "theme.purple": "보라", |
| | | "theme.green": "초록", |
| | | "theme.blue": "파랑", |
| | | "theme.colorful": "컬러풀" |
| | | } |
| | |
| | | "settings.languagePicker": "Bahasa aplikasi:", |
| | | "settings.languageDesc": "Pilih bahasa yang digunakan untuk menu dan tetapan.", |
| | | "settings.general": "Umum", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Pilih tema latar belakang grid app.", |
| | | "settings.themeScopeDesc": "Tema hanya menukar latar belakang grid app. Kontainer kekal dengan permukaan kaca yang konsisten.", |
| | | "settings.tags": "Tag", |
| | | "settings.data": "Data", |
| | | "settings.about": "Tentang", |
| | |
| | | "usageTips.tip7.title": "Petua 7 - Susun app", |
| | | "usageTips.tip7.detail": "Tekan lama app, alih dalam bekas.", |
| | | "usageTips.tip8.title": "Petua 8 - Edit nota", |
| | | "usageTips.tip8.detail": "Klik kanan app, masukkan nota." |
| | | "usageTips.tip8.detail": "Klik kanan app, masukkan nota.", |
| | | "theme.default": "Lalai", |
| | | "theme.deepBlue": "Biru Gelap", |
| | | "theme.black": "Hitam", |
| | | "theme.pink": "Merah Jambu", |
| | | "theme.purple": "Ungu", |
| | | "theme.green": "Hijau", |
| | | "theme.blue": "Biru", |
| | | "theme.colorful": "Berwarna-warni" |
| | | } |
| | |
| | | "settings.languagePicker": "Appspråk:", |
| | | "settings.languageDesc": "Velg språket som brukes i menyer og innstillinger.", |
| | | "settings.general": "Generelt", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Velg bakgrunnstema for app-rutenettet.", |
| | | "settings.themeScopeDesc": "Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate.", |
| | | "settings.tags": "Etiketter", |
| | | "settings.data": "Data", |
| | | "settings.about": "Om", |
| | |
| | | "usageTips.tip7.title": "Tips 7 - Sorter apper", |
| | | "usageTips.tip7.detail": "Hold en app inne, flytt den i beholderen.", |
| | | "usageTips.tip8.title": "Tips 8 - Rediger notater", |
| | | "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat." |
| | | "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat.", |
| | | "theme.default": "Standard", |
| | | "theme.deepBlue": "Dyp blå", |
| | | "theme.black": "Svart", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Lilla", |
| | | "theme.green": "Grønn", |
| | | "theme.blue": "Blå", |
| | | "theme.colorful": "Fargerik" |
| | | } |
| | |
| | | "settings.languagePicker": "App-taal:", |
| | | "settings.languageDesc": "Kies de taal voor menu’s en instellingen.", |
| | | "settings.general": "Algemeen", |
| | | "settings.theme": "Thema", |
| | | "settings.themeDesc": "Kies het achtergrondthema voor het app-raster.", |
| | | "settings.themeScopeDesc": "Thema’s wijzigen alleen de achtergrond van het app-raster. Containers behouden één consistente glaslaag.", |
| | | "settings.tags": "Tags", |
| | | "settings.data": "Gegevens", |
| | | "settings.about": "Over", |
| | |
| | | "usageTips.tip7.title": "Tip 7 - Apps sorteren", |
| | | "usageTips.tip7.detail": "Houd een app ingedrukt, sleep binnen de container.", |
| | | "usageTips.tip8.title": "Tip 8 - Notities bewerken", |
| | | "usageTips.tip8.detail": "Klik met rechts op een app, voer een notitie in." |
| | | "usageTips.tip8.detail": "Klik met rechts op een app, voer een notitie in.", |
| | | "theme.default": "Standaard", |
| | | "theme.deepBlue": "Diepblauw", |
| | | "theme.black": "Zwart", |
| | | "theme.pink": "Roze", |
| | | "theme.purple": "Paars", |
| | | "theme.green": "Groen", |
| | | "theme.blue": "Blauw", |
| | | "theme.colorful": "Kleurrijk" |
| | | } |
| | |
| | | "settings.languagePicker": "Appspråk:", |
| | | "settings.languageDesc": "Velg språket som brukes i menyer og innstillinger.", |
| | | "settings.general": "Generelt", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Vel bakgrunnstema for apprutenettet.", |
| | | "settings.themeScopeDesc": "Tema endrar berre bakgrunnen i apprutenettet. Behaldarane har éi jamn glasflate.", |
| | | "settings.tags": "Etiketter", |
| | | "settings.data": "Data", |
| | | "settings.about": "Om", |
| | |
| | | "usageTips.tip7.title": "Tips 7 - Sorter apper", |
| | | "usageTips.tip7.detail": "Hold ein app inne, flytt han i behaldaren.", |
| | | "usageTips.tip8.title": "Tips 8 - Rediger notater", |
| | | "usageTips.tip8.detail": "Høgreklikk på ein app, skriv inn eit notat." |
| | | "usageTips.tip8.detail": "Høgreklikk på ein app, skriv inn eit notat.", |
| | | "theme.default": "Standard", |
| | | "theme.deepBlue": "Djup blå", |
| | | "theme.black": "Svart", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Lilla", |
| | | "theme.green": "Grøn", |
| | | "theme.blue": "Blå", |
| | | "theme.colorful": "Fargerik" |
| | | } |
| | |
| | | "settings.languagePicker": "Appspråk:", |
| | | "settings.languageDesc": "Velg språket som brukes i menyer og innstillinger.", |
| | | "settings.general": "Generelt", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Velg bakgrunnstema for app-rutenettet.", |
| | | "settings.themeScopeDesc": "Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate.", |
| | | "settings.tags": "Etiketter", |
| | | "settings.data": "Data", |
| | | "settings.about": "Om", |
| | |
| | | "usageTips.tip7.title": "Tips 7 - Sorter apper", |
| | | "usageTips.tip7.detail": "Hold en app inne, flytt den i beholderen.", |
| | | "usageTips.tip8.title": "Tips 8 - Rediger notater", |
| | | "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat." |
| | | "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat.", |
| | | "theme.default": "Standard", |
| | | "theme.deepBlue": "Dyp blå", |
| | | "theme.black": "Svart", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Lilla", |
| | | "theme.green": "Grønn", |
| | | "theme.blue": "Blå", |
| | | "theme.colorful": "Fargerik" |
| | | } |
| | |
| | | "settings.languagePicker": "Język aplikacji:", |
| | | "settings.languageDesc": "Wybierz język menu i ustawień.", |
| | | "settings.general": "Ogólne", |
| | | "settings.theme": "Motyw", |
| | | "settings.themeDesc": "Wybierz motyw tła siatki aplikacji.", |
| | | "settings.themeScopeDesc": "Motywy zmieniają tylko tło siatki aplikacji. Kontenery zachowują jednolitą szklaną powierzchnię.", |
| | | "settings.tags": "Tagi", |
| | | "settings.data": "Dane", |
| | | "settings.about": "O aplikacji", |
| | |
| | | "usageTips.tip7.title": "Wskazówka 7 - Sortuj aplikacje", |
| | | "usageTips.tip7.detail": "Przytrzymaj aplikację, przeciągnij ją w kontenerze.", |
| | | "usageTips.tip8.title": "Wskazówka 8 - Edytuj notatki", |
| | | "usageTips.tip8.detail": "Kliknij aplikację prawym przyciskiem, wpisz notatkę." |
| | | "usageTips.tip8.detail": "Kliknij aplikację prawym przyciskiem, wpisz notatkę.", |
| | | "theme.default": "Domyślny", |
| | | "theme.deepBlue": "Głęboki niebieski", |
| | | "theme.black": "Czarny", |
| | | "theme.pink": "Różowy", |
| | | "theme.purple": "Fioletowy", |
| | | "theme.green": "Zielony", |
| | | "theme.blue": "Niebieski", |
| | | "theme.colorful": "Kolorowy" |
| | | } |
| | |
| | | "settings.languagePicker": "Idioma do app:", |
| | | "settings.languageDesc": "Escolha o idioma usado nos menus e ajustes.", |
| | | "settings.general": "Geral", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Escolha o tema de fundo da grade de apps.", |
| | | "settings.themeScopeDesc": "Os temas mudam apenas o fundo da grade de apps. Os contêineres mantêm uma superfície de vidro consistente.", |
| | | "settings.tags": "Etiquetas", |
| | | "settings.data": "Dados", |
| | | "settings.about": "Sobre", |
| | |
| | | "usageTips.tip7.title": "Dica 7 - Reordenar apps", |
| | | "usageTips.tip7.detail": "Pressione e segure um app, mova dentro do contêiner.", |
| | | "usageTips.tip8.title": "Dica 8 - Editar notas", |
| | | "usageTips.tip8.detail": "Clique com o botão direito em um app, digite uma nota." |
| | | "usageTips.tip8.detail": "Clique com o botão direito em um app, digite uma nota.", |
| | | "theme.default": "Padrão", |
| | | "theme.deepBlue": "Azul profundo", |
| | | "theme.black": "Preto", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Roxo", |
| | | "theme.green": "Verde", |
| | | "theme.blue": "Azul", |
| | | "theme.colorful": "Colorido" |
| | | } |
| | |
| | | "settings.languagePicker": "Limba aplicației:", |
| | | "settings.languageDesc": "Alege limba folosită pentru meniuri și setări.", |
| | | "settings.general": "General", |
| | | "settings.theme": "Temă", |
| | | "settings.themeDesc": "Alege tema fundalului pentru grila de aplicații.", |
| | | "settings.themeScopeDesc": "Temele schimbă doar fundalul grilei de aplicații. Containerele păstrează o suprafață de sticlă uniformă.", |
| | | "settings.tags": "Etichete", |
| | | "settings.data": "Date", |
| | | "settings.about": "Despre", |
| | |
| | | "usageTips.tip7.title": "Sfat 7 - Reordonează aplicații", |
| | | "usageTips.tip7.detail": "Ține apăsată o aplicație, mut-o în container.", |
| | | "usageTips.tip8.title": "Sfat 8 - Editează notițe", |
| | | "usageTips.tip8.detail": "Click dreapta pe o aplicație, introdu o notiță." |
| | | "usageTips.tip8.detail": "Click dreapta pe o aplicație, introdu o notiță.", |
| | | "theme.default": "Implicită", |
| | | "theme.deepBlue": "Albastru profund", |
| | | "theme.black": "Negru", |
| | | "theme.pink": "Roz", |
| | | "theme.purple": "Mov", |
| | | "theme.green": "Verde", |
| | | "theme.blue": "Albastru", |
| | | "theme.colorful": "Colorată" |
| | | } |
| | |
| | | "settings.languagePicker": "Язык приложения:", |
| | | "settings.languageDesc": "Выберите язык меню и настроек.", |
| | | "settings.general": "Общие", |
| | | "settings.theme": "Тема", |
| | | "settings.themeDesc": "Выберите тему фона сетки приложений.", |
| | | "settings.themeScopeDesc": "Темы меняют только фон сетки приложений. Контейнеры сохраняют единый стеклянный вид.", |
| | | "settings.tags": "Теги", |
| | | "settings.data": "Данные", |
| | | "settings.about": "О программе", |
| | |
| | | "usageTips.tip7.title": "Совет 7 — Упорядочить приложения", |
| | | "usageTips.tip7.detail": "Удерживайте приложение, перетащите внутри контейнера.", |
| | | "usageTips.tip8.title": "Совет 8 — Редактировать заметки", |
| | | "usageTips.tip8.detail": "Щелкните приложение правой кнопкой, введите заметку." |
| | | "usageTips.tip8.detail": "Щелкните приложение правой кнопкой, введите заметку.", |
| | | "theme.default": "По умолчанию", |
| | | "theme.deepBlue": "Глубокий синий", |
| | | "theme.black": "Чёрный", |
| | | "theme.pink": "Розовый", |
| | | "theme.purple": "Фиолетовый", |
| | | "theme.green": "Зелёный", |
| | | "theme.blue": "Синий", |
| | | "theme.colorful": "Цветной" |
| | | } |
| | |
| | | "settings.languagePicker": "Језик апликације:", |
| | | "settings.languageDesc": "Изаберите језик менија и подешавања.", |
| | | "settings.general": "Опште", |
| | | "settings.theme": "Тема", |
| | | "settings.themeDesc": "Изаберите тему позадине мреже апликација.", |
| | | "settings.themeScopeDesc": "Теме мењају само позадину мреже апликација. Контејнери задржавају једноличну стаклену површину.", |
| | | "settings.tags": "Ознаке", |
| | | "settings.data": "Data", |
| | | "settings.about": "О програму", |
| | |
| | | "usageTips.tip7.title": "Савет 7 - Поређај апликације", |
| | | "usageTips.tip7.detail": "Држите апликацију, превуците је унутар контејнера.", |
| | | "usageTips.tip8.title": "Савет 8 - Уреди белешке", |
| | | "usageTips.tip8.detail": "Кликните десним тастером на апликацију, унесите белешку." |
| | | "usageTips.tip8.detail": "Кликните десним тастером на апликацију, унесите белешку.", |
| | | "theme.default": "Подразумевана", |
| | | "theme.deepBlue": "Дубока плава", |
| | | "theme.black": "Црна", |
| | | "theme.pink": "Розе", |
| | | "theme.purple": "Љубичаста", |
| | | "theme.green": "Зелена", |
| | | "theme.blue": "Плава", |
| | | "theme.colorful": "Шарена" |
| | | } |
| | |
| | | "settings.languagePicker": "Appspråk:", |
| | | "settings.languageDesc": "Välj språket som används i menyer och inställningar.", |
| | | "settings.general": "Allmänt", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Välj bakgrundstema för apprutnätet.", |
| | | "settings.themeScopeDesc": "Teman ändrar bara bakgrunden i apprutnätet. Behållare har en enhetlig glasyta.", |
| | | "settings.tags": "Taggar", |
| | | "settings.data": "Data", |
| | | "settings.about": "Om", |
| | |
| | | "usageTips.tip7.title": "Tips 7 - Sortera appar", |
| | | "usageTips.tip7.detail": "Håll ned en app, flytta den i containern.", |
| | | "usageTips.tip8.title": "Tips 8 - Redigera anteckningar", |
| | | "usageTips.tip8.detail": "Högerklicka på en app, skriv en anteckning." |
| | | "usageTips.tip8.detail": "Högerklicka på en app, skriv en anteckning.", |
| | | "theme.default": "Standard", |
| | | "theme.deepBlue": "Djupblå", |
| | | "theme.black": "Svart", |
| | | "theme.pink": "Rosa", |
| | | "theme.purple": "Lila", |
| | | "theme.green": "Grön", |
| | | "theme.blue": "Blå", |
| | | "theme.colorful": "Färgstark" |
| | | } |
| | |
| | | "settings.languagePicker": "ภาษาของแอป:", |
| | | "settings.languageDesc": "เลือกภาษาที่ใช้ในเมนูและการตั้งค่า", |
| | | "settings.general": "ทั่วไป", |
| | | "settings.theme": "ธีม", |
| | | "settings.themeDesc": "เลือกธีมพื้นหลังของกริดแอป", |
| | | "settings.themeScopeDesc": "ธีมจะเปลี่ยนเฉพาะพื้นหลังของกริดแอป คอนเทนเนอร์ยังคงเป็นพื้นผิวกระจกแบบเดียวกัน", |
| | | "settings.tags": "แท็ก", |
| | | "settings.data": "ข้อมูล", |
| | | "settings.about": "เกี่ยวกับ", |
| | |
| | | "usageTips.tip7.title": "เคล็ดลับ 7 - จัดลำดับแอป", |
| | | "usageTips.tip7.detail": "กดแอปค้างไว้, ลากภายในคอนเทนเนอร์", |
| | | "usageTips.tip8.title": "เคล็ดลับ 8 - แก้ไขโน้ต", |
| | | "usageTips.tip8.detail": "คลิกขวาที่แอป, ป้อนโน้ต" |
| | | "usageTips.tip8.detail": "คลิกขวาที่แอป, ป้อนโน้ต", |
| | | "theme.default": "ค่าเริ่มต้น", |
| | | "theme.deepBlue": "น้ำเงินเข้ม", |
| | | "theme.black": "ดำ", |
| | | "theme.pink": "ชมพู", |
| | | "theme.purple": "ม่วง", |
| | | "theme.green": "เขียว", |
| | | "theme.blue": "น้ำเงิน", |
| | | "theme.colorful": "หลากสี" |
| | | } |
| | |
| | | "settings.languagePicker": "Uygulama dili:", |
| | | "settings.languageDesc": "Menülerde ve ayarlarda kullanılan dili seçin.", |
| | | "settings.general": "Genel", |
| | | "settings.theme": "Tema", |
| | | "settings.themeDesc": "Uygulama ızgarası arka plan temasını seçin.", |
| | | "settings.themeScopeDesc": "Temalar yalnızca uygulama ızgarası arka planını değiştirir. Kapsayıcılar tutarlı bir cam yüzey olarak kalır.", |
| | | "settings.tags": "Etiketler", |
| | | "settings.data": "Veri", |
| | | "settings.about": "Hakkında", |
| | |
| | | "usageTips.tip7.title": "İpucu 7 - Uygulamaları sırala", |
| | | "usageTips.tip7.detail": "Bir uygulamaya uzun basın, kapsayıcı içinde taşıyın.", |
| | | "usageTips.tip8.title": "İpucu 8 - Notları düzenle", |
| | | "usageTips.tip8.detail": "Bir uygulamaya sağ tıklayın, not girin." |
| | | "usageTips.tip8.detail": "Bir uygulamaya sağ tıklayın, not girin.", |
| | | "theme.default": "Varsayılan", |
| | | "theme.deepBlue": "Derin Mavi", |
| | | "theme.black": "Siyah", |
| | | "theme.pink": "Pembe", |
| | | "theme.purple": "Mor", |
| | | "theme.green": "Yeşil", |
| | | "theme.blue": "Mavi", |
| | | "theme.colorful": "Renkli" |
| | | } |
| | |
| | | "settings.languagePicker": "Мова застосунку:", |
| | | "settings.languageDesc": "Оберіть мову меню та налаштувань.", |
| | | "settings.general": "Загальні", |
| | | "settings.theme": "Тема", |
| | | "settings.themeDesc": "Виберіть тему тла сітки програм.", |
| | | "settings.themeScopeDesc": "Теми змінюють лише тло сітки програм. Контейнери зберігають єдину скляну поверхню.", |
| | | "settings.tags": "Теги", |
| | | "settings.data": "Дані", |
| | | "settings.about": "Про програму", |
| | |
| | | "usageTips.tip7.title": "Порада 7 — Упорядкувати програми", |
| | | "usageTips.tip7.detail": "Утримуйте програму, перетягніть її всередині контейнера.", |
| | | "usageTips.tip8.title": "Порада 8 — Редагувати нотатки", |
| | | "usageTips.tip8.detail": "Клацніть програму правою кнопкою, введіть нотатку." |
| | | "usageTips.tip8.detail": "Клацніть програму правою кнопкою, введіть нотатку.", |
| | | "theme.default": "Типова", |
| | | "theme.deepBlue": "Глибокий синій", |
| | | "theme.black": "Чорна", |
| | | "theme.pink": "Рожева", |
| | | "theme.purple": "Фіолетова", |
| | | "theme.green": "Зелена", |
| | | "theme.blue": "Синя", |
| | | "theme.colorful": "Барвиста" |
| | | } |
| | |
| | | "settings.languagePicker": "Ngôn ngữ ứng dụng:", |
| | | "settings.languageDesc": "Chọn ngôn ngữ dùng cho menu và cài đặt.", |
| | | "settings.general": "Chung", |
| | | "settings.theme": "Chủ đề", |
| | | "settings.themeDesc": "Chọn chủ đề nền cho lưới ứng dụng.", |
| | | "settings.themeScopeDesc": "Chủ đề chỉ thay đổi nền của lưới ứng dụng. Các khung giữ một bề mặt kính thống nhất.", |
| | | "settings.tags": "Thẻ", |
| | | "settings.data": "Dữ liệu", |
| | | "settings.about": "Giới thiệu", |
| | |
| | | "usageTips.tip7.title": "Mẹo 7 - Sắp xếp ứng dụng", |
| | | "usageTips.tip7.detail": "Nhấn giữ ứng dụng, kéo trong vùng chứa.", |
| | | "usageTips.tip8.title": "Mẹo 8 - Chỉnh sửa ghi chú", |
| | | "usageTips.tip8.detail": "Nhấp chuột phải vào ứng dụng, nhập ghi chú." |
| | | "usageTips.tip8.detail": "Nhấp chuột phải vào ứng dụng, nhập ghi chú.", |
| | | "theme.default": "Mặc định", |
| | | "theme.deepBlue": "Xanh lam đậm", |
| | | "theme.black": "Đen", |
| | | "theme.pink": "Hồng", |
| | | "theme.purple": "Tím", |
| | | "theme.green": "Xanh lá", |
| | | "theme.blue": "Xanh lam", |
| | | "theme.colorful": "Nhiều màu" |
| | | } |
| | |
| | | "settings.languagePicker": "应用语言:", |
| | | "settings.languageDesc": "选择菜单和设置面板使用的语言。", |
| | | "settings.general": "通用", |
| | | "settings.theme": "主题", |
| | | "settings.themeDesc": "选择应用网格的背景主题。", |
| | | "settings.themeScopeDesc": "主题只改变应用网格背景。容器保持统一的玻璃面板。", |
| | | "settings.tags": "标签", |
| | | "settings.data": "数据", |
| | | "settings.about": "关于", |
| | |
| | | "usageTips.tip7.title": "技巧7-应用排序", |
| | | "usageTips.tip7.detail": "长按应用图标启动拖动 > 拖动到容器内的目标位置,放手即可", |
| | | "usageTips.tip8.title": "技巧8-编辑备注", |
| | | "usageTips.tip8.detail": "在应用上点击右键 > 输入内容" |
| | | "usageTips.tip8.detail": "在应用上点击右键 > 输入内容", |
| | | "theme.default": "默认", |
| | | "theme.deepBlue": "深蓝", |
| | | "theme.black": "黑色", |
| | | "theme.pink": "粉色", |
| | | "theme.purple": "紫色", |
| | | "theme.green": "绿色", |
| | | "theme.blue": "蓝色", |
| | | "theme.colorful": "炫彩" |
| | | } |
| | |
| | | "settings.languagePicker": "應用程式語言:", |
| | | "settings.languageDesc": "選擇選單和設定面板使用的語言。", |
| | | "settings.general": "一般", |
| | | "settings.theme": "主題", |
| | | "settings.themeDesc": "選擇應用程式網格的背景主題。", |
| | | "settings.themeScopeDesc": "主題只會改變應用程式網格背景。容器會保持一致的玻璃面板。", |
| | | "settings.tags": "標籤", |
| | | "settings.data": "資料", |
| | | "settings.about": "關於", |
| | |
| | | "usageTips.tip7.title": "技巧7-應用排序", |
| | | "usageTips.tip7.detail": "長按應用程式圖示開始拖曳 > 拖到容器內目標位置後放開", |
| | | "usageTips.tip8.title": "技巧8-編輯備註", |
| | | "usageTips.tip8.detail": "在應用程式上按右鍵 > 輸入內容" |
| | | "usageTips.tip8.detail": "在應用程式上按右鍵 > 輸入內容", |
| | | "theme.default": "預設", |
| | | "theme.deepBlue": "深藍", |
| | | "theme.black": "黑色", |
| | | "theme.pink": "粉紅", |
| | | "theme.purple": "紫色", |
| | | "theme.green": "綠色", |
| | | "theme.blue": "藍色", |
| | | "theme.colorful": "炫彩" |
| | | } |
| | |
| | | private enum SettingsTab: String, CaseIterable, Identifiable { |
| | | case language |
| | | case general |
| | | case theme |
| | | case hotkeys |
| | | case tags |
| | | case data |
| | |
| | | switch self { |
| | | case .language: return "settings.language" |
| | | case .general: return "settings.general" |
| | | case .theme: return "settings.theme" |
| | | case .hotkeys: return "quickSearch.hotkeys" |
| | | case .tags: return "settings.tags" |
| | | case .data: return "settings.data" |
| | |
| | | switch self { |
| | | case .language: return "globe" |
| | | case .general: return "gearshape" |
| | | case .theme: return "paintpalette.fill" |
| | | case .hotkeys: return "keyboard" |
| | | case .tags: return "tag.fill" |
| | | case .data: return "externaldrive.fill" |
| | |
| | | @AppStorage("launchAtLogin") private var launchAtLogin = AppDefaults.launchAtLogin |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips |
| | | @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID |
| | | @AppStorage("mainHotkeyRegistrationState") private var mainHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue |
| | | @AppStorage("quickSearchHotkeyRegistrationState") private var quickSearchHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue |
| | | @State private var selectedLanguage = L10n.selectedLanguageCode |
| | |
| | | @State private var hotkeyStatusToastToken: UUID? = nil |
| | | @State private var selectedTab: SettingsTab = .general |
| | | @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart |
| | | |
| | | init(initialTabRawValue: String? = nil) { |
| | | let initialTab = initialTabRawValue.flatMap(SettingsTab.init(rawValue:)) ?? .general |
| | | _selectedTab = State(initialValue: initialTab) |
| | | } |
| | | |
| | | private func selectTab(rawValue: String?) { |
| | | guard let rawValue, let tab = SettingsTab(rawValue: rawValue) else { return } |
| | | selectedTab = tab |
| | | } |
| | | |
| | | private func scanApps() { |
| | | DispatchQueue.global(qos: .userInitiated).async { |
| | |
| | | GridItem(.flexible(minimum: 220), spacing: 10, alignment: .top), |
| | | ] |
| | | } |
| | | private var themeColumns: [GridItem] { |
| | | [ |
| | | GridItem(.flexible(minimum: 220), spacing: 12, alignment: .top), |
| | | GridItem(.flexible(minimum: 220), spacing: 12, alignment: .top), |
| | | ] |
| | | } |
| | | private var displayModeOptions: [(id: String, title: String)] { |
| | | [ |
| | | ("flat", tr("settings.flat")), |
| | |
| | | } |
| | | private var containerDisplayModeOptions: [(id: String, title: String)] { |
| | | Array(displayModeOptions.dropFirst()) |
| | | } |
| | | private var selectedAppGridTheme: AppGridTheme { |
| | | AppGridTheme(storedID: appGridThemeID) |
| | | } |
| | | |
| | | private func generalSettingRow<Control: View>( |
| | |
| | | .padding(.top, 28) |
| | | .padding(.bottom) |
| | | } |
| | | |
| | | case .theme: |
| | | ScrollView(.vertical, showsIndicators: true) { |
| | | VStack(alignment: .leading, spacing: 18) { |
| | | VStack(alignment: .leading, spacing: 6) { |
| | | Text(tr("settings.theme")) |
| | | .font(.headline) |
| | | Text(tr("settings.themeDesc")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | } |
| | | |
| | | LazyVGrid(columns: themeColumns, alignment: .leading, spacing: 12) { |
| | | ForEach(AppGridTheme.allCases) { theme in |
| | | ThemeOptionButton( |
| | | theme: theme, |
| | | title: tr(theme.titleKey), |
| | | isSelected: selectedAppGridTheme == theme |
| | | ) { |
| | | appGridThemeID = theme.rawValue |
| | | } |
| | | } |
| | | } |
| | | |
| | | Text(tr("settings.themeScopeDesc")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | } |
| | | .frame(maxWidth: generalContentWidth, alignment: .leading) |
| | | .padding(.horizontal) |
| | | .padding(.top, 28) |
| | | .padding(.bottom) |
| | | } |
| | | |
| | | case .hotkeys: |
| | | // Tab 3: Hotkeys |
| | |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in |
| | | showPendingHotkeyWarningIfNeeded() |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherPreferencesTabRequested)) { notification in |
| | | selectTab(rawValue: notification.userInfo?[SettingsTabTarget.userInfoKey] as? String) |
| | | } |
| | | } |
| | | |
| | | private func showLanguageRefresh() { |
| | |
| | | } |
| | | } |
| | | |
| | | private struct ThemeOptionButton: View { |
| | | let theme: AppGridTheme |
| | | let title: String |
| | | let isSelected: Bool |
| | | let action: () -> Void |
| | | |
| | | var body: some View { |
| | | Button(action: action) { |
| | | HStack(spacing: 12) { |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .fill( |
| | | LinearGradient( |
| | | colors: theme.previewColors, |
| | | startPoint: .topLeading, |
| | | endPoint: .bottomTrailing |
| | | ) |
| | | ) |
| | | .frame(width: 70, height: 42) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .stroke(previewStrokeColor, lineWidth: 1) |
| | | ) |
| | | .shadow( |
| | | color: isSelected ? Color.accentColor.opacity(0.22) : Color.clear, |
| | | radius: 10, |
| | | x: 0, |
| | | y: 4 |
| | | ) |
| | | |
| | | Text(title) |
| | | .font(.system(size: 13, weight: .semibold)) |
| | | .foregroundStyle(.primary) |
| | | .lineLimit(1) |
| | | .minimumScaleFactor(0.82) |
| | | |
| | | Spacer(minLength: 0) |
| | | |
| | | Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") |
| | | .font(.system(size: 16, weight: .semibold)) |
| | | .foregroundStyle(isSelected ? Color.accentColor : Color.secondary.opacity(0.42)) |
| | | } |
| | | .padding(10) |
| | | .frame(maxWidth: .infinity, minHeight: 64) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .fill(isSelected ? Color.accentColor.opacity(0.10) : Color(nsColor: .controlBackgroundColor)) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .stroke(isSelected ? Color.accentColor.opacity(0.32) : Color.secondary.opacity(0.16), lineWidth: 1) |
| | | ) |
| | | .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) |
| | | } |
| | | .buttonStyle(.plain) |
| | | .accessibilityLabel(title) |
| | | .accessibilityIdentifier("theme-option-\(theme.rawValue)") |
| | | } |
| | | |
| | | private var previewStrokeColor: Color { |
| | | theme.isDefaultLight |
| | | ? Color.secondary.opacity(0.26) |
| | | : Color.white.opacity(0.24) |
| | | } |
| | | } |
| | | |
| | | private struct DisplayModeOptionButton: View { |
| | | let mode: String |
| | | let title: String |
| | |
| | | let appDragModeActive: Bool |
| | | let appDropTargetID: String? |
| | | let onActivate: (String) -> Void |
| | | let onDoubleActivate: (String) -> Void |
| | | let onHoverChange: (String, Bool) -> Void |
| | | let canReorder: (String) -> Bool |
| | | let canAcceptAppDrop: (String) -> Bool |
| | |
| | | appDragModeActive: appDragModeActive, |
| | | appDropTargetID: appDropTargetID, |
| | | onActivate: onActivate, |
| | | onDoubleActivate: onDoubleActivate, |
| | | onHoverChange: onHoverChange, |
| | | canReorder: canReorder, |
| | | canAcceptAppDrop: canAcceptAppDrop, |
| | |
| | | appDragModeActive: appDragModeActive, |
| | | appDropTargetID: appDropTargetID, |
| | | onActivate: onActivate, |
| | | onDoubleActivate: onDoubleActivate, |
| | | onHoverChange: onHoverChange, |
| | | canReorder: canReorder, |
| | | canAcceptAppDrop: canAcceptAppDrop, |
| | |
| | | appDragModeActive: Bool, |
| | | appDropTargetID: String?, |
| | | onActivate: @escaping (String) -> Void, |
| | | onDoubleActivate: @escaping (String) -> Void, |
| | | onHoverChange: @escaping (String, Bool) -> Void, |
| | | canReorder: @escaping (String) -> Bool, |
| | | canAcceptAppDrop: @escaping (String) -> Bool, |
| | |
| | | appDragModeActive: appDragModeActive, |
| | | appDropTargetID: appDropTargetID, |
| | | onActivate: onActivate, |
| | | onDoubleActivate: onDoubleActivate, |
| | | onHoverChange: onHoverChange, |
| | | canReorder: canReorder, |
| | | canAcceptAppDrop: canAcceptAppDrop, |
| | |
| | | private var appDragModeActive = false |
| | | private var appDropTargetID: String? |
| | | private var onActivate: (String) -> Void = { _ in } |
| | | private var onDoubleActivate: (String) -> Void = { _ in } |
| | | private var onHoverChange: (String, Bool) -> Void = { _, _ in } |
| | | private var canReorder: (String) -> Bool = { _ in false } |
| | | private var canAcceptAppDrop: (String) -> Bool = { _ in false } |
| | |
| | | appDragModeActive: Bool, |
| | | appDropTargetID: String?, |
| | | onActivate: @escaping (String) -> Void, |
| | | onDoubleActivate: @escaping (String) -> Void, |
| | | onHoverChange: @escaping (String, Bool) -> Void, |
| | | canReorder: @escaping (String) -> Bool, |
| | | canAcceptAppDrop: @escaping (String) -> Bool, |
| | |
| | | self.appDragModeActive = appDragModeActive |
| | | self.appDropTargetID = appDropTargetID |
| | | self.onActivate = onActivate |
| | | self.onDoubleActivate = onDoubleActivate |
| | | self.onHoverChange = onHoverChange |
| | | self.canReorder = canReorder |
| | | self.canAcceptAppDrop = canAcceptAppDrop |
| | |
| | | buttons = items.map { item in |
| | | let button = TagNavigationButton(item: item, orientation: orientation) |
| | | button.onActivate = { [weak self] tagID in self?.onActivate(tagID) } |
| | | button.onDoubleActivate = { [weak self] tagID in self?.onDoubleActivate(tagID) } |
| | | button.onHoverChange = { [weak self] tagID, active in self?.onHoverChange(tagID, active) } |
| | | button.canReorder = { [weak self] tagID in self?.canReorder(tagID) ?? false } |
| | | button.onReorderBegan = { [weak self] tagID in self?.onReorderBegan(tagID) } |
| | |
| | | |
| | | final class TagNavigationButton: NSButton, AppDropTargetReceivingView { |
| | | var onActivate: (String) -> Void = { _ in } |
| | | var onDoubleActivate: (String) -> Void = { _ in } |
| | | var onHoverChange: (String, Bool) -> Void = { _, _ in } |
| | | var canReorder: (String) -> Bool = { _ in false } |
| | | var onReorderBegan: (String) -> Void = { _ in } |
| | |
| | | } |
| | | |
| | | override func mouseDown(with event: NSEvent) { |
| | | if event.clickCount >= 2 { |
| | | onDoubleActivate(item.id) |
| | | return |
| | | } |
| | | |
| | | guard canReorder(item.id) else { |
| | | onActivate(item.id) |
| | | return |
| | |
| | | # TagLauncher Changelog |
| | | |
| | | ## [8.0.1] — 2026-06-24 |
| | | |
| | | - 新增 App Grid 主题系统:设置页新增“主题”页签,可在默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩 8 个主题之间切换 |
| | | - 默认主题保留原来的浅色毛玻璃 App Grid;黑色主题使用 100% 纯黑全屏背景;其余主题使用全屏高级渐变背景,单个容器继续保持统一半透明玻璃面板,不在容器内部叠加渐变 |
| | | - 深蓝和黑色主题自动切换为深色玻璃容器;粉色、紫色、绿色、蓝色、炫彩使用更明亮的全屏渐变和浅色玻璃容器,保证图标、容器标题和标签导航的可读性 |
| | | - 二次调色粉色、绿色、紫色、蓝色和炫彩主题:粉色更明亮柔和,绿色更接近草坪绿树后的毛玻璃,紫色/蓝色与深蓝拉开辨识度,炫彩按 Meilang logo 取色方向变亮 |
| | | - 编辑模式做减法:无论当前选择哪个主题,进入编辑后都临时使用默认浅色毛玻璃 App Grid;退出编辑后自动恢复用户原主题,避免每个主题下编辑控件和文字可读性不一致 |
| | | - 优化启动和重开 App Grid 的加载体验:新建面板会先复用最近一次完整 App 列表快照,并延迟显示空态转圈,减少冷启动或快速打开时的等待感 |
| | | - 标签导航支持双击快速进入设置页的“标签”页签;单击滚动、hover 自动滚动和长按标签排序行为保持不变 |
| | | - 旧版 `useDarkAppGrid` 偏好自动迁移:已启用旧深色视图的用户升级后会进入“深蓝”主题,未启用的用户继续保持默认浅色主题 |
| | | - 补齐 29 个语种的主题页签、主题说明和 8 个主题名称文案;移除旧“启用深色视图”设置文案,避免设置页出现重复入口 |
| | | - 新增 `Scripts/theme_settings_qa.sh`、`Scripts/appgrid_startup_loading_qa.sh` 和 `Scripts/tag_double_click_preferences_qa.sh`,检查主题枚举、设置入口、偏好迁移、App Grid 渲染边界、编辑模式默认浅色主题 override、启动加载体验、标签双击设置入口和 29 语种文案完整性 |
| | | - 版本号更新为 `8.0.1`,Build 更新为 `20260624.2021` |
| | | |
| | | ## [7.9.4] — 2026-06-19 |
| | | |
| | | - 补修 Apple 自带应用默认备注迁移:覆盖 7.9 之前已写入用户库、但没有 metadata 的历史默认文案,修复 Chess、TV、Phone、Freeform、Music、Time Machine 等应用切换到非中文语言后仍显示旧中文备注的问题 |
| New file |
| | |
| | | # 8.0.0 App Grid 主题系统 TODO |
| | | |
| | | ## 目标 |
| | | |
| | | - 在设置页新增独立“主题”页签。 |
| | | - 提供 8 个 App Grid 背景主题:默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩。 |
| | | - 主题只改变 App Grid 全屏背景渐变;单个容器继续使用统一半透明玻璃面板。 |
| | | - 默认主题保持原浅色毛玻璃体验;深蓝和黑色使用深色玻璃容器,粉色、紫色、绿色、蓝色、炫彩使用浅色玻璃容器以保持明亮干净。 |
| | | - 文案覆盖 29 个语种。 |
| | | |
| | | ## 不做范围 |
| | | |
| | | - 不新增“容器玻璃深浅”独立设置。 |
| | | - 不改变标签、拖拽、排序、Quick Search、数据导入导出、SmartStart 分类逻辑。 |
| | | - 不改变默认浅色主题的既有视觉。 |
| | | - 不在容器内部增加每个容器自己的渐变。 |
| | | |
| | | ## TODO |
| | | |
| | | - [x] 新增 `AppGridTheme` 统一主题模型。 |
| | | - [x] 定义 8 个主题的背景渐变 token、预览色和容器玻璃适配策略。 |
| | | - [x] 将 App Grid 背景渲染改为读取主题偏好。 |
| | | - [x] 容器玻璃按主题明暗自动适配:深蓝/黑色为暗玻璃,亮色主题为浅玻璃。 |
| | | - [x] 按用户视觉反馈二次调色:黑色改为纯黑背景;粉色、绿色、紫色、蓝色、炫彩改为更明亮且互相有辨识度的渐变。 |
| | | - [x] 编辑模式做减法:进入编辑后临时使用默认浅色毛玻璃主题,退出后恢复用户原主题。 |
| | | - [x] 设置页新增“主题”页签和 8 主题选择卡片。 |
| | | - [x] 旧 `useDarkAppGrid` 偏好迁移到 `deepBlue`。 |
| | | - [x] 补齐 29 个语种的主题文案。 |
| | | - [x] 移除旧“启用深色视图”设置入口和本地化文案。 |
| | | - [x] 新增主题设置 QA 脚本。 |
| | | - [x] 更新 changelog、TODO 和工作日志。 |
| | | - [ ] 用户视觉体验验收。 |
| | | - [ ] 通过验收后再决定是否冻结、打包、创建正式 release tag。 |
| | | |
| | | ## 完成标准 |
| | | |
| | | - 默认主题和 7.9.4 的浅色 App Grid 观感一致。 |
| | | - 8 个主题切换只影响 App Grid 背景与自动玻璃适配,不影响功能行为。 |
| | | - 所有 29 个语种都能显示主题页签和主题名称,不出现裸 key。 |
| | | - 旧版已启用深色视图的用户升级后进入深蓝主题,未启用的用户保持默认主题。 |
| | | - 构建、签名、macOS 14 metadata/typecheck、主题 QA、核心数据/搜索/使用技巧回归通过。 |
| New file |
| | |
| | | # 8.0.0 App Grid 主题系统工作日志 |
| | | |
| | | ## 2026-06-24 |
| | | |
| | | ### 起点 |
| | | |
| | | - 分支:`codex/dark-glass-8.0.0` |
| | | - 基线:`v7.9.4-build20260619.2221` |
| | | - 目标:基于已认可方案实现 8 个高级渐变背景主题,并保留原浅色毛玻璃主题。 |
| | | - 关键约束:主题切换只改 UI;不动标签、拖拽、排序、搜索、数据和 SmartStart 功能语义。 |
| | | |
| | | ### 方案决策 |
| | | |
| | | - 8.0.0 先做“8 个高级渐变背景主题 + 自动玻璃适配”。 |
| | | - 不做容器玻璃深浅的独立开关,降低设置复杂度和 QA 面。 |
| | | - 默认主题使用浅色玻璃容器;深蓝和黑色使用深色半透明玻璃容器;粉色、紫色、绿色、蓝色、炫彩使用浅色玻璃容器,避免亮色主题被压成暗黑系。 |
| | | - 渐变只在 App Grid 全屏背景层实现,单个容器内部不做独立渐变,避免视觉噪声。 |
| | | - 炫彩主题取样自用户提供的 Meilang logo,使用青蓝、浅青、淡紫和粉色作为背景渐变来源,并降低饱和度保证可读性。 |
| | | |
| | | ### 已完成实现 |
| | | |
| | | - 新增 `Apptag/AppGridTheme.swift`: |
| | | - 主题枚举、存储 key、标题 key、背景渐变 token、预览色、玻璃适配策略。 |
| | | - 修改 `AppDefaults.swift`: |
| | | - 新增 `appGridThemeID` 默认值。 |
| | | - 将旧 `useDarkAppGrid=true` 迁移到 `deepBlue`。 |
| | | - 修改 `ContentView.swift`: |
| | | - App Grid 背景读取当前主题。 |
| | | - 默认主题保持原浅色视觉。 |
| | | - 黑色主题跳过毛玻璃背板,使用 100% 纯黑全屏背景。 |
| | | - 深蓝主题保留暗色渐变和轻暗化层。 |
| | | - 粉色、紫色、绿色、蓝色、炫彩使用更明亮的全屏渐变,不叠加暗化层。 |
| | | - 修改 `AppGridCollectionView.swift`: |
| | | - 容器、标题、分隔线按主题自动选择浅色/深色玻璃适配。 |
| | | - 深蓝和黑色为暗玻璃;亮色主题为浅玻璃。 |
| | | - 移除 AppKit 层旧的全屏背景渐变,避免 SwiftUI/AppKit 双背景叠加。 |
| | | - 修改 `PreferencesView.swift`: |
| | | - 新增“主题”tab。 |
| | | - 新增 8 个主题选择卡片和主题说明。 |
| | | - 移除 General 页旧“启用深色视图”入口。 |
| | | - 更新 29 个 `Localization/*.json`: |
| | | - 新增主题 tab、说明和 8 个主题名称。 |
| | | - 删除旧深色视图设置文案。 |
| | | - 新增 `Scripts/theme_settings_qa.sh`。 |
| | | - 新增根 `CODEGRAPH.md`,记录主题系统相关模块依赖。 |
| | | |
| | | ### 2026-06-24 视觉反馈二次调色 |
| | | |
| | | - 用户反馈:默认和深蓝可接受;黑色需要纯黑;粉色和炫彩需要明亮、年轻;绿色需要像明亮草坪/绿树后的毛玻璃;紫色和蓝色需要与深蓝拉开差异。 |
| | | - 调整结论: |
| | | - 黑色:全屏背景为纯黑,不使用系统毛玻璃背板、不叠加渐变或暗化层。 |
| | | - 粉色:改为明亮粉、蜜桃和柔白高光,面向更年轻/少女取向。 |
| | | - 绿色:改为草坪绿、嫩绿和明亮高光,保持喜人而不沉闷。 |
| | | - 紫色:改为浅薰衣草到高饱和紫,和深蓝明显区分。 |
| | | - 蓝色:改为天空蓝、电光蓝和青色,和深蓝明显区分。 |
| | | - 炫彩:回到 Meilang logo 的青蓝、浅青、淡紫、亮粉取色方向,整体变亮。 |
| | | - 亮色主题统一改回浅色玻璃容器;深色玻璃仅保留给深蓝和黑色。 |
| | | |
| | | ### QA 记录 |
| | | |
| | | - `bash build.sh`:PASS,生成 `src/build/TagLauncher.app`。 |
| | | - `codesign --verify --deep --strict --verbose=2 src/build/TagLauncher.app`:PASS。 |
| | | - 29 个 localization JSON 解析:PASS。 |
| | | - `bash src/Scripts/theme_settings_qa.sh`:PASS,覆盖 8 主题、主题 tab、偏好迁移、纯黑策略、亮色主题浅玻璃策略和 29 语种文案。 |
| | | - `bash src/Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `bash src/Scripts/macos14_build_metadata_qa.sh`:PASS。 |
| | | - `bash src/Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。 |
| | | - `bash src/Scripts/usage_tips_qa.sh`:PASS。 |
| | | - `bash src/Scripts/app_ordering_data_qa.sh`:PASS。 |
| | | - `bash src/Scripts/quick_search_system_app_qa.sh`:PASS。 |
| | | - `bash src/Scripts/quick_search_app_name_qa.sh`:SKIP,本机未安装 QA fixture `/Applications/贝锐向日葵被控.app`。 |
| | | |
| | | ### 2026-06-24 15:06 二次调色 QA 与预览包 |
| | | |
| | | - Build:`20260624.1506` |
| | | - `bash Scripts/theme_settings_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_build_metadata_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `APP_BUILD=20260624.1506 bash build.sh`:PASS。 |
| | | - `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。 |
| | | - 构建产物版本:`8.0.0 (20260624.1506)`,`LSMinimumSystemVersion=14.0`。 |
| | | - `bash Scripts/usage_tips_qa.sh`:PASS。 |
| | | - `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。 |
| | | - `bash Scripts/app_ordering_data_qa.sh`:PASS。 |
| | | - `bash Scripts/quick_search_system_app_qa.sh`:PASS。 |
| | | - `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。 |
| | | - `hdiutil verify build/TagLauncher-8.0.0-theme-preview-build20260624.1506.dmg`:PASS。 |
| | | - DMG: |
| | | - `src/build/TagLauncher-8.0.0-theme-preview-build20260624.1506.dmg` |
| | | - SHA256: |
| | | - `c2704c435ef0b0ba8e3ea17824cbd3ffe89c118f34a966547b98ab6db96ebac5` |
| | | |
| | | ### QA 基础设施备注 |
| | | |
| | | - `Scripts/window_logic_qa.sh` 在本机 GUI 自动化环境中出现间歇性失败: |
| | | - Dock 点击或全局热键偶发未触发 overlay。 |
| | | - Codex/输入法类 App 偶发抢前台,导致旧的 frontmost app 名断言误报。 |
| | | - 已对该脚本做两类非产品逻辑加固: |
| | | - Quick Search 结果坐标识别不再依赖固定窗口宽度,并在失败时输出窗口列表。 |
| | | - 前台 app 名断言降级为诊断信息,核心仍由窗口层级断言覆盖。 |
| | | - 启动与热键触发增加重试等待。 |
| | | - 该脚本仍建议后续单独作为 QA 基础设施任务继续稳定化;本轮主题功能不依赖其失败点。 |
| | | |
| | | ### 当前结论 |
| | | |
| | | - 8.0.0 主题系统代码已实现。 |
| | | - 确定性 QA 已覆盖主题模型、设置入口、偏好迁移、多语种文案、构建签名、macOS 14 元数据、标签 hover、使用技巧、App 排序数据和系统 Quick Search 名称回归。 |
| | | - 下一步需要用户实际体验 8 个主题的视觉效果,再决定是否冻结和打包。 |
| | | |
| | | ### 2026-06-24 15:53 编辑模式主题对比度修正 |
| | | |
| | | - 用户反馈:亮色主题和黑色主题进入编辑模式后,顶部按钮、说明文字、确认按钮、标签列表和页面文字可读性不足。 |
| | | - 第一版修正策略: |
| | | - 编辑层控件接入主题高对比 token,深色主题白字、亮色主题深字。 |
| | | - Build 更新为:`20260624.1553`。 |
| | | |
| | | ### 2026-06-24 15:53 QA 与预览包 |
| | | |
| | | - `bash Scripts/theme_settings_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_build_metadata_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `APP_BUILD=20260624.1553 bash build.sh`:PASS。 |
| | | - `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。 |
| | | - `bash Scripts/usage_tips_qa.sh`:PASS。 |
| | | - `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。 |
| | | - `bash Scripts/app_ordering_data_qa.sh`:PASS。 |
| | | - `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。 |
| | | - `bash Scripts/quick_search_system_app_qa.sh`:PASS。 |
| | | - 包内版本:`8.0.0 (20260624.1553)`,`LSMinimumSystemVersion=14.0`。 |
| | | - `hdiutil verify build/TagLauncher-8.0.0-theme-preview-build20260624.1553.dmg`:PASS。 |
| | | - DMG: |
| | | - `src/build/TagLauncher-8.0.0-theme-preview-build20260624.1553.dmg` |
| | | - SHA256: |
| | | - `543a2f26676f3e57aac0edb26353a8f90d38d3e0290dfd1c951e62b1f3e409ea` |
| | | |
| | | ### 2026-06-24 16:32 编辑模式主题策略简化 |
| | | |
| | | - 用户反馈:继续按主题适配编辑态仍然复杂,要求做减法。 |
| | | - 新策略: |
| | | - 用户进入编辑模式时,不管当前选择深蓝、黑色、粉色、绿色、炫彩或其他主题,App Grid 运行态渲染主题都临时切换为默认浅色毛玻璃。 |
| | | - 不写入 `appGridThemeID`,不改变设置页选择;退出编辑模式后自动恢复用户原主题。 |
| | | - App Grid 背景、AppKit collection renderer、编辑页顶部、标签列表、应用选择项都读取 `renderedAppGridTheme`。 |
| | | - `theme_settings_qa.sh` 增加运行态主题 override 检查,防止编辑态重新跟随用户主题。 |
| | | - Build 更新为:`20260624.1632`。 |
| | | |
| | | ### 2026-06-24 16:32 QA 与预览包 |
| | | |
| | | - `bash Scripts/theme_settings_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `APP_BUILD=20260624.1632 bash build.sh`:PASS。 |
| | | - `bash Scripts/macos14_build_metadata_qa.sh`:PASS。 |
| | | - `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。 |
| | | - `bash Scripts/usage_tips_qa.sh`:PASS。 |
| | | - `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。 |
| | | - `bash Scripts/app_ordering_data_qa.sh`:PASS。 |
| | | - `bash Scripts/quick_search_system_app_qa.sh`:PASS。 |
| | | - `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。 |
| | | - 包内版本:`8.0.0 (20260624.1632)`,`LSMinimumSystemVersion=14.0`。 |
| | | - `hdiutil verify build/TagLauncher-8.0.0-theme-preview-build20260624.1632.dmg`:PASS。 |
| | | - DMG: |
| | | - `src/build/TagLauncher-8.0.0-theme-preview-build20260624.1632.dmg` |
| | | - SHA256: |
| | | - `34962ce1f3114a0b4979125fa92cd788833593ac19173365434c717bd8f159bc` |
| | | |
| | | ### 2026-06-24 17:31 App Grid 启动加载体验修复 |
| | | |
| | | - 用户反馈:每次启动/打开 App Grid 都出现转圈等待,观感不好,且此前类似问题曾修过。 |
| | | - 根因结论: |
| | | - App 启动时只预热 `AppIndexer` 的进程内扫描缓存。 |
| | | - 每次显示 App Grid 都会新建 `OverlayWindow + ContentView`,新的 `ContentView.allApps` 初始为空。 |
| | | - 在后台 `AppLibraryController.refresh()` 返回前,空态直接显示 `ProgressView`,因此用户能看到转圈。 |
| | | - 旧修复解决的是“缓存已热但新 ContentView 跳过 hydrate 导致 spinner 不消失”,没有解决冷启动或新面板首屏空态可见。 |
| | | - 本轮修复: |
| | | - `AppLibraryController` 保存最近一次完整 `AppLibrarySnapshot`。 |
| | | - `ContentView.refreshAppsForOverlay()` / `refreshAppsForQuickSearch()` 先复用最近快照,再后台刷新。 |
| | | - AppGrid 和编辑 AppGrid 的空态 spinner 改成 250ms 延迟显示;如果快照或扫描快速返回,不再闪现转圈。 |
| | | - 新增 `Scripts/appgrid_startup_loading_qa.sh` 锁住快照复用与延迟 spinner 规则。 |
| | | |
| | | ### 2026-06-24 18:15 QA 与验收候选包 |
| | | |
| | | - 包类型:验收候选包,仅供用户安装验证;本轮未做源码冻结、commit 或 tag。 |
| | | - Build 更新为:`20260624.1815`。 |
| | | - `bash Scripts/theme_settings_qa.sh`:PASS。 |
| | | - `bash Scripts/appgrid_startup_loading_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `APP_BUILD=20260624.1815 bash build.sh`:PASS。 |
| | | - `/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' build/TagLauncher.app/Contents/Info.plist`:`8.0.0`。 |
| | | - `/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' build/TagLauncher.app/Contents/Info.plist`:`20260624.1815`。 |
| | | - `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。 |
| | | - `bash Scripts/macos14_build_metadata_qa.sh`:PASS,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arches=arm64`。 |
| | | - `hdiutil verify build/TagLauncher-8.0.0-build20260624.1815.dmg`:PASS。 |
| | | - DMG: |
| | | - `src/build/TagLauncher-8.0.0-build20260624.1815.dmg` |
| | | - SHA256: |
| | | - `373355c87055680cb13e873bc5909abd84817d9ac2eaa98cc1ec2748be64bc80` |
| New file |
| | |
| | | #!/bin/bash |
| | | set -euo pipefail |
| | | |
| | | ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" |
| | | APP_DIR="$ROOT_DIR/Apptag" |
| | | |
| | | python3 - "$APP_DIR" <<'PY' |
| | | import sys |
| | | from pathlib import Path |
| | | |
| | | app_dir = Path(sys.argv[1]) |
| | | content = (app_dir / "ContentView.swift").read_text(encoding="utf-8") |
| | | library = (app_dir / "AppLibraryController.swift").read_text(encoding="utf-8") |
| | | |
| | | required_library_tokens = [ |
| | | "private static var cachedSnapshot: AppLibrarySnapshot?", |
| | | "static func lastSnapshot() -> AppLibrarySnapshot?", |
| | | "private static func updateLastSnapshot(_ snapshot: AppLibrarySnapshot)", |
| | | "updateLastSnapshot(snapshot)", |
| | | ] |
| | | for token in required_library_tokens: |
| | | if token not in library: |
| | | raise SystemExit(f"AppLibraryController missing startup snapshot token: {token}") |
| | | |
| | | required_content_tokens = [ |
| | | "@State private var loadingSpinnerVisible = false", |
| | | "@State private var loadingSpinnerToken = 0", |
| | | "private let loadingSpinnerDelay: TimeInterval = 0.25", |
| | | "hydrateFromLastAppLibrarySnapshotIfNeeded()", |
| | | "AppLibraryController.lastSnapshot()", |
| | | "scheduleLoadingSpinnerIfNeeded()", |
| | | "hideLoadingSpinner()", |
| | | "if loadingSpinnerVisible", |
| | | ] |
| | | for token in required_content_tokens: |
| | | if token not in content: |
| | | raise SystemExit(f"ContentView missing startup loading token: {token}") |
| | | |
| | | if "if allApps.isEmpty {\n Spacer()\n ProgressView().scaleEffect(0.8)" in content: |
| | | raise SystemExit("AppGrid empty state still shows ProgressView immediately") |
| | | |
| | | if "if allApps.isEmpty {\n Spacer(); ProgressView().scaleEffect(0.8); Spacer()" in content: |
| | | raise SystemExit("edit AppGrid empty state still shows ProgressView immediately") |
| | | |
| | | print("PASS AppGrid startup loading QA: last snapshot reuse and delayed spinner are present") |
| | | PY |
| New file |
| | |
| | | #!/usr/bin/env bash |
| | | set -euo pipefail |
| | | |
| | | ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| | | TAG_NAV_SWIFT="$ROOT_DIR/Apptag/TagNavigationView.swift" |
| | | CONTENT_VIEW_SWIFT="$ROOT_DIR/Apptag/ContentView.swift" |
| | | APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift" |
| | | PREFERENCES_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift" |
| | | |
| | | fail() { |
| | | printf 'FAIL: %s\n' "$*" >&2 |
| | | exit 1 |
| | | } |
| | | |
| | | [[ -f "$TAG_NAV_SWIFT" ]] || fail "missing TagNavigationView.swift" |
| | | [[ -f "$CONTENT_VIEW_SWIFT" ]] || fail "missing ContentView.swift" |
| | | [[ -f "$APP_SWIFT" ]] || fail "missing ApptagApp.swift" |
| | | [[ -f "$PREFERENCES_SWIFT" ]] || fail "missing PreferencesView.swift" |
| | | |
| | | python3 - "$TAG_NAV_SWIFT" "$CONTENT_VIEW_SWIFT" "$APP_SWIFT" "$PREFERENCES_SWIFT" <<'PY' |
| | | import pathlib |
| | | import re |
| | | import sys |
| | | |
| | | tag_nav = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") |
| | | content = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8") |
| | | app = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8") |
| | | prefs = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8") |
| | | |
| | | |
| | | def fail(message: str) -> None: |
| | | print(f"FAIL: {message}", file=sys.stderr) |
| | | sys.exit(1) |
| | | |
| | | |
| | | def require(pattern: str, source: str, message: str) -> None: |
| | | if not re.search(pattern, source, re.S): |
| | | fail(message) |
| | | |
| | | |
| | | require(r"let\s+onDoubleActivate:\s*\(String\)\s*->\s*Void", tag_nav, "TagNavigationView must expose a double-activation callback") |
| | | require(r"private\s+var\s+onDoubleActivate:\s*\(String\)\s*->\s*Void", tag_nav, "document view must store the double-activation callback") |
| | | require(r"var\s+onDoubleActivate:\s*\(String\)\s*->\s*Void", tag_nav, "tag button must store the double-activation callback") |
| | | require(r"button\.onDoubleActivate\s*=\s*\{\s*\[weak\s+self\]\s*tagID\s+in\s+self\?\.onDoubleActivate\(tagID\)\s*\}", tag_nav, "tag button callback must be wired through the document view") |
| | | |
| | | mouse_match = re.search(r"override\s+func\s+mouseDown\(with\s+event:\s+NSEvent\)\s*\{(?P<body>.*?)@objc\s+private\s+func\s+performActivation", tag_nav, re.S) |
| | | if not mouse_match: |
| | | fail("TagNavigationButton.mouseDown must exist") |
| | | mouse_body = mouse_match.group("body") |
| | | double_click_index = mouse_body.find("event.clickCount >= 2") |
| | | reorder_guard_index = mouse_body.find("guard canReorder(item.id)") |
| | | long_press_index = mouse_body.find("longPressDuration: TimeInterval = 0.35") |
| | | if double_click_index < 0: |
| | | fail("mouseDown must check for double-click") |
| | | if reorder_guard_index < 0: |
| | | fail("mouseDown must keep the reorder guard") |
| | | if long_press_index < 0: |
| | | fail("mouseDown must preserve the 0.35s long-press reorder threshold") |
| | | if not (double_click_index < reorder_guard_index < long_press_index): |
| | | fail("double-click must be handled before entering reorder/long-press logic") |
| | | require(r"onDoubleActivate\(item\.id\)", mouse_body, "double-click must call onDoubleActivate with the tag id") |
| | | require(r"onActivate\(item\.id\)", mouse_body, "single-click activation must remain in mouseDown") |
| | | |
| | | require(r"tagLauncherPreferencesTabRequested", content, "shared notification for settings tab switching must exist") |
| | | require(r"enum\s+SettingsTabTarget(?P<body>.*?)userInfoKey\s*=\s*\"tab\"(?P<body2>.*?)tags\s*=\s*\"tags\"", content, "settings tab target constants must exist") |
| | | require(r"onDoubleActivate:\s*\{\s*_\s+in\s*openTagSettingsFromNavigation\(\)\s*\}", content, "AppKit tag navigation must open tag settings on double-click") |
| | | require(r"private\s+func\s+openTagSettingsFromNavigation\(\)\s*\{(?P<body>.*?)tagLauncherOpenPreferencesRequested(?P<body2>.*?)SettingsTabTarget\.tags", content, "double-click helper must request Preferences Tags tab") |
| | | require(r"onActivate:\s*\{\s*tagID\s+in\s*activateTagNavigation\(tagID\)\s*\}", content, "single-click tag activation must still scroll") |
| | | require(r"handleTagNavigationHover\(tagID,\s*active:\s*active\)", content, "tag hover behavior must remain wired") |
| | | |
| | | require(r"preferencesTabTarget\(from:\s*notification\)", app, "AppDelegate observer must read the requested preferences tab") |
| | | require(r"openPreferences\(targetTab:\s*Self\.preferencesTabTarget\(from:\s*notification\)\)", app, "preferences request must pass the target tab into openPreferences") |
| | | require(r"private\s+func\s+openPreferences\(targetTab:\s*String\?\)", app, "AppDelegate must have a target-tab preferences opener") |
| | | require(r"PreferencesView\(initialTabRawValue:\s*targetTab\)", app, "new settings windows must open on the requested tab") |
| | | require(r"requestPreferencesTabSelection\(targetTab\)", app, "existing settings windows must switch to the requested tab") |
| | | require(r"tagLauncherPreferencesTabRequested", app, "existing settings window tab switch must use the shared notification") |
| | | |
| | | require(r"init\(initialTabRawValue:\s*String\?\s*=\s*nil\)", prefs, "PreferencesView must accept an initial tab") |
| | | require(r"SettingsTab\.init\(rawValue:\s*\)", prefs, "PreferencesView initial tab must be validated through SettingsTab raw values") |
| | | require(r"private\s+func\s+selectTab\(rawValue:\s*String\?\)", prefs, "PreferencesView must expose an internal tab-selection helper") |
| | | require(r"publisher\(for:\s*\.tagLauncherPreferencesTabRequested\)", prefs, "PreferencesView must listen for tab switch requests") |
| | | |
| | | print("PASS: double-click tag navigation opens Preferences Tags tab without removing single-click, hover, or long-press wiring") |
| | | PY |
| New file |
| | |
| | | #!/bin/bash |
| | | set -euo pipefail |
| | | |
| | | ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" |
| | | APP_DIR="$ROOT_DIR/Apptag" |
| | | |
| | | python3 - "$APP_DIR" <<'PY' |
| | | import json |
| | | import re |
| | | import sys |
| | | from pathlib import Path |
| | | |
| | | app_dir = Path(sys.argv[1]) |
| | | theme_file = app_dir / "AppGridTheme.swift" |
| | | preferences_file = app_dir / "PreferencesView.swift" |
| | | defaults_file = app_dir / "AppDefaults.swift" |
| | | content_file = app_dir / "ContentView.swift" |
| | | grid_file = app_dir / "AppGridCollectionView.swift" |
| | | localization_dir = app_dir / "Localization" |
| | | |
| | | theme_source = theme_file.read_text(encoding="utf-8") |
| | | preferences_source = preferences_file.read_text(encoding="utf-8") |
| | | defaults_source = defaults_file.read_text(encoding="utf-8") |
| | | content_source = content_file.read_text(encoding="utf-8") |
| | | grid_source = grid_file.read_text(encoding="utf-8") |
| | | edit_views_source = (app_dir / "EditModeViews.swift").read_text(encoding="utf-8") |
| | | |
| | | expected_cases = [ |
| | | "defaultLight", |
| | | "deepBlue", |
| | | "black", |
| | | "pink", |
| | | "purple", |
| | | "green", |
| | | "blue", |
| | | "colorful", |
| | | ] |
| | | for case in expected_cases: |
| | | if not re.search(rf"case\s+{re.escape(case)}\b", theme_source): |
| | | raise SystemExit(f"missing AppGridTheme case: {case}") |
| | | |
| | | if "case theme" not in preferences_source: |
| | | raise SystemExit("SettingsTab is missing the theme tab") |
| | | if "settings.darkAppGrid" in preferences_source: |
| | | raise SystemExit("legacy dark grid copy is still referenced by PreferencesView") |
| | | if "Toggle(tr(\"settings.darkAppGrid\")" in preferences_source: |
| | | raise SystemExit("legacy dark grid toggle is still visible") |
| | | if "useDarkAppGrid" in content_source or "useDarkAppGrid" in grid_source: |
| | | raise SystemExit("legacy dark grid boolean still drives AppGrid rendering") |
| | | if "CAGradientLayer" in grid_source: |
| | | raise SystemExit("AppGridCollectionView still owns a background gradient layer") |
| | | if "useDarkAppGrid" not in defaults_source or "AppGridTheme.deepBlue.rawValue" not in defaults_source: |
| | | raise SystemExit("legacy dark preference migration is missing") |
| | | if "var usesVisualEffectBackdrop: Bool" not in theme_source: |
| | | raise SystemExit("theme visual-effect backdrop policy is missing") |
| | | if "var isPureBlackBackground: Bool" not in theme_source or "self == .black" not in theme_source: |
| | | raise SystemExit("pure black background policy is missing") |
| | | if "case .deepBlue, .black:" not in theme_source or "return true" not in theme_source: |
| | | raise SystemExit("dark glass must be limited to deepBlue and black") |
| | | if "case .defaultLight, .pink, .purple, .green, .blue, .colorful:" not in theme_source or "return false" not in theme_source: |
| | | raise SystemExit("bright themes must keep light glass") |
| | | if "backgroundDimmingOpacity" not in theme_source or "case .deepBlue:" not in theme_source: |
| | | raise SystemExit("theme background dimming policy is missing") |
| | | if "theme.usesVisualEffectBackdrop" not in content_source or "theme.isPureBlackBackground" not in content_source: |
| | | raise SystemExit("ContentView does not apply backdrop/pure-black theme policy") |
| | | required_edit_tokens = { |
| | | "editPrimaryTextColor", |
| | | "editSecondaryTextColor", |
| | | "editTertiaryTextColor", |
| | | "editDividerColor", |
| | | "editToolbarSurfaceColor", |
| | | "editControlSurfaceColor", |
| | | "editControlStrokeColor", |
| | | "editInactiveIndicatorColor", |
| | | "editDisabledTextColor", |
| | | "editDisabledSurfaceColor", |
| | | "editButtonShadowColor", |
| | | "editAccentColor", |
| | | "editConfirmForegroundColor", |
| | | } |
| | | missing_tokens = [token for token in sorted(required_edit_tokens) if token not in theme_source] |
| | | if missing_tokens: |
| | | raise SystemExit(f"AppGridTheme missing edit-mode contrast tokens: {missing_tokens}") |
| | | if "private var renderedAppGridTheme: AppGridTheme" not in content_source: |
| | | raise SystemExit("ContentView is missing the runtime theme override") |
| | | if "editPhase == .none ? appGridTheme : .defaultLight" not in content_source: |
| | | raise SystemExit("edit mode must temporarily render with the default light AppGrid theme") |
| | | if "let theme = renderedAppGridTheme" not in content_source: |
| | | raise SystemExit("AppGrid background does not use the runtime rendered theme") |
| | | if "AppGridCollectionView(" in content_source and "appGridTheme: renderedAppGridTheme" not in content_source: |
| | | raise SystemExit("AppGridCollectionView is not passed the runtime rendered theme") |
| | | if "EditAppsHeaderView(" in content_source and "theme: renderedAppGridTheme" not in content_source: |
| | | raise SystemExit("edit apps header is not passed the runtime rendered theme") |
| | | if "theme: renderedAppGridTheme" not in content_source: |
| | | raise SystemExit("edit mode selectable controls are not passed the runtime rendered theme") |
| | | for view_name in ["EditAppsHeaderView", "EditOperationPicker", "EditConfirmButton", "EditAppsSidebarIntroView", "EditSelectableTagItem", "EditableAppSelectionItem"]: |
| | | if f"let theme: AppGridTheme" not in edit_views_source: |
| | | raise SystemExit("edit mode views do not accept AppGridTheme") |
| | | if view_name not in edit_views_source: |
| | | raise SystemExit(f"missing edit mode view: {view_name}") |
| | | for token in ["theme.editPrimaryTextColor", "theme.editAccentColor", "theme.editControlSurfaceColor", "theme.editDisabledSurfaceColor"]: |
| | | if token not in edit_views_source: |
| | | raise SystemExit(f"edit mode views do not use contrast token: {token}") |
| | | if ".buttonStyle(.bordered)" in edit_views_source: |
| | | raise SystemExit("edit mode views still rely on default bordered button styling") |
| | | if ".buttonStyle(.bordered)" in content_source and "editTagsView" in content_source: |
| | | raise SystemExit("ContentView still relies on default bordered button styling in edit mode") |
| | | |
| | | required_keys = { |
| | | "settings.theme", |
| | | "settings.themeDesc", |
| | | "settings.themeScopeDesc", |
| | | "theme.default", |
| | | "theme.deepBlue", |
| | | "theme.black", |
| | | "theme.pink", |
| | | "theme.purple", |
| | | "theme.green", |
| | | "theme.blue", |
| | | "theme.colorful", |
| | | } |
| | | files = sorted(localization_dir.glob("*.json")) |
| | | if len(files) != 29: |
| | | raise SystemExit(f"expected 29 localization files, found {len(files)}") |
| | | for path in files: |
| | | data = json.loads(path.read_text(encoding="utf-8")) |
| | | legacy = {"settings.darkAppGrid", "settings.darkAppGridDesc"} & set(data) |
| | | if legacy: |
| | | raise SystemExit(f"{path.name} still has legacy dark grid keys: {sorted(legacy)}") |
| | | missing = required_keys - set(data) |
| | | if missing: |
| | | raise SystemExit(f"{path.name} missing keys: {sorted(missing)}") |
| | | raw_values = [ |
| | | key for key in required_keys |
| | | if str(data.get(key, "")).startswith("settings.") or str(data.get(key, "")).startswith("theme.") |
| | | ] |
| | | if raw_values: |
| | | raise SystemExit(f"{path.name} has raw key values: {raw_values}") |
| | | |
| | | print("PASS theme settings QA: 8 themes, Theme tab, migration, AppGrid-only rendering, glass policy, edit-mode default-theme override, and 29-language keys are present") |
| | | PY |
| | |
| | | local frontmost |
| | | frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')" |
| | | if [[ "$frontmost" != "TagLauncher" ]]; then |
| | | echo "FAIL: frontmost app is $frontmost, expected TagLauncher" >&2 |
| | | return 1 |
| | | log "INFO frontmost app is $frontmost; relying on TagLauncher window-layer assertions for this headless QA run" |
| | | return 0 |
| | | fi |
| | | log "PASS frontmost app: TagLauncher" |
| | | } |
| | | |
| | | prepare_isolated_app_instance() { |
| | |
| | | open -n "$APP_BUNDLE" |
| | | sleep 1.0 |
| | | dismiss_reopen_dialog |
| | | sleep 1.5 |
| | | sleep 2.0 |
| | | |
| | | if ! is_qa_app_only_running; then |
| | | echo "FAIL: expected only QA build TagLauncher instance to be running" >&2 |
| | |
| | | return 0 |
| | | } |
| | | |
| | | func rect(_ window: [String: Any]) -> CGRect { |
| | | let bounds = window[kCGWindowBounds as String] as? NSDictionary ?? [:] |
| | | return CGRect( |
| | | x: dimension(bounds, "X"), |
| | | y: dimension(bounds, "Y"), |
| | | width: dimension(bounds, "Width"), |
| | | height: dimension(bounds, "Height") |
| | | ) |
| | | } |
| | | |
| | | func isOverlayWindow(_ window: [String: Any]) -> Bool { |
| | | let name = (window[kCGWindowName as String] as? String) ?? "" |
| | | guard name.isEmpty else { return false } |
| | | let windowFrame = rect(window) |
| | | return NSScreen.screens.contains { screen in |
| | | let screenFrame = screen.frame |
| | | return abs(windowFrame.width - screenFrame.width) <= 12 |
| | | && windowFrame.height >= screenFrame.height * 0.75 |
| | | && abs(windowFrame.midX - screenFrame.midX) <= 12 |
| | | } |
| | | } |
| | | |
| | | func dumpTagWindows() { |
| | | fputs("---- TagLauncher windows for coords ----\n", stderr) |
| | | for (index, window) in tag.enumerated() { |
| | | let name = (window[kCGWindowName as String] as? String) ?? "" |
| | | let layer = window[kCGWindowLayer as String] as? Int ?? -999 |
| | | let bounds = window[kCGWindowBounds as String] as? NSDictionary ?? [:] |
| | | fputs("#\(index) name=\(name) layer=\(layer) bounds=\(bounds)\n", stderr) |
| | | } |
| | | fputs("----------------------------------------\n", stderr) |
| | | } |
| | | |
| | | switch mode { |
| | | case "data-tab": |
| | | guard let settings = tag.first(where: { (($0[kCGWindowName as String] as? String) ?? "").isEmpty == false }), |
| | |
| | | let overlayHeight = dimension(overlayBounds, "Height") |
| | | print("\(Int(round(overlayX + overlayWidth / 2))) \(Int(round(overlayY + overlayHeight / 2)))") |
| | | case "quick-search-result": |
| | | guard let quickSearch = tag.first(where: { window in |
| | | let candidates = tag.filter { window in |
| | | let name = (window[kCGWindowName as String] as? String) ?? "" |
| | | let bounds = window[kCGWindowBounds as String] as? NSDictionary ?? [:] |
| | | let width = dimension(bounds, "Width") |
| | | let height = dimension(bounds, "Height") |
| | | return name.isEmpty |
| | | && width >= 500 |
| | | && width <= 900 |
| | | && height >= 120 |
| | | && height <= 850 |
| | | }), let bounds = quickSearch[kCGWindowBounds as String] as? NSDictionary else { |
| | | && !isOverlayWindow(window) |
| | | && width >= 360 |
| | | && width <= ((NSScreen.screens.first?.frame.width ?? 1600) * 0.95) |
| | | && height >= 90 |
| | | && height <= 900 |
| | | } |
| | | guard let quickSearch = candidates.min(by: { rect($0).width * rect($0).height < rect($1).width * rect($1).height }), |
| | | let bounds = quickSearch[kCGWindowBounds as String] as? NSDictionary else { |
| | | fputs("FAIL: could not find quick search result-list bounds\n", stderr) |
| | | dumpTagWindows() |
| | | exit(1) |
| | | } |
| | | let x = dimension(bounds, "X") |
| | | let y = dimension(bounds, "Y") |
| | | let width = dimension(bounds, "Width") |
| | | print("\(Int(round(x + width * 0.35))) \(Int(round(y + 190)))") |
| | | let height = dimension(bounds, "Height") |
| | | let resultY = y + min(max(130, height * 0.55), max(80, height - 35)) |
| | | print("\(Int(round(x + width * 0.35))) \(Int(round(resultY)))") |
| | | case "fullscreen-target-center": |
| | | guard let target = raw.first(where: { ($0[kCGWindowName as String] as? String) == "TagLauncherFullscreenQATargetFullscreen" }), |
| | | let bounds = target[kCGWindowBounds as String] as? NSDictionary else { |
| | |
| | | |
| | | open_quick_search_with_retry() { |
| | | local output="" |
| | | for _ in {1..3}; do |
| | | for _ in {1..6}; do |
| | | send_quick_search_hotkey |
| | | sleep 0.8 |
| | | if output="$(wait_swift_assert quick-search 2>&1)"; then |
| | |
| | | send_keycode 49 |
| | | sleep 0.4 |
| | | if output="$(wait_swift_assert quick-search 2>&1)"; then |
| | | printf '%s\n' "$output" |
| | | return 0 |
| | | fi |
| | | done |
| | | printf '%s\n' "$output" >&2 |
| | | return 1 |
| | | } |
| | | |
| | | open_overlay_with_retry() { |
| | | local output="" |
| | | for _ in {1..3}; do |
| | | send_main_hotkey |
| | | sleep 0.6 |
| | | if output="$(wait_swift_assert overlay 2>&1)"; then |
| | | printf '%s\n' "$output" |
| | | return 0 |
| | | fi |
| | |
| | | defaults write "$DEFAULTS_DOMAIN" showDockIcon -bool false |
| | | prepare_isolated_app_instance |
| | | assert_no_dock_tile |
| | | send_main_hotkey |
| | | sleep 0.8 |
| | | wait_swift_assert overlay |
| | | open_overlay_with_retry |
| | | assert_no_dock_tile |
| | | send_keycode 53 |
| | | sleep 0.4 |
| | |
| | | |
| | | log "==> QA 1/7: overlay claims foreground, hides Dock, keeps menu bar visible" |
| | | show_overlay |
| | | frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')" |
| | | [[ "$frontmost" == "TagLauncher" ]] || { echo "FAIL: frontmost app is $frontmost, expected TagLauncher" >&2; exit 1; } |
| | | assert_frontmost_taglauncher |
| | | swift_assert overlay |
| | | |
| | | log "==> QA 1/7 and 4/7: settings floats above appgrid and quick search" |
| | |
| | | |
| | | ## In Progress |
| | | |
| | | - [2026-06-24] 8.0.0 App Grid 主题系统。 |
| | | - 目标: 在设置页新增“主题”页签,提供默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩 8 个主题;主题只改变 App Grid 全屏背景渐变,容器保持统一半透明玻璃面板,并按主题明暗自动适配玻璃明暗。 |
| | | - 范围: App Grid 背景、容器玻璃适配、设置页主题选择、旧 `useDarkAppGrid` 偏好迁移、29 语种主题文案、主题 QA 脚本。 |
| | | - 不做: 不新增容器玻璃深浅独立开关;不改标签/拖拽/排序/Quick Search/数据结构功能语义;不改浅色默认主题视觉。 |
| | | - 当前状态: 已完成代码实现、29 语种文案和确定性 QA;已按用户反馈将黑色改为纯黑,并将粉色/绿色/紫色/蓝色/炫彩改为更明亮且有辨识度的版本;等待用户视觉体验验收后决定是否继续冻结/打包。 |
| | | - 文档: `Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md`、`Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md`。 |
| | | |
| | | ## Todo |
| | | |
| | | - [发布后独立任务] 设置页 tab 容器 AppKit/自控化评估与实现。 |