From 0ae3a9359e4cdf6ae5b669c4affd91662463f370 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 01 Jul 2026 23:17:36 +0800
Subject: [PATCH] Prepare TagLauncher 8.3.1 candidate
---
src/Apptag/ContentView.swift | 168 +++++++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 149 insertions(+), 19 deletions(-)
diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 4a9cd47..3fede8e 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -38,8 +38,8 @@
// MARK: - Native NSTextField (avoids SwiftUI TextField event issues)
-/// Custom container that wraps NSTextField so that hitTest returns the container
-/// and mouseDown can reliably make the text field first responder.
+/// Custom container that wraps NSTextField so first focus is reliable while
+/// subsequent clicks keep native caret placement and selection behavior.
final class TextFieldContainer: NSView {
let textField: NSTextField
@@ -58,14 +58,33 @@
textField.frame = bounds
}
+ private var isEditing: Bool {
+ guard let window else { return false }
+ return window.firstResponder === textField || textField.currentEditor() != nil
+ }
+
override func hitTest(_ point: NSPoint) -> NSView? {
- if bounds.contains(point) { return self }
- return nil
+ guard bounds.contains(point) else { return nil }
+ if isEditing {
+ let fieldPoint = textField.convert(point, from: self)
+ return textField.hitTest(fieldPoint) ?? textField
+ }
+ return self
+ }
+
+ func focusTextField() {
+ guard let w = window else { return }
+ let shouldSelectAll = !isEditing
+ w.makeFirstResponder(textField)
+ if shouldSelectAll {
+ textField.selectText(nil)
+ }
}
override func mouseDown(with event: NSEvent) {
- if let w = window { w.makeFirstResponder(textField) }
- textField.mouseDown(with: event)
+ // Do not forward this same NSEvent to NSTextField.mouseDown. AppKit can
+ // forward it back to this container during control tracking and recurse.
+ focusTextField()
}
}
@@ -374,6 +393,7 @@
let id = UUID()
let feature: ProFeature
let noteQuotaStatus: ProNoteQuotaStatus?
+ let customContainerQuotaStatus: ProCustomContainerQuotaStatus?
}
struct ContentView: View {
@@ -387,6 +407,7 @@
@State private var tagColors: [String: Int] = [:]
@State private var tagCustomColors: [String: TagCustomColor] = [:]
@State private var tagDefinitions: [String: TagDatabase.TagDef] = [:]
+ @State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()
@State private var containerAppOrder: [String: [String]] = [:]
@State private var groupLayoutVersion = 0
@State private var appGridScrollTargetID: String? = nil
@@ -697,11 +718,11 @@
benefitText: tr(prompt.feature.benefitKey),
operationText: proEntitlement.operationState.statusMessageKey.map(tr),
unlockTitle: tr("pro.card.unlock"),
- restoreTitle: tr("pro.card.restore"),
+ restoreTitle: proPromptSecondaryTitle(for: prompt),
isBusy: proEntitlement.operationState.isBusy,
onClose: dismissProPrompt,
onUnlock: { proEntitlement.purchasePro() },
- onRestore: { proEntitlement.restorePurchases() }
+ onRestore: { handleProPromptSecondaryAction(prompt) }
)
}
.transition(.opacity)
@@ -757,6 +778,7 @@
tagCustomColors = store.tags.compactMapValues { $0.customColor }
tagDefinitions = store.tags
draggedTagNames = TagEditor.orderedTagNames()
+ refreshCustomContainerQuotaStatus(in: store)
}
editPhase = phase
if phase == .none {
@@ -1399,9 +1421,47 @@
excludedTagNames: ["Mac自带", defaultGroupName],
isCustomColorUnlocked: proEntitlement.isUnlocked,
onLockedCustomColor: { presentProPrompt(for: .customTagColors) },
- onRefresh: { refreshApps() }
+ onCustomContainerQuotaExceeded: presentCustomContainerQuotaPrompt,
+ onCustomContainerQuotaStatusChanged: refreshCustomContainerQuotaStatus,
+ topLeadingAccessory: AnyView(customContainerQuotaAccessory),
+ onRefresh: {
+ refreshCustomContainerQuotaStatus()
+ refreshApps()
+ }
)
}
+ }
+
+ private var effectiveCustomContainerQuotaStatus: ProCustomContainerQuotaStatus {
+ if proEntitlement.isUnlocked {
+ return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
+ }
+ return customContainerQuotaStatus
+ }
+
+ private var customContainerQuotaAccessory: some View {
+ let status = effectiveCustomContainerQuotaStatus
+ let text = status.isUnlimited
+ ? tr("pro.customContainers.quotaUnlimitedShort")
+ : tr(
+ "pro.customContainers.quotaShort",
+ replacements: [
+ "%used%": "\(status.used)",
+ "%limit%": "\(status.limit)"
+ ]
+ )
+ return Text(text)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+
+ private func refreshCustomContainerQuotaStatus(_ status: ProCustomContainerQuotaStatus) {
+ customContainerQuotaStatus = status
+ }
+
+ private func refreshCustomContainerQuotaStatus(in store: TagDatabase.Store? = nil) {
+ customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus(in: store)
}
// MARK: - Edit Apps View
@@ -1553,7 +1613,7 @@
.onEnded { _ in
guard canReorderTag(tagName) else { return }
dragItem = nil
- TagEditor.reorderTags(draggedTagNames)
+ persistTagOrderIfAllowed()
}
}
@@ -1581,9 +1641,11 @@
switch editTagOperation {
case .add:
- TagEditor.appendTags(tags, to: paths)
+ let result = TagEditor.appendTags(tags, to: paths)
+ guard handleTagMutationResult(result) else { return }
case .remove:
- TagEditor.removeTags(tags, from: paths)
+ let result = TagEditor.removeTags(tags, from: paths)
+ guard handleTagMutationResult(result) else { return }
}
selectedAppPaths = []
selectedTagNames = []
@@ -2231,16 +2293,68 @@
status.remaining <= 0 {
return tr("pro.notes.limitReached")
}
+ if prompt.feature == .customContainerQuota,
+ let status = prompt.customContainerQuotaStatus,
+ !status.isUnlimited {
+ return tr(
+ "pro.customContainers.quotaPrompt",
+ replacements: [
+ "%used%": "\(status.used)",
+ "%limit%": "\(status.limit)",
+ "%remaining%": "\(status.remaining)"
+ ]
+ )
+ }
return tr(prompt.feature.promptMessageKey)
}
- private func presentProPrompt(for feature: ProFeature, noteQuotaStatus: ProNoteQuotaStatus? = nil) {
+ private func proPromptSecondaryTitle(for prompt: PendingProPrompt) -> String {
+ prompt.feature == .customContainerQuota
+ ? tr("pro.customContainers.manageExisting")
+ : tr("pro.card.restore")
+ }
+
+ private func handleProPromptSecondaryAction(_ prompt: PendingProPrompt) {
+ if prompt.feature == .customContainerQuota {
+ dismissProPrompt()
+ setEditPhase(.editingTags)
+ return
+ }
+ proEntitlement.restorePurchases()
+ }
+
+ private func presentProPrompt(
+ for feature: ProFeature,
+ noteQuotaStatus: ProNoteQuotaStatus? = nil,
+ customContainerQuotaStatus: ProCustomContainerQuotaStatus? = nil
+ ) {
proEntitlement.clearTransientOperationState()
withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
appGridInteraction.proPrompt = PendingProPrompt(
feature: feature,
- noteQuotaStatus: noteQuotaStatus
+ noteQuotaStatus: noteQuotaStatus,
+ customContainerQuotaStatus: customContainerQuotaStatus
)
+ }
+ }
+
+ private func presentCustomContainerQuotaPrompt(_ status: ProCustomContainerQuotaStatus) {
+ presentProPrompt(
+ for: .customContainerQuota,
+ customContainerQuotaStatus: status
+ )
+ }
+
+ private func handleTagMutationResult(_ result: TagEditorMutationResult) -> Bool {
+ switch result {
+ case .saved:
+ refreshCustomContainerQuotaStatus()
+ return true
+ case .noChange:
+ return false
+ case .blockedCustomContainerQuota(let status):
+ presentCustomContainerQuotaPrompt(status)
+ return false
}
}
@@ -2530,11 +2644,22 @@
let hadDragState = tagNavDragModeActive || tagNavDragItem != nil
guard hadDragState else { return }
if tagNavDragModeActive && tagNavReorderDidMove {
- TagEditor.reorderTags(draggedTagNames)
+ persistTagOrderIfAllowed()
}
tagNavDragModeActive = false
tagNavDragItem = nil
tagNavReorderDidMove = false
+ }
+
+ private func persistTagOrderIfAllowed() {
+ guard proEntitlement.isUnlocked else {
+ showDropWarning(message: tr("pro.tagSorting.previewToast"))
+ return
+ }
+ guard TagEditor.reorderTags(draggedTagNames) else {
+ showDropWarning(message: tr("pro.tagSorting.previewToast"))
+ return
+ }
}
private func cancelTagNavReorderVisualState() {
@@ -2582,13 +2707,14 @@
}
guard tagColors[targetTag] != nil else { return }
- TagEditor.moveApp(
+ let result = TagEditor.moveApp(
path: path,
from: sourceTag,
to: targetTag,
color: tagColors[targetTag] ?? 0,
copy: copy
)
+ guard handleTagMutationResult(result) else { return }
showDropRefresh()
refreshApps(forceLayoutRefresh: true)
}
@@ -2598,7 +2724,8 @@
guard canAssignDroppedAppToTag(targetTag) else { return }
guard let app = allApps.first(where: { $0.path.path == path }) else { return }
guard !appHasTag(app, tagName: targetTag) else { return }
- TagEditor.appendTags([targetTag], to: [path])
+ let result = TagEditor.appendTags([targetTag], to: [path])
+ guard handleTagMutationResult(result) else { return }
showDropRefresh()
refreshApps(forceLayoutRefresh: true)
}
@@ -2735,7 +2862,8 @@
private func moveDroppedAppToUncategorized(path: String, tags: [String]) {
guard !tags.isEmpty else { return }
- TagEditor.removeTags(tags, from: [path])
+ let result = TagEditor.removeTags(tags, from: [path])
+ guard handleTagMutationResult(result) else { return }
showDropRefresh()
refreshApps(forceLayoutRefresh: true)
}
@@ -2802,7 +2930,8 @@
guard isRemovableRegularTag(tagName),
appHasTag(app, tagName: tagName)
else { return }
- TagEditor.removeTags([tagName], from: [app.path.path])
+ let result = TagEditor.removeTags([tagName], from: [app.path.path])
+ guard handleTagMutationResult(result) else { return }
showDropRefresh()
refreshApps(forceLayoutRefresh: true)
}
@@ -2981,6 +3110,7 @@
tagCustomColors = snapshot.tagDefinitions.compactMapValues { $0.customColor }
containerAppOrder = snapshot.containerAppOrder
draggedTagNames = snapshot.tagOrder
+ refreshCustomContainerQuotaStatus()
rebuildDisplayGroups(apps: snapshot.apps, tagOrder: snapshot.tagOrder)
if quickSearchVisible {
refreshQuickSearchResults()
--
Gitblit v1.9.3