Add usage tips close reminder
37 files modified
3 files added
| | |
| | | "useAppKitTagNavigation": useAppKitTagNavigation, |
| | | "skipTagRemovalDropConfirm": false, |
| | | "skipUncategorizedDropConfirm": false, |
| | | "skipUsageTipsCloseReminder": false, |
| | | LauncherHotkeyRegistrationStore.mainStateKey: LauncherHotkeyRegistrationState.active.rawValue, |
| | | LauncherHotkeyRegistrationStore.quickSearchStateKey: LauncherHotkeyRegistrationState.active.rawValue |
| | | ]) |
| | |
| | | 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) } |
| | | |
| | |
| | | applyCoordinatorState() |
| | | } |
| | | |
| | | private func hideUsageTips() { |
| | | private func requestHideUsageTips() { |
| | | Diagnostics.log("usageTips.hud.close") |
| | | clearHoverState() |
| | | isHidden = true |
| | | coordinator?.onHideUsageTips() |
| | | } |
| | | |
| | |
| | | let closeHitOutset: CGFloat = 8 |
| | | if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) { |
| | | closeButton.performPressFeedback() |
| | | hideUsageTips() |
| | | requestHideUsageTips() |
| | | return true |
| | | } |
| | | let hitOutset: CGFloat = 6 |
| | |
| | | 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 |
| | |
| | | @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 |
| | |
| | | || appGridInteraction.appDragModeActive |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | } |
| | | private var appGridHighlightedGroupName: String? { |
| | | appGridInteraction.tagNavigationHoveredGroupName |
| | |
| | | } |
| | | |
| | | 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 |
| | | ] |
| | | ) |
| | | } |
| | |
| | | }, |
| | | onScrollActivity: handleAppGridScrollActivity, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onHideUsageTips: { hideUsageTips = true }, |
| | | onHideUsageTips: requestHideUsageTips, |
| | | onUsageTipsHoverChange: handleUsageTipsHoverChange |
| | | ) |
| | | } |
| | |
| | | } |
| | | .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 { |
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | |
| | | .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) |
| | | ) |
| | | } |
| | | } |
| | |
| | | <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> |
| | |
| | | "theme.purple": "بنفسجي", |
| | | "theme.green": "أخضر", |
| | | "theme.blue": "أزرق", |
| | | "theme.colorful": "ملوّن" |
| | | "theme.colorful": "ملوّن", |
| | | "usageTips.closeReminder.title": "إظهار التلميحات بعدين", |
| | | "usageTips.closeReminder.message": "تقدر ترجع تعرض التلميحات من الإعدادات > عام بإلغاء تحديد “إخفاء نصائح الاستخدام”.", |
| | | "usageTips.closeReminder.doNotRemind": "تم. لا تذكرني مرة ثانية إذا سكرت التلميحات.", |
| | | "usageTips.closeReminder.previewCallout": "شِلّ التحديد هنا عشان ترجع التلميحات" |
| | | } |
| | |
| | | "theme.purple": "بنفسجي", |
| | | "theme.green": "أخضر", |
| | | "theme.blue": "أزرق", |
| | | "theme.colorful": "ملوّن" |
| | | "theme.colorful": "ملوّن", |
| | | "usageTips.closeReminder.title": "إظهار التلميحات لاحقًا", |
| | | "usageTips.closeReminder.message": "يمكنك إظهار التلميحات مرة أخرى من الإعدادات > عام عبر إلغاء تحديد “إخفاء نصائح الاستخدام”.", |
| | | "usageTips.closeReminder.doNotRemind": "فهمت. لا تذكرني مرة أخرى عند إغلاق التلميحات.", |
| | | "usageTips.closeReminder.previewCallout": "ألغِ تحديد هذا الخيار لإظهار التلميحات مجددًا" |
| | | } |
| | |
| | | "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í" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "theme.purple": "パープル", |
| | | "theme.green": "グリーン", |
| | | "theme.blue": "ブルー", |
| | | "theme.colorful": "カラフル" |
| | | "theme.colorful": "カラフル", |
| | | "usageTips.closeReminder.title": "あとでヒントをもう一度見る", |
| | | "usageTips.closeReminder.message": "設定 > 一般で「使用ヒントを隠す」のチェックを外すと、ヒントを再表示できます。", |
| | | "usageTips.closeReminder.doNotRemind": "了解しました。次回ヒントを閉じるときは表示しません。", |
| | | "usageTips.closeReminder.previewCallout": "再表示するにはこのチェックを外します" |
| | | } |
| | |
| | | "theme.purple": "보라", |
| | | "theme.green": "초록", |
| | | "theme.blue": "파랑", |
| | | "theme.colorful": "컬러풀" |
| | | "theme.colorful": "컬러풀", |
| | | "usageTips.closeReminder.title": "나중에 팁 다시 보기", |
| | | "usageTips.closeReminder.message": "설정 > 일반에서 “사용 팁 숨기기”를 해제하면 팁을 다시 볼 수 있습니다.", |
| | | "usageTips.closeReminder.doNotRemind": "알겠습니다. 다음에 팁을 닫을 때 다시 알리지 마세요.", |
| | | "usageTips.closeReminder.previewCallout": "팁을 다시 보려면 이 선택을 해제하세요" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "theme.purple": "Фиолетовый", |
| | | "theme.green": "Зелёный", |
| | | "theme.blue": "Синий", |
| | | "theme.colorful": "Цветной" |
| | | "theme.colorful": "Цветной", |
| | | "usageTips.closeReminder.title": "Показать советы позже", |
| | | "usageTips.closeReminder.message": "Советы можно снова включить в Настройки > Общие, сняв флажок «Скрывать советы по использованию».", |
| | | "usageTips.closeReminder.doNotRemind": "Понятно. Больше не напоминать при закрытии советов.", |
| | | "usageTips.closeReminder.previewCallout": "Снимите этот флажок, чтобы снова показать советы" |
| | | } |
| | |
| | | "theme.purple": "Љубичаста", |
| | | "theme.green": "Зелена", |
| | | "theme.blue": "Плава", |
| | | "theme.colorful": "Шарена" |
| | | "theme.colorful": "Шарена", |
| | | "usageTips.closeReminder.title": "Поново прикажи савете касније", |
| | | "usageTips.closeReminder.message": "Савете можеш поново приказати у Подешавања > Опште тако што ћеш искључити „Сакриј савете за коришћење”.", |
| | | "usageTips.closeReminder.doNotRemind": "Разумем. Не подсећај ме следећи пут када затворим савете.", |
| | | "usageTips.closeReminder.previewCallout": "Искључи ову опцију да поново видиш савете" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "theme.purple": "ม่วง", |
| | | "theme.green": "เขียว", |
| | | "theme.blue": "น้ำเงิน", |
| | | "theme.colorful": "หลากสี" |
| | | "theme.colorful": "หลากสี", |
| | | "usageTips.closeReminder.title": "ดูเคล็ดลับอีกครั้งภายหลัง", |
| | | "usageTips.closeReminder.message": "คุณสามารถแสดงเคล็ดลับอีกครั้งได้ใน การตั้งค่า > ทั่วไป โดยเอาเครื่องหมายถูกออกจาก “ซ่อนเคล็ดลับการใช้งาน”", |
| | | "usageTips.closeReminder.doNotRemind": "เข้าใจแล้ว ไม่ต้องเตือนอีกเมื่อฉันปิดเคล็ดลับ", |
| | | "usageTips.closeReminder.previewCallout": "เอาเครื่องหมายถูกออกเพื่อแสดงเคล็ดลับอีกครั้ง" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "theme.purple": "Фіолетова", |
| | | "theme.green": "Зелена", |
| | | "theme.blue": "Синя", |
| | | "theme.colorful": "Барвиста" |
| | | "theme.colorful": "Барвиста", |
| | | "usageTips.closeReminder.title": "Показати поради пізніше", |
| | | "usageTips.closeReminder.message": "Поради можна знову показати в Параметри > Загальні, знявши позначку «Приховувати поради з використання».", |
| | | "usageTips.closeReminder.doNotRemind": "Зрозуміло. Більше не нагадувати, коли я закриваю поради.", |
| | | "usageTips.closeReminder.previewCallout": "Зніміть цей прапорець, щоб знову показати поради" |
| | | } |
| | |
| | | "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" |
| | | } |
| | |
| | | "theme.purple": "紫色", |
| | | "theme.green": "绿色", |
| | | "theme.blue": "蓝色", |
| | | "theme.colorful": "炫彩" |
| | | "theme.colorful": "炫彩", |
| | | "usageTips.closeReminder.title": "以后还能重新查看使用技巧", |
| | | "usageTips.closeReminder.message": "后续可在「设置 > 通用」去掉「隐藏使用技巧」的勾选,再次查看 tips。", |
| | | "usageTips.closeReminder.doNotRemind": "知道了,下次关闭时不要再提醒我", |
| | | "usageTips.closeReminder.previewCallout": "去掉这里的勾选" |
| | | } |
| | |
| | | "theme.purple": "紫色", |
| | | "theme.green": "綠色", |
| | | "theme.blue": "藍色", |
| | | "theme.colorful": "炫彩" |
| | | "theme.colorful": "炫彩", |
| | | "usageTips.closeReminder.title": "之後還能重新查看使用技巧", |
| | | "usageTips.closeReminder.message": "之後可在「設定 > 通用」取消勾選「隱藏使用技巧」,再次查看 tips。", |
| | | "usageTips.closeReminder.doNotRemind": "知道了,下次關閉時不要再提醒我", |
| | | "usageTips.closeReminder.previewCallout": "取消這裡的勾選" |
| | | } |
| | |
| | | |
| | | ## [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 |
| | | |
| New file |
| | |
| | | # 使用技巧关闭提醒 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)`。 |
| New file |
| | |
| | | # 使用技巧关闭提醒工作日志 |
| | | |
| | | ## 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` 均通过。 |
| New file |
| | |
| | | #!/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 |
| | |
| | | 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() { |
| | |
| | | [[ -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 |
| | |
| | | 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: |
| | |
| | | "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") |
| | |
| | | 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, |
| | |
| | | "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", |
| | |
| | | "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\(\)", |
| | |
| | | |
| | | ## 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 预览资格判断;不改标签数据、不改容器内空白语义、不改放手后的解除标签确认流程。 |