| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | @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 |
| | | @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 { |
| | | usageTipsHovered |
| | | || appGridInteraction.appDragModeActive |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | } |
| | | private var appGridHighlightedGroupName: String? { |
| | | appGridInteraction.tagNavigationHoveredGroupName |
| | |
| | | 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 { |
| | | 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() |
| | |
| | | } |
| | | } |
| | | |
| | | @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, |
| | |
| | | "active": smartStartNotice != nil |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | ] |
| | | ) |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | 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() |
| | | if loadingSpinnerVisible { |
| | | ProgressView().scaleEffect(0.8) |
| | | } |
| | | Spacer() |
| | | } else { |
| | | AppGridCollectionView( |
| | |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: !hideAppNames, |
| | | appGridTheme: renderedAppGridTheme, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | highlightedGroupName: appGridHighlightedGroupName, |
| | |
| | | }, |
| | | onScrollActivity: handleAppGridScrollActivity, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onHideUsageTips: requestHideUsageTips, |
| | | onUsageTipsHoverChange: handleUsageTipsHoverChange |
| | | ) |
| | | } |
| | |
| | | } 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) |
| | | } |
| | |
| | | } |
| | | .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 { |
| | |
| | | // 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() { |
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | |
| | | } |
| | | |
| | | 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 |