Ariver
2026-06-25 ab59aa3a800139e2fe80f7e802dfada2258ab0f6
Add usage tips close reminder
37 files modified
3 files added
859 ■■■■ changed files
src/Apptag/AppDefaults.swift 1 ●●●● patch | view | raw | blame | history
src/Apptag/AppGridCollectionView.swift 7 ●●●●● patch | view | raw | blame | history
src/Apptag/ContentView.swift 200 ●●●● patch | view | raw | blame | history
src/Apptag/EditModeViews.swift 215 ●●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar-Najdi.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/cs.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/da.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/de.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/en.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/es.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/fr.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/id.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/it.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ja.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ko.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ms.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/nb.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/nl.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/nn.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/no.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/pl.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/pt-BR.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ro.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ru.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/sr-Cyrl.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/sv.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/th.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/tr.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/uk.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/vi.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hans.json 6 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hant.json 6 ●●●● patch | view | raw | blame | history
src/CHANGELOG.md 6 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-25-usage-tips-close-reminder-todo.md 48 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-25-usage-tips-close-reminder-worklog.md 12 ●●●●● patch | view | raw | blame | history
src/Scripts/usage_tips_close_reminder_qa.sh 125 ●●●●● patch | view | raw | blame | history
src/Scripts/usage_tips_qa.sh 59 ●●●● patch | view | raw | blame | history
src/TODO.md 8 ●●●●● patch | view | raw | blame | history
src/Apptag/AppDefaults.swift
@@ -32,6 +32,7 @@
            "useAppKitTagNavigation": useAppKitTagNavigation,
            "skipTagRemovalDropConfirm": false,
            "skipUncategorizedDropConfirm": false,
            "skipUsageTipsCloseReminder": false,
            LauncherHotkeyRegistrationStore.mainStateKey: LauncherHotkeyRegistrationState.active.rawValue,
            LauncherHotkeyRegistrationStore.quickSearchStateKey: LauncherHotkeyRegistrationState.active.rawValue
        ])
src/Apptag/AppGridCollectionView.swift
@@ -1145,7 +1145,7 @@
        detailScrollView.borderType = .noBorder
        detailScrollView.documentView = detailLabel
        closeButton.action = { [weak self] in self?.hideUsageTips() }
        closeButton.action = { [weak self] in self?.requestHideUsageTips() }
        previousButton.action = { [weak self] in self?.selectUsageTip(offset: -1) }
        nextButton.action = { [weak self] in self?.selectUsageTip(offset: 1) }
@@ -1205,10 +1205,9 @@
        applyCoordinatorState()
    }
    private func hideUsageTips() {
    private func requestHideUsageTips() {
        Diagnostics.log("usageTips.hud.close")
        clearHoverState()
        isHidden = true
        coordinator?.onHideUsageTips()
    }
@@ -1217,7 +1216,7 @@
        let closeHitOutset: CGFloat = 8
        if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) {
            closeButton.performPressFeedback()
            hideUsageTips()
            requestHideUsageTips()
            return true
        }
        let hitOutset: CGFloat = 6
src/Apptag/ContentView.swift
@@ -328,6 +328,8 @@
    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
@@ -438,6 +440,7 @@
    @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
@@ -449,6 +452,7 @@
            || appGridInteraction.appDragModeActive
            || appGridInteraction.pendingUncategorizedDrop != nil
            || appGridInteraction.pendingTagRemovalDrop != nil
            || appGridInteraction.usageTipsCloseReminderVisible
    }
    private var appGridHighlightedGroupName: String? {
        appGridInteraction.tagNavigationHoveredGroupName
@@ -514,68 +518,13 @@
            }
            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()
@@ -681,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,
@@ -689,6 +708,7 @@
                "active": smartStartNotice != nil
                    || appGridInteraction.pendingUncategorizedDrop != nil
                    || appGridInteraction.pendingTagRemovalDrop != nil
                    || appGridInteraction.usageTipsCloseReminderVisible
            ]
        )
    }
@@ -1242,7 +1262,7 @@
                    },
                    onScrollActivity: handleAppGridScrollActivity,
                    onDragModeChange: { setAppDragMode($0) },
                    onHideUsageTips: { hideUsageTips = true },
                    onHideUsageTips: requestHideUsageTips,
                    onUsageTipsHoverChange: handleUsageTipsHoverChange
                )
            }
@@ -1689,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 {
@@ -2496,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)
src/Apptag/EditModeViews.swift
@@ -621,3 +621,218 @@
        .onExitCommand(perform: onCancel)
    }
}
struct UsageTipsCloseReminderBubble: View {
    let title: String
    let message: String
    let previewCallout: String
    let doNotRemindTitle: String
    @Binding var doNotRemind: Bool
    let confirmTitle: String
    let onCancel: () -> Void
    let onConfirm: () -> Void
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            HStack(alignment: .top, spacing: 12) {
                Text(title)
                    .font(.system(size: 22, weight: .bold))
                    .foregroundStyle(.white)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity, alignment: .leading)
                Button(action: onCancel) {
                    Image(systemName: "xmark")
                        .font(.system(size: 12, weight: .bold))
                        .foregroundStyle(.white.opacity(0.82))
                        .frame(width: 26, height: 26)
                        .background(Circle().fill(Color.white.opacity(0.12)))
                }
                .buttonStyle(.plain)
                .help(tr("tag.cancel"))
            }
            UsageTipsCloseReminderSettingsPreview(callout: previewCallout)
            Text(message)
                .font(.system(size: 15, weight: .medium))
                .lineSpacing(4)
                .foregroundStyle(.white.opacity(0.84))
                .multilineTextAlignment(.leading)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
            Button {
                doNotRemind.toggle()
            } label: {
                HStack(spacing: 10) {
                    ZStack {
                        RoundedRectangle(cornerRadius: 4, style: .continuous)
                            .fill(doNotRemind ? Color.accentColor : Color.white.opacity(0.10))
                        RoundedRectangle(cornerRadius: 4, style: .continuous)
                            .stroke(Color.white.opacity(0.92), lineWidth: 1.6)
                        if doNotRemind {
                            Image(systemName: "checkmark")
                                .font(.system(size: 12, weight: .bold))
                                .foregroundStyle(.white)
                        }
                    }
                    .frame(width: 18, height: 18)
                    Text(doNotRemindTitle)
                        .font(.system(size: 13, weight: .medium))
                        .foregroundStyle(.white.opacity(0.82))
                        .lineLimit(2)
                        .multilineTextAlignment(.leading)
                }
                .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            HStack {
                Spacer(minLength: 0)
                Button(action: onConfirm) {
                    Text(confirmTitle)
                        .font(.system(size: 13, weight: .semibold))
                        .foregroundStyle(.white)
                        .frame(minWidth: 96)
                        .frame(height: 32)
                        .padding(.horizontal, 10)
                        .background(
                            RoundedRectangle(cornerRadius: 8, style: .continuous)
                                .fill(Color.accentColor)
                        )
                }
                .buttonStyle(.plain)
            }
        }
        .padding(.horizontal, 24)
        .padding(.vertical, 20)
        .background(
            RoundedRectangle(cornerRadius: 18, style: .continuous)
                .fill(Color.black.opacity(0.92))
                .shadow(color: .black.opacity(0.34), radius: 24, y: 16)
                .shadow(color: .black.opacity(0.18), radius: 8, y: 3)
        )
        .onExitCommand(perform: onCancel)
    }
}
private struct UsageTipsCloseReminderSettingsPreview: View {
    let callout: String
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack(spacing: 6) {
                Circle().fill(Color.red.opacity(0.78)).frame(width: 10, height: 10)
                Circle().fill(Color.gray.opacity(0.45)).frame(width: 10, height: 10)
                Circle().fill(Color.gray.opacity(0.36)).frame(width: 10, height: 10)
                Text(verbatim: "TagLauncher Settings")
                    .font(.system(size: 11, weight: .semibold))
                    .foregroundStyle(Color.black.opacity(0.62))
                    .padding(.leading, 6)
                Spacer()
            }
            HStack(spacing: 18) {
                previewTab(systemImage: "globe", title: tr("settings.language"), active: false)
                previewTab(systemImage: "gearshape", title: tr("settings.general"), active: true)
                previewTab(systemImage: "keyboard", title: tr("quickSearch.hotkeys"), active: false)
                previewTab(systemImage: "tag", title: tr("settings.tags"), active: false)
                previewTab(systemImage: "externaldrive.fill", title: tr("settings.data"), active: false)
                previewTab(systemImage: "info.circle", title: tr("settings.about"), active: false)
            }
            .frame(maxWidth: .infinity, alignment: .center)
            .padding(.vertical, 8)
            HStack(spacing: 12) {
                previewCheckbox(title: tr("settings.launchAtLogin"), highlighted: false)
                previewCheckbox(title: tr("settings.showInDock"), highlighted: false)
                previewCheckbox(title: tr("settings.hideAppNames"), highlighted: false)
                previewCheckbox(title: tr("settings.hideUsageTips"), highlighted: true)
            }
            .padding(10)
            .background(
                RoundedRectangle(cornerRadius: 10, style: .continuous)
                    .fill(Color.white.opacity(0.84))
            )
            HStack(spacing: 8) {
                Image(systemName: "arrow.up.left")
                    .font(.system(size: 12, weight: .bold))
                    .foregroundStyle(Color.accentColor)
                Text(callout)
                    .font(.system(size: 11, weight: .semibold))
                    .foregroundStyle(Color.accentColor)
                    .lineLimit(2)
                    .fixedSize(horizontal: false, vertical: true)
            }
            .frame(maxWidth: .infinity, alignment: .trailing)
        }
        .padding(14)
        .background(
            RoundedRectangle(cornerRadius: 14, style: .continuous)
                .fill(
                    LinearGradient(
                        colors: [
                            Color.white.opacity(0.98),
                            Color(red: 0.93, green: 0.96, blue: 1.0).opacity(0.96)
                        ],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    )
                )
        )
        .overlay(
            RoundedRectangle(cornerRadius: 14, style: .continuous)
                .stroke(Color.white.opacity(0.64), lineWidth: 1)
        )
    }
    private func previewTab(systemImage: String, title: String, active: Bool) -> some View {
        VStack(spacing: 4) {
            Image(systemName: systemImage)
                .font(.system(size: 18, weight: .semibold))
            Text(title)
                .font(.system(size: 9, weight: .semibold))
                .lineLimit(1)
                .minimumScaleFactor(0.62)
        }
        .foregroundStyle(active ? Color.accentColor : Color.gray.opacity(0.82))
        .frame(width: 70, height: 48)
        .background(
            RoundedRectangle(cornerRadius: 9, style: .continuous)
                .fill(active ? Color.accentColor.opacity(0.12) : Color.clear)
        )
    }
    private func previewCheckbox(title: String, highlighted: Bool) -> some View {
        HStack(spacing: 6) {
            ZStack {
                RoundedRectangle(cornerRadius: 4, style: .continuous)
                    .fill(highlighted ? Color.accentColor : Color.gray.opacity(0.24))
                Image(systemName: "checkmark")
                    .font(.system(size: 9, weight: .bold))
                    .foregroundStyle(.white)
            }
            .frame(width: 16, height: 16)
            Text(title)
                .font(.system(size: 10, weight: highlighted ? .bold : .semibold))
                .foregroundStyle(Color.black.opacity(0.72))
                .lineLimit(1)
                .minimumScaleFactor(0.58)
        }
        .padding(.horizontal, 8)
        .frame(height: 30)
        .background(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .fill(highlighted ? Color.accentColor.opacity(0.12) : Color.clear)
        )
        .overlay(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .stroke(highlighted ? Color.accentColor.opacity(0.72) : Color.clear, lineWidth: 1.5)
        )
    }
}
src/Apptag/Info.plist
@@ -19,9 +19,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>8.0.2</string>
    <string>8.0.3</string>
    <key>CFBundleVersion</key>
    <string>20260624.2341</string>
    <string>20260625.1615</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/Apptag/Localization/ar-Najdi.json
@@ -239,5 +239,9 @@
  "theme.purple": "بنفسجي",
  "theme.green": "أخضر",
  "theme.blue": "أزرق",
  "theme.colorful": "ملوّن"
  "theme.colorful": "ملوّن",
  "usageTips.closeReminder.title": "إظهار التلميحات بعدين",
  "usageTips.closeReminder.message": "تقدر ترجع تعرض التلميحات من الإعدادات > عام بإلغاء تحديد “إخفاء نصائح الاستخدام”.",
  "usageTips.closeReminder.doNotRemind": "تم. لا تذكرني مرة ثانية إذا سكرت التلميحات.",
  "usageTips.closeReminder.previewCallout": "شِلّ التحديد هنا عشان ترجع التلميحات"
}
src/Apptag/Localization/ar.json
@@ -239,5 +239,9 @@
  "theme.purple": "بنفسجي",
  "theme.green": "أخضر",
  "theme.blue": "أزرق",
  "theme.colorful": "ملوّن"
  "theme.colorful": "ملوّن",
  "usageTips.closeReminder.title": "إظهار التلميحات لاحقًا",
  "usageTips.closeReminder.message": "يمكنك إظهار التلميحات مرة أخرى من الإعدادات > عام عبر إلغاء تحديد “إخفاء نصائح الاستخدام”.",
  "usageTips.closeReminder.doNotRemind": "فهمت. لا تذكرني مرة أخرى عند إغلاق التلميحات.",
  "usageTips.closeReminder.previewCallout": "ألغِ تحديد هذا الخيار لإظهار التلميحات مجددًا"
}
src/Apptag/Localization/cs.json
@@ -239,5 +239,9 @@
  "theme.purple": "Fialový",
  "theme.green": "Zelený",
  "theme.blue": "Modrý",
  "theme.colorful": "Barevný"
  "theme.colorful": "Barevný",
  "usageTips.closeReminder.title": "Zobrazit tipy později znovu",
  "usageTips.closeReminder.message": "Tipy můžete znovu zobrazit v Nastavení > Obecné zrušením volby „Skrýt tipy k používání“.",
  "usageTips.closeReminder.doNotRemind": "Rozumím. Při příštím zavření tipů už nepřipomínat.",
  "usageTips.closeReminder.previewCallout": "Zrušte tuto volbu a tipy se znovu zobrazí"
}
src/Apptag/Localization/da.json
@@ -239,5 +239,9 @@
  "theme.purple": "Lilla",
  "theme.green": "Grøn",
  "theme.blue": "Blå",
  "theme.colorful": "Farverig"
  "theme.colorful": "Farverig",
  "usageTips.closeReminder.title": "Vis tips igen senere",
  "usageTips.closeReminder.message": "Du kan vise tips igen i Indstillinger > Generelt ved at fjerne markeringen af “Skjul brugstips”.",
  "usageTips.closeReminder.doNotRemind": "Forstået. Mind mig ikke om det igen, når jeg lukker tips.",
  "usageTips.closeReminder.previewCallout": "Fjern markeringen for at vise tips igen"
}
src/Apptag/Localization/de.json
@@ -239,5 +239,9 @@
  "theme.purple": "Violett",
  "theme.green": "Grün",
  "theme.blue": "Blau",
  "theme.colorful": "Bunt"
  "theme.colorful": "Bunt",
  "usageTips.closeReminder.title": "Tipps später wieder anzeigen",
  "usageTips.closeReminder.message": "Du kannst die Tipps später unter Einstellungen > Allgemein wieder anzeigen, indem du „Nutzungstipps ausblenden“ deaktivierst.",
  "usageTips.closeReminder.doNotRemind": "Verstanden. Beim Schließen der Tipps nicht mehr erinnern.",
  "usageTips.closeReminder.previewCallout": "Deaktiviere diese Option, um Tipps wieder anzuzeigen"
}
src/Apptag/Localization/en.json
@@ -239,5 +239,9 @@
  "theme.purple": "Purple",
  "theme.green": "Green",
  "theme.blue": "Blue",
  "theme.colorful": "Colorful"
  "theme.colorful": "Colorful",
  "usageTips.closeReminder.title": "Show tips again later",
  "usageTips.closeReminder.message": "You can show tips again later in Settings > General by clearing “Hide usage tips”.",
  "usageTips.closeReminder.doNotRemind": "Got it. Don’t remind me next time I close tips.",
  "usageTips.closeReminder.previewCallout": "Clear this checkbox to show tips again"
}
src/Apptag/Localization/es.json
@@ -239,5 +239,9 @@
  "theme.purple": "Morado",
  "theme.green": "Verde",
  "theme.blue": "Azul",
  "theme.colorful": "Colorido"
  "theme.colorful": "Colorido",
  "usageTips.closeReminder.title": "Volver a ver los consejos más tarde",
  "usageTips.closeReminder.message": "Puedes volver a ver los consejos en Ajustes > General desmarcando “Ocultar consejos de uso”.",
  "usageTips.closeReminder.doNotRemind": "Entendido. No volver a recordármelo al cerrar los consejos.",
  "usageTips.closeReminder.previewCallout": "Desmarca esta opción para volver a verlos"
}
src/Apptag/Localization/fr.json
@@ -239,5 +239,9 @@
  "theme.purple": "Violet",
  "theme.green": "Vert",
  "theme.blue": "Bleu",
  "theme.colorful": "Coloré"
  "theme.colorful": "Coloré",
  "usageTips.closeReminder.title": "Retrouver les astuces plus tard",
  "usageTips.closeReminder.message": "Vous pourrez revoir les astuces dans Réglages > Général en décochant « Masquer les astuces d’utilisation ».",
  "usageTips.closeReminder.doNotRemind": "Compris. Ne plus me le rappeler quand je ferme les astuces.",
  "usageTips.closeReminder.previewCallout": "Décochez cette option pour revoir les astuces"
}
src/Apptag/Localization/id.json
@@ -239,5 +239,9 @@
  "theme.purple": "Ungu",
  "theme.green": "Hijau",
  "theme.blue": "Biru",
  "theme.colorful": "Berwarna"
  "theme.colorful": "Berwarna",
  "usageTips.closeReminder.title": "Tampilkan tips lagi nanti",
  "usageTips.closeReminder.message": "Kamu dapat menampilkan tips lagi di Pengaturan > Umum dengan menghapus centang “Sembunyikan tips penggunaan”.",
  "usageTips.closeReminder.doNotRemind": "Mengerti. Jangan ingatkan lagi saat saya menutup tips.",
  "usageTips.closeReminder.previewCallout": "Hapus centang ini untuk menampilkan tips lagi"
}
src/Apptag/Localization/it.json
@@ -239,5 +239,9 @@
  "theme.purple": "Viola",
  "theme.green": "Verde",
  "theme.blue": "Blu",
  "theme.colorful": "Colorato"
  "theme.colorful": "Colorato",
  "usageTips.closeReminder.title": "Rivedere i suggerimenti più tardi",
  "usageTips.closeReminder.message": "Puoi mostrarli di nuovo in Impostazioni > Generali disattivando “Nascondi suggerimenti d’uso”.",
  "usageTips.closeReminder.doNotRemind": "Ho capito. Non ricordarmelo più quando chiudo i suggerimenti.",
  "usageTips.closeReminder.previewCallout": "Disattiva questa opzione per rivedere i suggerimenti"
}
src/Apptag/Localization/ja.json
@@ -239,5 +239,9 @@
  "theme.purple": "パープル",
  "theme.green": "グリーン",
  "theme.blue": "ブルー",
  "theme.colorful": "カラフル"
  "theme.colorful": "カラフル",
  "usageTips.closeReminder.title": "あとでヒントをもう一度見る",
  "usageTips.closeReminder.message": "設定 > 一般で「使用ヒントを隠す」のチェックを外すと、ヒントを再表示できます。",
  "usageTips.closeReminder.doNotRemind": "了解しました。次回ヒントを閉じるときは表示しません。",
  "usageTips.closeReminder.previewCallout": "再表示するにはこのチェックを外します"
}
src/Apptag/Localization/ko.json
@@ -239,5 +239,9 @@
  "theme.purple": "보라",
  "theme.green": "초록",
  "theme.blue": "파랑",
  "theme.colorful": "컬러풀"
  "theme.colorful": "컬러풀",
  "usageTips.closeReminder.title": "나중에 팁 다시 보기",
  "usageTips.closeReminder.message": "설정 > 일반에서 “사용 팁 숨기기”를 해제하면 팁을 다시 볼 수 있습니다.",
  "usageTips.closeReminder.doNotRemind": "알겠습니다. 다음에 팁을 닫을 때 다시 알리지 마세요.",
  "usageTips.closeReminder.previewCallout": "팁을 다시 보려면 이 선택을 해제하세요"
}
src/Apptag/Localization/ms.json
@@ -239,5 +239,9 @@
  "theme.purple": "Ungu",
  "theme.green": "Hijau",
  "theme.blue": "Biru",
  "theme.colorful": "Berwarna-warni"
  "theme.colorful": "Berwarna-warni",
  "usageTips.closeReminder.title": "Tunjukkan petua lagi kemudian",
  "usageTips.closeReminder.message": "Anda boleh menunjukkan petua semula dalam Tetapan > Umum dengan menyahtanda “Sembunyikan petua penggunaan”.",
  "usageTips.closeReminder.doNotRemind": "Faham. Jangan ingatkan lagi apabila saya menutup petua.",
  "usageTips.closeReminder.previewCallout": "Nyahtanda pilihan ini untuk menunjukkan petua semula"
}
src/Apptag/Localization/nb.json
@@ -239,5 +239,9 @@
  "theme.purple": "Lilla",
  "theme.green": "Grønn",
  "theme.blue": "Blå",
  "theme.colorful": "Fargerik"
  "theme.colorful": "Fargerik",
  "usageTips.closeReminder.title": "Vis tips igjen senere",
  "usageTips.closeReminder.message": "Du kan vise tips igjen i Innstillinger > Generelt ved å fjerne avmerkingen for «Skjul brukstips».",
  "usageTips.closeReminder.doNotRemind": "Skjønner. Ikke minn meg på dette neste gang jeg lukker tips.",
  "usageTips.closeReminder.previewCallout": "Fjern denne avmerkingen for å vise tips igjen"
}
src/Apptag/Localization/nl.json
@@ -239,5 +239,9 @@
  "theme.purple": "Paars",
  "theme.green": "Groen",
  "theme.blue": "Blauw",
  "theme.colorful": "Kleurrijk"
  "theme.colorful": "Kleurrijk",
  "usageTips.closeReminder.title": "Tips later opnieuw tonen",
  "usageTips.closeReminder.message": "Je kunt tips opnieuw tonen via Instellingen > Algemeen door “Gebruikstips verbergen” uit te schakelen.",
  "usageTips.closeReminder.doNotRemind": "Begrepen. Niet meer herinneren wanneer ik tips sluit.",
  "usageTips.closeReminder.previewCallout": "Schakel dit uit om tips opnieuw te tonen"
}
src/Apptag/Localization/nn.json
@@ -239,5 +239,9 @@
  "theme.purple": "Lilla",
  "theme.green": "Grøn",
  "theme.blue": "Blå",
  "theme.colorful": "Fargerik"
  "theme.colorful": "Fargerik",
  "usageTips.closeReminder.title": "Vis tips att seinare",
  "usageTips.closeReminder.message": "Du kan vise tips att i Innstillingar > Generelt ved å fjerne avmerkinga for «Skjul brukstips».",
  "usageTips.closeReminder.doNotRemind": "Greitt. Ikkje minn meg på dette neste gong eg lukkar tips.",
  "usageTips.closeReminder.previewCallout": "Fjern denne avmerkinga for å vise tips att"
}
src/Apptag/Localization/no.json
@@ -239,5 +239,9 @@
  "theme.purple": "Lilla",
  "theme.green": "Grønn",
  "theme.blue": "Blå",
  "theme.colorful": "Fargerik"
  "theme.colorful": "Fargerik",
  "usageTips.closeReminder.title": "Vis tips igjen senere",
  "usageTips.closeReminder.message": "Du kan vise tips igjen i Innstillinger > Generelt ved å fjerne avmerkingen for «Skjul brukstips».",
  "usageTips.closeReminder.doNotRemind": "Skjønner. Ikke minn meg på dette neste gang jeg lukker tips.",
  "usageTips.closeReminder.previewCallout": "Fjern denne avmerkingen for å vise tips igjen"
}
src/Apptag/Localization/pl.json
@@ -239,5 +239,9 @@
  "theme.purple": "Fioletowy",
  "theme.green": "Zielony",
  "theme.blue": "Niebieski",
  "theme.colorful": "Kolorowy"
  "theme.colorful": "Kolorowy",
  "usageTips.closeReminder.title": "Pokaż wskazówki ponownie później",
  "usageTips.closeReminder.message": "Możesz ponownie wyświetlić wskazówki w Ustawienia > Ogólne, odznaczając „Ukryj wskazówki użycia”.",
  "usageTips.closeReminder.doNotRemind": "Rozumiem. Nie przypominaj ponownie, gdy zamknę wskazówki.",
  "usageTips.closeReminder.previewCallout": "Odznacz tę opcję, aby ponownie pokazać wskazówki"
}
src/Apptag/Localization/pt-BR.json
@@ -239,5 +239,9 @@
  "theme.purple": "Roxo",
  "theme.green": "Verde",
  "theme.blue": "Azul",
  "theme.colorful": "Colorido"
  "theme.colorful": "Colorido",
  "usageTips.closeReminder.title": "Ver dicas novamente depois",
  "usageTips.closeReminder.message": "Você pode mostrar as dicas novamente em Ajustes > Geral desmarcando “Ocultar dicas de uso”.",
  "usageTips.closeReminder.doNotRemind": "Entendi. Não lembrar de novo quando eu fechar as dicas.",
  "usageTips.closeReminder.previewCallout": "Desmarque esta opção para ver as dicas novamente"
}
src/Apptag/Localization/ro.json
@@ -239,5 +239,9 @@
  "theme.purple": "Mov",
  "theme.green": "Verde",
  "theme.blue": "Albastru",
  "theme.colorful": "Colorată"
  "theme.colorful": "Colorată",
  "usageTips.closeReminder.title": "Afișează sfaturile mai târziu",
  "usageTips.closeReminder.message": "Poți afișa din nou sfaturile în Setări > General debifând „Ascunde sfaturile de utilizare”.",
  "usageTips.closeReminder.doNotRemind": "Am înțeles. Nu-mi mai aminti când închid sfaturile.",
  "usageTips.closeReminder.previewCallout": "Debifează această opțiune pentru a afișa sfaturile"
}
src/Apptag/Localization/ru.json
@@ -239,5 +239,9 @@
  "theme.purple": "Фиолетовый",
  "theme.green": "Зелёный",
  "theme.blue": "Синий",
  "theme.colorful": "Цветной"
  "theme.colorful": "Цветной",
  "usageTips.closeReminder.title": "Показать советы позже",
  "usageTips.closeReminder.message": "Советы можно снова включить в Настройки > Общие, сняв флажок «Скрывать советы по использованию».",
  "usageTips.closeReminder.doNotRemind": "Понятно. Больше не напоминать при закрытии советов.",
  "usageTips.closeReminder.previewCallout": "Снимите этот флажок, чтобы снова показать советы"
}
src/Apptag/Localization/sr-Cyrl.json
@@ -239,5 +239,9 @@
  "theme.purple": "Љубичаста",
  "theme.green": "Зелена",
  "theme.blue": "Плава",
  "theme.colorful": "Шарена"
  "theme.colorful": "Шарена",
  "usageTips.closeReminder.title": "Поново прикажи савете касније",
  "usageTips.closeReminder.message": "Савете можеш поново приказати у Подешавања > Опште тако што ћеш искључити „Сакриј савете за коришћење”.",
  "usageTips.closeReminder.doNotRemind": "Разумем. Не подсећај ме следећи пут када затворим савете.",
  "usageTips.closeReminder.previewCallout": "Искључи ову опцију да поново видиш савете"
}
src/Apptag/Localization/sv.json
@@ -239,5 +239,9 @@
  "theme.purple": "Lila",
  "theme.green": "Grön",
  "theme.blue": "Blå",
  "theme.colorful": "Färgstark"
  "theme.colorful": "Färgstark",
  "usageTips.closeReminder.title": "Visa tips igen senare",
  "usageTips.closeReminder.message": "Du kan visa tips igen i Inställningar > Allmänt genom att avmarkera “Dölj användningstips”.",
  "usageTips.closeReminder.doNotRemind": "Jag förstår. Påminn mig inte igen när jag stänger tips.",
  "usageTips.closeReminder.previewCallout": "Avmarkera detta för att visa tips igen"
}
src/Apptag/Localization/th.json
@@ -239,5 +239,9 @@
  "theme.purple": "ม่วง",
  "theme.green": "เขียว",
  "theme.blue": "น้ำเงิน",
  "theme.colorful": "หลากสี"
  "theme.colorful": "หลากสี",
  "usageTips.closeReminder.title": "ดูเคล็ดลับอีกครั้งภายหลัง",
  "usageTips.closeReminder.message": "คุณสามารถแสดงเคล็ดลับอีกครั้งได้ใน การตั้งค่า > ทั่วไป โดยเอาเครื่องหมายถูกออกจาก “ซ่อนเคล็ดลับการใช้งาน”",
  "usageTips.closeReminder.doNotRemind": "เข้าใจแล้ว ไม่ต้องเตือนอีกเมื่อฉันปิดเคล็ดลับ",
  "usageTips.closeReminder.previewCallout": "เอาเครื่องหมายถูกออกเพื่อแสดงเคล็ดลับอีกครั้ง"
}
src/Apptag/Localization/tr.json
@@ -239,5 +239,9 @@
  "theme.purple": "Mor",
  "theme.green": "Yeşil",
  "theme.blue": "Mavi",
  "theme.colorful": "Renkli"
  "theme.colorful": "Renkli",
  "usageTips.closeReminder.title": "İpuçlarını daha sonra tekrar göster",
  "usageTips.closeReminder.message": "İpuçlarını Ayarlar > Genel bölümünde “Kullanım ipuçlarını gizle” seçimini kaldırarak tekrar gösterebilirsin.",
  "usageTips.closeReminder.doNotRemind": "Anladım. İpuçlarını kapatırken tekrar hatırlatma.",
  "usageTips.closeReminder.previewCallout": "İpuçlarını tekrar görmek için bu seçimi kaldır"
}
src/Apptag/Localization/uk.json
@@ -239,5 +239,9 @@
  "theme.purple": "Фіолетова",
  "theme.green": "Зелена",
  "theme.blue": "Синя",
  "theme.colorful": "Барвиста"
  "theme.colorful": "Барвиста",
  "usageTips.closeReminder.title": "Показати поради пізніше",
  "usageTips.closeReminder.message": "Поради можна знову показати в Параметри > Загальні, знявши позначку «Приховувати поради з використання».",
  "usageTips.closeReminder.doNotRemind": "Зрозуміло. Більше не нагадувати, коли я закриваю поради.",
  "usageTips.closeReminder.previewCallout": "Зніміть цей прапорець, щоб знову показати поради"
}
src/Apptag/Localization/vi.json
@@ -239,5 +239,9 @@
  "theme.purple": "Tím",
  "theme.green": "Xanh lá",
  "theme.blue": "Xanh lam",
  "theme.colorful": "Nhiều màu"
  "theme.colorful": "Nhiều màu",
  "usageTips.closeReminder.title": "Xem lại mẹo sau",
  "usageTips.closeReminder.message": "Bạn có thể xem lại mẹo trong Cài đặt > Chung bằng cách bỏ chọn “Ẩn mẹo sử dụng”.",
  "usageTips.closeReminder.doNotRemind": "Đã hiểu. Đừng nhắc lại khi tôi đóng mẹo.",
  "usageTips.closeReminder.previewCallout": "Bỏ chọn mục này để xem lại mẹo"
}
src/Apptag/Localization/zh-Hans.json
@@ -239,5 +239,9 @@
  "theme.purple": "紫色",
  "theme.green": "绿色",
  "theme.blue": "蓝色",
  "theme.colorful": "炫彩"
  "theme.colorful": "炫彩",
  "usageTips.closeReminder.title": "以后还能重新查看使用技巧",
  "usageTips.closeReminder.message": "后续可在「设置 > 通用」去掉「隐藏使用技巧」的勾选,再次查看 tips。",
  "usageTips.closeReminder.doNotRemind": "知道了,下次关闭时不要再提醒我",
  "usageTips.closeReminder.previewCallout": "去掉这里的勾选"
}
src/Apptag/Localization/zh-Hant.json
@@ -239,5 +239,9 @@
  "theme.purple": "紫色",
  "theme.green": "綠色",
  "theme.blue": "藍色",
  "theme.colorful": "炫彩"
  "theme.colorful": "炫彩",
  "usageTips.closeReminder.title": "之後還能重新查看使用技巧",
  "usageTips.closeReminder.message": "之後可在「設定 > 通用」取消勾選「隱藏使用技巧」,再次查看 tips。",
  "usageTips.closeReminder.doNotRemind": "知道了,下次關閉時不要再提醒我",
  "usageTips.closeReminder.previewCallout": "取消這裡的勾選"
}
src/CHANGELOG.md
@@ -2,7 +2,13 @@
## [Unreleased]
## [8.0.3] — 2026-06-25
- App Grid 解除标签拖拽新增原生 Core Animation 预览反馈:当应用图标拖到容器外/容器之间空白区域时,拖拽浮层会进入克制的碎纸切片动效;容器内部空白不触发该预览,实际解除标签仍沿用原有放手后确认流程
- App Grid 使用技巧关闭前新增提醒:首次关闭会提示用户可在“设置 > 通用”取消“隐藏使用技巧”后重新查看,并提供“不再提醒”选项;确认后才真正隐藏 tips
- 使用技巧关闭提醒补齐 29 个语种文案,并新增本地化设置页预览,高亮“隐藏使用技巧”开关
- 更新 `Scripts/usage_tips_qa.sh` 并新增 `Scripts/usage_tips_close_reminder_qa.sh`,覆盖关闭提醒链路、modal 防穿透和 29 语种文案完整性
- 版本号更新为 `8.0.3`,Build 更新为 `20260625.1615`
## [8.0.2] — 2026-06-24
src/Docs/Requirements/2026-06-25-usage-tips-close-reminder-todo.md
New file
@@ -0,0 +1,48 @@
# 使用技巧关闭提醒 TODO
## 目标
- 用户点击 App Grid 底部“使用技巧”关闭按钮时,先显示提醒弹窗。
- 弹窗说明:后续可在“设置 > 通用”取消“隐藏使用技巧”后再次查看 tips。
- 用户确认后才真正关闭 tips,并写入 `hideUsageTips`。
- 用户勾选“下次关闭时不要再提醒我”后,后续关闭 tips 不再弹提醒。
- 提醒内容和设置页预览必须覆盖 29 个语种。
## 不做范围
- 不改变 tips 默认展示:新用户仍默认显示。
- 不改变 8 条 tips 的教学文案、布局和翻页逻辑。
- 不改变设置页结构,只复用已有“隐藏使用技巧”开关。
- 不新增 29 张静态截图资源;用运行时本地化的设置页预览组件展示同等指引信息。
## TODO
- [x] 新增 `skipUsageTipsCloseReminder` 默认值和持久化偏好。
- [x] 将原生 AppKit tips 关闭按钮改为请求关闭,不再直接隐藏横幅。
- [x] 在 `ContentView` 中新增关闭提醒弹窗状态、确认/取消处理和 modal hit-testing。
- [x] 新增 `UsageTipsCloseReminderBubble`,复用黑底白字提醒风格。
- [x] 新增本地化设置页预览,高亮“隐藏使用技巧”开关。
- [x] 补齐 29 个语种的关闭提醒文案。
- [x] 更新 `usage_tips_qa.sh`,把关闭链路检查改为提醒链路。
- [x] 新增 `usage_tips_close_reminder_qa.sh` 专项门禁。
- [x] 自动化 QA 与构建验证。
- [ ] 用户体验验收后再决定是否冻结/打包。
## 完成标准
- 第一次点击 tips 关闭按钮时弹出提醒;取消后 tips 保持显示。
- 确认后 tips 关闭,并与设置页“隐藏使用技巧”保持一致。
- 勾选“不再提醒”后,后续点击关闭按钮直接隐藏 tips。
- 弹窗出现期间不穿透 AppGrid,不触发底层应用点击、气泡或拖拽。
- 29 个语种不缺 key、不显示裸 key,非中文语言不混入中文提醒正文。
## QA 结果
- `bash src/Scripts/usage_tips_qa.sh`:通过。
- `bash src/Scripts/usage_tips_close_reminder_qa.sh`:通过。
- `bash src/Scripts/theme_settings_qa.sh`:通过。
- `bash src/Scripts/macos14_availability_typecheck_qa.sh`:通过。
- `git diff --check`:通过。
- `APP_BUILD=20260625.1615 bash src/build.sh`:通过,产物为 `src/build/TagLauncher.app`。
- `bash src/Scripts/macos14_build_metadata_qa.sh src/build/TagLauncher.app`:通过。
- 构建版本:`8.0.3 (20260625.1615)`。
src/Docs/Requirements/2026-06-25-usage-tips-close-reminder-worklog.md
New file
@@ -0,0 +1,12 @@
# 使用技巧关闭提醒工作日志
## 2026-06-25
- 起点:`codex/dark-glass-8.0.0`,基线提交 `c8be92c Add remove tag shred preview`。
- 需求来源:`/Users/ar/Projects/Taglauncher/Docs/X-01产品文档/2026-06-25-PRD-tip关闭提醒.MD`。
- 产品边界:只改变关闭 tips 的提醒链路;tips 默认开启、设置页入口、教学内容和主题逻辑不变。
- 架构决策:用已有 AppKit tips 横幅发出关闭请求,由 SwiftUI `ContentView` 统一弹 modal 和写入 `hideUsageTips`;这样可以复用现有 overlay hit-testing,避免点击穿透。
- 视觉决策:提醒弹窗沿用黑色底部提醒/确认类弹窗风格;PRD 中“当前语言截图”用运行时本地化设置页预览实现,自动使用当前语言的设置项并高亮“隐藏使用技巧”。
- QA 决策:更新既有 `usage_tips_qa.sh`,新增 `usage_tips_close_reminder_qa.sh`,覆盖关闭提醒链路、modal 状态、29 语种文案完整性和非中文混入中文文案风险。
- 版本决策:用户明确后续用户可见代码改动默认应递增版本;本次从 `8.0.2 (20260624.2341)` 更新为 `8.0.3 (20260625.1615)`。
- QA 结果:`usage_tips_qa.sh`、`usage_tips_close_reminder_qa.sh`、`theme_settings_qa.sh`、`macos14_availability_typecheck_qa.sh`、`git diff --check`、`APP_BUILD=20260625.1615 bash src/build.sh`、`macos14_build_metadata_qa.sh src/build/TagLauncher.app` 均通过。
src/Scripts/usage_tips_close_reminder_qa.sh
New file
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONTENT_VIEW_SWIFT="$ROOT_DIR/Apptag/ContentView.swift"
APP_GRID_SWIFT="$ROOT_DIR/Apptag/AppGridCollectionView.swift"
EDIT_MODE_VIEWS_SWIFT="$ROOT_DIR/Apptag/EditModeViews.swift"
APP_DEFAULTS_SWIFT="$ROOT_DIR/Apptag/AppDefaults.swift"
LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
fail() {
  printf 'FAIL: %s\n' "$*" >&2
  exit 1
}
[[ -f "$CONTENT_VIEW_SWIFT" ]] || fail "missing ContentView.swift"
[[ -f "$APP_GRID_SWIFT" ]] || fail "missing AppGridCollectionView.swift"
[[ -f "$EDIT_MODE_VIEWS_SWIFT" ]] || fail "missing EditModeViews.swift"
[[ -f "$APP_DEFAULTS_SWIFT" ]] || fail "missing AppDefaults.swift"
[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$EDIT_MODE_VIEWS_SWIFT" "$APP_DEFAULTS_SWIFT" "$LOCALIZATION_DIR" <<'PY'
import json
import pathlib
import re
import sys
content_view = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
app_grid = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8")
edit_mode_views = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8")
defaults = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8")
localization_dir = pathlib.Path(sys.argv[5])
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)
required_keys = [
    "usageTips.closeReminder.title",
    "usageTips.closeReminder.message",
    "usageTips.closeReminder.doNotRemind",
    "usageTips.closeReminder.previewCallout",
]
require(
    r'"hideUsageTips"\s*:\s*hideUsageTips',
    defaults,
    "hideUsageTips must remain registered in AppDefaults",
)
require(
    r'"skipUsageTipsCloseReminder"\s*:\s*false',
    defaults,
    "close reminder suppression must default to false",
)
require(
    r"onHideUsageTips\s*:\s*requestHideUsageTips",
    content_view,
    "AppKit close callback must route through requestHideUsageTips",
)
require(
    r"private\s+func\s+requestHideUsageTips\s*\(\)\s*\{(?P<body>.*?)skipUsageTipsCloseReminder(?P<body2>.*?)usageTipsCloseReminderVisible\s*=\s*true",
    content_view,
    "requestHideUsageTips must show the reminder unless suppressed",
)
require(
    r"private\s+func\s+confirmUsageTipsCloseReminder\s*\(\)\s*\{(?P<body>.*?)skipUsageTipsCloseReminder\s*=\s*true(?P<body2>.*?)hideUsageTips\s*=\s*true",
    content_view,
    "confirmUsageTipsCloseReminder must close tips and persist suppression only after opt-in",
)
require(
    r"appBubbleDisabled(?P<body>.*?)usageTipsCloseReminderVisible",
    content_view,
    "reminder visibility must disable app bubbles to avoid click-through",
)
require(
    r"publishModalInteractionState(?P<body>.*?)usageTipsCloseReminderVisible",
    content_view,
    "reminder visibility must be published as modal state",
)
require(
    r"private\s+func\s+requestHideUsageTips\s*\(\)\s*\{(?P<body>.*?)(?!isHidden\s*=\s*true)",
    app_grid,
    "native requestHideUsageTips must not hide the banner before confirmation",
)
if re.search(r"private\s+func\s+hideUsageTips\s*\(\)", app_grid):
    fail("AppGridUsageTipsNSView must not keep a direct hideUsageTips handler")
require(
    r"struct\s+UsageTipsCloseReminderBubble\s*:\s*View",
    edit_mode_views,
    "missing UsageTipsCloseReminderBubble",
)
require(
    r"UsageTipsCloseReminderSettingsPreview\s*\(\s*callout\s*:\s*previewCallout\s*\)",
    edit_mode_views,
    "reminder bubble must include the settings preview",
)
require(
    r"settings\.hideUsageTips",
    edit_mode_views,
    "settings preview must highlight the localized Hide usage tips control",
)
files = sorted(localization_dir.glob("*.json"))
if len(files) != 29:
    fail(f"expected 29 localization files, found {len(files)}")
for path in files:
    data = json.loads(path.read_text(encoding="utf-8"))
    missing = [key for key in required_keys if not str(data.get(key, "")).strip()]
    if missing:
        fail(f"{path.name} missing keys: {', '.join(missing)}")
    if path.stem not in {"zh-Hans", "zh-Hant"}:
        for key in required_keys:
            value = str(data[key])
            if "后续可在" in value or "後續可在" in value or "隐藏使用技巧" in value or "隱藏使用技巧" in value:
                fail(f"{path.name} appears to contain copied Chinese reminder text for {key}")
print("PASS: usage tips close reminder QA")
PY
src/Scripts/usage_tips_qa.sh
@@ -8,6 +8,7 @@
PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
APP_DEFAULTS_SWIFT="$ROOT_DIR/Apptag/AppDefaults.swift"
APP_GRID_THEME_SWIFT="$ROOT_DIR/Apptag/AppGridTheme.swift"
EDIT_MODE_VIEWS_SWIFT="$ROOT_DIR/Apptag/EditModeViews.swift"
LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
fail() {
@@ -21,9 +22,10 @@
[[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift"
[[ -f "$APP_DEFAULTS_SWIFT" ]] || fail "missing AppDefaults.swift"
[[ -f "$APP_GRID_THEME_SWIFT" ]] || fail "missing AppGridTheme.swift"
[[ -f "$EDIT_MODE_VIEWS_SWIFT" ]] || fail "missing EditModeViews.swift"
[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$APPTAG_APP_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$APP_GRID_THEME_SWIFT" "$LOCALIZATION_DIR" <<'PY'
python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$APPTAG_APP_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$APP_GRID_THEME_SWIFT" "$EDIT_MODE_VIEWS_SWIFT" "$LOCALIZATION_DIR" <<'PY'
import json
import pathlib
import re
@@ -35,7 +37,8 @@
preferences = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8")
defaults = pathlib.Path(sys.argv[5]).read_text(encoding="utf-8")
theme = pathlib.Path(sys.argv[6]).read_text(encoding="utf-8")
localization_dir = pathlib.Path(sys.argv[7])
edit_mode_views = pathlib.Path(sys.argv[7]).read_text(encoding="utf-8")
localization_dir = pathlib.Path(sys.argv[8])
def fail(message: str) -> None:
@@ -54,6 +57,10 @@
    "usageTips.previous",
    "usageTips.next",
    "usageTips.close",
    "usageTips.closeReminder.title",
    "usageTips.closeReminder.message",
    "usageTips.closeReminder.doNotRemind",
    "usageTips.closeReminder.previewCallout",
]
for index in range(1, 9):
    required_keys.append(f"usageTips.tip{index}.title")
@@ -68,6 +75,11 @@
    r'"hideUsageTips"\s*:\s*hideUsageTips',
    defaults,
    "AppDefaults.register must register hideUsageTips",
)
require(
    r'"skipUsageTipsCloseReminder"\s*:\s*false',
    defaults,
    "AppDefaults.register must default the usage tips close reminder to visible",
)
for source_name, source in {
    "ContentView.swift": content_view,
@@ -114,9 +126,29 @@
    "ContentView must bind usage tip selection to AppGrid",
)
require(
    r"onHideUsageTips\s*:\s*\{\s*hideUsageTips\s*=\s*true\s*\}",
    r"onHideUsageTips\s*:\s*requestHideUsageTips",
    content_view,
    "closing the native usage tips banner must persist hideUsageTips",
    "closing the native usage tips banner must route through the reminder flow",
)
require(
    r'@AppStorage\("skipUsageTipsCloseReminder"\)\s+private\s+var\s+skipUsageTipsCloseReminder\s*=\s*false',
    content_view,
    "ContentView must persist the usage tips close reminder suppression flag",
)
require(
    r"private\s+func\s+requestHideUsageTips\s*\(\)\s*\{(?P<body>.*?)skipUsageTipsCloseReminder(?P<body2>.*?)usageTipsCloseReminderVisible\s*=\s*true",
    content_view,
    "requestHideUsageTips must show the reminder unless the user suppressed it",
)
require(
    r"private\s+func\s+confirmUsageTipsCloseReminder\s*\(\)\s*\{(?P<body>.*?)skipUsageTipsCloseReminder\s*=\s*true(?P<body2>.*?)hideUsageTips\s*=\s*true",
    content_view,
    "confirmUsageTipsCloseReminder must persist suppression only after opt-in and then hide tips",
)
require(
    r"usageTipsCloseReminderVisible",
    content_view,
    "usage tips close reminder must participate in modal state and hit testing",
)
require(
    r"let\s+onHideUsageTips\s*:\s*\(\)\s*->\s*Void",
@@ -124,15 +156,30 @@
    "AppGridCollectionView must accept an onHideUsageTips callback",
)
require(
    r"closeButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.hideUsageTips\(\)\s*\}",
    r"closeButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.requestHideUsageTips\(\)\s*\}",
    app_grid,
    "close button must call the native hideUsageTips handler",
    "close button must call the native requestHideUsageTips handler",
)
require(
    r"coordinator\?\.onHideUsageTips\(\)",
    app_grid,
    "native hideUsageTips must call back to persisted SwiftUI state",
)
require(
    r"struct\s+UsageTipsCloseReminderBubble\s*:\s*View",
    edit_mode_views,
    "EditModeViews must provide the close reminder bubble",
)
require(
    r"UsageTipsCloseReminderSettingsPreview\s*\(\s*callout\s*:\s*previewCallout\s*\)",
    edit_mode_views,
    "close reminder must pass the localized preview callout into the settings preview",
)
require(
    r"struct\s+UsageTipsCloseReminderSettingsPreview\s*:\s*View(?P<body>.*?)settings\.hideUsageTips",
    edit_mode_views,
    "close reminder must include a localized Settings > General preview highlighting Hide usage tips",
)
require(
    r"private\s+let\s+titlePanelView\s*=\s*NSView\(\)",
src/TODO.md
@@ -30,6 +30,14 @@
## Done
- [2026-06-25] 使用技巧关闭提醒。
  - 结果: 点击 App Grid 使用技巧关闭按钮时先弹出提醒,说明可在“设置 > 通用”取消“隐藏使用技巧”重新查看;用户确认后才隐藏 tips,勾选“不再提醒”后后续关闭直接隐藏。
  - 范围: 关闭按钮链路、提醒弹窗、本地化设置页预览、`skipUsageTipsCloseReminder` 偏好、29 语种提醒文案、专项 QA。
  - 不改范围: tips 默认仍显示;8 条教学内容、主题/拖拽/排序/Quick Search 功能不变。
  - 验证: `usage_tips_qa.sh`、`usage_tips_close_reminder_qa.sh`、`theme_settings_qa.sh`、`macos14_availability_typecheck_qa.sh`、`git diff --check`、`APP_BUILD=20260625.1615 bash src/build.sh`、`macos14_build_metadata_qa.sh src/build/TagLauncher.app` 通过。
  - 版本: `8.0.3 (20260625.1615)`。
  - 文档: `Docs/Requirements/2026-06-25-usage-tips-close-reminder-todo.md`、`Docs/Requirements/2026-06-25-usage-tips-close-reminder-worklog.md`。
- [2026-06-25] App Grid 解除标签碎纸预览动效。
  - 结果: 拖动应用到容器外/容器之间空白区域时,拖拽浮层进入原生 Core Animation 碎纸切片预览;离开该区域恢复正常。
  - 范围: 只改拖拽浮层视觉反馈和空白 drop 预览资格判断;不改标签数据、不改容器内空白语义、不改放手后的解除标签确认流程。