Ariver
2026-06-13 ed4ebcfa763f66ecd69cae57df2c949a7a9a8a45
Prepare 7.9.1 usage tips release
6 files modified
147 ■■■■■ changed files
src/Apptag/AppGridCollectionView.swift 97 ●●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hans.json 2 ●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hant.json 2 ●●● patch | view | raw | blame | history
src/CHANGELOG.md 8 ●●●●● patch | view | raw | blame | history
src/Scripts/usage_tips_qa.sh 34 ●●●● patch | view | raw | blame | history
src/Apptag/AppGridCollectionView.swift
@@ -38,8 +38,8 @@
}
enum AppGridUsageTipsMetrics {
    static let barHeight: CGFloat = 84
    static let reservedHeight: CGFloat = 128
    static let barHeight: CGFloat = 136
    static let reservedHeight: CGFloat = 176
    static let bottomMargin: CGFloat = 20
    static let horizontalInset: CGFloat = 24
    static let minWidth: CGFloat = 640
@@ -943,44 +943,42 @@
        let paddingRight: CGFloat = 16
        let iconSize: CGFloat = 24
        let iconTextGap: CGFloat = 10
        let titleDetailGap: CGFloat = 22
        let detailControlsGap: CGFloat = 24
        let controlZoneWidth: CGFloat = 176
        let buttonGap: CGFloat = 6
        let buttonSize: CGFloat = 48
        let buttonDotsGap: CGFloat = 6
        let dotsHeight: CGFloat = 10
        let dotsWidth = dotsView.preferredWidth
        let fullTitleWidth = ceil(titleLabel.attributedStringValue.size().width)
        let centerY = visualFrame.midY
        let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap
        let buttonsWidth = buttonSize * 2 + buttonGap
        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
        let textRight = visualFrame.maxX - controlZoneWidth
        let availableTextWidth = max(1, textRight - titleX)
        let titleWidth = min(fullTitleWidth, availableTextWidth)
        let titleHeight: CGFloat = 32
        let detailHeight: CGFloat = 68
        let lineGap: CGFloat = 8
        let textBlockHeight = titleHeight + lineGap + detailHeight
        let textBlockY = visualFrame.minY + max(0, (visualFrame.height - textBlockHeight) / 2)
        iconView.frame = NSRect(
            x: visualFrame.minX + paddingLeft,
            y: centerY - iconSize / 2,
            y: visualFrame.midY - iconSize / 2,
            width: iconSize,
            height: iconSize
        )
        let titleX = iconView.frame.maxX + iconTextGap
        let buttonsWidth = buttonSize * 2 + buttonGap
        let trailingWidth = paddingRight + buttonsWidth + detailControlsGap
        let availableTextWidth = max(1, visualFrame.maxX - titleX - titleDetailGap - trailingWidth)
        let minimumReadableDetailWidth: CGFloat = 220
        let titleWidth: CGFloat
        if availableTextWidth >= fullTitleWidth + minimumReadableDetailWidth {
            titleWidth = fullTitleWidth
        } else if availableTextWidth >= fullTitleWidth {
            titleWidth = fullTitleWidth
        } else {
            titleWidth = max(1, availableTextWidth)
        }
        titleLabel.frame = NSRect(
            x: titleX,
            y: visualFrame.minY,
            y: textBlockY,
            width: titleWidth,
            height: visualFrame.height
            height: titleHeight
        )
        let buttonY = visualFrame.minY + 10
        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
        let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
        let buttonY = visualFrame.minY + max(0, (visualFrame.height - controlGroupHeight) / 2)
        nextButton.frame = NSRect(
            x: buttonsGroupMinX + buttonSize + buttonGap,
            y: buttonY,
@@ -995,18 +993,18 @@
        )
        dotsView.frame = NSRect(
            x: buttonsGroupMinX + (buttonsWidth - dotsWidth) / 2,
            y: previousButton.frame.maxY + 6,
            y: previousButton.frame.maxY + buttonDotsGap,
            width: dotsWidth,
            height: 10
            height: dotsHeight
        )
        let detailX = titleLabel.frame.maxX + titleDetailGap
        let detailRight = previousButton.frame.minX - detailControlsGap
        let detailX = titleX
        let detailRight = textRight
        detailScrollView.frame = NSRect(
            x: detailX,
            y: visualFrame.minY,
            y: titleLabel.frame.maxY + lineGap,
            width: max(1, detailRight - detailX),
            height: visualFrame.height
            height: detailHeight
        )
        layoutDetailLabel()
    }
@@ -1048,10 +1046,10 @@
    func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat {
        let padding: CGFloat = 22 + 16
        let fixedWidth: CGFloat = 24 + 10 + 22 + 24 + 48 + 6 + 48
        let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48
        let titleWidth = ceil(titleLabel.attributedStringValue.size().width)
        let detailWidth = ceil(detailLabel.attributedStringValue.size().width)
        let contentWidth = padding + fixedWidth + titleWidth + detailWidth
        let detailWidth = measuredDetailLineWidth()
        let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth)
        let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth)
        return min(maxAvailableWidth, preferredWidth)
    }
@@ -1081,7 +1079,7 @@
        addSubview(iconView)
        configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
        configureLabel(detailLabel, font: detailFont, lineBreakMode: .byClipping)
        configureDetailLabel()
        detailScrollView.drawsBackground = false
        detailScrollView.hasVerticalScroller = false
@@ -1112,6 +1110,19 @@
        label.lineBreakMode = lineBreakMode
        label.maximumNumberOfLines = 1
        label.font = font
    }
    private func configureDetailLabel() {
        detailLabel.cell = NSTextFieldCell(textCell: "")
        detailLabel.isEditable = false
        detailLabel.isSelectable = false
        detailLabel.drawsBackground = false
        detailLabel.isBordered = false
        detailLabel.lineBreakMode = .byWordWrapping
        detailLabel.maximumNumberOfLines = 2
        detailLabel.font = detailFont
        detailLabel.cell?.usesSingleLineMode = false
        detailLabel.cell?.wraps = true
    }
    private func claimInteractionFocus() {
@@ -1180,9 +1191,17 @@
    }
    private func layoutDetailLabel() {
        let textWidth = detailLabel.attributedStringValue.size().width
        let width = max(detailScrollView.contentView.bounds.width, ceil(textWidth) + 18)
        let width = max(1, detailScrollView.contentView.bounds.width)
        detailLabel.preferredMaxLayoutWidth = width
        detailLabel.frame = NSRect(x: 0, y: 0, width: width, height: detailScrollView.bounds.height)
    }
    private func measuredDetailLineWidth() -> CGFloat {
        let attributes: [NSAttributedString.Key: Any] = [.font: detailFont]
        let lineWidths = detailLabel.stringValue
            .components(separatedBy: .newlines)
            .map { NSAttributedString(string: $0, attributes: attributes).size().width }
        return ceil(lineWidths.max() ?? detailLabel.attributedStringValue.size().width)
    }
    private func currentVisualFrame() -> NSRect {
@@ -1198,8 +1217,10 @@
    private func formattedTipDetail(_ text: String) -> String {
        text
            .replacingOccurrences(of: " > ", with: " -> ")
            .replacingOccurrences(of: ">", with: "->")
            .replacingOccurrences(of: "\\s*(?:-->|->|>|→|>)\\s*", with: "\n", options: .regularExpression)
            .replacingOccurrences(of: "[ \\t]{2,}", with: " ", options: .regularExpression)
            .replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression)
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }
    private func updateColors() {
src/Apptag/Info.plist
@@ -19,9 +19,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>7.9.0</string>
    <string>7.9.1</string>
    <key>CFBundleVersion</key>
    <string>20260612.1121</string>
    <string>20260613.2020</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/Apptag/Localization/zh-Hans.json
@@ -221,7 +221,7 @@
  "usageTips.tip4.title": "技巧4-复制应用:",
  "usageTips.tip4.detail": "长按应用图标启动拖动 > 按住 Alt/Option 拖动到容器是复制",
  "usageTips.tip5.title": "技巧5-解除标签:",
  "usageTips.tip5.detail": "长按应用图标启动拖动 > 把应用拖动到容器间的空白处",
  "usageTips.tip5.detail": "长按应用图标启动拖动 > 拖动到容器间的空白处",
  "usageTips.tip6.title": "技巧6-标签排序:",
  "usageTips.tip6.detail": "长按标签启动拖动 > 拖动到目标位置,放手即可",
  "usageTips.tip7.title": "技巧7-应用排序:",
src/Apptag/Localization/zh-Hant.json
@@ -221,7 +221,7 @@
  "usageTips.tip4.title": "技巧4-複製應用:",
  "usageTips.tip4.detail": "長按應用程式圖示開始拖曳 > 按住 Alt/Option 拖到容器即可複製",
  "usageTips.tip5.title": "技巧5-解除標籤:",
  "usageTips.tip5.detail": "長按應用程式圖示開始拖曳 > 把應用拖到容器之間的空白處",
  "usageTips.tip5.detail": "長按應用程式圖示開始拖曳 > 拖到容器之間的空白處",
  "usageTips.tip6.title": "技巧6-標籤排序:",
  "usageTips.tip6.detail": "長按標籤開始拖曳 > 拖到目標位置後放開",
  "usageTips.tip7.title": "技巧7-應用排序:",
src/CHANGELOG.md
@@ -1,5 +1,13 @@
# TagLauncher Changelog
## [7.9.1] — 2026-06-13
- 修复新建空标签后 App Grid 左侧标签栏和主体区域可能不显示该标签的问题;空标签现在会按当前视图显示最小容器,避免用户以为创建失败
- 修复拖动应用到容器外空白处解除标签时,部分 macOS 14 环境可能没有反应的问题;容器内部空白仍不会触发解除标签,保持产品口径一致
- App Grid 新增原生 AppKit 底部使用技巧悬浮条,支持翻页、可在设置中关闭、不会穿透点击到底层应用,并为 29 个语种的较长文案预留换行展示能力
- 优化使用技巧悬浮条排版:标题完整展示,正文分隔符按换行处理,页签指示点移动到左右箭头下方,右侧控制区固定,减少遮挡和误触
- 版本号更新为 `7.9.1`,Build 更新为 `20260613.2020`
## [7.9.0] — 2026-06-11
- 新增容器内 App 手动拖拽排序:同一容器内拖动 App 可改变显示顺序,不改变 App 与标签的归属关系
src/Scripts/usage_tips_qa.sh
@@ -86,9 +86,9 @@
    "Usage tips must be implemented as a native AppKit NSView",
)
require(
    r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*128",
    r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+barHeight\s*:\s*CGFloat\s*=\s*136(?P<body2>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*176",
    app_grid,
    "Usage tips overlay must reserve stable bottom space for the native HUD bar",
    "Usage tips overlay must reserve a design-reviewed multi-line bottom space for the native HUD bar",
)
require(
    r"NSVisualEffectView\s*\(",
@@ -125,19 +125,34 @@
    "Usage tips must ask the host to recompute width after localized text changes",
)
require(
    r"availableTextWidth\s*=\s*max\s*\(\s*1\s*,\s*visualFrame\.maxX\s*-\s*titleX\s*-\s*titleDetailGap\s*-\s*trailingWidth\s*\)",
    r"controlZoneWidth\s*:\s*CGFloat\s*=\s*176",
    app_grid,
    "Usage tips layout must reserve a fixed right-side control zone",
)
require(
    r"availableTextWidth\s*=\s*max\s*\(\s*1\s*,\s*textRight\s*-\s*titleX\s*\)",
    app_grid,
    "Usage tips layout must allocate text from actual available AppGrid width",
)
require(
    r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*6",
    r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*buttonDotsGap",
    app_grid,
    "Usage tips page dots must sit centered below the previous/next arrow buttons",
)
require(
    r"detailRight\s*=\s*previousButton\.frame\.minX\s*-\s*detailControlsGap",
    r"detailRight\s*=\s*textRight",
    app_grid,
    "Usage tips detail text must stop before the arrow controls when dots move below them",
    "Usage tips detail text must stop before the fixed right-side arrow control zone",
)
require(
    r"configureDetailLabel\s*\(\s*\)(?P<body>.*?)detailLabel\.lineBreakMode\s*=\s*\.byWordWrapping",
    app_grid,
    "Usage tip detail text must wrap naturally inside the reserved text column",
)
require(
    r"detailLabel\.maximumNumberOfLines\s*=\s*2",
    app_grid,
    "Usage tip detail text must use the design-reviewed two-line body layout",
)
require(
    r"return\s+min\s*\(\s*maxAvailableWidth\s*,\s*preferredWidth\s*\)",
@@ -346,7 +361,12 @@
require(
    r"formattedTipDetail\s*\(_\s+text\s*:\s*String\s*\)",
    app_grid,
    "Usage tips detail text must normalize > separators to -> for display",
    "Usage tips detail text must normalize separators for display",
)
require(
    r"replacingOccurrences\s*\(\s*of\s*:\s*\"\\\\s\*\(\?:-->\|->\|>\|→\|>\)\\\\s\*\"\s*,\s*with\s*:\s*\"\\n\"",
    app_grid,
    "Usage tips detail text must convert -->, ->, and > separators into hard line breaks",
)
require(
    r"setUsageTipsBubbleDisabled\s*\(\s*inside\s*\)",