From 77896f65b92c756da699e84499b180d35b0f8835 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 25 Jun 2026 18:52:06 +0800
Subject: [PATCH] Merge dark glass 8.0.5 freeze
---
src/Apptag/ContentView.swift | 588 +++++++++++++++++++++++++++++++++++-----------------------
1 files changed, 353 insertions(+), 235 deletions(-)
diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 122addf..7ab2869 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -8,9 +8,15 @@
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
@@ -82,6 +88,10 @@
}
func updateNSView(_ container: TextFieldContainer, context: Context) {
+ if let editor = container.textField.currentEditor() as? NSTextView,
+ editor.hasMarkedText() {
+ return
+ }
if container.textField.stringValue != text {
container.textField.stringValue = text
}
@@ -106,6 +116,9 @@
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+ if textView.hasMarkedText() {
+ return false
+ }
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
onSubmit?()
return true
@@ -315,10 +328,14 @@
var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil
var uncategorizedDropSuppressFuturePrompt = false
var tagRemovalDropSuppressFuturePrompt = false
+ var usageTipsCloseReminderVisible = false
+ var usageTipsCloseSuppressFuturePrompt = false
var appDragResetToken = 0
var bubbleDraftNote = ""
var tagNavigationHoveredGroupName: String? = nil
var tagNavigationAppDropTargetName: String? = nil
+ var tagNavigationLastHoverScrollID: String? = nil
+ var tagNavigationLastHoverScrollAt: Date? = nil
}
private struct EditActionFeedback: Identifiable {
@@ -340,120 +357,6 @@
let tagName: String
}
-private struct AppGridUsageTip: Identifiable {
- let id: Int
- let titleKey: String
- let detailKey: String
-}
-
-private struct AppGridUsageTipsBar: View {
- static let reservedHeight: CGFloat = 64
-
- let tips: [AppGridUsageTip]
- @Binding var selectedIndex: Int
- let dragModeActive: Bool
-
- @State private var textHovered = false
-
- private var safeIndex: Int {
- guard tips.indices.contains(selectedIndex) else { return 0 }
- return selectedIndex
- }
-
- private var currentTip: AppGridUsageTip? {
- guard !tips.isEmpty else { return nil }
- return tips[safeIndex]
- }
-
- var body: some View {
- if let tip = currentTip {
- HStack(spacing: 0) {
- Text(tr(tip.titleKey))
- .font(.system(size: 14, weight: .semibold))
- .foregroundStyle(Color.primary)
- .lineLimit(1)
- .minimumScaleFactor(0.82)
- .padding(.horizontal, 14)
- .frame(width: 174, height: 42, alignment: .leading)
- .background(Color(nsColor: .controlBackgroundColor).opacity(0.96))
-
- tipDetailText(tip)
- .frame(height: 42)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(Color.black.opacity(0.88))
-
- tipsControls
- .frame(width: 86, height: 42)
- .background(Color.black.opacity(0.88))
- }
- .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
- .overlay(
- RoundedRectangle(cornerRadius: 8, style: .continuous)
- .stroke(Color.primary.opacity(0.18), lineWidth: 1)
- )
- .shadow(color: .black.opacity(0.20), radius: 14, y: 7)
- .frame(maxWidth: 980)
- .padding(.horizontal, 18)
- .padding(.bottom, 12)
- .opacity(dragModeActive ? 0.72 : 1.0)
- .allowsHitTesting(!dragModeActive)
- .accessibilityElement(children: .combine)
- .accessibilityLabel("\(tr(tip.titleKey)) \(tr(tip.detailKey))")
- }
- }
-
- private func tipDetailText(_ tip: AppGridUsageTip) -> some View {
- ScrollView(.horizontal, showsIndicators: textHovered) {
- Text(tr(tip.detailKey))
- .font(.system(size: 15, weight: .medium))
- .foregroundStyle(Color.white.opacity(0.96))
- .lineLimit(1)
- .fixedSize(horizontal: true, vertical: false)
- .padding(.horizontal, 14)
- .frame(minHeight: 42, alignment: .center)
- }
- .onHover { textHovered = $0 }
- .help(tr(tip.detailKey))
- }
-
- private var tipsControls: some View {
- VStack(spacing: 4) {
- HStack(spacing: 8) {
- usageTipNavigationButton(systemImage: "chevron.left", labelKey: "usageTips.previous") {
- selectedIndex = (safeIndex - 1 + tips.count) % tips.count
- }
- usageTipNavigationButton(systemImage: "chevron.right", labelKey: "usageTips.next") {
- selectedIndex = (safeIndex + 1) % tips.count
- }
- }
-
- HStack(spacing: 3) {
- ForEach(tips.indices, id: \.self) { index in
- Circle()
- .fill(index == safeIndex ? Color.accentColor : Color.white.opacity(0.58))
- .frame(width: 4, height: 4)
- }
- }
- }
- }
-
- private func usageTipNavigationButton(
- systemImage: String,
- labelKey: String,
- action: @escaping () -> Void
- ) -> some View {
- Button(action: action) {
- Image(systemName: systemImage)
- .font(.system(size: 15, weight: .bold))
- .foregroundStyle(Color.white.opacity(0.96))
- .frame(width: 24, height: 22)
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .accessibilityLabel(tr(labelKey))
- }
-}
-
struct ContentView: View {
let hideOverlay: () -> Void
private let initialQuickSearchSource: String?
@@ -468,6 +371,7 @@
@State private var appGridScrollTargetID: String? = nil
@State private var appGridScrollRequestToken = 0
@State private var selectedUsageTipIndex = 0
+ @State private var usageTipsHovered = false
// Edit mode
@State private var editPhase: EditPhase = .none
@@ -489,6 +393,8 @@
@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
@@ -530,28 +436,43 @@
@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
+ @AppStorage("skipUsageTipsCloseReminder") private var skipUsageTipsCloseReminder = false
private let editSidebarWidth: CGFloat = 188
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 {
- appGridInteraction.appDragModeActive
+ usageTipsHovered
+ || appGridInteraction.appDragModeActive
|| appGridInteraction.pendingUncategorizedDrop != nil
|| appGridInteraction.pendingTagRemovalDrop != nil
+ || appGridInteraction.usageTipsCloseReminderVisible
}
private var appGridHighlightedGroupName: String? {
appGridInteraction.tagNavigationHoveredGroupName
?? (isColorlessContainerMode ? filledColorlessContainer : nil)
}
private let rightSidebarFloatingClearance: CGFloat = 44
+ 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 {
@@ -593,74 +514,17 @@
var body: some View {
ZStack {
if shouldRenderAppGridBehindQuickSearch {
- VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
- .ignoresSafeArea()
- .allowsHitTesting(false)
+ appGridBackground
}
if shouldRenderAppGridBehindQuickSearch {
- if notchHeight > 0 {
- VStack {
- Rectangle().fill(.black)
- .frame(height: notchHeight)
- .ignoresSafeArea(edges: .top)
- Spacer()
- }
- .allowsHitTesting(false)
- }
-
- switch editPhase {
- case .none:
- normalContent
- case .editingTags:
- editTagsView
- case .editingApps:
- editAppsView
- }
-
- uncommonAppBubbleOverlay
- smartStartNoticeOverlay
- editActionFeedbackOverlay
- uncategorizedDropConfirmOverlay
- tagRemovalDropConfirmOverlay
+ appGridInteractionLayer
}
quickSearchOverlay
- if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast {
- Text(message)
- .font(.system(size: 16, weight: .semibold))
- .foregroundStyle(.primary)
- .padding(.horizontal, 22)
- .padding(.vertical, 12)
- .background(
- RoundedRectangle(cornerRadius: 10)
- .fill(.ultraThickMaterial)
- .shadow(color: .black.opacity(0.22), radius: 18, y: 10)
- )
- .transition(.scale(scale: 0.96).combined(with: .opacity))
- .allowsHitTesting(false)
- }
-
- if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible {
- Color.black.opacity(0.08)
- .ignoresSafeArea()
- .transition(.opacity)
- .allowsHitTesting(false)
-
- ProgressView()
- .progressViewStyle(.circular)
- .controlSize(.large)
- .scaleEffect(1.15)
- .padding(24)
- .background(
- RoundedRectangle(cornerRadius: 12)
- .fill(.ultraThickMaterial)
- .shadow(color: .black.opacity(0.26), radius: 24, y: 12)
- )
- .transition(.scale(scale: 0.92).combined(with: .opacity))
- .allowsHitTesting(false)
- }
+ dropWarningToastOverlay
+ dropRefreshOverlay
}
.onAppear {
refreshNotchHeight()
@@ -766,6 +630,76 @@
}
}
+ @ViewBuilder
+ private var appGridInteractionLayer: some View {
+ if notchHeight > 0 {
+ VStack {
+ Rectangle().fill(.black)
+ .frame(height: notchHeight)
+ .ignoresSafeArea(edges: .top)
+ Spacer()
+ }
+ .allowsHitTesting(false)
+ }
+
+ switch editPhase {
+ case .none:
+ normalContent
+ case .editingTags:
+ editTagsView
+ case .editingApps:
+ editAppsView
+ }
+
+ uncommonAppBubbleOverlay
+ smartStartNoticeOverlay
+ editActionFeedbackOverlay
+ uncategorizedDropConfirmOverlay
+ tagRemovalDropConfirmOverlay
+ usageTipsCloseReminderOverlay
+ }
+
+ @ViewBuilder
+ private var dropWarningToastOverlay: some View {
+ if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast {
+ Text(message)
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundStyle(.primary)
+ .padding(.horizontal, 22)
+ .padding(.vertical, 12)
+ .background(
+ RoundedRectangle(cornerRadius: 10)
+ .fill(.ultraThickMaterial)
+ .shadow(color: .black.opacity(0.22), radius: 18, y: 10)
+ )
+ .transition(.scale(scale: 0.96).combined(with: .opacity))
+ .allowsHitTesting(false)
+ }
+ }
+
+ @ViewBuilder
+ private var dropRefreshOverlay: some View {
+ if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible {
+ Color.black.opacity(0.08)
+ .ignoresSafeArea()
+ .transition(.opacity)
+ .allowsHitTesting(false)
+
+ ProgressView()
+ .progressViewStyle(.circular)
+ .controlSize(.large)
+ .scaleEffect(1.15)
+ .padding(24)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(.ultraThickMaterial)
+ .shadow(color: .black.opacity(0.26), radius: 24, y: 12)
+ )
+ .transition(.scale(scale: 0.92).combined(with: .opacity))
+ .allowsHitTesting(false)
+ }
+ }
+
private func publishModalInteractionState() {
NotificationCenter.default.post(
name: .tagLauncherModalInteractionChanged,
@@ -774,6 +708,7 @@
"active": smartStartNotice != nil
|| appGridInteraction.pendingUncategorizedDrop != nil
|| appGridInteraction.pendingTagRemovalDrop != nil
+ || appGridInteraction.usageTipsCloseReminderVisible
]
)
}
@@ -1057,6 +992,39 @@
}
}
+ 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") {
@@ -1154,6 +1122,9 @@
onActivate: { tagID in
activateTagNavigation(tagID)
},
+ onDoubleActivate: { _ in
+ openTagSettingsFromNavigation()
+ },
onHoverChange: { tagID, active in
handleTagNavigationHover(tagID, active: active)
},
@@ -1178,6 +1149,14 @@
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]
)
}
@@ -1242,53 +1221,50 @@
Group {
if allApps.isEmpty {
Spacer()
- ProgressView().scaleEffect(0.8)
+ if loadingSpinnerVisible {
+ ProgressView().scaleEffect(0.8)
+ }
Spacer()
} else {
- ZStack(alignment: .bottom) {
- AppGridCollectionView(
- groups: displayGroups,
- tagColors: tagColors,
- displayMode: displayMode,
- iconSize: iconSize,
- showNames: !hideAppNames,
- bubbleDisabled: appBubbleDisabled,
- showUncommonAppBubbles: showUncommonAppBubbles,
- highlightedGroupName: appGridHighlightedGroupName,
- bottomContentPadding: shouldShowUsageTips ? AppGridUsageTipsBar.reservedHeight : 0,
- contentRevision: groupLayoutVersion,
- scrollTargetID: appGridScrollTargetID,
- scrollRequestToken: appGridScrollRequestToken,
- onSelectApp: { app in openApp(app) },
- onBubbleHover: handleBubbleHover,
- onEditNote: beginEditingBubbleNote,
- onDropApp: { path, source, target, copy in
- dropApp(path: path, sourceTag: source, targetTag: target, copy: copy)
- },
- onDropOutsideGroup: { path, source, copy in
- dropAppOutsideGroup(path: path, sourceTag: source, copy: copy)
- },
- onReorderApps: { containerID, orderedPaths in
- reorderApps(inContainer: containerID, orderedPaths: orderedPaths)
- },
- onGroupActivate: { groupName in
- if isColorlessContainerMode {
- toggleColorlessFill(groupName)
- }
- },
- onScrollActivity: handleAppGridScrollActivity,
- onDragModeChange: { setAppDragMode($0) }
- )
-
- if shouldShowUsageTips {
- AppGridUsageTipsBar(
- tips: appGridUsageTips,
- selectedIndex: $selectedUsageTipIndex,
- dragModeActive: appGridInteraction.appDragModeActive
- )
- .zIndex(3)
- }
- }
+ AppGridCollectionView(
+ groups: displayGroups,
+ tagColors: tagColors,
+ displayMode: displayMode,
+ iconSize: iconSize,
+ showNames: !hideAppNames,
+ appGridTheme: renderedAppGridTheme,
+ bubbleDisabled: appBubbleDisabled,
+ showUncommonAppBubbles: showUncommonAppBubbles,
+ highlightedGroupName: appGridHighlightedGroupName,
+ bottomContentPadding: shouldShowUsageTips ? AppGridUsageTipsMetrics.reservedHeight : 0,
+ usageTipsVisible: shouldShowUsageTips,
+ usageTips: appGridUsageTips,
+ selectedUsageTipIndex: $selectedUsageTipIndex,
+ contentRevision: groupLayoutVersion,
+ scrollTargetID: appGridScrollTargetID,
+ scrollRequestToken: appGridScrollRequestToken,
+ onSelectApp: { app in openApp(app) },
+ onBubbleHover: handleBubbleHover,
+ onEditNote: beginEditingBubbleNote,
+ onDropApp: { path, source, target, copy in
+ dropApp(path: path, sourceTag: source, targetTag: target, copy: copy)
+ },
+ onDropOutsideGroup: { path, source, copy in
+ dropAppOutsideGroup(path: path, sourceTag: source, copy: copy)
+ },
+ onReorderApps: { containerID, orderedPaths in
+ reorderApps(inContainer: containerID, orderedPaths: orderedPaths)
+ },
+ onGroupActivate: { groupName in
+ if isColorlessContainerMode {
+ toggleColorlessFill(groupName)
+ }
+ },
+ onScrollActivity: handleAppGridScrollActivity,
+ onDragModeChange: { setAppDragMode($0) },
+ onHideUsageTips: requestHideUsageTips,
+ onUsageTipsHoverChange: handleUsageTipsHoverChange
+ )
}
}
}
@@ -1349,17 +1325,32 @@
} 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,
@@ -1374,6 +1365,7 @@
private var editAppsView: some View {
VStack(spacing: 0) {
EditAppsHeaderView(
+ theme: renderedAppGridTheme,
operation: editTagOperation,
hintText: editModeHintText,
confirmTitle: editConfirmTitle,
@@ -1387,11 +1379,12 @@
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
)
@@ -1423,10 +1416,14 @@
.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
}
@@ -1450,12 +1447,12 @@
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)
@@ -1488,7 +1485,8 @@
colorIndex: colorIndex,
operation: editTagOperation,
isSelected: isSelected,
- isRemovableCandidate: isRemovableCandidate
+ isRemovableCandidate: isRemovableCandidate,
+ theme: renderedAppGridTheme
) {
if isSelected {
selectedTagNames.remove(tagName)
@@ -1562,7 +1560,8 @@
return EditableAppSelectionItem(
app: app,
iconSize: iconSize,
- isSelected: isSelected
+ isSelected: isSelected,
+ theme: renderedAppGridTheme
) {
toggleEditableAppSelection(app)
}
@@ -1710,6 +1709,34 @@
}
.ignoresSafeArea()
.allowsHitTesting(appGridInteraction.pendingTagRemovalDrop != nil)
+ }
+
+ private var usageTipsCloseReminderOverlay: some View {
+ GeometryReader { proxy in
+ if appGridInteraction.usageTipsCloseReminderVisible {
+ ZStack {
+ Color.black.opacity(0.14)
+ .ignoresSafeArea()
+
+ UsageTipsCloseReminderBubble(
+ title: tr("usageTips.closeReminder.title"),
+ message: tr("usageTips.closeReminder.message"),
+ previewCallout: tr("usageTips.closeReminder.previewCallout"),
+ doNotRemindTitle: tr("usageTips.closeReminder.doNotRemind"),
+ doNotRemind: $appGridInteraction.usageTipsCloseSuppressFuturePrompt,
+ confirmTitle: tr("edit.confirm"),
+ onCancel: dismissUsageTipsCloseReminder,
+ onConfirm: confirmUsageTipsCloseReminder
+ )
+ .frame(width: min(760, max(420, proxy.size.width - 120)))
+ .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
+ .transition(.scale(scale: 0.94).combined(with: .opacity))
+ }
+ .zIndex(712)
+ }
+ }
+ .ignoresSafeArea()
+ .allowsHitTesting(appGridInteraction.usageTipsCloseReminderVisible)
}
private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback {
@@ -1985,16 +2012,24 @@
// 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() {
@@ -2007,6 +2042,13 @@
private func handleAppGridScrollActivity() {
if appGridInteraction.hoveredBubble != nil {
+ appGridInteraction.hoveredBubble = nil
+ }
+ }
+
+ private func handleUsageTipsHoverChange(_ hovering: Bool) {
+ usageTipsHovered = hovering
+ if hovering, appGridInteraction.hoveredBubble != nil {
appGridInteraction.hoveredBubble = nil
}
}
@@ -2152,7 +2194,7 @@
}
private func handleTagNavigationHover(_ id: String, active: Bool) {
- if appGridInteraction.appDragModeActive {
+ if appGridInteraction.appDragModeActive || tagNavDragModeActive {
if !active, appGridInteraction.tagNavigationHoveredGroupName == id {
appGridInteraction.tagNavigationHoveredGroupName = nil
}
@@ -2168,6 +2210,7 @@
appGridInteraction.tagNavigationHoveredGroupName = id
fillColorlessContainer(id)
+ scheduleTagNavigationHoverScroll(id)
}
private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2186,6 +2229,26 @@
}
appGridInteraction.tagNavigationAppDropTargetName = id
+ }
+
+ private func scheduleTagNavigationHoverScroll(_ id: String) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + tagNavigationHoverScrollDelay) {
+ guard appGridInteraction.tagNavigationHoveredGroupName == id else { return }
+ guard !appGridInteraction.appDragModeActive && !tagNavDragModeActive else { return }
+ scrollToTagFromHover(id)
+ }
+ }
+
+ private func scrollToTagFromHover(_ id: String) {
+ let now = Date()
+ if appGridInteraction.tagNavigationLastHoverScrollID == id,
+ let lastScrollAt = appGridInteraction.tagNavigationLastHoverScrollAt,
+ now.timeIntervalSince(lastScrollAt) < tagNavigationHoverScrollInterval {
+ return
+ }
+ appGridInteraction.tagNavigationLastHoverScrollID = id
+ appGridInteraction.tagNavigationLastHoverScrollAt = now
+ scrollTo(id)
}
private func fillColorlessContainer(_ id: String) {
@@ -2481,6 +2544,40 @@
}
}
+ private func requestHideUsageTips() {
+ guard !hideUsageTips else { return }
+ guard !skipUsageTipsCloseReminder else {
+ hideUsageTips = true
+ return
+ }
+ clearAppBubbleState()
+ appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
+ withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) {
+ appGridInteraction.usageTipsCloseReminderVisible = true
+ }
+ publishModalInteractionState()
+ }
+
+ private func dismissUsageTipsCloseReminder() {
+ appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
+ withAnimation(.easeOut(duration: 0.18)) {
+ appGridInteraction.usageTipsCloseReminderVisible = false
+ }
+ publishModalInteractionState()
+ }
+
+ private func confirmUsageTipsCloseReminder() {
+ if appGridInteraction.usageTipsCloseSuppressFuturePrompt {
+ skipUsageTipsCloseReminder = true
+ }
+ appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
+ withAnimation(.easeOut(duration: 0.16)) {
+ appGridInteraction.usageTipsCloseReminderVisible = false
+ }
+ hideUsageTips = true
+ publishModalInteractionState()
+ }
+
private func removeTagFromDroppedApp(app: AppInfo, tagName: String) {
guard isRemovableRegularTag(tagName),
appHasTag(app, tagName: tagName)
@@ -2614,6 +2711,7 @@
}
refreshInProgress = true
+ scheduleLoadingSpinnerIfNeeded()
DispatchQueue.global(qos: .userInitiated).async {
let result = AppLibraryController.refresh(useCache: useCache)
DispatchQueue.main.async {
@@ -2623,6 +2721,7 @@
finishDropRefreshAfterMinimumDuration()
}
refreshInProgress = false
+ hideLoadingSpinner()
if refreshAgainAfterCurrent {
let shouldForceLayout = refreshAgainForceLayout
let shouldUseCache = refreshAgainUseCache
@@ -2635,6 +2734,25 @@
}
}
+ 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
--
Gitblit v1.9.3