Freeze 7.6.0 release candidate and move docs out of repo
48 files modified
25 files deleted
33 files added
| | |
| | | } |
| | | |
| | | static func hasStoredValue(for key: String) -> Bool { |
| | | let domain = Bundle.main.bundleIdentifier ?? "com.apptag.launcher" |
| | | let domain = Bundle.main.bundleIdentifier ?? AppIdentity.bundleIdentifier |
| | | return UserDefaults.standard.persistentDomain(forName: domain)?[key] != nil |
| | | } |
| | | |
| | |
| | | static let shared = AppDragCoordinator() |
| | | |
| | | struct DropTarget { |
| | | weak var view: NSView? |
| | | weak var view: AppDropTargetReceivingView? |
| | | var tag: String |
| | | var onDrop: (String, String, Bool) -> Void |
| | | } |
| | | |
| | | private var targets: [UUID: DropTarget] = [:] |
| | |
| | | |
| | | private init() {} |
| | | |
| | | func register(id: UUID, view: NSView, tag: String, onDrop: @escaping (String, String, Bool) -> Void) { |
| | | targets[id] = DropTarget(view: view, tag: tag, onDrop: onDrop) |
| | | var hasActiveDrag: Bool { |
| | | dragLayer != nil || dragWindow != nil || !activePayload.isEmpty |
| | | } |
| | | |
| | | func register(id: UUID, view: AppDropTargetReceivingView, tag: String) { |
| | | if let existing = targets[id], existing.view === view, existing.tag == tag { |
| | | return |
| | | } |
| | | if targets.count > 256 { |
| | | pruneDeadTargets() |
| | | } |
| | | targets[id] = DropTarget(view: view, tag: tag) |
| | | } |
| | | |
| | | func unregister(id: UUID) { |
| | |
| | | panel.isFloatingPanel = true |
| | | panel.hidesOnDeactivate = false |
| | | panel.ignoresMouseEvents = true |
| | | panel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow))) |
| | | panel.level = .normal |
| | | panel.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .stationary, .transient, .ignoresCycle] |
| | | panel.isReleasedWhenClosed = false |
| | | let contentView = NSView(frame: NSRect(origin: .zero, size: image.size)) |
| | |
| | | let parts = activePayload.components(separatedBy: "\n") |
| | | guard let path = parts.first, !path.isEmpty else { return } |
| | | let source = parts.dropFirst().first ?? "" |
| | | pruneDeadTargets() |
| | | |
| | | let hitTarget = targets.values |
| | | .compactMap { target -> (DropTarget, CGFloat)? in |
| | | guard let frame = target.view?.screenFrame(), frame.contains(screenPoint) else { return nil } |
| | | return (target, frame.width * frame.height) |
| | | .compactMap { target -> (AppDropTargetReceivingView, CGFloat)? in |
| | | guard let view = target.view, |
| | | let frame = view.screenFrame(), |
| | | frame.contains(screenPoint) |
| | | else { return nil } |
| | | return (view, frame.width * frame.height) |
| | | } |
| | | .sorted { $0.1 < $1.1 } |
| | | .first?.0 |
| | | |
| | | hitTarget?.onDrop(path, source, copy) |
| | | hitTarget?.performDrop(path: path, source: source, copy: copy) |
| | | } |
| | | |
| | | func cancelDrag() { |
| | |
| | | dragWindow = nil |
| | | activePayload = "" |
| | | dragImageSize = .zero |
| | | } |
| | | |
| | | private func pruneDeadTargets() { |
| | | targets = targets.filter { $0.value.view != nil } |
| | | } |
| | | |
| | | private static func cgImage(from image: NSImage) -> CGImage? { |
| | |
| | | } |
| | | } |
| | | |
| | | private extension NSView { |
| | | protocol AppDropTargetReceivingView: AnyObject { |
| | | func screenFrame() -> NSRect? |
| | | func performDrop(path: String, source: String, copy: Bool) |
| | | } |
| | | |
| | | extension AppDropTargetReceivingView where Self: NSView { |
| | | func screenFrame() -> NSRect? { |
| | | guard let window else { return nil } |
| | | let rectInWindow = convert(bounds, to: nil) |
| | |
| | | |
| | | func makeNSView(context: Context) -> AppDropTargetNSView { |
| | | let view = AppDropTargetNSView() |
| | | view.targetTag = targetTag |
| | | view.onDropApp = onDropApp |
| | | view.configure(targetTag: targetTag, onDropApp: onDropApp) |
| | | return view |
| | | } |
| | | |
| | | func updateNSView(_ view: AppDropTargetNSView, context: Context) { |
| | | view.targetTag = targetTag |
| | | view.onDropApp = onDropApp |
| | | view.registerTarget() |
| | | view.configure(targetTag: targetTag, onDropApp: onDropApp) |
| | | } |
| | | |
| | | static func dismantleNSView(_ view: AppDropTargetNSView, coordinator: ()) { |
| | | view.onDropApp = nil |
| | | AppDragCoordinator.shared.unregister(id: view.id) |
| | | } |
| | | } |
| | | |
| | | final class AppDropTargetNSView: NSView { |
| | | final class AppDropTargetNSView: NSView, AppDropTargetReceivingView { |
| | | let id = UUID() |
| | | var targetTag = "" |
| | | var onDropApp: ((String, String, Bool) -> Void)? |
| | | |
| | | override func viewDidMoveToWindow() { |
| | | super.viewDidMoveToWindow() |
| | | func configure(targetTag: String, onDropApp: @escaping (String, String, Bool) -> Void) { |
| | | let tagChanged = self.targetTag != targetTag |
| | | self.targetTag = targetTag |
| | | self.onDropApp = onDropApp |
| | | if tagChanged { |
| | | registerTarget() |
| | | } |
| | | } |
| | | |
| | | override func layout() { |
| | | super.layout() |
| | | override func viewDidMoveToWindow() { |
| | | super.viewDidMoveToWindow() |
| | | if window == nil { |
| | | AppDragCoordinator.shared.unregister(id: id) |
| | | } else { |
| | | registerTarget() |
| | | } |
| | | } |
| | | |
| | | func registerTarget() { |
| | | guard window != nil, let onDropApp else { return } |
| | | AppDragCoordinator.shared.register(id: id, view: self, tag: targetTag, onDrop: onDropApp) |
| | | guard window != nil else { return } |
| | | AppDragCoordinator.shared.register(id: id, view: self, tag: targetTag) |
| | | } |
| | | |
| | | func performDrop(path: String, source: String, copy: Bool) { |
| | | onDropApp?(path, source, copy) |
| | | } |
| | | |
| | | deinit { |
| | | AppDragCoordinator.shared.unregister(id: id) |
| | | } |
| | | } |
| New file |
| | |
| | | import SwiftUI |
| | | import AppKit |
| | | |
| | | struct AppGridCollectionView: NSViewRepresentable { |
| | | let groups: [TagGroup] |
| | | let tagColors: [String: Int] |
| | | let displayMode: String |
| | | let iconSize: CGFloat |
| | | let showNames: Bool |
| | | let bubbleDisabled: Bool |
| | | let showUncommonAppBubbles: Bool |
| | | let highlightedGroupName: String? |
| | | let contentRevision: Int |
| | | let scrollTargetID: String? |
| | | let scrollRequestToken: Int |
| | | let onSelectApp: (AppInfo) -> Void |
| | | let onBubbleHover: (AppInfo, CGRect, AppBubbleHoverEvent) -> Void |
| | | let onEditNote: (AppInfo, CGRect) -> Void |
| | | let onDropApp: (String, String, String, Bool) -> Void |
| | | let onGroupActivate: (String) -> Void |
| | | let onScrollActivity: () -> Void |
| | | let onDragModeChange: (Bool) -> Void |
| | | |
| | | func makeCoordinator() -> Coordinator { |
| | | Coordinator() |
| | | } |
| | | |
| | | func makeNSView(context: Context) -> AppGridCollectionHostView { |
| | | let view = AppGridCollectionHostView() |
| | | view.configure(coordinator: context.coordinator) |
| | | return view |
| | | } |
| | | |
| | | func updateNSView(_ view: AppGridCollectionHostView, context: Context) { |
| | | context.coordinator.update( |
| | | groups: groups, |
| | | tagColors: tagColors, |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: showNames, |
| | | bubbleDisabled: bubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | highlightedGroupName: highlightedGroupName, |
| | | contentRevision: contentRevision, |
| | | scrollTargetID: scrollTargetID, |
| | | scrollRequestToken: scrollRequestToken, |
| | | onSelectApp: onSelectApp, |
| | | onBubbleHover: onBubbleHover, |
| | | onEditNote: onEditNote, |
| | | onDropApp: onDropApp, |
| | | onGroupActivate: onGroupActivate, |
| | | onScrollActivity: onScrollActivity, |
| | | onDragModeChange: onDragModeChange |
| | | ) |
| | | view.applyCoordinatorUpdate() |
| | | } |
| | | |
| | | final class Coordinator: NSObject, NSCollectionViewDataSource, NSCollectionViewDelegate { |
| | | var groups: [TagGroup] = [] |
| | | var tagColors: [String: Int] = [:] |
| | | var displayMode = AppDefaults.displayMode |
| | | var iconSize: CGFloat = AppDefaults.iconSize |
| | | var showNames = true |
| | | var bubbleDisabled = false |
| | | var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | var highlightedGroupName: String? |
| | | var contentRevision = 0 |
| | | var scrollTargetID: String? |
| | | var scrollRequestToken = 0 |
| | | var lastHandledScrollRequestToken = 0 |
| | | var contentSignature = "" |
| | | var needsReload = true |
| | | |
| | | var onSelectApp: (AppInfo) -> Void = { _ in } |
| | | var onBubbleHover: (AppInfo, CGRect, AppBubbleHoverEvent) -> Void = { _, _, _ in } |
| | | var onEditNote: (AppInfo, CGRect) -> Void = { _, _ in } |
| | | var onDropApp: (String, String, String, Bool) -> Void = { _, _, _, _ in } |
| | | var onGroupActivate: (String) -> Void = { _ in } |
| | | var onScrollActivity: () -> Void = {} |
| | | var onDragModeChange: (Bool) -> Void = { _ in } |
| | | |
| | | func update( |
| | | groups: [TagGroup], |
| | | tagColors: [String: Int], |
| | | displayMode: String, |
| | | iconSize: CGFloat, |
| | | showNames: Bool, |
| | | bubbleDisabled: Bool, |
| | | showUncommonAppBubbles: Bool, |
| | | highlightedGroupName: String?, |
| | | contentRevision: Int, |
| | | scrollTargetID: String?, |
| | | scrollRequestToken: Int, |
| | | onSelectApp: @escaping (AppInfo) -> Void, |
| | | onBubbleHover: @escaping (AppInfo, CGRect, AppBubbleHoverEvent) -> Void, |
| | | onEditNote: @escaping (AppInfo, CGRect) -> Void, |
| | | onDropApp: @escaping (String, String, String, Bool) -> Void, |
| | | onGroupActivate: @escaping (String) -> Void, |
| | | onScrollActivity: @escaping () -> Void, |
| | | onDragModeChange: @escaping (Bool) -> Void |
| | | ) { |
| | | self.groups = groups |
| | | self.tagColors = tagColors |
| | | self.displayMode = displayMode |
| | | self.iconSize = iconSize |
| | | self.showNames = showNames |
| | | self.bubbleDisabled = bubbleDisabled |
| | | self.showUncommonAppBubbles = showUncommonAppBubbles |
| | | self.highlightedGroupName = highlightedGroupName |
| | | self.contentRevision = contentRevision |
| | | self.scrollTargetID = scrollTargetID |
| | | self.scrollRequestToken = scrollRequestToken |
| | | self.onSelectApp = onSelectApp |
| | | self.onBubbleHover = onBubbleHover |
| | | self.onEditNote = onEditNote |
| | | self.onDropApp = onDropApp |
| | | self.onGroupActivate = onGroupActivate |
| | | self.onScrollActivity = onScrollActivity |
| | | self.onDragModeChange = onDragModeChange |
| | | |
| | | let nextSignature = Self.signature( |
| | | tagColors: tagColors, |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: showNames, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | highlightedGroupName: highlightedGroupName, |
| | | contentRevision: contentRevision |
| | | ) |
| | | if nextSignature != contentSignature { |
| | | contentSignature = nextSignature |
| | | needsReload = true |
| | | } |
| | | } |
| | | |
| | | func numberOfSections(in collectionView: NSCollectionView) -> Int { |
| | | 1 |
| | | } |
| | | |
| | | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | groups.count |
| | | } |
| | | |
| | | func collectionView( |
| | | _ collectionView: NSCollectionView, |
| | | itemForRepresentedObjectAt indexPath: IndexPath |
| | | ) -> NSCollectionViewItem { |
| | | let item = collectionView.makeItem( |
| | | withIdentifier: AppGridGroupCollectionItem.reuseIdentifier, |
| | | for: indexPath |
| | | ) |
| | | guard let groupItem = item as? AppGridGroupCollectionItem, |
| | | indexPath.item < groups.count |
| | | else { return item } |
| | | |
| | | groupItem.configure(group: groups[indexPath.item], coordinator: self) |
| | | return groupItem |
| | | } |
| | | |
| | | func scrollIndex(for tagID: String) -> Int? { |
| | | groups.firstIndex { $0.id == tagID || $0.name == tagID } |
| | | } |
| | | |
| | | private static func signature( |
| | | tagColors: [String: Int], |
| | | displayMode: String, |
| | | iconSize: CGFloat, |
| | | showNames: Bool, |
| | | showUncommonAppBubbles: Bool, |
| | | highlightedGroupName: String?, |
| | | contentRevision: Int |
| | | ) -> String { |
| | | let colorPart = tagColors |
| | | .sorted { $0.key < $1.key } |
| | | .map { "\($0.key)=\($0.value)" } |
| | | .joined(separator: ",") |
| | | return [ |
| | | displayMode, |
| | | "\(Int(iconSize.rounded()))", |
| | | showNames ? "names" : "nonames", |
| | | showUncommonAppBubbles ? "uncommon" : "allbubbles", |
| | | highlightedGroupName ?? "", |
| | | colorPart, |
| | | "rev=\(contentRevision)" |
| | | ].joined(separator: "|") |
| | | } |
| | | } |
| | | } |
| | | |
| | | final class AppGridCollectionHostView: NSView { |
| | | private let scrollView = NSScrollView() |
| | | private let collectionView = NSCollectionView() |
| | | private let gridLayout = AppGridContainerCollectionLayout() |
| | | private weak var coordinator: AppGridCollectionView.Coordinator? |
| | | private var scrollObserver: NSObjectProtocol? |
| | | private var lastLayoutSize: NSSize = .zero |
| | | private var lastReportedBoundsOrigin: NSPoint? |
| | | |
| | | override var isFlipped: Bool { true } |
| | | |
| | | override init(frame frameRect: NSRect) { |
| | | super.init(frame: frameRect) |
| | | setup() |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | super.init(coder: coder) |
| | | setup() |
| | | } |
| | | |
| | | deinit { |
| | | if let scrollObserver { |
| | | NotificationCenter.default.removeObserver(scrollObserver) |
| | | } |
| | | } |
| | | |
| | | func configure(coordinator: AppGridCollectionView.Coordinator) { |
| | | self.coordinator = coordinator |
| | | gridLayout.coordinator = coordinator |
| | | collectionView.dataSource = coordinator |
| | | collectionView.delegate = coordinator |
| | | } |
| | | |
| | | func applyCoordinatorUpdate() { |
| | | guard let coordinator else { return } |
| | | if coordinator.needsReload { |
| | | coordinator.needsReload = false |
| | | collectionView.reloadData() |
| | | gridLayout.invalidateLayout() |
| | | } else { |
| | | collectionView.visibleItems().forEach { item in |
| | | (item as? AppGridGroupCollectionItem)?.refreshRuntimeState() |
| | | } |
| | | } |
| | | |
| | | if coordinator.scrollRequestToken != coordinator.lastHandledScrollRequestToken { |
| | | coordinator.lastHandledScrollRequestToken = coordinator.scrollRequestToken |
| | | if let target = coordinator.scrollTargetID, |
| | | let index = coordinator.scrollIndex(for: target) { |
| | | DispatchQueue.main.async { [weak self] in |
| | | self?.collectionView.scrollToItems( |
| | | at: [IndexPath(item: index, section: 0)], |
| | | scrollPosition: .top |
| | | ) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func layout() { |
| | | super.layout() |
| | | scrollView.frame = bounds |
| | | if lastLayoutSize != bounds.size { |
| | | lastLayoutSize = bounds.size |
| | | gridLayout.invalidateLayout() |
| | | } |
| | | } |
| | | |
| | | private func setup() { |
| | | wantsLayer = true |
| | | layer?.backgroundColor = NSColor.clear.cgColor |
| | | |
| | | collectionView.collectionViewLayout = gridLayout |
| | | collectionView.backgroundColors = [.clear] |
| | | collectionView.isSelectable = false |
| | | collectionView.register( |
| | | AppGridGroupCollectionItem.self, |
| | | forItemWithIdentifier: AppGridGroupCollectionItem.reuseIdentifier |
| | | ) |
| | | |
| | | scrollView.drawsBackground = false |
| | | scrollView.hasVerticalScroller = true |
| | | scrollView.hasHorizontalScroller = false |
| | | scrollView.autohidesScrollers = true |
| | | scrollView.borderType = .noBorder |
| | | scrollView.documentView = collectionView |
| | | addSubview(scrollView) |
| | | |
| | | scrollView.contentView.postsBoundsChangedNotifications = true |
| | | scrollObserver = NotificationCenter.default.addObserver( |
| | | forName: NSView.boundsDidChangeNotification, |
| | | object: scrollView.contentView, |
| | | queue: .main |
| | | ) { [weak self] _ in |
| | | guard let self, |
| | | self.recordScrollIfNeeded() |
| | | else { return } |
| | | self.coordinator?.onScrollActivity() |
| | | } |
| | | } |
| | | |
| | | private func recordScrollIfNeeded() -> Bool { |
| | | let origin = scrollView.contentView.bounds.origin |
| | | guard let last = lastReportedBoundsOrigin else { |
| | | lastReportedBoundsOrigin = origin |
| | | return false |
| | | } |
| | | let didScroll = abs(origin.x - last.x) > 0.5 || abs(origin.y - last.y) > 0.5 |
| | | if didScroll { |
| | | lastReportedBoundsOrigin = origin |
| | | } |
| | | return didScroll |
| | | } |
| | | } |
| | | |
| | | private final class AppGridContainerCollectionLayout: NSCollectionViewLayout { |
| | | weak var coordinator: AppGridCollectionView.Coordinator? |
| | | |
| | | private var itemAttributes: [IndexPath: NSCollectionViewLayoutAttributes] = [:] |
| | | private var contentSize: NSSize = .zero |
| | | |
| | | override var collectionViewContentSize: NSSize { |
| | | contentSize |
| | | } |
| | | |
| | | override func prepare() { |
| | | super.prepare() |
| | | guard let collectionView else { |
| | | itemAttributes = [:] |
| | | contentSize = .zero |
| | | return |
| | | } |
| | | |
| | | let groups = coordinator?.groups ?? [] |
| | | let itemCount = collectionView.numberOfItems(inSection: 0) |
| | | let visibleGroups = Array(groups.prefix(itemCount)) |
| | | let iconSize: CGFloat = coordinator?.iconSize ?? CGFloat(AppDefaults.iconSize) |
| | | let plan = Self.makePlan( |
| | | groups: visibleGroups, |
| | | contentWidth: collectionView.bounds.width, |
| | | iconSize: iconSize |
| | | ) |
| | | |
| | | var nextAttributes: [IndexPath: NSCollectionViewLayoutAttributes] = [:] |
| | | for item in plan.items { |
| | | let indexPath = IndexPath(item: item.index, section: 0) |
| | | let attributes = NSCollectionViewLayoutAttributes(forItemWith: indexPath) |
| | | attributes.frame = item.frame |
| | | nextAttributes[indexPath] = attributes |
| | | } |
| | | |
| | | itemAttributes = nextAttributes |
| | | contentSize = plan.contentSize |
| | | } |
| | | |
| | | override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] { |
| | | itemAttributes.values.filter { $0.frame.intersects(rect) } |
| | | } |
| | | |
| | | override func layoutAttributesForItem(at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes? { |
| | | itemAttributes[indexPath] |
| | | } |
| | | |
| | | override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool { |
| | | false |
| | | } |
| | | |
| | | private struct LayoutItem { |
| | | let index: Int |
| | | let frame: NSRect |
| | | } |
| | | |
| | | private struct LayoutPlan { |
| | | let items: [LayoutItem] |
| | | let contentSize: NSSize |
| | | } |
| | | |
| | | private struct Candidate { |
| | | let spans: [Int] |
| | | let cost: CGFloat |
| | | } |
| | | |
| | | private static func makePlan( |
| | | groups: [TagGroup], |
| | | contentWidth: CGFloat, |
| | | iconSize: CGFloat |
| | | ) -> LayoutPlan { |
| | | let boundedContentWidth = max(1, contentWidth) |
| | | let outerPadding = AppGridCollectionMetrics.outerPadding |
| | | let gap = AppGridCollectionMetrics.cardGap |
| | | let availableWidth = max(1, boundedContentWidth - outerPadding * 2) |
| | | let trackCount = preferredTrackCount(availableWidth: availableWidth, iconSize: iconSize) |
| | | let trackWidth = floor((availableWidth - gap * CGFloat(trackCount - 1)) / CGFloat(trackCount)) |
| | | let rows = layoutRows( |
| | | groups: groups, |
| | | trackCount: trackCount, |
| | | trackWidth: trackWidth, |
| | | availableWidth: availableWidth, |
| | | gap: gap, |
| | | iconSize: iconSize |
| | | ) |
| | | |
| | | var y = outerPadding |
| | | var items: [LayoutItem] = [] |
| | | for row in rows { |
| | | var x = outerPadding |
| | | let height = AppGridCollectionMetrics.cardHeight(rowCount: row.fixedRows, iconSize: iconSize) |
| | | for item in row.items { |
| | | let frame = NSRect(x: x, y: y, width: item.width, height: height) |
| | | items.append(LayoutItem(index: item.index, frame: frame)) |
| | | x += item.width + gap |
| | | } |
| | | y += height + gap |
| | | } |
| | | |
| | | let contentHeight = rows.isEmpty ? outerPadding * 2 : y - gap + outerPadding |
| | | return LayoutPlan( |
| | | items: items, |
| | | contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight)) |
| | | ) |
| | | } |
| | | |
| | | private struct RowItem { |
| | | let index: Int |
| | | let width: CGFloat |
| | | } |
| | | |
| | | private struct Row { |
| | | let items: [RowItem] |
| | | let fixedRows: Int |
| | | } |
| | | |
| | | private static func layoutRows( |
| | | groups: [TagGroup], |
| | | trackCount: Int, |
| | | trackWidth: CGFloat, |
| | | availableWidth: CGFloat, |
| | | gap: CGFloat, |
| | | iconSize: CGFloat |
| | | ) -> [Row] { |
| | | let n = groups.count |
| | | guard n > 0 else { return [] } |
| | | let patterns = spanPatterns(trackCount: trackCount) |
| | | var bestCost = Array(repeating: CGFloat.greatestFiniteMagnitude, count: n + 1) |
| | | var bestPattern = Array(repeating: [Int](), count: n) |
| | | bestCost[n] = 0 |
| | | |
| | | for index in stride(from: n - 1, through: 0, by: -1) { |
| | | for pattern in patterns where index + pattern.count <= n { |
| | | let candidate = rowCandidate( |
| | | groups: groups, |
| | | startIndex: index, |
| | | spans: pattern, |
| | | trackWidth: trackWidth, |
| | | availableWidth: availableWidth, |
| | | gap: gap, |
| | | iconSize: iconSize |
| | | ) |
| | | let totalCost = candidate.cost + bestCost[index + pattern.count] |
| | | if totalCost < bestCost[index] { |
| | | bestCost[index] = totalCost |
| | | bestPattern[index] = candidate.spans |
| | | } |
| | | } |
| | | } |
| | | |
| | | var rows: [Row] = [] |
| | | var index = 0 |
| | | while index < n { |
| | | let spans = bestPattern[index].isEmpty ? [trackCount] : bestPattern[index] |
| | | let widths = spans.map { width(trackWidth: trackWidth, span: $0, gap: gap) } |
| | | let fixedRows = widths.indices.map { |
| | | AppGridCollectionMetrics.iconRows( |
| | | appCount: groups[index + $0].apps.count, |
| | | width: widths[$0], |
| | | iconSize: iconSize |
| | | ) |
| | | }.max() ?? 1 |
| | | let items = widths.indices.map { |
| | | RowItem(index: index + $0, width: widths[$0]) |
| | | } |
| | | rows.append(Row(items: items, fixedRows: fixedRows)) |
| | | index += spans.count |
| | | } |
| | | return rows |
| | | } |
| | | |
| | | private static func rowCandidate( |
| | | groups: [TagGroup], |
| | | startIndex: Int, |
| | | spans: [Int], |
| | | trackWidth: CGFloat, |
| | | availableWidth: CGFloat, |
| | | gap: CGFloat, |
| | | iconSize: CGFloat |
| | | ) -> Candidate { |
| | | let widths = spans.map { width(trackWidth: trackWidth, span: $0, gap: gap) } |
| | | let rowCounts = widths.indices.map { |
| | | AppGridCollectionMetrics.iconRows( |
| | | appCount: groups[startIndex + $0].apps.count, |
| | | width: widths[$0], |
| | | iconSize: iconSize |
| | | ) |
| | | } |
| | | let fixedRows = rowCounts.max() ?? 1 |
| | | let rowArea = CGFloat(fixedRows) * AppGridCollectionMetrics.iconCellHeight(iconSize: iconSize) * availableWidth |
| | | let paddingCost = CGFloat(spans.count) * 0.001 |
| | | return Candidate(spans: spans, cost: rowArea + paddingCost) |
| | | } |
| | | |
| | | private static func preferredTrackCount(availableWidth: CGFloat, iconSize: CGFloat) -> Int { |
| | | let minCardWidth = max(260, AppGridCollectionMetrics.iconCellWidth(iconSize: iconSize) * 3 + 64) |
| | | if availableWidth >= minCardWidth * 3 + AppGridCollectionMetrics.cardGap * 2 { return 3 } |
| | | if availableWidth >= minCardWidth * 2 + AppGridCollectionMetrics.cardGap { return 2 } |
| | | return 1 |
| | | } |
| | | |
| | | private static func spanPatterns(trackCount: Int) -> [[Int]] { |
| | | switch trackCount { |
| | | case 3: |
| | | return [[1, 1, 1], [1, 2], [2, 1], [3]] |
| | | case 2: |
| | | return [[1, 1], [2]] |
| | | default: |
| | | return [[1]] |
| | | } |
| | | } |
| | | |
| | | private static func width(trackWidth: CGFloat, span: Int, gap: CGFloat) -> CGFloat { |
| | | let safeSpan = max(1, span) |
| | | return trackWidth * CGFloat(safeSpan) + gap * CGFloat(safeSpan - 1) |
| | | } |
| | | } |
| | | |
| | | private enum AppGridCollectionMetrics { |
| | | static let outerPadding: CGFloat = 20 |
| | | static let cardGap: CGFloat = 16 |
| | | static let cardPadding: CGFloat = 16 |
| | | static let headerHeight: CGFloat = 28 |
| | | static let headerBottomGap: CGFloat = 6 |
| | | static let iconColumnGap: CGFloat = 6 |
| | | static let iconRowGap: CGFloat = 2 |
| | | static let hoverScale: CGFloat = 1.22 |
| | | static let labelHeight: CGFloat = 14 |
| | | |
| | | static func iconCellWidth(iconSize: CGFloat) -> CGFloat { |
| | | iconSize * hoverScale + 8 |
| | | } |
| | | |
| | | static func iconCellHeight(iconSize: CGFloat) -> CGFloat { |
| | | iconSize * hoverScale + labelHeight + 22 |
| | | } |
| | | |
| | | static func columns(width: CGFloat, iconSize: CGFloat) -> Int { |
| | | let inner = max(1, width - cardPadding * 2) |
| | | let itemW = iconCellWidth(iconSize: iconSize) |
| | | return max(1, Int((inner + iconColumnGap) / (itemW + iconColumnGap))) |
| | | } |
| | | |
| | | static func cardHeight(appCount: Int, width: CGFloat, iconSize: CGFloat) -> CGFloat { |
| | | cardHeight(rowCount: iconRows(appCount: appCount, width: width, iconSize: iconSize), iconSize: iconSize) |
| | | } |
| | | |
| | | static func cardHeight(rowCount: Int, iconSize: CGFloat) -> CGFloat { |
| | | let rows = max(1, rowCount) |
| | | return cardPadding * 2 |
| | | + headerHeight |
| | | + headerBottomGap |
| | | + CGFloat(rows) * iconCellHeight(iconSize: iconSize) |
| | | + CGFloat(max(0, rows - 1)) * iconRowGap |
| | | } |
| | | |
| | | static func iconRows(appCount: Int, width: CGFloat, iconSize: CGFloat) -> Int { |
| | | let cols = columns(width: width, iconSize: iconSize) |
| | | return max(1, (appCount + cols - 1) / cols) |
| | | } |
| | | } |
| | | |
| | | private final class AppGridGroupCollectionItem: NSCollectionViewItem { |
| | | static let reuseIdentifier = NSUserInterfaceItemIdentifier("AppGridGroupCollectionItem") |
| | | |
| | | private var cardView: AppGridGroupCardView { |
| | | view as! AppGridGroupCardView |
| | | } |
| | | |
| | | override func loadView() { |
| | | view = AppGridGroupCardView() |
| | | } |
| | | |
| | | func configure(group: TagGroup, coordinator: AppGridCollectionView.Coordinator) { |
| | | cardView.configure(group: group, coordinator: coordinator) |
| | | } |
| | | |
| | | func refreshRuntimeState() { |
| | | cardView.refreshRuntimeState() |
| | | } |
| | | |
| | | override func prepareForReuse() { |
| | | super.prepareForReuse() |
| | | cardView.prepareForReuse() |
| | | } |
| | | } |
| | | |
| | | private final class AppGridGroupCardView: NSView, AppDropTargetReceivingView { |
| | | private let dropTargetID = UUID() |
| | | private weak var coordinator: AppGridCollectionView.Coordinator? |
| | | private var group: TagGroup? |
| | | private var iconViews: [AppGridIconNSView] = [] |
| | | private var isHovered = false |
| | | private var trackingAreaRef: NSTrackingArea? |
| | | |
| | | override var isFlipped: Bool { true } |
| | | |
| | | override init(frame frameRect: NSRect) { |
| | | super.init(frame: frameRect) |
| | | wantsLayer = true |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | super.init(coder: coder) |
| | | wantsLayer = true |
| | | } |
| | | |
| | | deinit { |
| | | AppDragCoordinator.shared.unregister(id: dropTargetID) |
| | | } |
| | | |
| | | func configure(group: TagGroup, coordinator: AppGridCollectionView.Coordinator) { |
| | | self.group = group |
| | | self.coordinator = coordinator |
| | | rebuildIconViews() |
| | | refreshRuntimeState() |
| | | registerDropTargetIfNeeded() |
| | | needsDisplay = true |
| | | needsLayout = true |
| | | } |
| | | |
| | | override func prepareForReuse() { |
| | | AppDragCoordinator.shared.unregister(id: dropTargetID) |
| | | group = nil |
| | | coordinator = nil |
| | | iconViews.forEach { |
| | | $0.prepareForReuse() |
| | | $0.removeFromSuperview() |
| | | } |
| | | iconViews = [] |
| | | } |
| | | |
| | | func refreshRuntimeState() { |
| | | let runtime = AppGridIconRuntimeState( |
| | | bubbleDisabled: coordinator?.bubbleDisabled ?? false, |
| | | showUncommonAppBubbles: coordinator?.showUncommonAppBubbles ?? AppDefaults.showUncommonAppBubbles |
| | | ) |
| | | iconViews.forEach { $0.runtimeState = runtime } |
| | | needsDisplay = true |
| | | } |
| | | |
| | | override func viewDidMoveToWindow() { |
| | | super.viewDidMoveToWindow() |
| | | if window == nil { |
| | | AppDragCoordinator.shared.unregister(id: dropTargetID) |
| | | } else { |
| | | registerDropTargetIfNeeded() |
| | | } |
| | | } |
| | | |
| | | override func updateTrackingAreas() { |
| | | super.updateTrackingAreas() |
| | | if let trackingAreaRef { |
| | | removeTrackingArea(trackingAreaRef) |
| | | } |
| | | let area = NSTrackingArea( |
| | | rect: .zero, |
| | | options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], |
| | | owner: self, |
| | | userInfo: nil |
| | | ) |
| | | addTrackingArea(area) |
| | | trackingAreaRef = area |
| | | } |
| | | |
| | | override func mouseEntered(with event: NSEvent) { |
| | | isHovered = true |
| | | needsDisplay = true |
| | | } |
| | | |
| | | override func mouseExited(with event: NSEvent) { |
| | | isHovered = false |
| | | needsDisplay = true |
| | | } |
| | | |
| | | override func mouseUp(with event: NSEvent) { |
| | | guard let group, |
| | | bounds.contains(convert(event.locationInWindow, from: nil)) |
| | | else { return } |
| | | coordinator?.onGroupActivate(group.name) |
| | | } |
| | | |
| | | override func layout() { |
| | | super.layout() |
| | | guard let coordinator, let group else { return } |
| | | let cols = AppGridCollectionMetrics.columns(width: bounds.width, iconSize: coordinator.iconSize) |
| | | let innerWidth = max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2) |
| | | let cellWidth = max( |
| | | 1, |
| | | (innerWidth - AppGridCollectionMetrics.iconColumnGap * CGFloat(cols - 1)) / CGFloat(cols) |
| | | ) |
| | | let cellHeight = AppGridCollectionMetrics.iconCellHeight(iconSize: coordinator.iconSize) |
| | | let startY = AppGridCollectionMetrics.cardPadding |
| | | + AppGridCollectionMetrics.headerHeight |
| | | + AppGridCollectionMetrics.headerBottomGap |
| | | |
| | | for index in group.apps.indices { |
| | | guard index < iconViews.count else { continue } |
| | | let row = index / cols |
| | | let col = index % cols |
| | | let x = AppGridCollectionMetrics.cardPadding |
| | | + CGFloat(col) * (cellWidth + AppGridCollectionMetrics.iconColumnGap) |
| | | let y = startY + CGFloat(row) * (cellHeight + AppGridCollectionMetrics.iconRowGap) |
| | | iconViews[index].frame = NSRect(x: x, y: y, width: cellWidth, height: cellHeight) |
| | | } |
| | | } |
| | | |
| | | override func draw(_ dirtyRect: NSRect) { |
| | | guard let coordinator, let group else { return } |
| | | let rect = bounds.insetBy(dx: 0.5, dy: 0.5) |
| | | let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14) |
| | | let tagColor = TagColor.nsColor(for: coordinator.tagColors[group.name] ?? 0) |
| | | let isColored = coordinator.displayMode == "coloredGridContainer" |
| | | let isColorlessActive = coordinator.displayMode == "gridContainer" |
| | | && (isHovered || coordinator.highlightedGroupName == group.name) |
| | | |
| | | cardSurfaceColor().setFill() |
| | | path.fill() |
| | | if isColored || isColorlessActive { |
| | | tagColor.withAlphaComponent(0.30).setFill() |
| | | path.fill() |
| | | } |
| | | NSColor.labelColor.withAlphaComponent(0.08).setStroke() |
| | | path.lineWidth = 1 |
| | | path.stroke() |
| | | |
| | | drawHeader(title: group.name) |
| | | } |
| | | |
| | | func performDrop(path: String, source: String, copy: Bool) { |
| | | guard let group else { return } |
| | | coordinator?.onDropApp(path, source, group.name, copy) |
| | | if source != group.name || copy { |
| | | coordinator?.onDragModeChange(false) |
| | | } |
| | | } |
| | | |
| | | private func rebuildIconViews() { |
| | | iconViews.forEach { |
| | | $0.prepareForReuse() |
| | | $0.removeFromSuperview() |
| | | } |
| | | guard let coordinator, let group else { |
| | | iconViews = [] |
| | | return |
| | | } |
| | | iconViews = group.apps.map { app in |
| | | let iconView = AppGridIconNSView() |
| | | iconView.configure( |
| | | app: app, |
| | | sourceTag: group.name, |
| | | iconSize: coordinator.iconSize, |
| | | showName: coordinator.showNames, |
| | | coordinator: coordinator |
| | | ) |
| | | addSubview(iconView) |
| | | return iconView |
| | | } |
| | | } |
| | | |
| | | private func registerDropTargetIfNeeded() { |
| | | guard window != nil, let group else { return } |
| | | AppDragCoordinator.shared.register(id: dropTargetID, view: self, tag: group.name) |
| | | } |
| | | |
| | | private func cardSurfaceColor() -> NSColor { |
| | | if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { |
| | | return NSColor.white.withAlphaComponent(0.055) |
| | | } |
| | | return NSColor.white.withAlphaComponent(0.62) |
| | | } |
| | | |
| | | private func drawHeader(title: String) { |
| | | let headerRect = NSRect( |
| | | x: AppGridCollectionMetrics.cardPadding, |
| | | y: AppGridCollectionMetrics.cardPadding, |
| | | width: max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2), |
| | | height: AppGridCollectionMetrics.headerHeight |
| | | ) |
| | | let attributes: [NSAttributedString.Key: Any] = [ |
| | | .font: NSFont.systemFont(ofSize: 18, weight: .semibold), |
| | | .foregroundColor: NSColor.secondaryLabelColor, |
| | | .paragraphStyle: centeredParagraph(lineBreak: .byTruncatingMiddle) |
| | | ] |
| | | let titleSize = title.size(withAttributes: attributes) |
| | | let titleWidth = min(headerRect.width * 0.64, titleSize.width + 20) |
| | | let titleRect = NSRect( |
| | | x: headerRect.midX - titleWidth / 2, |
| | | y: headerRect.minY + 3, |
| | | width: titleWidth, |
| | | height: headerRect.height - 6 |
| | | ) |
| | | let lineY = headerRect.midY |
| | | NSColor.secondaryLabelColor.withAlphaComponent(0.25).setStroke() |
| | | let leftLine = NSBezierPath() |
| | | leftLine.move(to: NSPoint(x: headerRect.minX, y: lineY)) |
| | | leftLine.line(to: NSPoint(x: max(headerRect.minX, titleRect.minX - 2), y: lineY)) |
| | | leftLine.stroke() |
| | | let rightLine = NSBezierPath() |
| | | rightLine.move(to: NSPoint(x: min(headerRect.maxX, titleRect.maxX + 2), y: lineY)) |
| | | rightLine.line(to: NSPoint(x: headerRect.maxX, y: lineY)) |
| | | rightLine.stroke() |
| | | title.draw(with: titleRect, options: [.usesLineFragmentOrigin], attributes: attributes) |
| | | } |
| | | |
| | | private func centeredParagraph(lineBreak: NSLineBreakMode) -> NSParagraphStyle { |
| | | let paragraph = NSMutableParagraphStyle() |
| | | paragraph.alignment = .center |
| | | paragraph.lineBreakMode = lineBreak |
| | | return paragraph |
| | | } |
| | | } |
| | | |
| | | private struct AppGridIconRuntimeState { |
| | | var bubbleDisabled: Bool |
| | | var showUncommonAppBubbles: Bool |
| | | } |
| | | |
| | | private final class AppGridIconNSView: NSView { |
| | | private weak var coordinator: AppGridCollectionView.Coordinator? |
| | | private var app: AppInfo? |
| | | private var sourceTag = "" |
| | | private var iconSize: CGFloat = AppDefaults.iconSize |
| | | private var showName = true |
| | | private var isHovered = false |
| | | private var trackingAreaRef: NSTrackingArea? |
| | | private var mouseDownEvent: NSEvent? |
| | | private var didStartDrag = false |
| | | private var isLongPressActive = false |
| | | private var longPressWorkItem: DispatchWorkItem? |
| | | |
| | | var runtimeState = AppGridIconRuntimeState( |
| | | bubbleDisabled: false, |
| | | showUncommonAppBubbles: AppDefaults.showUncommonAppBubbles |
| | | ) { |
| | | didSet { |
| | | if runtimeState.bubbleDisabled { |
| | | setHover(false, notify: true) |
| | | } |
| | | } |
| | | } |
| | | |
| | | override var isFlipped: Bool { true } |
| | | |
| | | func configure( |
| | | app: AppInfo, |
| | | sourceTag: String, |
| | | iconSize: CGFloat, |
| | | showName: Bool, |
| | | coordinator: AppGridCollectionView.Coordinator |
| | | ) { |
| | | self.app = app |
| | | self.sourceTag = sourceTag |
| | | self.iconSize = iconSize |
| | | self.showName = showName |
| | | self.coordinator = coordinator |
| | | runtimeState = AppGridIconRuntimeState( |
| | | bubbleDisabled: coordinator.bubbleDisabled, |
| | | showUncommonAppBubbles: coordinator.showUncommonAppBubbles |
| | | ) |
| | | needsDisplay = true |
| | | } |
| | | |
| | | override func prepareForReuse() { |
| | | longPressWorkItem?.cancel() |
| | | if isHovered { |
| | | setHover(false, notify: true) |
| | | } |
| | | mouseDownEvent = nil |
| | | didStartDrag = false |
| | | isLongPressActive = false |
| | | longPressWorkItem = nil |
| | | } |
| | | |
| | | override func updateTrackingAreas() { |
| | | super.updateTrackingAreas() |
| | | if let trackingAreaRef { |
| | | removeTrackingArea(trackingAreaRef) |
| | | } |
| | | let area = NSTrackingArea( |
| | | rect: .zero, |
| | | options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], |
| | | owner: self, |
| | | userInfo: nil |
| | | ) |
| | | addTrackingArea(area) |
| | | trackingAreaRef = area |
| | | } |
| | | |
| | | override func draw(_ dirtyRect: NSRect) { |
| | | guard let app else { return } |
| | | NSGraphicsContext.current?.imageInterpolation = .high |
| | | let scale = isHovered ? AppGridCollectionMetrics.hoverScale : 1 |
| | | let drawSize = iconSize * scale |
| | | let iconSlot = iconSize * AppGridCollectionMetrics.hoverScale |
| | | let iconRect = NSRect( |
| | | x: (bounds.width - drawSize) / 2, |
| | | y: 8 + (iconSlot - drawSize) / 2, |
| | | width: drawSize, |
| | | height: drawSize |
| | | ) |
| | | |
| | | if isHovered { |
| | | let shadow = NSShadow() |
| | | shadow.shadowColor = NSColor.black.withAlphaComponent(0.35) |
| | | shadow.shadowBlurRadius = 14 |
| | | shadow.shadowOffset = NSSize(width: 0, height: -8) |
| | | NSGraphicsContext.saveGraphicsState() |
| | | shadow.set() |
| | | app.icon.draw(in: iconRect) |
| | | NSGraphicsContext.restoreGraphicsState() |
| | | } else { |
| | | app.icon.draw(in: iconRect) |
| | | } |
| | | |
| | | if showName || shouldShowHoverName { |
| | | let labelRect = NSRect( |
| | | x: 0, |
| | | y: 8 + iconSlot + 6, |
| | | width: bounds.width, |
| | | height: AppGridCollectionMetrics.labelHeight |
| | | ) |
| | | let paragraph = NSMutableParagraphStyle() |
| | | paragraph.alignment = .center |
| | | paragraph.lineBreakMode = .byTruncatingTail |
| | | let alpha: CGFloat = showName ? 1 : 0.85 |
| | | let attributes: [NSAttributedString.Key: Any] = [ |
| | | .font: NSFont.systemFont(ofSize: 11, weight: .medium), |
| | | .foregroundColor: NSColor.labelColor.withAlphaComponent(alpha), |
| | | .paragraphStyle: paragraph |
| | | ] |
| | | app.name.draw(with: labelRect, options: [.usesLineFragmentOrigin], attributes: attributes) |
| | | } |
| | | } |
| | | |
| | | override func mouseEntered(with event: NSEvent) { |
| | | guard !runtimeState.bubbleDisabled else { return } |
| | | setHover(true, notify: true) |
| | | } |
| | | |
| | | override func mouseExited(with event: NSEvent) { |
| | | setHover(false, notify: true) |
| | | } |
| | | |
| | | override func mouseDown(with event: NSEvent) { |
| | | mouseDownEvent = event |
| | | didStartDrag = false |
| | | isLongPressActive = false |
| | | let workItem = DispatchWorkItem { [weak self] in |
| | | guard let self, self.mouseDownEvent != nil else { return } |
| | | self.isLongPressActive = true |
| | | self.coordinator?.onDragModeChange(true) |
| | | self.setHover(false, notify: true) |
| | | } |
| | | longPressWorkItem = workItem |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem) |
| | | } |
| | | |
| | | override func mouseDragged(with event: NSEvent) { |
| | | if didStartDrag { |
| | | AppDragCoordinator.shared.updateDragLocation( |
| | | screenPoint(for: event), |
| | | copy: event.modifierFlags.contains(.option) |
| | | ) |
| | | return |
| | | } |
| | | |
| | | guard let mouseDownEvent, isLongPressActive else { return } |
| | | let dx = event.locationInWindow.x - mouseDownEvent.locationInWindow.x |
| | | let dy = event.locationInWindow.y - mouseDownEvent.locationInWindow.y |
| | | guard hypot(dx, dy) > 3 else { return } |
| | | |
| | | didStartDrag = true |
| | | longPressWorkItem?.cancel() |
| | | AppDragCoordinator.shared.beginDrag( |
| | | image: makeDragImage(), |
| | | payload: dragPayload, |
| | | at: screenPoint(for: event), |
| | | copy: event.modifierFlags.contains(.option), |
| | | in: window |
| | | ) |
| | | } |
| | | |
| | | override func mouseUp(with event: NSEvent) { |
| | | longPressWorkItem?.cancel() |
| | | if didStartDrag { |
| | | AppDragCoordinator.shared.finishDrag( |
| | | at: screenPoint(for: event), |
| | | copy: event.modifierFlags.contains(.option) |
| | | ) |
| | | coordinator?.onDragModeChange(false) |
| | | } else if !isLongPressActive, let app { |
| | | coordinator?.onSelectApp(app) |
| | | } else { |
| | | coordinator?.onDragModeChange(false) |
| | | } |
| | | didStartDrag = false |
| | | isLongPressActive = false |
| | | mouseDownEvent = nil |
| | | longPressWorkItem = nil |
| | | } |
| | | |
| | | override func rightMouseDown(with event: NSEvent) { |
| | | guard let app else { return } |
| | | coordinator?.onEditNote(app, rootLocalFrame()) |
| | | } |
| | | |
| | | override func menu(for event: NSEvent) -> NSMenu? { |
| | | let menu = NSMenu() |
| | | let item = NSMenuItem(title: tr("appNote.edit"), action: #selector(editNoteFromMenu), keyEquivalent: "") |
| | | item.target = self |
| | | menu.addItem(item) |
| | | return menu |
| | | } |
| | | |
| | | @objc private func editNoteFromMenu() { |
| | | guard let app else { return } |
| | | coordinator?.onEditNote(app, rootLocalFrame()) |
| | | } |
| | | |
| | | private var shouldShowAppBubble: Bool { |
| | | guard let app else { return false } |
| | | return !runtimeState.showUncommonAppBubbles || app.isUncommon |
| | | } |
| | | |
| | | private var shouldShowHoverName: Bool { |
| | | !showName && !shouldShowAppBubble && isHovered |
| | | } |
| | | |
| | | private var dragPayload: String { |
| | | guard let app else { return "" } |
| | | return "\(app.path.path)\n\(sourceTag)" |
| | | } |
| | | |
| | | private func setHover(_ hover: Bool, notify: Bool) { |
| | | guard isHovered != hover else { return } |
| | | isHovered = hover |
| | | needsDisplay = true |
| | | guard notify, let app else { return } |
| | | if hover { |
| | | coordinator?.onBubbleHover(app, rootLocalFrame(), .entered(canShowBubble: shouldShowAppBubble)) |
| | | } else { |
| | | coordinator?.onBubbleHover(app, rootLocalFrame(), .exited) |
| | | } |
| | | } |
| | | |
| | | private func makeDragImage() -> NSImage { |
| | | guard let app else { return NSImage(size: .zero) } |
| | | let scale: CGFloat = AppGridCollectionMetrics.hoverScale * 1.5 |
| | | let imageSize = iconSize * scale |
| | | let padding = iconSize * 0.45 |
| | | let canvasSize = NSSize(width: imageSize + padding * 2, height: imageSize + padding * 2) |
| | | let dragImage = NSImage(size: canvasSize) |
| | | dragImage.lockFocus() |
| | | NSGraphicsContext.current?.imageInterpolation = .high |
| | | let shadow = NSShadow() |
| | | shadow.shadowColor = NSColor.black.withAlphaComponent(0.48) |
| | | shadow.shadowBlurRadius = 24 |
| | | shadow.shadowOffset = NSSize(width: 0, height: -14) |
| | | shadow.set() |
| | | app.icon.draw(in: NSRect(x: padding, y: padding, width: imageSize, height: imageSize)) |
| | | dragImage.unlockFocus() |
| | | return dragImage |
| | | } |
| | | |
| | | private func rootLocalFrame() -> CGRect { |
| | | guard let contentView = window?.contentView else { return .zero } |
| | | let rectInContent = contentView.convert(bounds, from: self) |
| | | let y = contentView.isFlipped |
| | | ? rectInContent.minY |
| | | : contentView.bounds.height - rectInContent.maxY |
| | | return CGRect( |
| | | x: rectInContent.minX, |
| | | y: y, |
| | | width: rectInContent.width, |
| | | height: rectInContent.height |
| | | ) |
| | | } |
| | | |
| | | private func screenPoint(for event: NSEvent) -> NSPoint { |
| | | window?.convertPoint(toScreen: event.locationInWindow) ?? NSEvent.mouseLocation |
| | | } |
| | | } |
| | |
| | | var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil |
| | | var onEditNote: ((AppInfo, CGRect) -> Void)? = nil |
| | | var bubbleDisabled: Bool = false |
| | | var showUncommonAppBubbles: Bool = AppDefaults.showUncommonAppBubbles |
| | | var itemID: String |
| | | var dragResetToken: Int = 0 |
| | | @Binding var hoveredAppItemID: String? |
| | | let onSelect: () -> Void |
| | | |
| | | @State private var interactionFrame: CGRect = .zero |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | @State private var isHovered = false |
| | | |
| | | static let hoverScale: CGFloat = 1.22 |
| | | static let labelHeight: CGFloat = 14 |
| | |
| | | private var iconSlotSize: CGFloat { iconSize * Self.hoverScale } |
| | | private var labelWidth: CGFloat { iconSize + 20 } |
| | | private var labelHeight: CGFloat { Self.labelHeight } |
| | | private var isHovered: Bool { hoveredAppItemID == itemID } |
| | | |
| | | var body: some View { |
| | | VStack(spacing: 6) { |
| | |
| | | icon: app.icon, |
| | | iconSize: iconSize, |
| | | payload: "\(app.path.path)\n\(sourceTag ?? "")", |
| | | resetToken: dragResetToken, |
| | | onLongPress: { onDragModeChange?(true) }, |
| | | onDragEnd: { onDragModeChange?(false) }, |
| | | onClick: onSelect |
| | |
| | | .padding(.horizontal, 4) |
| | | .frame(width: Self.stableWidth(iconSize: iconSize), height: Self.stableHeight(iconSize: iconSize)) |
| | | .background( |
| | | AppGridItemHoverTracker { hovering, frame in |
| | | AppHoverTrackingView { hovering, frame in |
| | | interactionFrame = frame |
| | | if bubbleDisabled || dragModeActive { |
| | | setHoverState(false) |
| | |
| | | } |
| | | .opacity(dragModeActive ? 0.92 : 1) |
| | | .animation(.easeOut(duration: 0.08), value: dragModeActive) |
| | | .id("\(itemID)|reset-\(dragResetToken)") |
| | | .onChange(of: dragModeActive) { _, active in |
| | | if active { |
| | | setHoverState(false) |
| | |
| | | } |
| | | } |
| | | .onDisappear { |
| | | if hoveredAppItemID == itemID { |
| | | hoveredAppItemID = nil |
| | | if isHovered { |
| | | isHovered = false |
| | | onBubbleHover?(app, interactionFrame, .exited) |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | private func setHoverState(_ hovering: Bool) { |
| | | guard isHovered != hovering else { return } |
| | | withAnimation(hovering ? Self.hoverInAnimation : Self.hoverOutAnimation) { |
| | | if hovering { |
| | | hoveredAppItemID = itemID |
| | | } else if hoveredAppItemID == itemID { |
| | | hoveredAppItemID = nil |
| | | } |
| | | isHovered = hovering |
| | | } |
| | | } |
| | | } |
| | |
| | | case exited |
| | | } |
| | | |
| | | private struct AppGridItemHoverTracker: NSViewRepresentable { |
| | | struct AppHoverTrackingView: NSViewRepresentable { |
| | | let onHover: (Bool, CGRect) -> Void |
| | | |
| | | func makeNSView(context: Context) -> HoverTrackingNSView { |
| | | let view = HoverTrackingNSView() |
| | | func makeNSView(context: Context) -> AppHoverTrackingNSView { |
| | | let view = AppHoverTrackingNSView() |
| | | view.onHover = onHover |
| | | return view |
| | | } |
| | | |
| | | func updateNSView(_ view: HoverTrackingNSView, context: Context) { |
| | | func updateNSView(_ view: AppHoverTrackingNSView, context: Context) { |
| | | view.onHover = onHover |
| | | } |
| | | } |
| | | |
| | | private final class HoverTrackingNSView: NSView { |
| | | final class AppHoverTrackingNSView: NSView { |
| | | var onHover: ((Bool, CGRect) -> Void)? |
| | | |
| | | override func updateTrackingAreas() { |
| | |
| | | let icon: NSImage |
| | | let iconSize: CGFloat |
| | | let payload: String |
| | | let resetToken: Int |
| | | let onLongPress: () -> Void |
| | | let onDragEnd: () -> Void |
| | | let onClick: () -> Void |
| | |
| | | view.image = icon |
| | | view.iconSize = iconSize |
| | | view.payload = payload |
| | | view.resetToken = resetToken |
| | | view.onLongPress = onLongPress |
| | | view.onDragEnd = onDragEnd |
| | | view.onClick = onClick |
| | |
| | | view.image = icon |
| | | view.iconSize = iconSize |
| | | view.payload = payload |
| | | if view.resetToken != resetToken { |
| | | view.resetToken = resetToken |
| | | view.resetInteractionState() |
| | | } |
| | | view.onLongPress = onLongPress |
| | | view.onDragEnd = onDragEnd |
| | | view.onClick = onClick |
| | |
| | | var image: NSImage = NSImage() |
| | | var iconSize: CGFloat = 56 |
| | | var payload: String = "" |
| | | var resetToken = 0 |
| | | var onLongPress: (() -> Void)? |
| | | var onDragEnd: (() -> Void)? |
| | | var onClick: (() -> Void)? |
| | |
| | | private var isLongPressActive = false |
| | | private var longPressWorkItem: DispatchWorkItem? |
| | | override var isFlipped: Bool { true } |
| | | |
| | | func resetInteractionState() { |
| | | longPressWorkItem?.cancel() |
| | | longPressWorkItem = nil |
| | | mouseDownEvent = nil |
| | | didStartDrag = false |
| | | isLongPressActive = false |
| | | } |
| | | |
| | | override func draw(_ dirtyRect: NSRect) { |
| | | super.draw(dirtyRect) |
| | |
| | | didStartDrag = false |
| | | isLongPressActive = false |
| | | mouseDownEvent = nil |
| | | longPressWorkItem = nil |
| | | } |
| | | |
| | | private func makeDragImage() -> NSImage { |
| New file |
| | |
| | | import Foundation |
| | | |
| | | enum AppIdentity { |
| | | static let displayName = "TagLauncher" |
| | | static let bundleIdentifier = "com.taglauncher.app" |
| | | static let applicationSupportDirectoryName = "TagLauncher" |
| | | |
| | | static var applicationSupportDirectory: URL { |
| | | FileManager.default.homeDirectoryForCurrentUser |
| | | .appendingPathComponent("Library/Application Support/\(applicationSupportDirectoryName)") |
| | | } |
| | | |
| | | static let statusItemAutosaveName = "\(bundleIdentifier).statusItem" |
| | | static let launchAgentLabel = bundleIdentifier |
| | | static let categorySchemeBatchQueueLabel = "\(bundleIdentifier).category-scheme-batch" |
| | | } |
| | |
| | | |
| | | final class AppDelegate: NSObject, NSApplicationDelegate { |
| | | private static let showDockIconKey = "showDockIcon" |
| | | private static let statusItemAutosaveName = "com.apptag.launcher.statusItem" |
| | | private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName |
| | | private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton") |
| | | private static let statusItemAccessibilityLabel = "TagLauncher" |
| | | private static let statusItemAccessibilityLabel = AppIdentity.displayName |
| | | private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem") |
| | | private static let overlayDefaultLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow))) |
| | | private static let overlayTextInputLevel = NSWindow.Level.modalPanel |
| | | private static let overlayDefaultLevel = NSWindow.Level.normal |
| | | private static let overlayTextInputLevel = NSWindow.Level.normal |
| | | |
| | | private var statusItem: NSStatusItem? |
| | | private var overlayWindow: NSWindow? |
| | |
| | | private var isEditingAppNote = false |
| | | private var isConfiguringApplicationMenu = false |
| | | private var lastShowDockIcon: Bool? |
| | | |
| | | private var isOverlayVisible: Bool { |
| | | overlayWindow?.isVisible == true |
| | | } |
| | | |
| | | private var isSettingsVisible: Bool { |
| | | settingsWindow?.isVisible == true |
| | | } |
| | | |
| | | private var requiresForegroundOwnership: Bool { |
| | | isOverlayVisible || isSettingsVisible |
| | | } |
| | | |
| | | private var currentOverlayLevel: NSWindow.Level { |
| | | if isEditingAppNote || isQuickSearchOpen { |
| | | return Self.overlayTextInputLevel |
| | | } |
| | | return Self.overlayDefaultLevel |
| | | } |
| | | |
| | | private func overlayLevel(initialQuickSearchSource: String? = nil) -> NSWindow.Level { |
| | | if initialQuickSearchSource != nil { |
| | | return Self.overlayTextInputLevel |
| | | } |
| | | return currentOverlayLevel |
| | | } |
| | | |
| | | static func refreshChromeSettings() { |
| | | (NSApp.delegate as? AppDelegate)?.syncChromeSettings(force: true) |
| | |
| | | } |
| | | |
| | | func applicationWillTerminate(_ notification: Notification) { |
| | | hideOverlay(force: true, discardWindow: true) |
| | | unregisterHotkey(for: .main) |
| | | unregisterHotkey(for: .quickSearch) |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | removeOverlayKeyMonitor() |
| | | removeQuickSearchExternalMouseMonitor() |
| | | } |
| | | |
| | |
| | | let dockChanged = lastShowDockIcon != showDock |
| | | |
| | | if force || dockChanged { |
| | | NSApp.setActivationPolicy(showDock ? .regular : .accessory) |
| | | lastShowDockIcon = showDock |
| | | refreshLauncherChromeState() |
| | | } |
| | | |
| | | if force { |
| | | setupMenuBar() |
| | | } |
| | | } |
| | | |
| | | private func beginLauncherForegroundOwnership(activate: Bool = true) { |
| | | if NSApp.activationPolicy() != .regular { |
| | | NSApp.setActivationPolicy(.regular) |
| | | } |
| | | if activate { |
| | | NSApp.activate(ignoringOtherApps: true) |
| | | configureApplicationMenuWhenAvailable(retries: 4) |
| | | } |
| | | } |
| | | |
| | | private func refreshLauncherChromeState(activate: Bool = false) { |
| | | let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey) |
| | | lastShowDockIcon = showDock |
| | | |
| | | let desiredPolicy: NSApplication.ActivationPolicy = requiresForegroundOwnership |
| | | ? .regular |
| | | : (showDock ? .regular : .accessory) |
| | | if NSApp.activationPolicy() != desiredPolicy { |
| | | NSApp.setActivationPolicy(desiredPolicy) |
| | | } |
| | | |
| | | let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : [] |
| | | if NSApp.presentationOptions != desiredPresentation { |
| | | NSApp.presentationOptions = desiredPresentation |
| | | } |
| | | |
| | | if activate && requiresForegroundOwnership { |
| | | NSApp.activate(ignoringOtherApps: true) |
| | | configureApplicationMenuWhenAvailable(retries: 4) |
| | | } |
| | | } |
| | | |
| | | private func handleApplicationDidResignActive() { |
| | | if isQuickSearchOpen { |
| | | NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil) |
| | | } |
| | | |
| | | guard isOverlayVisible else { |
| | | refreshLauncherChromeState() |
| | | return |
| | | } |
| | | |
| | | // Once another app takes the menu bar, don't leave the overlay stranded onscreen. |
| | | hideOverlay(force: true) |
| | | } |
| | | |
| | | /// Keep app chrome in sync when language changes from any entry point. |
| | |
| | | |
| | | // MARK: - Launch at Login (LaunchAgent, zero permissions) |
| | | |
| | | private static let launchAgentLabel = "com.apptag.launcher" |
| | | private static let launchAgentLabel = AppIdentity.launchAgentLabel |
| | | static var supportsLaunchAtLogin: Bool { |
| | | ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] == nil |
| | | } |
| | |
| | | |
| | | @objc private func toggleOverlay() { |
| | | if overlayWindow?.isVisible == true { |
| | | hideOverlay() |
| | | hideOverlay(force: true) |
| | | } else { |
| | | showOverlay() |
| | | } |
| | |
| | | }) ?? NSScreen.main ?? NSScreen.screens.first else { return } |
| | | |
| | | let window: NSWindow |
| | | let createdWindow: Bool |
| | | if let existingWindow = overlayWindow { |
| | | window = existingWindow |
| | | createdWindow = false |
| | | } else { |
| | | window = makeOverlayWindow(on: screen, initialQuickSearchSource: initialQuickSearchSource) |
| | | overlayWindow = window |
| | | createdWindow = true |
| | | } |
| | | |
| | | window.setFrame(screen.frame, display: true) |
| | | window.level = isEditingAppNote ? Self.overlayTextInputLevel : Self.overlayDefaultLevel |
| | | |
| | | installOverlayKeyMonitor() |
| | | beginLauncherForegroundOwnership() |
| | | |
| | | let targetFrame = screen.frame |
| | | let targetLevel = overlayLevel(initialQuickSearchSource: initialQuickSearchSource) |
| | | let canReuseVisibleWindow = !createdWindow |
| | | && window.isVisible |
| | | && NSEqualRects(window.frame, targetFrame) |
| | | && window.level == targetLevel |
| | | |
| | | if canReuseVisibleWindow { |
| | | if !window.isKeyWindow { |
| | | window.makeKeyAndOrderFront(nil) |
| | | } |
| | | refreshLauncherChromeState(activate: true) |
| | | if let initialQuickSearchSource { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherQuickSearchRequested, |
| | | object: nil, |
| | | userInfo: ["source": initialQuickSearchSource] |
| | | ) |
| | | } |
| | | return |
| | | } |
| | | |
| | | window.setFrame(targetFrame, display: true) |
| | | window.level = targetLevel |
| | | |
| | | if let initialQuickSearchSource, !createdWindow { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherQuickSearchRequested, |
| | | object: nil, |
| | |
| | | |
| | | window.makeKeyAndOrderFront(nil) |
| | | window.orderFrontRegardless() |
| | | refreshLauncherChromeState(activate: true) |
| | | NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil) |
| | | } |
| | | |
| | |
| | | NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil) |
| | | return nil |
| | | } |
| | | self.hideOverlay() |
| | | self.hideOverlay(force: true) |
| | | return nil |
| | | } |
| | | if self.shouldOpenQuickSearch(for: event) { |
| | |
| | | private func makeOverlayWindow(on screen: NSScreen, initialQuickSearchSource: String? = nil) -> NSWindow { |
| | | let panel = OverlayPanel( |
| | | contentRect: screen.frame, |
| | | styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel], |
| | | styleMask: [.borderless, .fullSizeContentView], |
| | | backing: .buffered, |
| | | defer: false |
| | | ) |
| | |
| | | return panel |
| | | } |
| | | |
| | | private func hideOverlay(force: Bool = false) { |
| | | private func hideOverlay(force: Bool = false, discardWindow: Bool = false) { |
| | | guard force || !isInEditMode else { return } |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | if let settingsWindow, settingsWindow.parent == overlayWindow { |
| | |
| | | overlayWindow?.orderOut(nil) |
| | | removeOverlayKeyMonitor() |
| | | removeQuickSearchExternalMouseMonitor() |
| | | refreshLauncherChromeState() |
| | | NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil) |
| | | if force { |
| | | if discardWindow { |
| | | overlayWindow = nil |
| | | } |
| | | } |
| | |
| | | window.makeKeyAndOrderFront(nil) |
| | | window.orderFrontRegardless() |
| | | settingsWindow = window |
| | | refreshLauncherChromeState(activate: true) |
| | | } |
| | | |
| | | private func attachSettingsWindow(_ window: NSWindow, to overlayWindow: NSWindow) { |
| | |
| | | else { return } |
| | | self.detachSettingsWindow(closingWindow) |
| | | self.settingsWindow = nil |
| | | self.refreshLauncherChromeState() |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /// Lower the overlay while editing app notes so IME candidate windows are not hidden behind it. |
| | | /// Lower the overlay while text input is active so IME and cursor services are not hidden behind it. |
| | | private func observeAppNoteEditing() { |
| | | NotificationCenter.default.addObserver( |
| | | forName: .tagLauncherAppNoteEditingChanged, |
| | |
| | | ) { [weak self] notification in |
| | | guard let self else { return } |
| | | self.isQuickSearchOpen = (notification.userInfo?["active"] as? Bool) ?? false |
| | | self.updateOverlayLevelForTextInput() |
| | | if self.isQuickSearchOpen { |
| | | self.installQuickSearchExternalMouseMonitor() |
| | | } else { |
| | |
| | | object: NSApp, |
| | | queue: .main |
| | | ) { [weak self] _ in |
| | | guard self?.isQuickSearchOpen == true else { return } |
| | | NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil) |
| | | self?.handleApplicationDidResignActive() |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | private func updateOverlayLevelForTextInput() { |
| | | guard let overlayWindow else { return } |
| | | overlayWindow.level = isEditingAppNote ? Self.overlayTextInputLevel : Self.overlayDefaultLevel |
| | | overlayWindow.level = currentOverlayLevel |
| | | if let settingsWindow, settingsWindow.parent == overlayWindow { |
| | | settingsWindow.level = overlayWindow.level |
| | | } |
| | |
| | | |
| | | @objc private func openPreferences() { |
| | | TagDatabase.flushPendingCategorySchemeBackupBatch() |
| | | beginLauncherForegroundOwnership() |
| | | // Don't hide overlay — keep it visible for real-time setting preview. |
| | | if let overlayWindow, overlayWindow.isVisible { |
| | | overlayWindow.makeKeyAndOrderFront(nil) |
| | | overlayWindow.orderFrontRegardless() |
| | | } |
| | | NSApp.activate(ignoringOtherApps: true) |
| | | |
| | | if let settingsWindow { |
| | | prepareSettingsWindow(settingsWindow) |
| | |
| | | let colorIndex: Int |
| | | var dragModeActive: Bool = false |
| | | var isDragging: Bool = false |
| | | var dragResetToken: Int = 0 |
| | | let action: () -> Void |
| | | |
| | | private var bgColor: Color { |
| | |
| | | .opacity(dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0) |
| | | .animation(.easeOut(duration: 0.08), value: isDragging) |
| | | .animation(.easeOut(duration: 0.08), value: dragModeActive) |
| | | .id("\(name)|reset-\(dragResetToken)") |
| | | .contentShape(RoundedRectangle(cornerRadius: 7)) |
| | | .onTapGesture { |
| | | action() |
| | |
| | | let colorIndex: Int |
| | | var dragModeActive: Bool = false |
| | | var isDragging: Bool = false |
| | | var dragResetToken: Int = 0 |
| | | let action: () -> Void |
| | | |
| | | private var bgColor: Color { |
| | |
| | | .opacity(dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0) |
| | | .animation(.easeOut(duration: 0.08), value: isDragging) |
| | | .animation(.easeOut(duration: 0.08), value: dragModeActive) |
| | | .id("\(name)|reset-\(dragResetToken)") |
| | | .contentShape(RoundedRectangle(cornerRadius: 6)) |
| | | .onTapGesture { |
| | | action() |
| | |
| | | let hideOverlay: () -> Void |
| | | private let initialQuickSearchSource: String? |
| | | |
| | | @Environment(\.colorScheme) private var colorScheme |
| | | @State private var allApps: [AppInfo] = [] |
| | | @State private var displayGroups: [TagGroup] = [] |
| | | @State private var tagColors: [String: Int] = [:] |
| | |
| | | @State private var groupLayoutVersion = 0 |
| | | @State private var cachedGridContainerRowsKey: GridContainerRowsKey? = nil |
| | | @State private var cachedGridContainerRows: [GridContainerLayoutRow] = [] |
| | | @State private var appGridScrollTargetID: String? = nil |
| | | @State private var appGridScrollRequestToken = 0 |
| | | |
| | | // Edit mode |
| | | @State private var editPhase: EditPhase = .none |
| | |
| | | @State private var tagReorderFrames: [String: CGRect] = [:] |
| | | @State private var tagNavDragModeActive = false |
| | | @State private var tagNavDragItem: String? = nil |
| | | @State private var tagNavDragResetToken = 0 |
| | | @State private var tagNavReorderFrames: [String: CGRect] = [:] |
| | | @State private var tagNavReorderDidMove = false |
| | | @State private var hoveredContainer: String? = nil // colored container lift |
| | |
| | | @State private var refreshInProgress = false |
| | | @State private var refreshAgainAfterCurrent = false |
| | | @State private var refreshAgainForceLayout = false |
| | | @State private var hoveredAppItemID: String? = nil |
| | | @State private var hoveredBubble: AppBubbleContext? = nil |
| | | @State private var editingBubble: AppBubbleContext? = nil |
| | | @State private var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil |
| | |
| | | @State private var quickSearchSelectedID: URL? = nil |
| | | @State private var quickSearchManualSelection = false |
| | | @State private var quickSearchFocusToken = 0 |
| | | @State private var quickSearchSelectionScrollToken = 0 |
| | | @State private var quickSearchCloseHidesOverlay = false |
| | | @State private var quickSearchErrorMessage: String? = nil |
| | | @State private var initialQuickSearchConsumed = false |
| | |
| | | @State private var notchHeight: CGFloat = 0 |
| | | @AppStorage("displayMode") private var displayMode = AppDefaults.displayMode |
| | | @AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | |
| | | private let editSidebarWidth: CGFloat = 188 |
| | | private let editSidebarHorizontalInset: CGFloat = 12 |
| | |
| | | appDragModeActive || pendingUncategorizedDrop != nil || scrollInteractionState.isFrozen |
| | | } |
| | | private let rightSidebarFloatingClearance: CGFloat = 44 |
| | | |
| | | private var cardSurfaceColor: Color { |
| | | colorScheme == .dark |
| | | ? Color.white.opacity(0.055) |
| | | : Color.white.opacity(0.62) |
| | | } |
| | | |
| | | private var floatingButtonSurfaceColor: Color { |
| | | colorScheme == .dark |
| | | ? Color.white.opacity(0.10) |
| | | : Color.white.opacity(0.78) |
| | | } |
| | | |
| | | private var isSideLayout: Bool { |
| | | tagPosition == "left" || tagPosition == "right" |
| | |
| | | displayMode == "coloredContainer" || displayMode == "coloredGridContainer" |
| | | } |
| | | |
| | | private var quickSearchOnlyMode: Bool { |
| | | quickSearchVisible && quickSearchCloseHidesOverlay |
| | | private var usesAppKitContainerGrid: Bool { |
| | | displayMode == "gridContainer" || displayMode == "coloredGridContainer" |
| | | } |
| | | |
| | | var body: some View { |
| | | ZStack { |
| | | if !quickSearchOnlyMode { |
| | | if !quickSearchVisible { |
| | | VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(false) |
| | | } |
| | | |
| | | if !quickSearchOnlyMode { |
| | | if !quickSearchVisible { |
| | | if notchHeight > 0 { |
| | | VStack { |
| | | Rectangle().fill(.black) |
| | |
| | | |
| | | quickSearchOverlay |
| | | |
| | | if !quickSearchOnlyMode, let message = dropWarningToast { |
| | | if !quickSearchVisible, let message = dropWarningToast { |
| | | Text(message) |
| | | .font(.system(size: 16, weight: .semibold)) |
| | | .foregroundStyle(.primary) |
| | |
| | | .allowsHitTesting(false) |
| | | } |
| | | |
| | | if !quickSearchOnlyMode && dropRefreshVisible { |
| | | if !quickSearchVisible && dropRefreshVisible { |
| | | Color.black.opacity(0.08) |
| | | .ignoresSafeArea() |
| | | .transition(.opacity) |
| | |
| | | if let initialQuickSearchSource, !initialQuickSearchConsumed { |
| | | initialQuickSearchConsumed = true |
| | | quickSearchCloseHidesOverlay = initialQuickSearchSource == QuickSearchOpenSource.globalHidden |
| | | if !quickSearchVisible { |
| | | quickSearchVisible = true |
| | | quickSearchFocusToken &+= 1 |
| | | } |
| | | refreshQuickSearchResults() |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherQuickSearchVisibilityChanged, |
| | | object: nil, |
| | |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in |
| | | resetTransientDragState() |
| | | refreshNotchHeight() |
| | | if quickSearchVisible { |
| | | refreshAppsIfNeeded() |
| | | } else { |
| | | refreshApps() |
| | | } |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in |
| | | resetTransientDragState() |
| | |
| | | results: quickSearchResults, |
| | | selectedID: quickSearchSelectedID, |
| | | focusToken: quickSearchFocusToken, |
| | | selectionScrollToken: quickSearchSelectionScrollToken, |
| | | isLoading: quickSearchDocuments.isEmpty && refreshInProgress, |
| | | maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size), |
| | | errorMessage: quickSearchErrorMessage, |
| | |
| | | let nextIndex = min(max(currentIndex + delta, 0), quickSearchResults.count - 1) |
| | | quickSearchSelectedID = quickSearchResults[nextIndex].id |
| | | quickSearchManualSelection = true |
| | | quickSearchSelectionScrollToken &+= 1 |
| | | } |
| | | |
| | | private func selectQuickSearchResult(_ result: QuickSearchResult) { |
| | | guard quickSearchSelectedID != result.id || !quickSearchManualSelection else { return } |
| | | quickSearchSelectedID = result.id |
| | | quickSearchManualSelection = true |
| | | } |
| | |
| | | ) -> some View { |
| | | NativeFloatingIconButton(systemImage: systemImage, action: action) |
| | | .frame(width: 36, height: 36) |
| | | .background(Circle().fill(.ultraThinMaterial)) |
| | | .background(Circle().fill(floatingButtonSurfaceColor)) |
| | | } |
| | | |
| | | // MARK: - Top / Side Layouts |
| | |
| | | TagPill(name: tag.name, colorIndex: tag.colorIndex, |
| | | dragModeActive: tagNavDragModeActive && canReorderTag(tag.name), |
| | | isDragging: tagNavDragItem == tag.name, |
| | | dragResetToken: tagNavDragResetToken, |
| | | action: { |
| | | activateTagNavigation(tag.id) |
| | | }) |
| | |
| | | SideTagPill(name: tag.name, colorIndex: tag.colorIndex, |
| | | dragModeActive: tagNavDragModeActive && canReorderTag(tag.name), |
| | | isDragging: tagNavDragItem == tag.name, |
| | | dragResetToken: tagNavDragResetToken, |
| | | action: { |
| | | activateTagNavigation(tag.id) |
| | | }) |
| | |
| | | Spacer() |
| | | ProgressView().scaleEffect(0.8) |
| | | Spacer() |
| | | } else if displayMode == "gridContainer" || displayMode == "coloredGridContainer" { |
| | | gridContainerGrid |
| | | } else if usesAppKitContainerGrid { |
| | | AppGridCollectionView( |
| | | groups: displayGroups, |
| | | tagColors: tagColors, |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: !hideAppNames, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | highlightedGroupName: filledColorlessContainer, |
| | | contentRevision: groupLayoutVersion, |
| | | scrollTargetID: appGridScrollTargetID, |
| | | scrollRequestToken: appGridScrollRequestToken, |
| | | onSelectApp: { app in openApp(app) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | onDropApp: { path, source, target, copy in |
| | | dropApp(path: path, sourceTag: source, targetTag: target, copy: copy) |
| | | }, |
| | | onGroupActivate: { groupName in |
| | | if displayMode == "gridContainer" { |
| | | toggleColorlessFill(groupName) |
| | | } |
| | | }, |
| | | onScrollActivity: handleAppGridScrollActivity, |
| | | onDragModeChange: { setAppDragMode($0) } |
| | | ) |
| | | } else if displayMode == "container" || displayMode == "coloredContainer" { |
| | | containerGrid |
| | | } else { |
| | |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | dragResetToken: appDragResetToken, |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onDropApp: { path, source, copy in |
| | | dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy) |
| | | } |
| | |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | itemID: "\(group.name)|\(app.path.path)", |
| | | dragResetToken: appDragResetToken, |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onSelect: { openApp(app) } |
| | | ) |
| | | } |
| | |
| | | .fill((isColored || isColorlessActive) ? tagColor.opacity(0.30) : Color.clear) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 14) |
| | | .fill(.ultraThinMaterial) |
| | | .fill(cardSurfaceColor) |
| | | ) |
| | | ) |
| | | .overlay( |
| | |
| | | .stroke(Color.primary.opacity(0.08), lineWidth: 1) |
| | | ) |
| | | .overlay { |
| | | if appDragModeActive { |
| | | AppDropTargetView(targetTag: group.name) { path, source, copy in |
| | | dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy) |
| | | } |
| | | .allowsHitTesting(false) |
| | | } |
| | | } |
| | | .shadow(color: .black.opacity((isColored && isHovered) || isColorlessActive ? 0.22 : 0), |
| | | radius: (isColored && isHovered) || isColorlessActive ? 8 : 0, |
| | |
| | | .zIndex(isHovered ? 50 : 0) |
| | | .animation(.easeOut(duration: 0.045), value: isHovered) |
| | | .animation(.easeOut(duration: 0.08), value: isColorlessFilled) |
| | | .onHover { hovering in |
| | | .background(AppHoverTrackingView { hovering, _ in |
| | | guard !scrollInteractionState.isFrozen else { |
| | | if !hovering && hoveredContainer == group.name { |
| | | hoveredContainer = nil |
| | |
| | | if isColored || isColorless { |
| | | hoveredContainer = hovering ? group.name : nil |
| | | } |
| | | } |
| | | }) |
| | | .contentShape(RoundedRectangle(cornerRadius: 14)) |
| | | .onTapGesture { |
| | | if isColorless { |
| | |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | bubbleDisabled: appBubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | itemID: "\(group.name)|\(app.path.path)", |
| | | dragResetToken: appDragResetToken, |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onSelect: { openApp(app) } |
| | | ) |
| | | .frame(width: cellWidth) |
| | |
| | | .fill((isColored || isColorlessGridActive) ? tagColor.opacity(0.30) : Color.clear) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 14) |
| | | .fill(.ultraThinMaterial) |
| | | .fill(cardSurfaceColor) |
| | | ) |
| | | ) |
| | | .overlay( |
| | |
| | | .stroke(Color.primary.opacity(0.08), lineWidth: 1) |
| | | ) |
| | | .overlay { |
| | | if appDragModeActive { |
| | | AppDropTargetView(targetTag: group.name) { path, source, copy in |
| | | dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy) |
| | | } |
| | | .allowsHitTesting(false) |
| | | } |
| | | } |
| | | .shadow(color: .black.opacity((isColored && isHovered) || isColorlessGridActive ? 0.22 : 0), |
| | | radius: (isColored && isHovered) || isColorlessGridActive ? 8 : 0, |
| | |
| | | .zIndex(isHovered ? 50 : 0) |
| | | .animation(.easeOut(duration: 0.045), value: isHovered) |
| | | .animation(.easeOut(duration: 0.08), value: isColorlessFilled) |
| | | .onHover { hovering in |
| | | .background(AppHoverTrackingView { hovering, _ in |
| | | guard !scrollInteractionState.isFrozen else { |
| | | if !hovering && hoveredContainer == group.name { |
| | | hoveredContainer = nil |
| | |
| | | } else if hovering { |
| | | fillColorlessContainer(group.name) |
| | | } |
| | | } |
| | | }) |
| | | .contentShape(RoundedRectangle(cornerRadius: 14)) |
| | | .onTapGesture { |
| | | if isColorlessGrid { |
| | |
| | | |
| | | private func handleAppGridScrollActivity() { |
| | | if !scrollInteractionState.isFrozen { |
| | | hoveredAppItemID = nil |
| | | hoveredBubble = nil |
| | | hoveredContainer = nil |
| | | } |
| | |
| | | } |
| | | |
| | | private func clearAppBubbleState() { |
| | | hoveredAppItemID = nil |
| | | if hoveredBubble != nil { |
| | | hoveredBubble = nil |
| | | } |
| | | if editingBubble != nil { |
| | | notifyAppNoteEditing(active: false) |
| | | } |
| | | editingBubble = nil |
| | | } |
| | | if bubbleNoteFocused { |
| | | bubbleNoteFocused = false |
| | | } |
| | | } |
| | | |
| | | private func notifyAppNoteEditing(active: Bool) { |
| | |
| | | } |
| | | |
| | | func scrollTo(_ id: String) { |
| | | if usesAppKitContainerGrid { |
| | | appGridScrollTargetID = id |
| | | appGridScrollRequestToken &+= 1 |
| | | } else { |
| | | withAnimation(.easeInOut(duration: 0.25)) { scrollProxy?.scrollTo(id, anchor: .top) } |
| | | } |
| | | } |
| | | |
| | | private func activateTagNavigation(_ id: String) { |
| | |
| | | } |
| | | |
| | | private func endTagNavReorder() { |
| | | let hadDragState = tagNavDragModeActive || tagNavDragItem != nil |
| | | guard hadDragState else { return } |
| | | if tagNavDragModeActive && tagNavReorderDidMove { |
| | | TagEditor.reorderTags(draggedTagNames) |
| | | } |
| | | tagNavDragModeActive = false |
| | | tagNavDragItem = nil |
| | | tagNavReorderDidMove = false |
| | | tagNavDragResetToken &+= 1 |
| | | } |
| | | |
| | | private func cancelTagNavReorderVisualState() { |
| | |
| | | tagNavDragModeActive = false |
| | | tagNavDragItem = nil |
| | | tagNavReorderDidMove = false |
| | | tagNavDragResetToken &+= 1 |
| | | } |
| | | |
| | | private func reorderTagNavItem(at location: CGPoint) { |
| | |
| | | } |
| | | |
| | | private func setAppDragMode(_ active: Bool) { |
| | | guard appDragModeActive != active else { return } |
| | | if active { |
| | | endTagNavReorder() |
| | | clearAppBubbleState() |
| | |
| | | appDragModeActive = active |
| | | if active { |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 8) { |
| | | if appDragModeActive { |
| | | if appDragModeActive && !AppDragCoordinator.shared.hasActiveDrag { |
| | | appDragModeActive = false |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | private func resetTransientDragState(keepingPendingUncategorizedDrop: Bool = false) { |
| | | let hadAppDragState = appDragModeActive |
| | | AppDragCoordinator.shared.cancelDrag() |
| | | scrollInteractionState.reset() |
| | | if appDragModeActive { |
| | | appDragModeActive = false |
| | | } |
| | | if hadAppDragState { |
| | | appDragResetToken &+= 1 |
| | | } |
| | | if tagNavDragModeActive { |
| | | tagNavDragModeActive = false |
| | | } |
| | | if tagNavDragItem != nil { |
| | | tagNavDragItem = nil |
| | | } |
| | | if tagNavReorderDidMove { |
| | | tagNavReorderDidMove = false |
| | | tagNavDragResetToken &+= 1 |
| | | } |
| | | if dragItem != nil { |
| | | dragItem = nil |
| | | hoveredAppItemID = nil |
| | | } |
| | | if hoveredContainer != nil { |
| | | hoveredContainer = nil |
| | | } |
| | | clearAppBubbleState() |
| | | if !keepingPendingUncategorizedDrop { |
| | | if !keepingPendingUncategorizedDrop, pendingUncategorizedDrop != nil { |
| | | pendingUncategorizedDrop = nil |
| | | } |
| | | } |
| | |
| | | } |
| | | unfreezeWorkItem?.cancel() |
| | | let workItem = DispatchWorkItem { [weak self] in |
| | | self?.isFrozen = false |
| | | guard let self, self.isFrozen else { return } |
| | | self.isFrozen = false |
| | | } |
| | | unfreezeWorkItem = workItem |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 0.18, execute: workItem) |
| | |
| | | func reset() { |
| | | unfreezeWorkItem?.cancel() |
| | | unfreezeWorkItem = nil |
| | | if isFrozen { |
| | | isFrozen = false |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | func updateNSView(_ view: ScrollActivityNSView, context: Context) { |
| | | context.coordinator.onScroll = onScroll |
| | | view.coordinator = context.coordinator |
| | | view.installObserverIfPossible() |
| | | } |
| | | |
| | | final class Coordinator { |
| | | var onScroll: () -> Void |
| | | private weak var observedClipView: NSClipView? |
| | | private var observer: NSObjectProtocol? |
| | | private var lastReportedBoundsOrigin: NSPoint? |
| | | |
| | | init(onScroll: @escaping () -> Void) { |
| | | self.onScroll = onScroll |
| | |
| | | guard observedClipView !== clipView else { return } |
| | | removeObserver() |
| | | observedClipView = clipView |
| | | lastReportedBoundsOrigin = clipView.bounds.origin |
| | | clipView.postsBoundsChangedNotifications = true |
| | | observer = NotificationCenter.default.addObserver( |
| | | forName: NSView.boundsDidChangeNotification, |
| | | object: clipView, |
| | | queue: .main |
| | | ) { [weak self] _ in |
| | | self?.onScroll() |
| | | ) { [weak self, weak clipView] _ in |
| | | guard let self, |
| | | let clipView, |
| | | self.recordScrollIfNeeded(from: clipView) |
| | | else { return } |
| | | self.onScroll() |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | observer = nil |
| | | observedClipView = nil |
| | | lastReportedBoundsOrigin = nil |
| | | } |
| | | |
| | | private func recordScrollIfNeeded(from clipView: NSClipView) -> Bool { |
| | | let origin = clipView.bounds.origin |
| | | guard let last = lastReportedBoundsOrigin else { |
| | | lastReportedBoundsOrigin = origin |
| | | return false |
| | | } |
| | | let didScroll = abs(origin.x - last.x) > 0.5 |
| | | || abs(origin.y - last.y) > 0.5 |
| | | if didScroll { |
| | | lastReportedBoundsOrigin = origin |
| | | } |
| | | return didScroll |
| | | } |
| | | |
| | | deinit { |
| | |
| | | |
| | | final class ScrollActivityNSView: NSView { |
| | | weak var coordinator: Coordinator? |
| | | private var installScheduled = false |
| | | |
| | | override func viewDidMoveToWindow() { |
| | | super.viewDidMoveToWindow() |
| | |
| | | } |
| | | |
| | | func installObserverIfPossible() { |
| | | guard !installScheduled else { return } |
| | | installScheduled = true |
| | | DispatchQueue.main.async { [weak self] in |
| | | guard let self else { return } |
| | | installScheduled = false |
| | | coordinator?.install(from: self) |
| | | } |
| | | } |
| | |
| | | import Foundation |
| | | import AppKit |
| | | import CoreServices |
| | | |
| | | // MARK: - Data Models |
| | | |
| | |
| | | let path: URL |
| | | let tags: [String] |
| | | let bundleIdentifier: String? |
| | | let localizedNames: [String] |
| | | let icon: NSImage // Pre-loaded during background scan |
| | | var isUncommon: Bool = false |
| | | var note: String? = nil |
| | |
| | | let icon = NSWorkspace.shared.icon(forFile: displayURL.path) |
| | | icon.size = NSSize(width: 96, height: 96) |
| | | |
| | | let bundleId = Bundle(url: displayURL)?.bundleIdentifier |
| | | ?? Bundle(url: resolvedURL)?.bundleIdentifier |
| | | let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL) |
| | | let bundleId = bundle?.bundleIdentifier |
| | | let localizedNames = localizedAppNames( |
| | | for: displayURL, |
| | | bundle: bundle, |
| | | fallbackName: name |
| | | ) |
| | | |
| | | apps.append(AppInfo( |
| | | name: name, |
| | | path: displayURL, |
| | | tags: [], |
| | | bundleIdentifier: bundleId, |
| | | localizedNames: localizedNames, |
| | | icon: icon |
| | | )) |
| | | } |
| | | |
| | | private static func localizedAppNames( |
| | | for appURL: URL, |
| | | bundle: Bundle?, |
| | | fallbackName: String |
| | | ) -> [String] { |
| | | var values: [String?] = [] |
| | | values.append(bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String) |
| | | values.append(bundle?.localizedInfoDictionary?["CFBundleName"] as? String) |
| | | values.append(bundle?.infoDictionary?["CFBundleDisplayName"] as? String) |
| | | values.append(bundle?.infoDictionary?["CFBundleName"] as? String) |
| | | values.append(spotlightDisplayName(for: appURL)) |
| | | values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: "")) |
| | | |
| | | return uniqueLocalizedNames(values, excluding: fallbackName) |
| | | } |
| | | |
| | | private static func spotlightDisplayName(for appURL: URL) -> String? { |
| | | guard let item = MDItemCreate(nil, appURL.path as CFString), |
| | | let value = MDItemCopyAttribute(item, kMDItemDisplayName) as? String |
| | | else { return nil } |
| | | |
| | | let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) |
| | | return trimmed.isEmpty ? nil : trimmed |
| | | } |
| | | |
| | | private static func uniqueLocalizedNames(_ values: [String?], excluding excludedValue: String) -> [String] { |
| | | let normalizedExcluded = normalizedLocalizedName(excludedValue) |
| | | var seen = Set<String>() |
| | | var result: [String] = [] |
| | | |
| | | for value in values { |
| | | guard let value else { continue } |
| | | let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) |
| | | let normalized = normalizedLocalizedName(trimmed) |
| | | guard !trimmed.isEmpty, |
| | | normalized != normalizedExcluded, |
| | | seen.insert(normalized).inserted |
| | | else { continue } |
| | | result.append(trimmed) |
| | | } |
| | | return result |
| | | } |
| | | |
| | | private static func normalizedLocalizedName(_ value: String) -> String { |
| | | value |
| | | .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current) |
| | | .lowercased() |
| | | .trimmingCharacters(in: .whitespacesAndNewlines) |
| | | } |
| | | |
| | | private static func isNestedInsideAppBundle(_ url: URL) -> Bool { |
| | |
| | | case manual |
| | | } |
| | | |
| | | enum AppNoteOrigin: String, Codable { |
| | | case catalogDefault |
| | | case appleDefault |
| | | case manual |
| | | } |
| | | |
| | | struct AppNoteMetadata: Codable, Equatable { |
| | | var origin: AppNoteOrigin |
| | | var catalog: SmartDefaultNoteProvenance? = nil |
| | | var noteFingerprint: String |
| | | } |
| | | |
| | | // MARK: Storage types |
| | | |
| | | struct TagDef: Codable, Equatable { |
| | |
| | | var appLastOpenedAt: [String: Date] = [:] // successful launches opened from TagLauncher |
| | | var knownAppPaths: [String] = [] // baseline set to detect newly installed apps |
| | | var appNotes: [String: String] = [:] // path → user note; retained even if marker is removed |
| | | var appNoteMetadata: [String: AppNoteMetadata] = [:] // path → note source and edit protection |
| | | var disabledSystemCategoryIDs: [SmartCategoryID] = [] // system categories the user deleted |
| | | var smartStart: SmartStartState = SmartStartState() |
| | | var categoryScheme: CategorySchemeState = CategorySchemeState() |
| | |
| | | case appLastOpenedAt |
| | | case knownAppPaths |
| | | case appNotes |
| | | case appNoteMetadata |
| | | case disabledSystemCategoryIDs |
| | | case smartStart |
| | | case categoryScheme |
| | |
| | | appLastOpenedAt = try container.decodeIfPresent([String: Date].self, forKey: .appLastOpenedAt) ?? [:] |
| | | knownAppPaths = try container.decodeIfPresent([String].self, forKey: .knownAppPaths) ?? [] |
| | | appNotes = try container.decodeIfPresent([String: String].self, forKey: .appNotes) ?? [:] |
| | | appNoteMetadata = try container.decodeIfPresent( |
| | | [String: AppNoteMetadata].self, |
| | | forKey: .appNoteMetadata |
| | | ) ?? [:] |
| | | disabledSystemCategoryIDs = try container.decodeIfPresent( |
| | | [SmartCategoryID].self, |
| | | forKey: .disabledSystemCategoryIDs |
| | |
| | | // MARK: Paths |
| | | |
| | | private static var storeDir: URL { |
| | | let dir = FileManager.default.homeDirectoryForCurrentUser |
| | | .appendingPathComponent("Library/Application Support/Apptag") |
| | | let dir = AppIdentity.applicationSupportDirectory |
| | | try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) |
| | | return dir |
| | | } |
| | |
| | | return dir |
| | | } |
| | | |
| | | private static var legacyStoreURL: URL? { |
| | | guard ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil else { |
| | | return nil |
| | | } |
| | | |
| | | let home = FileManager.default.homeDirectoryForCurrentUser |
| | | let bundleID = Bundle.main.bundleIdentifier ?? "com.apptag.launcher" |
| | | let marker = "/Library/Containers/\(bundleID)/Data" |
| | | guard let range = home.path.range(of: marker) else { return nil } |
| | | |
| | | let realHomePath = String(home.path[..<range.lowerBound]) |
| | | return URL(fileURLWithPath: realHomePath) |
| | | .appendingPathComponent("Library/Application Support/Apptag/tags.json") |
| | | } |
| | | |
| | | // MARK: Load / Save |
| | | |
| | | static func load() -> Store { |
| | | migrateLegacyStoreIfNeeded() |
| | | guard let data = try? Data(contentsOf: storeURL), |
| | | let store = try? JSONDecoder().decode(Store.self, from: data) |
| | | else { return Store() } |
| | |
| | | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] |
| | | guard let data = try? encoder.encode(store) else { return } |
| | | try? data.write(to: storeURL, options: .atomic) |
| | | } |
| | | |
| | | static func noteFingerprint(_ value: String) -> String { |
| | | let normalized = String(value.trimmingCharacters(in: .whitespacesAndNewlines).prefix(maxAppNoteLength)) |
| | | var hash: UInt64 = 0xcbf29ce484222325 |
| | | for byte in normalized.utf8 { |
| | | hash ^= UInt64(byte) |
| | | hash &*= 0x100000001b3 |
| | | } |
| | | return String(format: "%016llx", hash) |
| | | } |
| | | |
| | | static func backup(_ store: Store, reason: String, in directory: URL? = nil) -> URL? { |
| | |
| | | |
| | | private static let categorySchemeBatchDebounceSeconds: TimeInterval = 90 |
| | | private static let categorySchemeRetentionSeconds: TimeInterval = 30 * 24 * 60 * 60 |
| | | private static let categorySchemeBatchQueue = DispatchQueue(label: "com.apptag.category-scheme-batch") |
| | | private static let categorySchemeBatchQueue = DispatchQueue(label: AppIdentity.categorySchemeBatchQueueLabel) |
| | | private static var categorySchemeBatchActive = false |
| | | private static var categorySchemeBatchResetWorkItem: DispatchWorkItem? |
| | | |
| | |
| | | return true |
| | | } |
| | | |
| | | /// App Store sandbox builds read Application Support inside the app container. |
| | | /// Older non-sandbox builds stored the same database directly under ~/Library. |
| | | /// On first sandbox launch, migrate the richer legacy database before seeding defaults. |
| | | static func migrateLegacyStoreIfNeeded() { |
| | | let fm = FileManager.default |
| | | guard let legacyURL = legacyStoreURL, |
| | | fm.fileExists(atPath: legacyURL.path) |
| | | else { return } |
| | | |
| | | guard let legacyData = try? Data(contentsOf: legacyURL), |
| | | let legacyStore = try? JSONDecoder().decode(Store.self, from: legacyData) |
| | | else { return } |
| | | |
| | | if let currentData = try? Data(contentsOf: storeURL), |
| | | let currentStore = try? JSONDecoder().decode(Store.self, from: currentData), |
| | | storeScore(currentStore) >= storeScore(legacyStore) { |
| | | return |
| | | } |
| | | |
| | | try? fm.createDirectory(at: storeDir, withIntermediateDirectories: true) |
| | | try? legacyData.write(to: storeURL, options: .atomic) |
| | | } |
| | | |
| | | private static func storeScore(_ store: Store) -> Int { |
| | | store.appTags.count * 100 + store.tagOrder.count * 10 + store.tags.count |
| | | } |
| | | |
| | | // MARK: Localization |
| | | |
| | | @discardableResult |
| | |
| | | // MARK: Export / Import |
| | | |
| | | static func exportTo(_ url: URL) throws { |
| | | migrateLegacyStoreIfNeeded() |
| | | let store = loadWithEnsuredCategoryScheme() |
| | | let data = try JSONEncoder().encode(store) |
| | | try data.write(to: url, options: .atomic) |
| | |
| | | /// Seed starter system tags on first launch. Smart Start adds any additional |
| | | /// system tags it needs after scanning the user's installed apps. |
| | | static func seedDefaultTags() { |
| | | migrateLegacyStoreIfNeeded() |
| | | guard !FileManager.default.fileExists(atPath: storeURL.path) else { return } |
| | | |
| | | let starterCategoryIDs: [SmartCategoryID] = [ |
| | | .design, |
| | | .development, |
| | | .uiPrototyping, |
| | | .ide, |
| | | .writing, |
| | | .game, |
| | | .entertainment, |
| | | .system, |
| | | .productivity |
| | | .gtd |
| | | ] |
| | | |
| | | var store = Store() |
| | |
| | | let appTags = store.appTags[app.path.path] ?? [] |
| | | return AppInfo( |
| | | name: app.name, path: app.path, tags: appTags, |
| | | bundleIdentifier: app.bundleIdentifier, icon: app.icon, |
| | | bundleIdentifier: app.bundleIdentifier, |
| | | localizedNames: app.localizedNames, |
| | | icon: app.icon, |
| | | isUncommon: uncommonPaths.contains(app.path.path), |
| | | note: store.appNotes[app.path.path] |
| | | ) |
| | |
| | | } |
| | | |
| | | guard let note = AppleDefaultAppNotes.note(for: app) else { continue } |
| | | store.appNotes[path] = String(note.prefix(TagDatabase.maxAppNoteLength)) |
| | | let limited = String(note.prefix(TagDatabase.maxAppNoteLength)) |
| | | store.appNotes[path] = limited |
| | | store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata( |
| | | origin: .appleDefault, |
| | | noteFingerprint: TagDatabase.noteFingerprint(limited) |
| | | ) |
| | | changed = true |
| | | } |
| | | |
| | |
| | | let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength)) |
| | | if limited.isEmpty { |
| | | store.appNotes.removeValue(forKey: path) |
| | | store.appNoteMetadata.removeValue(forKey: path) |
| | | } else { |
| | | store.appNotes[path] = limited |
| | | store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata( |
| | | origin: .manual, |
| | | noteFingerprint: TagDatabase.noteFingerprint(limited) |
| | | ) |
| | | var uncommonPaths = Set(store.uncommonAppPaths) |
| | | if uncommonPaths.insert(path).inserted { |
| | | store.uncommonAppPaths = uncommonPaths.sorted() |
| | |
| | | <key>CFBundleIconFile</key> |
| | | <string>AppIcon</string> |
| | | <key>CFBundleIdentifier</key> |
| | | <string>com.apptag.launcher</string> |
| | | <string>com.taglauncher.app</string> |
| | | <key>CFBundleInfoDictionaryVersion</key> |
| | | <string>7.0</string> |
| | | <key>CFBundleName</key> |
| | |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>7.6.0</string> |
| | | <key>CFBundleVersion</key> |
| | | <string>20260520.1250</string> |
| | | <string>20260520.1452</string> |
| | | <key>LSApplicationCategoryType</key> |
| | | <string>public.app-category.utilities</string> |
| | | <key>LSMinimumSystemVersion</key> |
| | | <string>15.0</string> |
| | | <key>NSHighResolutionCapable</key> |
| | |
| | | "edit.feedbackAddFormat": "هالعملية اضافت %tagCount% من الوسوم على %appCount% تطبيقات (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "هالعملية شالت %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم اللي انشالت: %tagNames%.", |
| | | "edit.removeHint": "هنا ما تقدر تشيل الصح الا من الوسوم الموجودة من قبل. اي وسم يطلع معه رمز حذف احمر بينشال بعد التاكيد.", |
| | | "smart.category.browser": "المتصفحات", |
| | | "smart.category.communication": "التواصل", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "الإنتاجية", |
| | | "smart.category.file-management": "إدارة الملفات", |
| | | "smart.category.transfer": "الرفع والتنزيل", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "البرمجة", |
| | | "smart.category.design": "التصميم", |
| | | "smart.category.writing": "الكتابة", |
| | | "smart.category.media": "الوسائط", |
| | | "smart.category.video": "الفيديو", |
| | | "smart.category.audio": "الصوت", |
| | | "smart.category.picture-photo": "الصور", |
| | | "smart.category.utilities": "الأدوات", |
| | | "smart.category.system": "تطبيقات النظام", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "تعزيز النظام", |
| | | "smart.category.entertainment": "الترفيه", |
| | | "smart.category.game": "الألعاب", |
| | | "smart.category.finance": "المال", |
| | | "smart.category.education": "التعلّم", |
| | | "smart.category.ai-tools": "أدوات الذكاء الاصطناعي", |
| | | "smart.category.security": "الأمان", |
| | | "smart.category.other": "أخرى", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start رتّب تطبيقاتك", |
| | | "smartstart.auto.messageFormat": "ترتّبت %appCount% تطبيقات مع %tagCount% تعيينات وسوم.", |
| | | "smartstart.suggestion.title": "اقتراحات Smart Start جاهزة", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "اضافت هذه العملية %tagCount% من الوسوم عبر %appCount% تطبيقات (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "ازالت هذه العملية %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم التي تمت ازالتها: %tagNames%.", |
| | | "edit.removeHint": "هنا يمكنك الغاء تحديد الوسوم الموجودة بالفعل فقط. الوسوم التي يظهر بجانبها رمز حذف احمر ستزال بعد التاكيد.", |
| | | "smart.category.browser": "المتصفحات", |
| | | "smart.category.communication": "التواصل", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "الإنتاجية", |
| | | "smart.category.file-management": "إدارة الملفات", |
| | | "smart.category.transfer": "الرفع والتنزيل", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "البرمجة", |
| | | "smart.category.design": "التصميم", |
| | | "smart.category.writing": "الكتابة", |
| | | "smart.category.media": "الوسائط", |
| | | "smart.category.video": "الفيديو", |
| | | "smart.category.audio": "الصوت", |
| | | "smart.category.picture-photo": "الصور والفوتوغرافيا", |
| | | "smart.category.utilities": "الأدوات", |
| | | "smart.category.system": "تطبيقات النظام", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "تعزيز النظام", |
| | | "smart.category.entertainment": "الترفيه", |
| | | "smart.category.game": "الألعاب", |
| | | "smart.category.finance": "المال", |
| | | "smart.category.education": "التعلّم", |
| | | "smart.category.ai-tools": "أدوات الذكاء الاصطناعي", |
| | | "smart.category.security": "الأمان", |
| | | "smart.category.other": "أخرى", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "نظّم Smart Start تطبيقاتك", |
| | | "smartstart.auto.messageFormat": "تم تنظيم %appCount% تطبيقًا مع %tagCount% تعيينات وسوم.", |
| | | "smartstart.suggestion.title": "اقتراحات Smart Start جاهزة", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Tato akce pridala %tagCount% stitku do %appCount% aplikaci (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Tato akce odebrala %tagCount% stitku z %appCount% aplikaci (%appNames%). Odebrane stitky: %tagNames%.", |
| | | "edit.removeHint": "Zde lze odskrtnout jen jiz existujici stitky. Stitky s cervenou ikonou odstraneni budou po potvrzeni odebrany.", |
| | | "smart.category.browser": "Prohlížeče", |
| | | "smart.category.communication": "Komunikace", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivita", |
| | | "smart.category.file-management": "Správa souborů", |
| | | "smart.category.transfer": "Nahrávání a stahování", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programování", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Psaní", |
| | | "smart.category.media": "Média", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Obrázky a fotky", |
| | | "smart.category.utilities": "Nástroje", |
| | | "smart.category.system": "Systémové aplikace", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Vylepšení systému", |
| | | "smart.category.entertainment": "Zábava", |
| | | "smart.category.game": "Hry", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Vzdělávání", |
| | | "smart.category.ai-tools": "Nástroje AI", |
| | | "smart.category.security": "Zabezpečení", |
| | | "smart.category.other": "Ostatní", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start uspořádal aplikace", |
| | | "smartstart.auto.messageFormat": "Uspořádáno %appCount% aplikací pomocí %tagCount% přiřazení tagů.", |
| | | "smartstart.suggestion.title": "Návrhy Smart Start jsou připravené", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Denne handling tilfojede %tagCount% tags til %appCount% apps (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Denne handling fjernede %tagCount% tags fra %appCount% apps (%appNames%). Fjernede tags: %tagNames%.", |
| | | "edit.removeHint": "Her kan kun eksisterende tags fjernes fra markeringen. Tags med et rodt sletteikon fjernes efter bekraeftelse.", |
| | | "smart.category.browser": "Browsere", |
| | | "smart.category.communication": "Kommunikation", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivitet", |
| | | "smart.category.file-management": "Filhåndtering", |
| | | "smart.category.transfer": "Uploads og downloads", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmering", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Skrivning", |
| | | "smart.category.media": "Medier", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Lyd", |
| | | "smart.category.picture-photo": "Billeder og fotos", |
| | | "smart.category.utilities": "Værktøjer", |
| | | "smart.category.system": "Systemapps", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systemforbedringer", |
| | | "smart.category.entertainment": "Underholdning", |
| | | "smart.category.game": "Spil", |
| | | "smart.category.finance": "Finans", |
| | | "smart.category.education": "Uddannelse", |
| | | "smart.category.ai-tools": "AI-værktøjer", |
| | | "smart.category.security": "Sikkerhed", |
| | | "smart.category.other": "Andet", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start organiserede dine apps", |
| | | "smartstart.auto.messageFormat": "Organiserede %appCount% apps med %tagCount% tagtildelinger.", |
| | | "smartstart.suggestion.title": "Smart Start-forslag er klar", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Dieser Vorgang hat %tagCount% Tags zu %appCount% Apps hinzugefugt (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Dieser Vorgang hat %tagCount% Tags von %appCount% Apps entfernt (%appNames%). Entfernte Tags: %tagNames%.", |
| | | "edit.removeHint": "Hier konnen nur bereits vorhandene Tags abgewahlt werden. Tags mit rotem Loschsymbol werden nach der Bestatigung entfernt.", |
| | | "smart.category.browser": "Browser", |
| | | "smart.category.communication": "Kommunikation", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivität", |
| | | "smart.category.file-management": "Dateiverwaltung", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmierung", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Schreiben", |
| | | "smart.category.media": "Medien", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Bilder & Fotos", |
| | | "smart.category.utilities": "Dienstprogramme", |
| | | "smart.category.system": "System-Apps", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systemerweiterungen", |
| | | "smart.category.entertainment": "Unterhaltung", |
| | | "smart.category.game": "Spiele", |
| | | "smart.category.finance": "Finanzen", |
| | | "smart.category.education": "Lernen", |
| | | "smart.category.ai-tools": "KI-Tools", |
| | | "smart.category.security": "Sicherheit", |
| | | "smart.category.other": "Sonstiges", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start hat deine Apps sortiert", |
| | | "smartstart.auto.messageFormat": "%appCount% Apps mit %tagCount% Tag-Zuweisungen sortiert.", |
| | | "smartstart.suggestion.title": "Smart-Start-Vorschläge sind bereit", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Esta operacion agrego %tagCount% etiquetas a %appCount% apps (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Esta operacion elimino %tagCount% etiquetas de %appCount% apps (%appNames%). Etiquetas eliminadas: %tagNames%.", |
| | | "edit.removeHint": "Aqui solo se pueden desmarcar etiquetas ya existentes. Las etiquetas con icono rojo de eliminacion se quitaran tras confirmar.", |
| | | "smart.category.browser": "Navegadores", |
| | | "smart.category.communication": "Comunicación", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Productividad", |
| | | "smart.category.file-management": "Gestión de archivos", |
| | | "smart.category.transfer": "Subidas y descargas", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programación", |
| | | "smart.category.design": "Diseño", |
| | | "smart.category.writing": "Escritura", |
| | | "smart.category.media": "Medios", |
| | | "smart.category.video": "Vídeo", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Imágenes y fotos", |
| | | "smart.category.utilities": "Utilidades", |
| | | "smart.category.system": "Apps del sistema", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Mejoras del sistema", |
| | | "smart.category.entertainment": "Entretenimiento", |
| | | "smart.category.game": "Juegos", |
| | | "smart.category.finance": "Finanzas", |
| | | "smart.category.education": "Educación", |
| | | "smart.category.ai-tools": "Herramientas de IA", |
| | | "smart.category.security": "Seguridad", |
| | | "smart.category.other": "Otros", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start organizó tus apps", |
| | | "smartstart.auto.messageFormat": "Se organizaron %appCount% apps con %tagCount% asignaciones de etiquetas.", |
| | | "smartstart.suggestion.title": "Sugerencias de Smart Start listas", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Cette operation a ajoute %tagCount% etiquettes sur %appCount% apps (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Cette operation a supprime %tagCount% etiquettes sur %appCount% apps (%appNames%). Etiquettes retirees : %tagNames%.", |
| | | "edit.removeHint": "Ici, seuls les tags existants peuvent etre decoches. Les tags affichant une icone rouge de suppression seront retires apres confirmation.", |
| | | "smart.category.browser": "Navigateurs", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Productivité", |
| | | "smart.category.file-management": "Gestion des fichiers", |
| | | "smart.category.transfer": "Téléversements et téléchargements", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmation", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Écriture", |
| | | "smart.category.media": "Média", |
| | | "smart.category.video": "Vidéo", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Images et photos", |
| | | "smart.category.utilities": "Utilitaires", |
| | | "smart.category.system": "Apps système", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Améliorations système", |
| | | "smart.category.entertainment": "Divertissement", |
| | | "smart.category.game": "Jeux", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Éducation", |
| | | "smart.category.ai-tools": "Outils IA", |
| | | "smart.category.security": "Sécurité", |
| | | "smart.category.other": "Autre", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start a organisé vos apps", |
| | | "smartstart.auto.messageFormat": "%appCount% apps organisées avec %tagCount% attributions de tags.", |
| | | "smartstart.suggestion.title": "Suggestions Smart Start prêtes", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Operasi ini menambahkan %tagCount% tag ke %appCount% app (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Operasi ini menghapus %tagCount% tag dari %appCount% app (%appNames%). Tag yang dihapus: %tagNames%.", |
| | | "edit.removeHint": "Di sini hanya tag yang sudah ada yang bisa dihapus centangnya. Tag dengan ikon hapus merah akan dihapus setelah konfirmasi.", |
| | | "smart.category.browser": "Browser", |
| | | "smart.category.communication": "Komunikasi", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivitas", |
| | | "smart.category.file-management": "Manajemen file", |
| | | "smart.category.transfer": "Unggah dan unduh", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Pemrograman", |
| | | "smart.category.design": "Desain", |
| | | "smart.category.writing": "Menulis", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Gambar dan foto", |
| | | "smart.category.utilities": "Utilitas", |
| | | "smart.category.system": "App sistem", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Peningkatan sistem", |
| | | "smart.category.entertainment": "Hiburan", |
| | | "smart.category.game": "Game", |
| | | "smart.category.finance": "Keuangan", |
| | | "smart.category.education": "Pendidikan", |
| | | "smart.category.ai-tools": "Alat AI", |
| | | "smart.category.security": "Keamanan", |
| | | "smart.category.other": "Lainnya", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start mengatur app Anda", |
| | | "smartstart.auto.messageFormat": "%appCount% app diatur dengan %tagCount% penetapan tag.", |
| | | "smartstart.suggestion.title": "Saran Smart Start siap", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Questa operazione ha aggiunto %tagCount% tag a %appCount% app (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Questa operazione ha rimosso %tagCount% tag da %appCount% app (%appNames%). Tag rimossi: %tagNames%.", |
| | | "edit.removeHint": "Qui puoi deselezionare solo i tag gia esistenti. I tag che mostrano un'icona rossa di eliminazione verranno rimossi dopo la conferma.", |
| | | "smart.category.browser": "Browser", |
| | | "smart.category.communication": "Comunicazione", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produttività", |
| | | "smart.category.file-management": "Gestione file", |
| | | "smart.category.transfer": "Caricamenti e download", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmazione", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Scrittura", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Immagini e foto", |
| | | "smart.category.utilities": "Utility", |
| | | "smart.category.system": "App di sistema", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Potenziamenti del sistema", |
| | | "smart.category.entertainment": "Intrattenimento", |
| | | "smart.category.game": "Giochi", |
| | | "smart.category.finance": "Finanza", |
| | | "smart.category.education": "Formazione", |
| | | "smart.category.ai-tools": "Strumenti IA", |
| | | "smart.category.security": "Sicurezza", |
| | | "smart.category.other": "Altro", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start ha organizzato le tue app", |
| | | "smartstart.auto.messageFormat": "Organizzate %appCount% app con %tagCount% assegnazioni di tag.", |
| | | "smartstart.suggestion.title": "Suggerimenti Smart Start pronti", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)に%tagCount%個のタグを追加しました。", |
| | | "edit.feedbackRemoveFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)から%tagCount%個のタグを削除しました。削除したタグ: %tagNames%。", |
| | | "edit.removeHint": "ここでは既存のタグだけチェックを外せます。赤い削除アイコンが表示されたタグは、確認後に削除されます。", |
| | | "smart.category.browser": "ブラウザ", |
| | | "smart.category.communication": "コミュニケーション", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "生産性", |
| | | "smart.category.file-management": "ファイル管理", |
| | | "smart.category.transfer": "アップロードとダウンロード", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "プログラミング", |
| | | "smart.category.design": "デザイン", |
| | | "smart.category.writing": "執筆", |
| | | "smart.category.media": "メディア", |
| | | "smart.category.video": "動画", |
| | | "smart.category.audio": "音声", |
| | | "smart.category.picture-photo": "画像と写真", |
| | | "smart.category.utilities": "ユーティリティ", |
| | | "smart.category.system": "システムアプリ", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "システム強化", |
| | | "smart.category.entertainment": "エンタメ", |
| | | "smart.category.game": "ゲーム", |
| | | "smart.category.finance": "ファイナンス", |
| | | "smart.category.education": "学習", |
| | | "smart.category.ai-tools": "AIツール", |
| | | "smart.category.security": "セキュリティ", |
| | | "smart.category.other": "その他", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start がアプリを整理しました", |
| | | "smartstart.auto.messageFormat": "%appCount% 個のアプリに %tagCount% 件のタグを割り当てました。", |
| | | "smartstart.suggestion.title": "Smart Start の提案があります", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에 태그 %tagCount%개가 새로 추가되었습니다.", |
| | | "edit.feedbackRemoveFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에서 태그 %tagCount%개가 제거되었습니다. 제거된 태그: %tagNames%.", |
| | | "edit.removeHint": "여기서는 이미 있는 태그만 체크 해제할 수 있습니다. 빨간 삭제 아이콘이 보이는 태그는 확인 후 제거됩니다.", |
| | | "smart.category.browser": "브라우저", |
| | | "smart.category.communication": "커뮤니케이션", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "생산성", |
| | | "smart.category.file-management": "파일 관리", |
| | | "smart.category.transfer": "업로드 및 다운로드", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "프로그래밍", |
| | | "smart.category.design": "디자인", |
| | | "smart.category.writing": "글쓰기", |
| | | "smart.category.media": "미디어", |
| | | "smart.category.video": "비디오", |
| | | "smart.category.audio": "오디오", |
| | | "smart.category.picture-photo": "이미지 및 사진", |
| | | "smart.category.utilities": "유틸리티", |
| | | "smart.category.system": "시스템 앱", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "시스템 강화", |
| | | "smart.category.entertainment": "엔터테인먼트", |
| | | "smart.category.game": "게임", |
| | | "smart.category.finance": "금융", |
| | | "smart.category.education": "교육", |
| | | "smart.category.ai-tools": "AI 도구", |
| | | "smart.category.security": "보안", |
| | | "smart.category.other": "기타", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start가 앱을 정리했습니다", |
| | | "smartstart.auto.messageFormat": "%appCount%개 앱에 %tagCount%개 태그를 지정했습니다.", |
| | | "smartstart.suggestion.title": "Smart Start 제안 준비 완료", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Operasi ini menambah %tagCount% tag pada %appCount% app (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Operasi ini membuang %tagCount% tag daripada %appCount% app (%appNames%). Tag yang dibuang: %tagNames%.", |
| | | "edit.removeHint": "Di sini hanya tag yang sedia ada boleh dinyah tanda. Tag yang memaparkan ikon padam merah akan dibuang selepas pengesahan.", |
| | | "smart.category.browser": "Pelayar", |
| | | "smart.category.communication": "Komunikasi", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktiviti", |
| | | "smart.category.file-management": "Pengurusan fail", |
| | | "smart.category.transfer": "Muat naik dan muat turun", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Pengaturcaraan", |
| | | "smart.category.design": "Reka bentuk", |
| | | "smart.category.writing": "Penulisan", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Imej dan foto", |
| | | "smart.category.utilities": "Utiliti", |
| | | "smart.category.system": "App sistem", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Peningkatan sistem", |
| | | "smart.category.entertainment": "Hiburan", |
| | | "smart.category.game": "Permainan", |
| | | "smart.category.finance": "Kewangan", |
| | | "smart.category.education": "Pendidikan", |
| | | "smart.category.ai-tools": "Alat AI", |
| | | "smart.category.security": "Keselamatan", |
| | | "smart.category.other": "Lain-lain", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start menyusun app anda", |
| | | "smartstart.auto.messageFormat": "Menyusun %appCount% app dengan %tagCount% penugasan tag.", |
| | | "smartstart.suggestion.title": "Cadangan Smart Start sedia", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Denne handlingen la til %tagCount% tagger i %appCount% apper (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Denne handlingen fjernet %tagCount% tagger fra %appCount% apper (%appNames%). Fjernede tagger: %tagNames%.", |
| | | "edit.removeHint": "Her kan bare eksisterende tagger fjernes fra avkrysningen. Tagger med rodt sletteikon blir fjernet etter bekreftelse.", |
| | | "smart.category.browser": "Nettlesere", |
| | | "smart.category.communication": "Kommunikasjon", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivitet", |
| | | "smart.category.file-management": "Filbehandling", |
| | | "smart.category.transfer": "Opplastinger og nedlastinger", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmering", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Skriving", |
| | | "smart.category.media": "Medier", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Lyd", |
| | | "smart.category.picture-photo": "Bilder og foto", |
| | | "smart.category.utilities": "Verktøy", |
| | | "smart.category.system": "Systemapper", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systemforbedringer", |
| | | "smart.category.entertainment": "Underholdning", |
| | | "smart.category.game": "Spill", |
| | | "smart.category.finance": "Finans", |
| | | "smart.category.education": "Utdanning", |
| | | "smart.category.ai-tools": "AI-verktøy", |
| | | "smart.category.security": "Sikkerhet", |
| | | "smart.category.other": "Annet", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start organiserade dina appar", |
| | | "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.", |
| | | "smartstart.suggestion.title": "Smart Start-förslag är klara", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackRemoveFormat": "Deze actie heeft %tagCount% tags verwijderd van %appCount% apps (%appNames%). Verwijderde tags: %tagNames%.", |
| | | "edit.removeHint": "Hier kun je alleen bestaande tags uitvinken. Tags met een rood verwijderpictogram worden na bevestiging verwijderd.", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communicatie", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Productiviteit", |
| | | "smart.category.file-management": "Bestandsbeheer", |
| | | "smart.category.transfer": "Uploads en downloads", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmeren", |
| | | "smart.category.design": "Ontwerp", |
| | | "smart.category.writing": "Schrijven", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Afbeeldingen en foto’s", |
| | | "smart.category.utilities": "Hulpprogramma’s", |
| | | "smart.category.system": "Systeemapps", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systeemuitbreidingen", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Financiën", |
| | | "smart.category.education": "Onderwijs", |
| | | "smart.category.ai-tools": "AI-tools", |
| | | "smart.category.security": "Beveiliging", |
| | | "smart.category.other": "Overig", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start heeft je apps georganiseerd", |
| | | "smartstart.auto.messageFormat": "%appCount% apps georganiseerd met %tagCount% tagtoewijzingen.", |
| | | "smartstart.suggestion.title": "Smart Start-suggesties zijn klaar", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Denne handlinga la til %tagCount% taggar i %appCount% appar (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Denne handlinga fjerna %tagCount% taggar fra %appCount% appar (%appNames%). Fjerna taggar: %tagNames%.", |
| | | "edit.removeHint": "Her kan berre eksisterande taggar fjernast frå avkryssinga. Taggar med raudt sletteikon blir fjerna etter stadfesting.", |
| | | "smart.category.browser": "Nettlesere", |
| | | "smart.category.communication": "Kommunikasjon", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivitet", |
| | | "smart.category.file-management": "Filbehandling", |
| | | "smart.category.transfer": "Opplastinger og nedlastinger", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmering", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Skriving", |
| | | "smart.category.media": "Medier", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Lyd", |
| | | "smart.category.picture-photo": "Bilder og foto", |
| | | "smart.category.utilities": "Verktøy", |
| | | "smart.category.system": "Systemapper", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systemforbedringer", |
| | | "smart.category.entertainment": "Underholdning", |
| | | "smart.category.game": "Spill", |
| | | "smart.category.finance": "Finans", |
| | | "smart.category.education": "Utdanning", |
| | | "smart.category.ai-tools": "AI-verktøy", |
| | | "smart.category.security": "Sikkerhet", |
| | | "smart.category.other": "Annet", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start organiserade dina appar", |
| | | "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.", |
| | | "smartstart.suggestion.title": "Smart Start-förslag är klara", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Denne handlingen la til %tagCount% tagger i %appCount% apper (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Denne handlingen fjernet %tagCount% tagger fra %appCount% apper (%appNames%). Fjernede tagger: %tagNames%.", |
| | | "edit.removeHint": "Her kan bare eksisterende tagger fjernes fra avkrysningen. Tagger med rodt sletteikon blir fjernet etter bekreftelse.", |
| | | "smart.category.browser": "Nettlesere", |
| | | "smart.category.communication": "Kommunikasjon", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivitet", |
| | | "smart.category.file-management": "Filbehandling", |
| | | "smart.category.transfer": "Opplastinger og nedlastinger", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmering", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Skriving", |
| | | "smart.category.media": "Medier", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Lyd", |
| | | "smart.category.picture-photo": "Bilder og foto", |
| | | "smart.category.utilities": "Verktøy", |
| | | "smart.category.system": "Systemapper", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systemforbedringer", |
| | | "smart.category.entertainment": "Underholdning", |
| | | "smart.category.game": "Spill", |
| | | "smart.category.finance": "Finans", |
| | | "smart.category.education": "Utdanning", |
| | | "smart.category.ai-tools": "AI-verktøy", |
| | | "smart.category.security": "Sikkerhet", |
| | | "smart.category.other": "Annet", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start organiserade dina appar", |
| | | "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.", |
| | | "smartstart.suggestion.title": "Smart Start-förslag är klara", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Ta operacja dodala %tagCount% tagow do %appCount% aplikacji (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Ta operacja usunela %tagCount% tagow z %appCount% aplikacji (%appNames%). Usuniete tagi: %tagNames%.", |
| | | "edit.removeHint": "Tutaj mozna odznaczyc tylko istniejace tagi. Tagi z czerwona ikona usuniecia zostana usuniete po potwierdzeniu.", |
| | | "smart.category.browser": "Przeglądarki", |
| | | "smart.category.communication": "Komunikacja", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktywność", |
| | | "smart.category.file-management": "Zarządzanie plikami", |
| | | "smart.category.transfer": "Wysyłanie i pobieranie", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programowanie", |
| | | "smart.category.design": "Projektowanie", |
| | | "smart.category.writing": "Pisanie", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Wideo", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Obrazy i zdjęcia", |
| | | "smart.category.utilities": "Narzędzia", |
| | | "smart.category.system": "Aplikacje systemowe", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Ulepszenia systemu", |
| | | "smart.category.entertainment": "Rozrywka", |
| | | "smart.category.game": "Gry", |
| | | "smart.category.finance": "Finanse", |
| | | "smart.category.education": "Edukacja", |
| | | "smart.category.ai-tools": "Narzędzia AI", |
| | | "smart.category.security": "Bezpieczeństwo", |
| | | "smart.category.other": "Inne", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start uporządkował aplikacje", |
| | | "smartstart.auto.messageFormat": "Uporządkowano %appCount% aplikacji z %tagCount% przypisaniami tagów.", |
| | | "smartstart.suggestion.title": "Sugestie Smart Start są gotowe", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Esta operacao adicionou %tagCount% tags em %appCount% apps (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Esta operacao removeu %tagCount% tags de %appCount% apps (%appNames%). Tags removidas: %tagNames%.", |
| | | "edit.removeHint": "Aqui so e possivel desmarcar tags ja existentes. As tags com icone vermelha de remocao serao removidas apos a confirmacao.", |
| | | "smart.category.browser": "Navegadores", |
| | | "smart.category.communication": "Comunicação", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produtividade", |
| | | "smart.category.file-management": "Gerenciamento de arquivos", |
| | | "smart.category.transfer": "Uploads e downloads", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programação", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Escrita", |
| | | "smart.category.media": "Mídia", |
| | | "smart.category.video": "Vídeo", |
| | | "smart.category.audio": "Áudio", |
| | | "smart.category.picture-photo": "Imagens e fotos", |
| | | "smart.category.utilities": "Utilitários", |
| | | "smart.category.system": "Apps do sistema", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Melhorias do sistema", |
| | | "smart.category.entertainment": "Entretenimento", |
| | | "smart.category.game": "Jogos", |
| | | "smart.category.finance": "Finanças", |
| | | "smart.category.education": "Educação", |
| | | "smart.category.ai-tools": "Ferramentas de IA", |
| | | "smart.category.security": "Segurança", |
| | | "smart.category.other": "Outros", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "O Smart Start organizou seus apps", |
| | | "smartstart.auto.messageFormat": "%appCount% apps organizados com %tagCount% atribuições de tags.", |
| | | "smartstart.suggestion.title": "Sugestões do Smart Start prontas", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Aceasta actiune a adaugat %tagCount% etichete in %appCount% aplicatii (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Aceasta actiune a eliminat %tagCount% etichete din %appCount% aplicatii (%appNames%). Etichete eliminate: %tagNames%.", |
| | | "edit.removeHint": "Aici pot fi debifate doar etichetele deja existente. Etichetele care afiseaza o pictograma rosie de stergere vor fi eliminate dupa confirmare.", |
| | | "smart.category.browser": "Browsere", |
| | | "smart.category.communication": "Comunicare", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Productivitate", |
| | | "smart.category.file-management": "Gestionare fișiere", |
| | | "smart.category.transfer": "Încărcări și descărcări", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programare", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Scriere", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Imagini și fotografii", |
| | | "smart.category.utilities": "Utilitare", |
| | | "smart.category.system": "Aplicații de sistem", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Îmbunătățiri de sistem", |
| | | "smart.category.entertainment": "Divertisment", |
| | | "smart.category.game": "Jocuri", |
| | | "smart.category.finance": "Finanțe", |
| | | "smart.category.education": "Educație", |
| | | "smart.category.ai-tools": "Instrumente AI", |
| | | "smart.category.security": "Securitate", |
| | | "smart.category.other": "Altele", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start ți-a organizat aplicațiile", |
| | | "smartstart.auto.messageFormat": "Au fost organizate %appCount% aplicații cu %tagCount% atribuiri de etichete.", |
| | | "smartstart.suggestion.title": "Sugestiile Smart Start sunt gata", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Эта операция добавила %tagCount% тегов для %appCount% приложений (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Эта операция удалила %tagCount% тегов у %appCount% приложений (%appNames%). Удаленные теги: %tagNames%.", |
| | | "edit.removeHint": "Здесь можно снять отметку только с уже существующих тегов. Теги с красным значком удаления будут удалены после подтверждения.", |
| | | "smart.category.browser": "Браузеры", |
| | | "smart.category.communication": "Общение", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Продуктивность", |
| | | "smart.category.file-management": "Управление файлами", |
| | | "smart.category.transfer": "Загрузки и выгрузки", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Программирование", |
| | | "smart.category.design": "Дизайн", |
| | | "smart.category.writing": "Письмо", |
| | | "smart.category.media": "Медиа", |
| | | "smart.category.video": "Видео", |
| | | "smart.category.audio": "Аудио", |
| | | "smart.category.picture-photo": "Изображения и фото", |
| | | "smart.category.utilities": "Утилиты", |
| | | "smart.category.system": "Системные приложения", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Улучшения системы", |
| | | "smart.category.entertainment": "Развлечения", |
| | | "smart.category.game": "Игры", |
| | | "smart.category.finance": "Финансы", |
| | | "smart.category.education": "Обучение", |
| | | "smart.category.ai-tools": "Инструменты ИИ", |
| | | "smart.category.security": "Безопасность", |
| | | "smart.category.other": "Другое", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start упорядочил приложения", |
| | | "smartstart.auto.messageFormat": "Упорядочено приложений: %appCount%, назначений тегов: %tagCount%.", |
| | | "smartstart.suggestion.title": "Предложения Smart Start готовы", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Ова радња је додала %tagCount% ознака за %appCount% апликација (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Ова радња је уклонила %tagCount% ознака са %appCount% апликација (%appNames%). Уклоњене ознаке: %tagNames%.", |
| | | "edit.removeHint": "Овде можете одчекирати само постојеће ознаке. Ознаке са црвеном иконицом брисања биће уклоњене након потврде.", |
| | | "smart.category.browser": "Прегледачи", |
| | | "smart.category.communication": "Комуникација", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Продуктивност", |
| | | "smart.category.file-management": "Управљање датотекама", |
| | | "smart.category.transfer": "Отпремања и преузимања", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Програмирање", |
| | | "smart.category.design": "Дизајн", |
| | | "smart.category.writing": "Писање", |
| | | "smart.category.media": "Медији", |
| | | "smart.category.video": "Видео", |
| | | "smart.category.audio": "Аудио", |
| | | "smart.category.picture-photo": "Слике и фотографије", |
| | | "smart.category.utilities": "Алатке", |
| | | "smart.category.system": "Системске апликације", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Побољшања система", |
| | | "smart.category.entertainment": "Забава", |
| | | "smart.category.game": "Игре", |
| | | "smart.category.finance": "Финансије", |
| | | "smart.category.education": "Образовање", |
| | | "smart.category.ai-tools": "AI алатке", |
| | | "smart.category.security": "Безбедност", |
| | | "smart.category.other": "Остало", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start је средио апликације", |
| | | "smartstart.auto.messageFormat": "Сређено је %appCount% апликација са %tagCount% додела ознака.", |
| | | "smartstart.suggestion.title": "Smart Start предлози су спремни", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Den har atgarden lade till %tagCount% taggar i %appCount% appar (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Den har atgarden tog bort %tagCount% taggar fran %appCount% appar (%appNames%). Borttagna taggar: %tagNames%.", |
| | | "edit.removeHint": "Har kan bara befintliga taggar avmarkeras. Taggar med en rod raderingsikon tas bort efter bekraftelse.", |
| | | "smart.category.browser": "Webbläsare", |
| | | "smart.category.communication": "Kommunikation", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Produktivitet", |
| | | "smart.category.file-management": "Filhantering", |
| | | "smart.category.transfer": "Uppladdningar och nedladdningar", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programmering", |
| | | "smart.category.design": "Design", |
| | | "smart.category.writing": "Skrivande", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Ljud", |
| | | "smart.category.picture-photo": "Bilder och foton", |
| | | "smart.category.utilities": "Verktyg", |
| | | "smart.category.system": "Systemappar", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Systemförbättringar", |
| | | "smart.category.entertainment": "Underhållning", |
| | | "smart.category.game": "Spel", |
| | | "smart.category.finance": "Ekonomi", |
| | | "smart.category.education": "Utbildning", |
| | | "smart.category.ai-tools": "AI-verktyg", |
| | | "smart.category.security": "Säkerhet", |
| | | "smart.category.other": "Annat", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start organiserade dina appar", |
| | | "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.", |
| | | "smartstart.suggestion.title": "Smart Start-förslag är klara", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "การทำงานครั้งนี้ได้เพิ่มแท็ก %tagCount% รายการให้กับแอป %appCount% แอป (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "การทำงานครั้งนี้ได้ลบแท็ก %tagCount% รายการออกจากแอป %appCount% แอป (%appNames%). แท็กที่ลบ: %tagNames%.", |
| | | "edit.removeHint": "ที่นี่สามารถยกเลิกเครื่องหมายได้เฉพาะแท็กที่มีอยู่แล้วเท่านั้น แท็กที่แสดงไอคอนลบสีแดงจะถูกลบหลังจากยืนยัน", |
| | | "smart.category.browser": "เบราว์เซอร์", |
| | | "smart.category.communication": "การสื่อสาร", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "ประสิทธิภาพการทำงาน", |
| | | "smart.category.file-management": "การจัดการไฟล์", |
| | | "smart.category.transfer": "อัปโหลดและดาวน์โหลด", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "การเขียนโปรแกรม", |
| | | "smart.category.design": "ออกแบบ", |
| | | "smart.category.writing": "การเขียน", |
| | | "smart.category.media": "สื่อ", |
| | | "smart.category.video": "วิดีโอ", |
| | | "smart.category.audio": "เสียง", |
| | | "smart.category.picture-photo": "รูปภาพและภาพถ่าย", |
| | | "smart.category.utilities": "ยูทิลิตี้", |
| | | "smart.category.system": "แอประบบ", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "การเสริมความสามารถระบบ", |
| | | "smart.category.entertainment": "ความบันเทิง", |
| | | "smart.category.game": "เกม", |
| | | "smart.category.finance": "การเงิน", |
| | | "smart.category.education": "การศึกษา", |
| | | "smart.category.ai-tools": "เครื่องมือ AI", |
| | | "smart.category.security": "ความปลอดภัย", |
| | | "smart.category.other": "อื่น ๆ", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start จัดระเบียบแอปของคุณแล้ว", |
| | | "smartstart.auto.messageFormat": "จัดระเบียบ %appCount% แอปด้วยการกำหนดแท็ก %tagCount% รายการ", |
| | | "smartstart.suggestion.title": "คำแนะนำ Smart Start พร้อมแล้ว", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Bu islem, %appCount% uygulamada (%appNames%) %tagCount% etiket ekledi.", |
| | | "edit.feedbackRemoveFormat": "Bu islem, %appCount% uygulamadan (%appNames%) %tagCount% etiketi kaldirdi. Kaldirilan etiketler: %tagNames%.", |
| | | "edit.removeHint": "Burada sadece mevcut etiketlerin isareti kaldirilabilir. Kirmizi silme simgesi gorunen etiketler onaydan sonra kaldirilir.", |
| | | "smart.category.browser": "Tarayıcılar", |
| | | "smart.category.communication": "İletişim", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Üretkenlik", |
| | | "smart.category.file-management": "Dosya yönetimi", |
| | | "smart.category.transfer": "Yüklemeler ve indirmeler", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Programlama", |
| | | "smart.category.design": "Tasarım", |
| | | "smart.category.writing": "Yazma", |
| | | "smart.category.media": "Medya", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Ses", |
| | | "smart.category.picture-photo": "Görseller ve fotoğraflar", |
| | | "smart.category.utilities": "Araçlar", |
| | | "smart.category.system": "Sistem uygulamaları", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Sistem geliştirmeleri", |
| | | "smart.category.entertainment": "Eğlence", |
| | | "smart.category.game": "Oyunlar", |
| | | "smart.category.finance": "Finans", |
| | | "smart.category.education": "Eğitim", |
| | | "smart.category.ai-tools": "AI araçları", |
| | | "smart.category.security": "Güvenlik", |
| | | "smart.category.other": "Diğer", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start uygulamalarını düzenledi", |
| | | "smartstart.auto.messageFormat": "%appCount% uygulama %tagCount% etiket atamasıyla düzenlendi.", |
| | | "smartstart.suggestion.title": "Smart Start önerileri hazır", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Ця дія додала %tagCount% тегів для %appCount% застосунків (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Ця дія вилучила %tagCount% тегів у %appCount% застосунків (%appNames%). Вилучені теги: %tagNames%.", |
| | | "edit.removeHint": "Тут можна зняти позначку лише з наявних тегів. Теги з червоною іконкою видалення буде вилучено після підтвердження.", |
| | | "smart.category.browser": "Браузери", |
| | | "smart.category.communication": "Спілкування", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Продуктивність", |
| | | "smart.category.file-management": "Керування файлами", |
| | | "smart.category.transfer": "Передавання й завантаження", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Програмування", |
| | | "smart.category.design": "Дизайн", |
| | | "smart.category.writing": "Письмо", |
| | | "smart.category.media": "Медіа", |
| | | "smart.category.video": "Відео", |
| | | "smart.category.audio": "Аудіо", |
| | | "smart.category.picture-photo": "Зображення й фото", |
| | | "smart.category.utilities": "Утиліти", |
| | | "smart.category.system": "Системні програми", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Покращення системи", |
| | | "smart.category.entertainment": "Розваги", |
| | | "smart.category.game": "Ігри", |
| | | "smart.category.finance": "Фінанси", |
| | | "smart.category.education": "Навчання", |
| | | "smart.category.ai-tools": "Інструменти ШІ", |
| | | "smart.category.security": "Безпека", |
| | | "smart.category.other": "Інше", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start упорядкував програми", |
| | | "smartstart.auto.messageFormat": "Упорядковано %appCount% програм із %tagCount% призначеннями тегів.", |
| | | "smartstart.suggestion.title": "Пропозиції Smart Start готові", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "edit.feedbackAddFormat": "Lan nay da them %tagCount% the cho %appCount% ung dung (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Lan nay da go %tagCount% the khoi %appCount% ung dung (%appNames%). The da go: %tagNames%.", |
| | | "edit.removeHint": "Tai day chi co the bo chon cac the da ton tai. Cac the hien bieu tuong xoa mau do se bi go sau khi xac nhan.", |
| | | "smart.category.browser": "Trình duyệt", |
| | | "smart.category.communication": "Liên lạc", |
| | | "smart.category.browser": "Browsers", |
| | | "smart.category.communication": "Communication", |
| | | "smart.category.productivity": "Năng suất", |
| | | "smart.category.file-management": "Quản lý tệp", |
| | | "smart.category.transfer": "Tải lên và tải xuống", |
| | | "smart.category.file-management": "File Management", |
| | | "smart.category.transfer": "Uploads & Downloads", |
| | | "smart.category.development": "Lập trình", |
| | | "smart.category.design": "Thiết kế", |
| | | "smart.category.writing": "Viết", |
| | | "smart.category.media": "Phương tiện", |
| | | "smart.category.writing": "Writing", |
| | | "smart.category.media": "Media", |
| | | "smart.category.video": "Video", |
| | | "smart.category.audio": "Âm thanh", |
| | | "smart.category.picture-photo": "Ảnh và hình ảnh", |
| | | "smart.category.utilities": "Tiện ích", |
| | | "smart.category.system": "Ứng dụng hệ thống", |
| | | "smart.category.audio": "Audio", |
| | | "smart.category.picture-photo": "Pictures & Photos", |
| | | "smart.category.utilities": "Utilities", |
| | | "smart.category.system": "System Apps", |
| | | "smart.category.system-enhancement": "Tăng cường hệ thống", |
| | | "smart.category.entertainment": "Giải trí", |
| | | "smart.category.game": "Trò chơi", |
| | | "smart.category.finance": "Tài chính", |
| | | "smart.category.education": "Giáo dục", |
| | | "smart.category.ai-tools": "Công cụ AI", |
| | | "smart.category.security": "Bảo mật", |
| | | "smart.category.other": "Khác", |
| | | "smart.category.entertainment": "Entertainment", |
| | | "smart.category.game": "Games", |
| | | "smart.category.finance": "Finance", |
| | | "smart.category.education": "Education", |
| | | "smart.category.ai-tools": "AI Tools", |
| | | "smart.category.security": "Security", |
| | | "smart.category.other": "Other", |
| | | "smartstart.auto.title": "Smart Start đã sắp xếp ứng dụng", |
| | | "smartstart.auto.messageFormat": "Đã sắp xếp %appCount% ứng dụng với %tagCount% lượt gán thẻ.", |
| | | "smartstart.suggestion.title": "Gợi ý Smart Start đã sẵn sàng", |
| | |
| | | "quickSearch.internalHotkeyStatus": "App list only", |
| | | "quickSearch.globalHotkey": "Quick Search (global)", |
| | | "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.", |
| | | "quickSearch.spaceDisplay": "Space" |
| | | "quickSearch.spaceDisplay": "Space", |
| | | "smart.category.GTD": "Tasks & GTD", |
| | | "smart.category.Notes": "Notes", |
| | | "smart.category.Meeting": "Meetings", |
| | | "smart.category.office": "Office", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API Tools", |
| | | "smart.category.database-tools": "Databases", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDEs", |
| | | "smart.category.runtime-sdk": "Runtimes & SDKs", |
| | | "smart.category.terminal-tools": "Terminal Tools", |
| | | "smart.category.Font": "Fonts", |
| | | "smart.category.ui-prototyping": "UI Prototyping", |
| | | "smart.category.3d-cad": "3D & CAD", |
| | | "smart.category.diagramming": "Diagramming", |
| | | "smart.category.system-maintenance": "System Maintenance", |
| | | "smart.category.window-management": "Window Management", |
| | | "smart.category.device-management": "Device Management", |
| | | "smart.category.input-tools": "Input Tools", |
| | | "smart.category.Automation": "Automation", |
| | | "smart.category.network-tools": "Network Tools" |
| | | } |
| | |
| | | "quickSearch.internalHotkeyStatus": "仅主界面有效", |
| | | "quickSearch.globalHotkey": "快捷搜索(全局)", |
| | | "quickSearch.globalHotkeyDesc": "不在 TagLauncher 主界面时,直接打开快捷搜索。全局有效。", |
| | | "quickSearch.spaceDisplay": "Space(空格键)" |
| | | "quickSearch.spaceDisplay": "Space(空格键)", |
| | | "smart.category.GTD": "任务与 GTD", |
| | | "smart.category.Notes": "笔记", |
| | | "smart.category.Meeting": "会议", |
| | | "smart.category.office": "办公", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API 工具", |
| | | "smart.category.database-tools": "数据库", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDE", |
| | | "smart.category.runtime-sdk": "运行时与 SDK", |
| | | "smart.category.terminal-tools": "终端工具", |
| | | "smart.category.Font": "字体", |
| | | "smart.category.ui-prototyping": "UI 原型", |
| | | "smart.category.3d-cad": "3D 与 CAD", |
| | | "smart.category.diagramming": "图表绘制", |
| | | "smart.category.system-maintenance": "系统维护", |
| | | "smart.category.window-management": "窗口管理", |
| | | "smart.category.device-management": "设备管理", |
| | | "smart.category.input-tools": "输入工具", |
| | | "smart.category.Automation": "自动化", |
| | | "smart.category.network-tools": "网络工具" |
| | | } |
| | |
| | | "quickSearch.internalHotkeyStatus": "僅主介面有效", |
| | | "quickSearch.globalHotkey": "快捷搜尋(全域)", |
| | | "quickSearch.globalHotkeyDesc": "不在 TagLauncher 主介面時,直接打開快捷搜尋。全域有效。", |
| | | "quickSearch.spaceDisplay": "Space(空格鍵)" |
| | | "quickSearch.spaceDisplay": "Space(空格鍵)", |
| | | "smart.category.GTD": "任務與 GTD", |
| | | "smart.category.Notes": "筆記", |
| | | "smart.category.Meeting": "會議", |
| | | "smart.category.office": "辦公", |
| | | "smart.category.PDF": "PDF", |
| | | "smart.category.api-tools": "API 工具", |
| | | "smart.category.database-tools": "資料庫", |
| | | "smart.category.devops": "DevOps", |
| | | "smart.category.ide": "IDE", |
| | | "smart.category.runtime-sdk": "執行環境與 SDK", |
| | | "smart.category.terminal-tools": "終端工具", |
| | | "smart.category.Font": "字型", |
| | | "smart.category.ui-prototyping": "UI 原型", |
| | | "smart.category.3d-cad": "3D 與 CAD", |
| | | "smart.category.diagramming": "圖表繪製", |
| | | "smart.category.system-maintenance": "系統維護", |
| | | "smart.category.window-management": "視窗管理", |
| | | "smart.category.device-management": "裝置管理", |
| | | "smart.category.input-tools": "輸入工具", |
| | | "smart.category.Automation": "自動化", |
| | | "smart.category.network-tools": "網路工具" |
| | | } |
| | |
| | | Text("万物之中,希望最美") |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | Text("永桔@2026-18602102518") |
| | | Text("永桔@shanghai3168@gmail.com") |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | } |
| | |
| | | let normalized: String |
| | | let acronym: String |
| | | let pinyinCandidates: [String] |
| | | let allowPinyinSubstring: Bool |
| | | let allowPinyinFuzzySubsequence: Bool |
| | | } |
| | | |
| | | private struct QuickSearchMatchOptions { |
| | |
| | | enum QuickSearchEngine { |
| | | static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] { |
| | | apps.map { app in |
| | | let localizedNames = localizedNames(for: app) |
| | | let localizedNames = uniqueOrdered(app.localizedNames) |
| | | let internalBundleNames = internalBundleNames(for: app) |
| | | let note = store.appNotes[app.path.path] ?? app.note ?? "" |
| | | let bundleIdentifier = app.bundleIdentifier ?? "" |
| | |
| | | ) -> [QuickSearchIndexedField] { |
| | | let names = uniqueOrdered([appName] + localizedNames) |
| | | let nameFields = names.map { |
| | | QuickSearchIndexedField( |
| | | let allowsRomanizedFuzzy = containsNonLatinLetter($0) |
| | | return QuickSearchIndexedField( |
| | | kind: .name, |
| | | text: $0, |
| | | normalized: normalizeField($0), |
| | | acronym: acronym(for: $0), |
| | | pinyinCandidates: pinyinCandidates(for: $0) |
| | | pinyinCandidates: pinyinCandidates(for: $0), |
| | | allowPinyinSubstring: allowsRomanizedFuzzy, |
| | | allowPinyinFuzzySubsequence: allowsRomanizedFuzzy |
| | | ) |
| | | } |
| | | let tagFields = tagNames.map { |
| | |
| | | text: $0, |
| | | normalized: normalizeField($0), |
| | | acronym: "", |
| | | pinyinCandidates: pinyinCandidates(for: $0) |
| | | pinyinCandidates: pinyinCandidates(for: $0), |
| | | allowPinyinSubstring: false, |
| | | allowPinyinFuzzySubsequence: false |
| | | ) |
| | | } |
| | | let noteFields = note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? [] : [ |
| | |
| | | text: note, |
| | | normalized: normalizeField(note), |
| | | acronym: "", |
| | | pinyinCandidates: pinyinCandidates(for: note) |
| | | pinyinCandidates: pinyinCandidates(for: note), |
| | | allowPinyinSubstring: true, |
| | | allowPinyinFuzzySubsequence: false |
| | | ) |
| | | ] |
| | | let bundleFields = bundleIdentifier.isEmpty ? [] : [ |
| | |
| | | text: bundleIdentifier, |
| | | normalized: normalizeField(bundleIdentifier), |
| | | acronym: "", |
| | | pinyinCandidates: [] |
| | | pinyinCandidates: [], |
| | | allowPinyinSubstring: false, |
| | | allowPinyinFuzzySubsequence: false |
| | | ) |
| | | ] |
| | | let internalBundleNameFields = internalBundleNames.map { |
| | |
| | | text: $0, |
| | | normalized: normalizeField($0), |
| | | acronym: "", |
| | | pinyinCandidates: [] |
| | | pinyinCandidates: [], |
| | | allowPinyinSubstring: false, |
| | | allowPinyinFuzzySubsequence: false |
| | | ) |
| | | } |
| | | return nameFields + tagFields + noteFields + bundleFields + internalBundleNameFields |
| | |
| | | if let pinyinCandidate = matchPinyinCandidate( |
| | | token: token, |
| | | normalized: pinyin, |
| | | allowSubstring: field.kind == .note |
| | | allowSubstring: field.allowPinyinSubstring, |
| | | allowFuzzySubsequence: field.allowPinyinFuzzySubsequence |
| | | ) { |
| | | candidates.append(pinyinCandidate) |
| | | } |
| | |
| | | private static func matchPinyinCandidate( |
| | | token: String, |
| | | normalized: String, |
| | | allowSubstring: Bool = false |
| | | allowSubstring: Bool = false, |
| | | allowFuzzySubsequence: Bool = false |
| | | ) -> (QuickSearchMatchKind, Int)? { |
| | | guard !normalized.isEmpty else { return nil } |
| | | if normalized == token { |
| | |
| | | } |
| | | if allowSubstring, let range = normalized.range(of: token) { |
| | | return (.substring, normalized.distance(from: normalized.startIndex, to: range.lowerBound)) |
| | | } |
| | | if allowFuzzySubsequence, token.count >= 3, isSubsequence(token, of: normalized) { |
| | | return (.fuzzy, 10) |
| | | } |
| | | return nil |
| | | } |
| | |
| | | } |
| | | |
| | | private static func pinyinCandidates(for value: String) -> [String] { |
| | | guard value.range(of: #"\p{Han}"#, options: .regularExpression) != nil else { return [] } |
| | | guard containsNonLatinLetter(value) else { return [] } |
| | | let mutable = NSMutableString(string: value) |
| | | CFStringTransform(mutable, nil, kCFStringTransformToLatin, false) |
| | | CFStringTransform(mutable, nil, kCFStringTransformStripCombiningMarks, false) |
| | |
| | | .map(String.init) |
| | | .joined() |
| | | return uniqueOrdered([spaced, compact, initials].filter { !$0.isEmpty }) |
| | | } |
| | | |
| | | private static func containsNonLatinLetter(_ value: String) -> Bool { |
| | | value.unicodeScalars.contains { scalar in |
| | | CharacterSet.letters.contains(scalar) && !isLatinScriptLetter(scalar) |
| | | } |
| | | } |
| | | |
| | | private static func isLatinScriptLetter(_ scalar: UnicodeScalar) -> Bool { |
| | | switch scalar.value { |
| | | case 0x0041...0x005A, // Basic Latin uppercase |
| | | 0x0061...0x007A, // Basic Latin lowercase |
| | | 0x00AA, |
| | | 0x00BA, |
| | | 0x00C0...0x024F, // Latin-1 Supplement, Extended-A/B |
| | | 0x1E00...0x1EFF, // Latin Extended Additional |
| | | 0x2C60...0x2C7F, // Latin Extended-C |
| | | 0xA720...0xA7FF, // Latin Extended-D |
| | | 0xAB30...0xAB6F, // Latin Extended-E |
| | | 0xFF21...0xFF3A, // Fullwidth Latin uppercase |
| | | 0xFF41...0xFF5A: // Fullwidth Latin lowercase |
| | | return true |
| | | default: |
| | | return false |
| | | } |
| | | } |
| | | |
| | | private static func isSubsequence(_ token: String, of value: String) -> Bool { |
| | |
| | | let results: [QuickSearchResult] |
| | | let selectedID: URL? |
| | | let focusToken: Int |
| | | let selectionScrollToken: Int |
| | | let isLoading: Bool |
| | | let maxVisibleRows: Int |
| | | let errorMessage: String? |
| | |
| | | let onHover: (QuickSearchResult) -> Void |
| | | let onLaunch: (QuickSearchResult) -> Void |
| | | |
| | | @Environment(\.colorScheme) private var colorScheme |
| | | |
| | | private let panelWidth: CGFloat = 760 |
| | | private let rowHeight: CGFloat = 74 |
| | | |
| | | private var panelBackgroundColor: Color { |
| | | colorScheme == .dark |
| | | ? Color(red: 0.105, green: 0.110, blue: 0.125).opacity(0.97) |
| | | : Color.white.opacity(0.97) |
| | | } |
| | | |
| | | var body: some View { |
| | | VStack(alignment: .leading, spacing: 0) { |
| | |
| | | Divider().opacity(0.35) |
| | | |
| | | if isLoading { |
| | | HStack(spacing: 10) { |
| | | ProgressView() |
| | | .controlSize(.small) |
| | | Text(tr("quickSearch.loading")) |
| | | .font(.system(size: 15, weight: .medium)) |
| | | .foregroundStyle(.secondary) |
| | | Spacer(minLength: 0) |
| | | } |
| | | .padding(.horizontal, 28) |
| | | .padding(.vertical, 24) |
| | | .frame(minHeight: 86) |
| | | QuickSearchMessageRow( |
| | | systemImage: "hourglass", |
| | | message: tr("quickSearch.loading"), |
| | | tint: .secondary |
| | | ) |
| | | } else if let errorMessage { |
| | | QuickSearchMessageRow( |
| | | systemImage: "exclamationmark.triangle.fill", |
| | |
| | | .padding(.vertical, 10) |
| | | } |
| | | .frame(height: CGFloat(min(results.count, maxVisibleRows)) * (rowHeight + 2) + 20) |
| | | .onChange(of: selectedID) { _, id in |
| | | guard let id else { return } |
| | | .onChange(of: selectionScrollToken) { _, _ in |
| | | guard let id = selectedID else { return } |
| | | withAnimation(.easeOut(duration: 0.08)) { |
| | | scrollProxy.scrollTo(id, anchor: .center) |
| | | } |
| | |
| | | .frame(width: panelWidth) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 34, style: .continuous) |
| | | .fill(.regularMaterial) |
| | | .fill(panelBackgroundColor) |
| | | .shadow(color: .black.opacity(0.18), radius: 36, y: 18) |
| | | ) |
| | | .overlay( |
| | |
| | | field.onCommand = onCommand |
| | | field.setAccessibilityLabel(tr("quickSearch.inputAccessibility")) |
| | | context.coordinator.field = field |
| | | DispatchQueue.main.async { |
| | | field.window?.makeFirstResponder(field) |
| | | } |
| | | requestFocus(field) |
| | | return field |
| | | } |
| | | |
| | |
| | | field.onCommand = onCommand |
| | | if context.coordinator.lastFocusToken != focusToken { |
| | | context.coordinator.lastFocusToken = focusToken |
| | | DispatchQueue.main.async { |
| | | field.window?.makeFirstResponder(field) |
| | | requestFocus(field) |
| | | } |
| | | } |
| | | |
| | | private func requestFocus(_ field: QuickSearchNativeTextField) { |
| | | DispatchQueue.main.async { [weak field] in |
| | | guard let field, |
| | | field.window?.firstResponder !== field |
| | | else { return } |
| | | field.window?.makeFirstResponder(field) |
| | | } |
| | | } |
| | | |
| | | func makeCoordinator() -> Coordinator { |
| | |
| | | } |
| | | } |
| | | |
| | | struct SmartDefaultNoteProvenance: Codable, Hashable { |
| | | let entryID: String |
| | | let languageCode: String |
| | | let notesVersion: Int |
| | | let noteFingerprint: String |
| | | } |
| | | |
| | | struct SmartAppCategorizationAssignment: Codable, Hashable, Identifiable { |
| | | let appName: String |
| | | let bundleIdentifier: String? |
| | |
| | | let provenance: [String] |
| | | let defaultNote: String? |
| | | let defaultNoteCandidates: [String] |
| | | let defaultNoteProvenance: SmartDefaultNoteProvenance? |
| | | |
| | | var id: String { |
| | | if let bundleIdentifier, !bundleIdentifier.isEmpty { |
| | |
| | | reason: String, |
| | | provenance: [String] = [], |
| | | defaultNote: String? = nil, |
| | | defaultNoteCandidates: [String] = [] |
| | | defaultNoteCandidates: [String] = [], |
| | | defaultNoteProvenance: SmartDefaultNoteProvenance? = nil |
| | | ) { |
| | | self.appName = appName |
| | | self.bundleIdentifier = bundleIdentifier |
| | |
| | | self.provenance = provenance |
| | | self.defaultNote = defaultNote |
| | | self.defaultNoteCandidates = defaultNoteCandidates |
| | | self.defaultNoteProvenance = defaultNoteProvenance |
| | | } |
| | | |
| | | private static func uniqueOrdered(_ ids: [SmartCategoryID]) -> [SmartCategoryID] { |
| | |
| | | enum SmartCategoryID: String, CaseIterable, Codable, Hashable, Identifiable { |
| | | case browser |
| | | case communication |
| | | case productivity |
| | | case gtd = "GTD" |
| | | case notes = "Notes" |
| | | case meeting = "Meeting" |
| | | case office |
| | | case pdf = "PDF" |
| | | case fileManagement = "file-management" |
| | | case transfer |
| | | case productivity |
| | | case development |
| | | case design |
| | | case aiTools = "ai-tools" |
| | | case apiTools = "api-tools" |
| | | case databaseTools = "database-tools" |
| | | case devops |
| | | case ide |
| | | case runtimeSDK = "runtime-sdk" |
| | | case terminalTools = "terminal-tools" |
| | | case font = "Font" |
| | | case uiPrototyping = "ui-prototyping" |
| | | case threeDCAD = "3d-cad" |
| | | case diagramming |
| | | case writing |
| | | case media |
| | | case video |
| | |
| | | case utilities |
| | | case system |
| | | case systemEnhancement = "system-enhancement" |
| | | case systemMaintenance = "system-maintenance" |
| | | case windowManagement = "window-management" |
| | | case deviceManagement = "device-management" |
| | | case inputTools = "input-tools" |
| | | case automation = "Automation" |
| | | case networkTools = "network-tools" |
| | | case entertainment |
| | | case game |
| | | case finance |
| | | case education |
| | | case aiTools = "ai-tools" |
| | | case security |
| | | case other |
| | | |
| | |
| | | return "Browsers" |
| | | case .communication: |
| | | return "Communication" |
| | | case .productivity: |
| | | return "Productivity" |
| | | case .gtd: |
| | | return "Tasks & GTD" |
| | | case .notes: |
| | | return "Notes" |
| | | case .meeting: |
| | | return "Meetings" |
| | | case .office: |
| | | return "Office" |
| | | case .pdf: |
| | | return "PDF" |
| | | case .fileManagement: |
| | | return "File Management" |
| | | case .transfer: |
| | | return "Uploads & Downloads" |
| | | case .productivity: |
| | | return "Productivity" |
| | | case .development: |
| | | return "Development" |
| | | case .design: |
| | | return "Design" |
| | | case .aiTools: |
| | | return "AI Tools" |
| | | case .apiTools: |
| | | return "API Tools" |
| | | case .databaseTools: |
| | | return "Databases" |
| | | case .devops: |
| | | return "DevOps" |
| | | case .ide: |
| | | return "IDEs" |
| | | case .runtimeSDK: |
| | | return "Runtimes & SDKs" |
| | | case .terminalTools: |
| | | return "Terminal Tools" |
| | | case .font: |
| | | return "Fonts" |
| | | case .uiPrototyping: |
| | | return "UI Prototyping" |
| | | case .threeDCAD: |
| | | return "3D & CAD" |
| | | case .diagramming: |
| | | return "Diagramming" |
| | | case .writing: |
| | | return "Writing" |
| | | case .media: |
| | |
| | | return "System Apps" |
| | | case .systemEnhancement: |
| | | return "System Enhancements" |
| | | case .systemMaintenance: |
| | | return "System Maintenance" |
| | | case .windowManagement: |
| | | return "Window Management" |
| | | case .deviceManagement: |
| | | return "Device Management" |
| | | case .inputTools: |
| | | return "Input Tools" |
| | | case .automation: |
| | | return "Automation" |
| | | case .networkTools: |
| | | return "Network Tools" |
| | | case .entertainment: |
| | | return "Entertainment" |
| | | case .game: |
| | |
| | | return "Finance" |
| | | case .education: |
| | | return "Education" |
| | | case .aiTools: |
| | | return "AI Tools" |
| | | case .security: |
| | | return "Security" |
| | | case .other: |
| | |
| | | return 4 |
| | | case .communication: |
| | | return 4 |
| | | case .productivity: |
| | | case .gtd: |
| | | return 2 |
| | | case .notes: |
| | | return 2 |
| | | case .meeting: |
| | | return 4 |
| | | case .office: |
| | | return 2 |
| | | case .pdf: |
| | | return 2 |
| | | case .fileManagement: |
| | | return 4 |
| | | case .transfer: |
| | | return 4 |
| | | case .productivity: |
| | | return 2 |
| | | case .development: |
| | | return 3 |
| | | case .design: |
| | | return 7 |
| | | case .aiTools: |
| | | return 3 |
| | | case .apiTools: |
| | | return 3 |
| | | case .databaseTools: |
| | | return 5 |
| | | case .devops: |
| | | return 5 |
| | | case .ide: |
| | | return 3 |
| | | case .runtimeSDK: |
| | | return 3 |
| | | case .terminalTools: |
| | | return 0 |
| | | case .font: |
| | | return 7 |
| | | case .uiPrototyping: |
| | | return 7 |
| | | case .threeDCAD: |
| | | return 7 |
| | | case .diagramming: |
| | | return 7 |
| | | case .writing: |
| | | return 2 |
| | |
| | | return 0 |
| | | case .systemEnhancement: |
| | | return 0 |
| | | case .systemMaintenance: |
| | | return 0 |
| | | case .windowManagement: |
| | | return 0 |
| | | case .deviceManagement: |
| | | return 0 |
| | | case .inputTools: |
| | | return 0 |
| | | case .automation: |
| | | return 5 |
| | | case .networkTools: |
| | | return 5 |
| | | case .entertainment: |
| | | return 7 |
| | | case .game: |
| | |
| | | return 2 |
| | | case .education: |
| | | return 2 |
| | | case .aiTools: |
| | | return 3 |
| | | case .security: |
| | | return 1 |
| | | case .other: |
| | |
| | | static let orderedIDs: [SmartCategoryID] = [ |
| | | .browser, |
| | | .communication, |
| | | .productivity, |
| | | .gtd, |
| | | .notes, |
| | | .meeting, |
| | | .office, |
| | | .pdf, |
| | | .fileManagement, |
| | | .transfer, |
| | | .development, |
| | | .design, |
| | | .aiTools, |
| | | .apiTools, |
| | | .databaseTools, |
| | | .devops, |
| | | .ide, |
| | | .runtimeSDK, |
| | | .terminalTools, |
| | | .font, |
| | | .uiPrototyping, |
| | | .threeDCAD, |
| | | .diagramming, |
| | | .writing, |
| | | .media, |
| | | .video, |
| | |
| | | .picturePhoto, |
| | | .utilities, |
| | | .system, |
| | | .systemEnhancement, |
| | | .systemMaintenance, |
| | | .windowManagement, |
| | | .deviceManagement, |
| | | .inputTools, |
| | | .automation, |
| | | .networkTools, |
| | | .entertainment, |
| | | .game, |
| | | .finance, |
| | | .education, |
| | | .aiTools, |
| | | .security, |
| | | .other |
| | | ] |
| | |
| | | import Compression |
| | | import Foundation |
| | | |
| | | // MARK: - Smart Start Catalog Runtime |
| | |
| | | } |
| | | |
| | | private struct SmartStartCatalogEntry { |
| | | let entryID: String |
| | | let rank: Int |
| | | let name: String |
| | | let normalizedName: String |
| | | let bundleIdentifier: String? |
| | | let categoryIDs: [SmartCategoryID] |
| | | let localizedNote: String? |
| | | let notes: [String: String]? |
| | | let localizedNote: SmartStartLocalizedNote? |
| | | let sourceEvidence: [String] |
| | | } |
| | | |
| | | private struct SmartStartRuntimeCatalog: Decodable { |
| | | let version: Int |
| | | let entries: [SmartStartRuntimeCatalogEntry] |
| | | private struct SmartStartLocalizedNote { |
| | | let note: String |
| | | let languageCode: String |
| | | let provenance: SmartDefaultNoteProvenance |
| | | } |
| | | |
| | | private struct SmartStartRuntimeCatalogEntry: Decodable { |
| | | private struct SmartStartBaseCatalog: Decodable { |
| | | let resourceFormatVersion: Int |
| | | let catalogContentVersion: Int |
| | | let noteLimit: Int? |
| | | let supportedLanguages: [String] |
| | | let fallbackLanguages: [String] |
| | | let entries: [SmartStartBaseCatalogEntry] |
| | | } |
| | | |
| | | private struct SmartStartBaseCatalogEntry: Decodable { |
| | | let entryID: String |
| | | let rank: Int? |
| | | let name: String |
| | | let normalizedName: String? |
| | | let bundleIdentifier: String? |
| | | let defaultTag: [String] |
| | | let notes: [String: String]? |
| | | let sourceEvidence: [String]? |
| | | } |
| | | |
| | | private struct SmartStartNotesCatalog: Decodable { |
| | | let resourceFormatVersion: Int |
| | | let catalogContentVersion: Int |
| | | let notesVersion: Int |
| | | let language: String |
| | | let entries: [SmartStartNoteEntry] |
| | | } |
| | | |
| | | private struct SmartStartNoteEntry: Decodable { |
| | | let entryID: String |
| | | let note: String |
| | | } |
| | | |
| | | private struct SmartStartBaseSnapshot { |
| | | let catalogContentVersion: Int |
| | | let supportedLanguages: [String] |
| | | let fallbackLanguages: [String] |
| | | let entries: [SmartStartBaseCatalogEntry] |
| | | let bundleIndex: [String: SmartStartBaseCatalogEntry] |
| | | let nameIndex: [String: SmartStartBaseCatalogEntry] |
| | | let entryIndex: [String: SmartStartBaseCatalogEntry] |
| | | } |
| | | |
| | | private struct SmartStartNotesSnapshot { |
| | | let languageCode: String |
| | | let notesVersion: Int |
| | | let notes: [String: String] |
| | | } |
| | | |
| | | private struct SmartStartCatalogSnapshot { |
| | | let entries: [SmartStartCatalogEntry] |
| | | let bundleIndex: [String: SmartStartCatalogEntry] |
| | | let nameIndex: [String: SmartStartCatalogEntry] |
| | | let entryIndex: [String: SmartStartCatalogEntry] |
| | | } |
| | | |
| | | enum SmartStartService { |
| | | static let catalogVersion = 2 |
| | | private static let catalogCacheLock = NSLock() |
| | | private static var cachedBaseSnapshot: SmartStartBaseSnapshot? |
| | | private static var cachedCatalogSnapshots: [String: SmartStartCatalogSnapshot] = [:] |
| | | private static var cachedNotesSnapshots: [String: SmartStartNotesSnapshot] = [:] |
| | | |
| | | static let systemInitialSchemeCreatedAt: Date = { |
| | | var components = DateComponents() |
| | | components.calendar = Calendar(identifier: .gregorian) |
| | |
| | | |
| | | @discardableResult |
| | | static func relocalizeDefaultNotesForCurrentLanguage(apps: [AppInfo]) -> Bool { |
| | | let catalog = loadCatalog() |
| | | guard !catalog.isEmpty else { return false } |
| | | |
| | | var bundleIndex: [String: SmartStartCatalogEntry] = [:] |
| | | for entry in catalog { |
| | | guard let bundleIdentifier = entry.bundleIdentifier?.lowercased(), !bundleIdentifier.isEmpty else { |
| | | continue |
| | | } |
| | | if let existing = bundleIndex[bundleIdentifier], existing.rank <= entry.rank { |
| | | continue |
| | | } |
| | | bundleIndex[bundleIdentifier] = entry |
| | | } |
| | | |
| | | let nameIndex = Dictionary( |
| | | grouping: catalog, |
| | | by: { $0.normalizedName } |
| | | ).compactMapValues { entries in |
| | | entries.sorted { $0.rank < $1.rank }.first |
| | | } |
| | | let catalog = loadCatalogSnapshot() |
| | | guard !catalog.entries.isEmpty else { return false } |
| | | |
| | | var store = TagDatabase.load() |
| | | var changed = false |
| | | let appPaths = Set(apps.map { $0.path.path }) |
| | | |
| | | for app in apps { |
| | | let path = app.path.path |
| | | for path in appPaths { |
| | | guard let currentNote = normalizedNote(store.appNotes[path]) else { continue } |
| | | guard let matched = match(app: app, bundleIndex: bundleIndex, nameIndex: nameIndex), |
| | | let notes = matched.notes, |
| | | !notes.isEmpty |
| | | guard let metadata = store.appNoteMetadata[path], |
| | | metadata.origin == .catalogDefault, |
| | | let currentCatalog = metadata.catalog, |
| | | currentCatalog.noteFingerprint == TagDatabase.noteFingerprint(currentNote), |
| | | let matched = catalog.entryIndex[currentCatalog.entryID], |
| | | let localizedNote = matched.localizedNote, |
| | | localizedNote.note != currentNote |
| | | else { continue } |
| | | |
| | | let knownDefaultNotes = Set(notes.values.compactMap(normalizedNote)) |
| | | guard knownDefaultNotes.contains(currentNote), |
| | | let localizedNote = localizedNote(from: notes), |
| | | localizedNote != currentNote |
| | | else { continue } |
| | | |
| | | store.appNotes[path] = localizedNote |
| | | store.appNotes[path] = localizedNote.note |
| | | store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata( |
| | | origin: .catalogDefault, |
| | | catalog: localizedNote.provenance, |
| | | noteFingerprint: localizedNote.provenance.noteFingerprint |
| | | ) |
| | | changed = true |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | static func makeDraft(apps: [AppInfo]) -> SmartCategorizationDraft { |
| | | let catalog = loadCatalog() |
| | | var bundleIndex: [String: SmartStartCatalogEntry] = [:] |
| | | for entry in catalog { |
| | | guard let bundleIdentifier = entry.bundleIdentifier?.lowercased(), !bundleIdentifier.isEmpty else { |
| | | continue |
| | | } |
| | | if let existing = bundleIndex[bundleIdentifier], existing.rank <= entry.rank { |
| | | continue |
| | | } |
| | | bundleIndex[bundleIdentifier] = entry |
| | | } |
| | | let nameIndex = Dictionary( |
| | | grouping: catalog, |
| | | by: { $0.normalizedName } |
| | | ).compactMapValues { entries in |
| | | entries.sorted { $0.rank < $1.rank }.first |
| | | } |
| | | let catalog = loadCatalogSnapshot() |
| | | |
| | | var seenAssignments = Set<String>() |
| | | var assignments: [SmartAppCategorizationAssignment] = [] |
| | | var unassigned: [SmartUnassignedApp] = [] |
| | | |
| | | for app in apps { |
| | | let matched = match(app: app, bundleIndex: bundleIndex, nameIndex: nameIndex) |
| | | let matched = match( |
| | | app: app, |
| | | bundleIndex: catalog.bundleIndex, |
| | | nameIndex: catalog.nameIndex |
| | | ) |
| | | guard let matched else { |
| | | unassigned.append(SmartUnassignedApp( |
| | | appName: app.name, |
| | |
| | | source: .localCatalog, |
| | | reason: "Matched Smart Start catalog entry: \(matched.name)", |
| | | provenance: matched.sourceEvidence, |
| | | defaultNote: matched.localizedNote, |
| | | defaultNoteCandidates: matched.notes?.values.map { $0 } ?? [] |
| | | defaultNote: matched.localizedNote?.note, |
| | | defaultNoteCandidates: matched.localizedNote.map { [$0.note] } ?? [], |
| | | defaultNoteProvenance: matched.localizedNote?.provenance |
| | | )) |
| | | } |
| | | |
| | |
| | | if let defaultNote = assignment.defaultNote?.trimmingCharacters(in: .whitespacesAndNewlines), |
| | | !defaultNote.isEmpty { |
| | | let currentNote = store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines) |
| | | let knownDefaultNotes = Set(assignment.defaultNoteCandidates.compactMap(normalizedNote)) |
| | | let normalizedCurrentNote = normalizedNote(currentNote) |
| | | let existingMetadata = store.appNoteMetadata[path] |
| | | let currentMatchesCatalogDefault = existingMetadata?.origin == .catalogDefault |
| | | && normalizedCurrentNote.map { TagDatabase.noteFingerprint($0) } == existingMetadata?.catalog?.noteFingerprint |
| | | let knownDefaultNotes = Set(assignment.defaultNoteCandidates.compactMap(normalizedNote)) |
| | | let shouldSeedNote = currentNote?.isEmpty != false |
| | | || currentMatchesCatalogDefault |
| | | || normalizedCurrentNote.map { knownDefaultNotes.contains($0) } == true |
| | | |
| | | if shouldSeedNote { |
| | | store.appNotes[path] = String(defaultNote.prefix(TagDatabase.maxAppNoteLength)) |
| | | if let provenance = assignment.defaultNoteProvenance { |
| | | store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata( |
| | | origin: .catalogDefault, |
| | | catalog: provenance, |
| | | noteFingerprint: provenance.noteFingerprint |
| | | ) |
| | | } |
| | | if currentNote?.isEmpty != false { |
| | | uncommonPaths.insert(path) |
| | | if store.uncommonSources[path] == nil { |
| | |
| | | return 0.68 |
| | | } |
| | | |
| | | private static func loadCatalog() -> [SmartStartCatalogEntry] { |
| | | if let catalog = loadRuntimeCatalog() { |
| | | return catalog |
| | | private static func loadCatalogSnapshot() -> SmartStartCatalogSnapshot { |
| | | let languageCode = L10n.currentCode |
| | | catalogCacheLock.lock() |
| | | if let snapshot = cachedCatalogSnapshots[languageCode] { |
| | | catalogCacheLock.unlock() |
| | | return snapshot |
| | | } |
| | | return loadLegacyCSVCatalog() |
| | | catalogCacheLock.unlock() |
| | | |
| | | guard let base = loadBaseSnapshot() else { |
| | | return SmartStartCatalogSnapshot(entries: [], bundleIndex: [:], nameIndex: [:], entryIndex: [:]) |
| | | } |
| | | let snapshot = makeCatalogSnapshot(base: base, languageCode: languageCode) |
| | | |
| | | catalogCacheLock.lock() |
| | | if let cached = cachedCatalogSnapshots[languageCode] { |
| | | catalogCacheLock.unlock() |
| | | return cached |
| | | } |
| | | cachedCatalogSnapshots[languageCode] = snapshot |
| | | catalogCacheLock.unlock() |
| | | return snapshot |
| | | } |
| | | |
| | | private static func loadRuntimeCatalog() -> [SmartStartCatalogEntry]? { |
| | | guard let url = Bundle.main.url(forResource: "SmartStartUltimateDefaultCatalog", withExtension: "json"), |
| | | let data = try? Data(contentsOf: url), |
| | | let catalog = try? JSONDecoder().decode(SmartStartRuntimeCatalog.self, from: data) |
| | | else { return nil } |
| | | |
| | | return catalog.entries.compactMap { entry in |
| | | private static func makeCatalogSnapshot( |
| | | base: SmartStartBaseSnapshot, |
| | | languageCode: String |
| | | ) -> SmartStartCatalogSnapshot { |
| | | let localizedNotes = loadLocalizedNotes( |
| | | for: languageFallbacks(preferredCode: languageCode, base: base) |
| | | ) |
| | | let entries = base.entries.compactMap { entry -> SmartStartCatalogEntry? in |
| | | let tagIDs = entry.defaultTag |
| | | .compactMap { SmartCategoryID(rawValue: $0) } |
| | | .filter { $0 != .other } |
| | | guard !tagIDs.isEmpty else { return nil } |
| | | |
| | | return SmartStartCatalogEntry( |
| | | entryID: entry.entryID, |
| | | rank: entry.rank ?? Int.max, |
| | | name: entry.name, |
| | | normalizedName: entry.normalizedName.flatMap { |
| | |
| | | } ?? normalizedName(entry.name), |
| | | bundleIdentifier: normalizedBundleIdentifier(entry.bundleIdentifier), |
| | | categoryIDs: uniqueOrdered(tagIDs), |
| | | localizedNote: localizedNote(from: entry.notes), |
| | | notes: entry.notes, |
| | | localizedNote: localizedNotes[entry.entryID], |
| | | sourceEvidence: entry.sourceEvidence ?? [] |
| | | ) |
| | | } |
| | | |
| | | var bundleIndex: [String: SmartStartCatalogEntry] = [:] |
| | | var nameIndex: [String: SmartStartCatalogEntry] = [:] |
| | | var entryIndex: [String: SmartStartCatalogEntry] = [:] |
| | | |
| | | for entry in entries { |
| | | entryIndex[entry.entryID] = entry |
| | | if let bundleIdentifier = entry.bundleIdentifier?.lowercased(), !bundleIdentifier.isEmpty { |
| | | if bundleIndex[bundleIdentifier].map({ $0.rank <= entry.rank }) != true { |
| | | bundleIndex[bundleIdentifier] = entry |
| | | } |
| | | } |
| | | |
| | | private static func loadLegacyCSVCatalog() -> [SmartStartCatalogEntry] { |
| | | guard let url = Bundle.main.url(forResource: "SmartStartAppDefaultTags", withExtension: "csv"), |
| | | let csv = try? String(contentsOf: url, encoding: .utf8) |
| | | else { return [] } |
| | | if nameIndex[entry.normalizedName].map({ $0.rank <= entry.rank }) != true { |
| | | nameIndex[entry.normalizedName] = entry |
| | | } |
| | | } |
| | | |
| | | let rows = parseCSV(csv) |
| | | guard let header = rows.first else { return [] } |
| | | let dataRows = rows.dropFirst() |
| | | return SmartStartCatalogSnapshot( |
| | | entries: entries, |
| | | bundleIndex: bundleIndex, |
| | | nameIndex: nameIndex, |
| | | entryIndex: entryIndex |
| | | ) |
| | | } |
| | | |
| | | return dataRows.compactMap { row in |
| | | let values = Dictionary(uniqueKeysWithValues: header.enumerated().map { index, key in |
| | | (key, index < row.count ? row[index] : "") |
| | | }) |
| | | let tagIDs = splitPipe(values["defaultTagIDs"] ?? "") |
| | | .compactMap { SmartCategoryID(rawValue: $0) } |
| | | .filter { $0 != .other } |
| | | guard !tagIDs.isEmpty else { return nil } |
| | | private static func loadBaseSnapshot() -> SmartStartBaseSnapshot? { |
| | | catalogCacheLock.lock() |
| | | if let snapshot = cachedBaseSnapshot { |
| | | catalogCacheLock.unlock() |
| | | return snapshot |
| | | } |
| | | catalogCacheLock.unlock() |
| | | |
| | | return SmartStartCatalogEntry( |
| | | rank: Int(values["rank"] ?? "") ?? Int.max, |
| | | name: values["name"] ?? "", |
| | | normalizedName: values["normalizedName"].flatMap { |
| | | $0.isEmpty ? nil : $0 |
| | | } ?? normalizedName(values["name"] ?? ""), |
| | | bundleIdentifier: normalizedBundleIdentifier(values["bundleIdentifier"]), |
| | | categoryIDs: uniqueOrdered(tagIDs), |
| | | localizedNote: nil, |
| | | notes: nil, |
| | | sourceEvidence: splitPipe(values["sourceEvidence"] ?? "") |
| | | guard let url = Bundle.main.url( |
| | | forResource: "SmartStartUltimateDefaultCatalog.base", |
| | | withExtension: "json" |
| | | ), |
| | | let data = try? Data(contentsOf: url), |
| | | let catalog = try? JSONDecoder().decode(SmartStartBaseCatalog.self, from: data), |
| | | catalog.resourceFormatVersion == 1 |
| | | else { return nil } |
| | | |
| | | var bundleIndex: [String: SmartStartBaseCatalogEntry] = [:] |
| | | var nameIndex: [String: SmartStartBaseCatalogEntry] = [:] |
| | | var entryIndex: [String: SmartStartBaseCatalogEntry] = [:] |
| | | for entry in catalog.entries { |
| | | entryIndex[entry.entryID] = entry |
| | | if let bundleIdentifier = normalizedBundleIdentifier(entry.bundleIdentifier)?.lowercased(), |
| | | !bundleIdentifier.isEmpty, |
| | | bundleIndex[bundleIdentifier].map({ ($0.rank ?? Int.max) <= (entry.rank ?? Int.max) }) != true { |
| | | bundleIndex[bundleIdentifier] = entry |
| | | } |
| | | let name = entry.normalizedName.flatMap { $0.isEmpty ? nil : $0 } ?? normalizedName(entry.name) |
| | | if nameIndex[name].map({ ($0.rank ?? Int.max) <= (entry.rank ?? Int.max) }) != true { |
| | | nameIndex[name] = entry |
| | | } |
| | | } |
| | | |
| | | let snapshot = SmartStartBaseSnapshot( |
| | | catalogContentVersion: catalog.catalogContentVersion, |
| | | supportedLanguages: catalog.supportedLanguages, |
| | | fallbackLanguages: catalog.fallbackLanguages, |
| | | entries: catalog.entries, |
| | | bundleIndex: bundleIndex, |
| | | nameIndex: nameIndex, |
| | | entryIndex: entryIndex |
| | | ) |
| | | |
| | | catalogCacheLock.lock() |
| | | if let cached = cachedBaseSnapshot { |
| | | catalogCacheLock.unlock() |
| | | return cached |
| | | } |
| | | cachedBaseSnapshot = snapshot |
| | | catalogCacheLock.unlock() |
| | | return snapshot |
| | | } |
| | | |
| | | private static func loadLocalizedNotes(for languageCodes: [String]) -> [String: SmartStartLocalizedNote] { |
| | | var result: [String: SmartStartLocalizedNote] = [:] |
| | | for languageCode in languageCodes { |
| | | guard let notes = loadNotesSnapshot(languageCode: languageCode) else { continue } |
| | | for (entryID, note) in notes.notes where result[entryID] == nil { |
| | | let fingerprint = TagDatabase.noteFingerprint(note) |
| | | result[entryID] = SmartStartLocalizedNote( |
| | | note: note, |
| | | languageCode: notes.languageCode, |
| | | provenance: SmartDefaultNoteProvenance( |
| | | entryID: entryID, |
| | | languageCode: notes.languageCode, |
| | | notesVersion: notes.notesVersion, |
| | | noteFingerprint: fingerprint |
| | | ) |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private static func localizedNote(from notes: [String: String]?, preferredCode: String = L10n.currentCode) -> String? { |
| | | guard let notes, !notes.isEmpty else { return nil } |
| | | let preferredCodes = [ |
| | | preferredCode, |
| | | "en", |
| | | "zh-Hans", |
| | | "zh-Hant" |
| | | ] |
| | | |
| | | for code in preferredCodes { |
| | | if let note = normalizedNote(notes[code]) { |
| | | return note |
| | | } |
| | | return result |
| | | } |
| | | |
| | | return notes.values |
| | | .compactMap(normalizedNote) |
| | | .first |
| | | private static func loadNotesSnapshot(languageCode: String) -> SmartStartNotesSnapshot? { |
| | | catalogCacheLock.lock() |
| | | if let snapshot = cachedNotesSnapshots[languageCode] { |
| | | catalogCacheLock.unlock() |
| | | return snapshot |
| | | } |
| | | catalogCacheLock.unlock() |
| | | |
| | | guard let data = loadNotesCatalogData(languageCode: languageCode), |
| | | let catalog = try? JSONDecoder().decode(SmartStartNotesCatalog.self, from: data), |
| | | catalog.resourceFormatVersion == 1 |
| | | else { return nil } |
| | | |
| | | let notes = Dictionary(uniqueKeysWithValues: catalog.entries.compactMap { entry in |
| | | normalizedNote(entry.note).map { (entry.entryID, $0) } |
| | | }) |
| | | let snapshot = SmartStartNotesSnapshot( |
| | | languageCode: catalog.language, |
| | | notesVersion: catalog.notesVersion, |
| | | notes: notes |
| | | ) |
| | | |
| | | catalogCacheLock.lock() |
| | | if let cached = cachedNotesSnapshots[languageCode] { |
| | | catalogCacheLock.unlock() |
| | | return cached |
| | | } |
| | | cachedNotesSnapshots[languageCode] = snapshot |
| | | catalogCacheLock.unlock() |
| | | return snapshot |
| | | } |
| | | |
| | | private static func loadNotesCatalogData(languageCode: String) -> Data? { |
| | | let resourceName = "SmartStartUltimateDefaultCatalog.notes.\(languageCode)" |
| | | |
| | | if let url = Bundle.main.url(forResource: resourceName, withExtension: "json.deflate"), |
| | | let compressedData = try? Data(contentsOf: url), |
| | | let data = inflatedDeflateData(from: compressedData) { |
| | | return data |
| | | } |
| | | |
| | | guard let url = Bundle.main.url(forResource: resourceName, withExtension: "json") else { |
| | | return nil |
| | | } |
| | | return try? Data(contentsOf: url) |
| | | } |
| | | |
| | | private static func inflatedDeflateData(from compressedData: Data) -> Data? { |
| | | guard !compressedData.isEmpty else { return Data() } |
| | | |
| | | let maxOutputSize = 16 * 1024 * 1024 |
| | | var outputSize = max(compressedData.count * 8, 64 * 1024) |
| | | |
| | | while outputSize <= maxOutputSize { |
| | | let decoded = compressedData.withUnsafeBytes { sourceBuffer -> Data? in |
| | | guard let sourcePointer = sourceBuffer.bindMemory(to: UInt8.self).baseAddress else { |
| | | return nil |
| | | } |
| | | |
| | | let destinationPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: outputSize) |
| | | defer { destinationPointer.deallocate() } |
| | | |
| | | let decodedCount = compression_decode_buffer( |
| | | destinationPointer, |
| | | outputSize, |
| | | sourcePointer, |
| | | compressedData.count, |
| | | nil, |
| | | COMPRESSION_ZLIB |
| | | ) |
| | | |
| | | guard decodedCount > 0 else { return nil } |
| | | return Data(bytes: destinationPointer, count: decodedCount) |
| | | } |
| | | |
| | | if let decoded { |
| | | return decoded |
| | | } |
| | | outputSize *= 2 |
| | | } |
| | | |
| | | return nil |
| | | } |
| | | |
| | | private static func languageFallbacks( |
| | | preferredCode: String, |
| | | base: SmartStartBaseSnapshot |
| | | ) -> [String] { |
| | | var codes: [String] = [] |
| | | func append(_ code: String) { |
| | | guard !code.isEmpty, |
| | | base.supportedLanguages.contains(code), |
| | | !codes.contains(code) |
| | | else { return } |
| | | codes.append(code) |
| | | } |
| | | |
| | | append(preferredCode) |
| | | switch preferredCode { |
| | | case "ar-Najdi": |
| | | append("ar") |
| | | case "nn": |
| | | append("nb") |
| | | append("no") |
| | | case "no": |
| | | append("nb") |
| | | append("nn") |
| | | case "nb": |
| | | append("no") |
| | | append("nn") |
| | | default: |
| | | break |
| | | } |
| | | for code in base.fallbackLanguages { |
| | | append(code) |
| | | } |
| | | append("en") |
| | | append("zh-Hans") |
| | | append("zh-Hant") |
| | | return codes |
| | | } |
| | | |
| | | private static func normalizedNote(_ value: String?) -> String? { |
| | |
| | | var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil |
| | | var onEditNote: ((AppInfo, CGRect) -> Void)? = nil |
| | | var bubbleDisabled: Bool = false |
| | | var showUncommonAppBubbles: Bool = AppDefaults.showUncommonAppBubbles |
| | | var dragResetToken: Int = 0 |
| | | @Binding var hoveredAppItemID: String? |
| | | var onDropApp: ((String, String, Bool) -> Void)? = nil |
| | | |
| | | /// Adaptive columns — auto-fit based on icon size and available width. |
| | |
| | | onBubbleHover: onBubbleHover, |
| | | onEditNote: onEditNote, |
| | | bubbleDisabled: bubbleDisabled, |
| | | showUncommonAppBubbles: showUncommonAppBubbles, |
| | | itemID: "\(group.name)|\(app.path.path)", |
| | | dragResetToken: dragResetToken, |
| | | hoveredAppItemID: $hoveredAppItemID, |
| | | onSelect: { onSelectApp(app) } |
| | | ) |
| | | } |
| | |
| | | } |
| | | .contentShape(Rectangle()) |
| | | .overlay { |
| | | if dragModeActive { |
| | | AppDropTargetView(targetTag: group.name) { path, source, copy in |
| | | onDropApp?(path, source, copy) |
| | | } |
| | | .allowsHitTesting(false) |
| | | } |
| | | } |
| | | .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in |
| | | handleDrop(providers) |
| | | } |
| | |
| | | <true/> |
| | | <key>com.apple.security.files.user-selected.read-write</key> |
| | | <true/> |
| | | <key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key> |
| | | <array> |
| | | <string>/Library/Application Support/Apptag/</string> |
| | | </array> |
| | | </dict> |
| | | </plist> |
| | |
| | | # Apptag Changelog |
| | | # TagLauncher Changelog |
| | | |
| | | ## [7.6.0] — 2026-05-20 |
| | | |
| | | - 重写默认 App 网格主路径:`coloredGridContainer` / `gridContainer` 改为 AppKit-backed 主网格,使用 `NSCollectionView` 管理分组卡片、滚动、图标 hover 和拖拽 drop,默认路径不再依赖 SwiftUI `LazyVStack` / `LazyVGrid` 布局事务 |
| | | - 修复 AppKit-backed 网格容器对齐:用自定义 `NSCollectionViewLayout` 复刻原来的行/span 规则,同一行卡片顶边一致,并按该行最高图标行数统一容器高度 |
| | | - 针对假死报告中的 SwiftUI `AttributeGraph` / Lazy layout 栈,新增主网格热键开关与打开后等待采样验证,20 次 overlay 开关后 CPU 回到空闲水平且采样未再出现 `LazyLayoutViewCache`、`LazyStack`、`ForEachState` |
| | | - 统一运行时品牌标识:本地数据目录改为 `~/Library/Application Support/TagLauncher`,Bundle ID、LaunchAgent、状态栏 autosave 名和 Smart Start 内置目录中的 TagLauncher 自身条目同步改为 TagLauncher 命名空间,并移除不再需要的旧路径沙盒临时例外 |
| | | - 优化 Smart Start 包体:App bundle 只打包 base/manifest 与按语言独立压缩的 notes 资源,构建时拒绝旧单体 JSON、CSV、translation cache、invalid cache 和未压缩 notes 混入包内 |
| | | - 扩展 Smart Start 运行时分类体系,支持新版终极目录中的 GTD、笔记、会议、Office、PDF、API、数据库、DevOps、IDE、终端、窗口管理等更细分类 |
| | | - Smart Start 运行时资源改为 base catalog + 29 个按语言拆分的 notes 文件,正常路径只加载当前语言和必要 fallback,不再长期持有 29 种语言 notes |
| | | - Smart Start 资源生成器新增 Swift runtime 分类兼容门禁、稳定 `entryID`、manifest 和 split resource 输出,避免未知分类被静默丢弃 |
| | | - Smart Start 默认用途备注新增来源 metadata,系统默认备注可随语言切换更新,用户手写备注不会被语言切换覆盖 |
| | | - 打包脚本改为只复制 split Smart Start 资源到 App bundle,不再把旧单体 runtime JSON/CSV 作为首发运行时资源 |
| | | - 移除 Smart Start 终极默认目录中错误的 `thunder` 条目,避免将 Thunder 错分为 VPN/安全网络工具并写入错误用途备注 |
| | | - 优化主界面滚动性能:网格容器改为懒加载,并在滚动期间临时冻结 App hover 放大、用途气泡和容器 hover 高亮 |
| | | - 将主界面的分组结果与网格容器行布局改为缓存状态,避免鼠标 hover、搜索输入等轻量状态变化反复触发分组和布局计算 |
| | | - 优化 Quick Search:打开浮层时复用已加载 App 数据,不再每次打开都触发全量刷新;搜索字段在索引阶段预计算,输入时避免重复规格化、首字母和拼音候选生成 |
| | | - Quick Search 增加 App 本地化显示名索引:读取所有语言 `InfoPlist.strings` 显示名,并用 Spotlight display name 兜底,让 AweSun 这类英文文件名 App 可通过“贝锐向日葵/向日葵”搜索命中 |
| | | - 对含非拉丁文字的 App 显示名启用拉丁化/转写候选,支持 `xiangrikui`、`beirui`、`xrk` 等连续片段和非连续缩写命中;该规则仅限 App 显示名,不扩散到 Bundle ID 或纯拉丁 App 名 |
| | | - 减少 App 图标重绘:只有图标对象或尺寸变化时才触发 `NSView` 重绘 |
| | | - 将当前满意版标记为 `7.6.0`,作为后续性能优化前的稳定归档基线 |
| | | - 保留 7.5.1 的 Quick Search、快捷键状态、Smart Start 目录和文档沉淀成果,不引入新的功能变更 |
| | | - 版本号更新为 `7.6.0`,基线 Build 为 `20260520.1250`;本次性能优化验证 Build 为 `20260520.1400` |
| | | - 版本号更新为 `7.6.0`,基线 Build 为 `20260520.1250`;本次本地化显示名搜索验证 Build 为 `20260520.1452` |
| | | |
| | | ## [7.5.1] — 2026-05-19 |
| | | |
| | |
| | | |
| | | ## [3.1.35] — 2026-05-10 |
| | | |
| | | - 修复 overlay 显示时打开设置窗口不可见的问题:设置窗口会强制浮动在 Apptag 列表之上,确保用户能实时预览设置效果 |
| | | - 修复 overlay 显示时打开设置窗口不可见的问题:设置窗口会强制浮动在 TagLauncher 列表之上,确保用户能实时预览设置效果 |
| | | |
| | | ## [3.1.34] — 2026-05-10 |
| | | |
| | |
| | | |
| | | ## [3.1.28] — 2026-05-10 |
| | | |
| | | - 修复全屏 Space 下 Shift+Option+Space 唤出 Apptag 后 overlay 被全屏应用遮挡的问题:显示时刷新当前屏幕 frame,提升窗口层级并强制前置 |
| | | - 修复全屏 Space 下 Shift+Option+Space 唤出 TagLauncher 后 overlay 被全屏应用遮挡的问题:显示时刷新当前屏幕 frame,提升窗口层级并强制前置 |
| | | |
| | | ## [3.1.27] — 2026-05-07 |
| | | |
| | |
| | | ## [3.1.13] — 2026-05-06 |
| | | |
| | | - 新增全局快捷键设置(Data 页签):点击按钮后按下新快捷键即可更改,支持任意组合键 |
| | | - 菜单栏「Show Apptag」右侧显示当前快捷键 |
| | | - 菜单栏「Show TagLauncher」右侧显示当前快捷键 |
| | | - HotkeyHelper:Carbon keycode → 人类可读字符串(⇧⌥Space, ⌘A, F1 等) |
| | | |
| | | ## [3.1.12] — 2026-05-06 |
| | |
| | | ## [3.1.11] — 2026-05-06 |
| | | |
| | | - General 标签页:改用 Grid 布局替代固定 frame,标签列按内容宽度自动右对齐 |
| | | - 点击 Dock 图标现在等同于菜单栏 "Show Apptag",直接全屏显示 APP 列表 |
| | | - 点击 Dock 图标现在等同于菜单栏 "Show TagLauncher",直接全屏显示 APP 列表 |
| | | |
| | | ## [3.1.10] — 2026-05-06 |
| | | |
| | |
| | | |
| | | ## [2.0] — 2026-05-06 |
| | | |
| | | - **架构级变更**:标签完全脱离 Finder,独立存储在本地 JSON 数据库 `~/Library/Application Support/Apptag/tags.json` |
| | | - **架构级变更**:标签完全脱离 Finder,独立存储在本地 JSON 数据库 `~/Library/Application Support/TagLauncher/tags.json` |
| | | - 首次启动一次性导入 Finder 存量标签(含"Mac自带"),之后不再触碰 Finder xattr |
| | | - 所有 CRUD(assign/rename/delete/setColor)写入本地 DB |
| | | - Preferences → Data 标签页:Export / Import JSON 备份恢复 |
| | |
| | | |
| | | import fs from "node:fs"; |
| | | import path from "node:path"; |
| | | import { spawnSync } from "node:child_process"; |
| | | import crypto from "node:crypto"; |
| | | import { fileURLToPath } from "node:url"; |
| | | |
| | | const scriptDir = path.dirname(fileURLToPath(import.meta.url)); |
| | |
| | | const outputs = { |
| | | reviewCSV: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.csv"), |
| | | runtimeJSON: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.json"), |
| | | runtimeBaseJSON: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.base.json"), |
| | | runtimeManifestJSON: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.manifest.json"), |
| | | report: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_Report.md"), |
| | | duplicateReview: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_DuplicateReview.csv"), |
| | | translationQA: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_TranslationQA.md"), |
| | | sourceNoteTranslationCache: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_SourceNoteTranslationCache.json"), |
| | | }; |
| | | |
| | | const resourceFormatVersion = 1; |
| | | const catalogContentVersion = 2; |
| | | const notesVersion = 1; |
| | | const requiredFallbackLanguages = ["en", "zh-Hans", "zh-Hant"]; |
| | | |
| | | const stableTagOrder = [ |
| | | "browser", |
| | | "communication", |
| | | "productivity", |
| | | "GTD", |
| | | "Notes", |
| | | "Meeting", |
| | | "office", |
| | | "PDF", |
| | | "file-management", |
| | | "transfer", |
| | | "development", |
| | | "design", |
| | | "ai-tools", |
| | | "api-tools", |
| | | "database-tools", |
| | | "devops", |
| | | "ide", |
| | | "runtime-sdk", |
| | | "terminal-tools", |
| | | "Font", |
| | | "ui-prototyping", |
| | | "3d-cad", |
| | | "diagramming", |
| | | "writing", |
| | | "media", |
| | | "video", |
| | |
| | | "picture-photo", |
| | | "utilities", |
| | | "system", |
| | | "system-enhancement", |
| | | "system-maintenance", |
| | | "window-management", |
| | | "device-management", |
| | | "input-tools", |
| | | "Automation", |
| | | "network-tools", |
| | | "entertainment", |
| | | "game", |
| | | "finance", |
| | | "education", |
| | | "ai-tools", |
| | | "security", |
| | | "other", |
| | | ]; |
| | | const stableTags = new Set(stableTagOrder); |
| | | const noteLimit = 80; |
| | | |
| | | const tagerToStableTags = new Map(Object.entries({ |
| | | "3d-cad": ["design"], |
| | | "Automation": ["productivity", "system-enhancement"], |
| | | "Font": ["design", "utilities"], |
| | | "GTD": ["productivity"], |
| | | "Meeting": ["communication", "productivity"], |
| | | "Notes": ["productivity", "writing"], |
| | | "PDF": ["writing", "utilities"], |
| | | "ai-tools": ["ai-tools"], |
| | | "api-tools": ["development"], |
| | | "audio": ["media", "audio"], |
| | | "browser": ["browser"], |
| | | "communication": ["communication"], |
| | | "database-tools": ["development"], |
| | | "design": ["design"], |
| | | "device-management": ["utilities", "system-enhancement"], |
| | | "devops": ["development"], |
| | | "diagramming": ["design"], |
| | | "education": ["education"], |
| | | "entertainment": ["entertainment"], |
| | | "file-management": ["file-management", "utilities"], |
| | | "finance": ["finance"], |
| | | "game": ["entertainment", "game"], |
| | | "ide": ["development"], |
| | | "input-tools": ["utilities", "system-enhancement"], |
| | | "media": ["media"], |
| | | "network-tools": ["utilities"], |
| | | "office": ["productivity", "writing"], |
| | | "picture-photo": ["media", "picture-photo"], |
| | | "runtime-sdk": ["development"], |
| | | "security": ["security"], |
| | | "system": ["system", "utilities"], |
| | | "system-maintenance": ["utilities", "system-enhancement"], |
| | | "terminal-tools": ["development"], |
| | | "transfer": ["transfer", "utilities"], |
| | | "ui-prototyping": ["design"], |
| | | "utilities": ["utilities"], |
| | | "video": ["media", "video"], |
| | | "window-management": ["utilities", "system-enhancement"], |
| | | "writing": ["writing"], |
| | | const translationProvider = "google-translate-gtx-source-notes-20260520"; |
| | | const translationBackend = process.env.SMARTSTART_TRANSLATION_BACKEND || "google-gtx"; |
| | | const lingvaBaseURL = process.env.SMARTSTART_LINGVA_BASE_URL || "https://lingva.ml"; |
| | | const translationBatchSize = Number(process.env.SMARTSTART_TRANSLATION_BATCH_SIZE || 50); |
| | | const translationPauseMS = Number(process.env.SMARTSTART_TRANSLATION_PAUSE_MS || 150); |
| | | const translationRetryLimit = Number(process.env.SMARTSTART_TRANSLATION_RETRY_LIMIT || 4); |
| | | const equivalentTargetLanguages = new Map(Object.entries({ |
| | | "ar-Najdi": ["ar"], |
| | | "nn": ["nb", "no"], |
| | | "no": ["nb", "nn"], |
| | | })); |
| | | |
| | | function parseCSV(text) { |
| | |
| | | |
| | | function readCSVObjects(filePath) { |
| | | const rows = parseCSV(fs.readFileSync(filePath, "utf8")); |
| | | const header = rows[0] ?? []; |
| | | const header = (rows[0] ?? []).map((column, index) => |
| | | index === 0 ? String(column ?? "").replace(/^\uFEFF/, "") : column, |
| | | ); |
| | | return rows.slice(1).map((row) => |
| | | Object.fromEntries(header.map((column, index) => [column, row[index] ?? ""])), |
| | | ); |
| | |
| | | cleaned.push(tag); |
| | | } |
| | | const meaningful = cleaned.filter((tag) => tag !== "other"); |
| | | const finalTags = meaningful.length > 0 ? meaningful : cleaned; |
| | | return stableTagOrder.filter((tag) => finalTags.includes(tag)); |
| | | return meaningful.length > 0 ? meaningful : cleaned; |
| | | } |
| | | |
| | | function normalizeName(value) { |
| | |
| | | .replace(/&/g, " and ") |
| | | .replace(/[^a-z0-9]+/g, "-") |
| | | .replace(/^-+|-+$/g, "") || String(value ?? "").trim().toLowerCase(); |
| | | } |
| | | |
| | | function sha256(value) { |
| | | return crypto.createHash("sha256").update(value).digest("hex"); |
| | | } |
| | | |
| | | function smartStartNotesPath(languageCode) { |
| | | return path.join(outputDir, `SmartStart_UltimateDefaultCatalog.notes.${languageCode}.json`); |
| | | } |
| | | |
| | | function entryIDFor(row) { |
| | | const baseSource = row.bundleIdentifier || row.normalizedName || row.name || "entry"; |
| | | const base = normalizeName(baseSource) || "entry"; |
| | | const identity = [ |
| | | row.bundleIdentifier ?? "", |
| | | row.normalizedName ?? "", |
| | | row.name ?? "", |
| | | ].join("\u0000"); |
| | | return `${base}-${sha256(identity).slice(0, 12)}`; |
| | | } |
| | | |
| | | function loadRuntimeSmartCategoryIDs() { |
| | | const filePath = path.join(projectDir, "Apptag", "SmartCategorization", "SmartCategory.swift"); |
| | | const source = fs.readFileSync(filePath, "utf8"); |
| | | const enumBody = source.match(/enum SmartCategoryID:[\s\S]*?\n}/)?.[0] ?? ""; |
| | | const ids = new Set(); |
| | | const casePattern = /^\s*case\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*=\s*"([^"]+)")?/gm; |
| | | let match; |
| | | while ((match = casePattern.exec(enumBody)) !== null) { |
| | | ids.add(match[2] ?? match[1]); |
| | | } |
| | | return ids; |
| | | } |
| | | |
| | | function normalizedForNoteComparison(value) { |
| | | return String(value ?? "") |
| | | .normalize("NFKD") |
| | | .replace(/[\u0300-\u036f]/g, "") |
| | | .toLowerCase() |
| | | .replace(/[^a-z0-9\u3400-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g, ""); |
| | | } |
| | | |
| | | function noteRepeatsAppName(note, name) { |
| | | const normalizedNameValue = normalizedForNoteComparison(name); |
| | | if (normalizedNameValue.length < 3) return false; |
| | | return normalizedForNoteComparison(note).includes(normalizedNameValue); |
| | | } |
| | | |
| | | function normalizeBundle(value) { |
| | |
| | | return stripTerminalPunctuation(sliced); |
| | | } |
| | | |
| | | function loadCategoryTranslations() { |
| | | const translations = new Map(); |
| | | function normalizeTranslatedNote(value) { |
| | | return truncateNote(value); |
| | | } |
| | | |
| | | function loadSupportedLanguageCodes() { |
| | | const codes = []; |
| | | for (const fileName of fs.readdirSync(sources.localizationDir).sort()) { |
| | | if (!fileName.endsWith(".json")) continue; |
| | | const code = fileName.replace(/\.json$/, ""); |
| | | const raw = JSON.parse(fs.readFileSync(path.join(sources.localizationDir, fileName), "utf8")); |
| | | const categories = {}; |
| | | for (const tag of stableTagOrder) { |
| | | categories[tag] = raw[`smart.category.${tag}`] ?? tag; |
| | | codes.push(fileName.replace(/\.json$/, "")); |
| | | } |
| | | translations.set(code, categories); |
| | | } |
| | | return translations; |
| | | return codes.sort(); |
| | | } |
| | | |
| | | function inferSourceNoteLanguage(note) { |
| | | const value = String(note ?? ""); |
| | | if (/[\u3040-\u30ff]/u.test(value)) return "ja"; |
| | | if (/[\uac00-\ud7af]/u.test(value)) return "ko"; |
| | | return "zh-Hans"; |
| | | function googleLanguageCode(code) { |
| | | switch (code) { |
| | | case "zh-Hans": return "zh-CN"; |
| | | case "zh-Hant": return "zh-TW"; |
| | | case "pt-BR": return "pt"; |
| | | case "sr-Cyrl": return "sr"; |
| | | case "ar-Najdi": return "ar"; |
| | | case "nb": |
| | | case "nn": |
| | | case "no": |
| | | return "no"; |
| | | default: |
| | | return code; |
| | | } |
| | | } |
| | | |
| | | function buildLocalizedNotes(noteZH) { |
| | | if (!noteZH) return {}; |
| | | function sourceNotesFingerprint(sourceNotes) { |
| | | return crypto |
| | | .createHash("sha256") |
| | | .update(JSON.stringify([...sourceNotes].sort())) |
| | | .digest("hex"); |
| | | } |
| | | |
| | | function translationCacheKey(targetLanguage, sourceNote) { |
| | | return ["zh-Hans", targetLanguage, sourceNote].join("\u0001"); |
| | | } |
| | | |
| | | function loadSourceNoteTranslationCache(sourceFingerprint) { |
| | | if (!fs.existsSync(outputs.sourceNoteTranslationCache)) { |
| | | return { |
| | | [inferSourceNoteLanguage(noteZH)]: truncateNote(noteZH), |
| | | version: 1, |
| | | provider: translationProvider, |
| | | sourceFingerprint, |
| | | entries: {}, |
| | | ignoredExistingCache: false, |
| | | }; |
| | | } |
| | | |
| | | const cache = JSON.parse(fs.readFileSync(outputs.sourceNoteTranslationCache, "utf8")); |
| | | if ( |
| | | cache.provider !== translationProvider || |
| | | cache.sourceFingerprint !== sourceFingerprint || |
| | | cache.version !== 1 |
| | | ) { |
| | | return { |
| | | version: 1, |
| | | provider: translationProvider, |
| | | sourceFingerprint, |
| | | entries: {}, |
| | | ignoredExistingCache: true, |
| | | }; |
| | | } |
| | | |
| | | return { |
| | | version: 1, |
| | | provider: translationProvider, |
| | | sourceFingerprint, |
| | | entries: cache.entries ?? {}, |
| | | ignoredExistingCache: false, |
| | | }; |
| | | } |
| | | |
| | | function saveSourceNoteTranslationCache(cache) { |
| | | const sortedEntries = Object.fromEntries( |
| | | Object.entries(cache.entries).sort(([left], [right]) => left.localeCompare(right)), |
| | | ); |
| | | fs.writeFileSync(outputs.sourceNoteTranslationCache, `${JSON.stringify({ |
| | | version: 1, |
| | | provider: translationProvider, |
| | | sourceFingerprint: cache.sourceFingerprint, |
| | | updatedAt: new Date().toISOString(), |
| | | entries: sortedEntries, |
| | | }, null, 2)}\n`); |
| | | } |
| | | |
| | | function sleep(ms) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function parseGoogleTranslation(payload) { |
| | | return (payload?.[0] ?? []) |
| | | .map((segment) => segment?.[0] ?? "") |
| | | .join(""); |
| | | } |
| | | |
| | | async function requestGoogleTranslationPayload(params, attempt = 0) { |
| | | const result = spawnSync("curl", [ |
| | | "-sS", |
| | | "--fail", |
| | | "--compressed", |
| | | "--max-time", |
| | | "45", |
| | | "--retry", |
| | | "2", |
| | | "--retry-delay", |
| | | "1", |
| | | "-A", |
| | | "TagLauncher-CatalogOps/1.0", |
| | | "-H", |
| | | "content-type: application/x-www-form-urlencoded;charset=UTF-8", |
| | | "--data", |
| | | params.toString(), |
| | | "https://translate.googleapis.com/translate_a/single", |
| | | ], { |
| | | encoding: "utf8", |
| | | maxBuffer: 16 * 1024 * 1024, |
| | | }); |
| | | |
| | | if (result.status === 0) { |
| | | try { |
| | | return JSON.parse(result.stdout); |
| | | } catch (error) { |
| | | if (attempt < translationRetryLimit) { |
| | | await sleep(2500 * (attempt + 1)); |
| | | return requestGoogleTranslationPayload(params, attempt + 1); |
| | | } |
| | | throw new Error(`translation response was not JSON: ${error.message}`); |
| | | } |
| | | } |
| | | |
| | | if (attempt < translationRetryLimit) { |
| | | await sleep(2500 * (attempt + 1)); |
| | | return requestGoogleTranslationPayload(params, attempt + 1); |
| | | } |
| | | |
| | | throw new Error(`translation request failed: ${result.stderr || result.stdout || result.error?.message}`); |
| | | } |
| | | |
| | | async function requestLingvaTranslationBatch(notes, targetLanguage, attempt = 0) { |
| | | const targetCode = googleLanguageCode(targetLanguage); |
| | | const query = encodeURIComponent(notes.map((note) => note.replace(/\//g, "/")).join("\n")); |
| | | const result = spawnSync("curl", [ |
| | | "-sS", |
| | | "--fail", |
| | | "--compressed", |
| | | "--max-time", |
| | | "60", |
| | | "--retry", |
| | | "4", |
| | | "--retry-all-errors", |
| | | "--retry-connrefused", |
| | | "--retry-delay", |
| | | "2", |
| | | "-A", |
| | | "TagLauncher-CatalogOps/1.0", |
| | | `${lingvaBaseURL.replace(/\/+$/, "")}/api/v1/zh/${targetCode}/${query}`, |
| | | ], { |
| | | encoding: "utf8", |
| | | maxBuffer: 16 * 1024 * 1024, |
| | | }); |
| | | |
| | | if (result.status === 0) { |
| | | try { |
| | | const payload = JSON.parse(result.stdout); |
| | | if (payload.error) throw new Error(payload.error); |
| | | const translated = String(payload.translation ?? "") |
| | | .replace(/\r\n/g, "\n") |
| | | .replace(/\r/g, "\n"); |
| | | const parts = translated.split("\n").map(normalizeTranslatedNote); |
| | | if (parts.length !== notes.length || parts.some((part) => !part)) { |
| | | throw new Error(`translation batch line mismatch: expected ${notes.length}, got ${parts.length}`); |
| | | } |
| | | return parts; |
| | | } catch (error) { |
| | | if (attempt < translationRetryLimit && isRemoteThrottleError(error)) { |
| | | await sleep(2500 * (attempt + 1)); |
| | | return requestLingvaTranslationBatch(notes, targetLanguage, attempt + 1); |
| | | } |
| | | throw error; |
| | | } |
| | | } |
| | | |
| | | if (attempt < translationRetryLimit) { |
| | | await sleep(2500 * (attempt + 1)); |
| | | return requestLingvaTranslationBatch(notes, targetLanguage, attempt + 1); |
| | | } |
| | | |
| | | throw new Error(`lingva translation request failed: ${result.stderr || result.stdout || result.error?.message}`); |
| | | } |
| | | |
| | | async function requestOnlineTranslationBatch(notes, targetLanguage) { |
| | | if (translationBackend === "lingva") { |
| | | return requestLingvaTranslationBatch(notes, targetLanguage); |
| | | } |
| | | |
| | | const params = new URLSearchParams({ |
| | | client: "gtx", |
| | | sl: googleLanguageCode("zh-Hans"), |
| | | tl: googleLanguageCode(targetLanguage), |
| | | dt: "t", |
| | | q: notes.join("\n"), |
| | | }); |
| | | const translated = parseGoogleTranslation(await requestGoogleTranslationPayload(params)) |
| | | .replace(/\r\n/g, "\n") |
| | | .replace(/\r/g, "\n"); |
| | | const parts = translated.split("\n").map(normalizeTranslatedNote); |
| | | if (parts.length !== notes.length || parts.some((part) => !part)) { |
| | | throw new Error(`translation batch line mismatch: expected ${notes.length}, got ${parts.length}`); |
| | | } |
| | | return parts; |
| | | } |
| | | |
| | | function isRemoteThrottleError(error) { |
| | | const message = String(error?.message ?? ""); |
| | | return message.includes("not JSON") || message.includes("translation request failed"); |
| | | } |
| | | |
| | | async function translateNotesWithSplit(notes, targetLanguage) { |
| | | try { |
| | | return await requestOnlineTranslationBatch(notes, targetLanguage); |
| | | } catch (error) { |
| | | if (isRemoteThrottleError(error) || notes.length <= 1) { |
| | | throw error; |
| | | } |
| | | const middle = Math.ceil(notes.length / 2); |
| | | const left = await translateNotesWithSplit(notes.slice(0, middle), targetLanguage); |
| | | const right = await translateNotesWithSplit(notes.slice(middle), targetLanguage); |
| | | return [...left, ...right]; |
| | | } |
| | | } |
| | | |
| | | function reusableTranslation(cache, targetLanguage, sourceNote) { |
| | | const directKey = translationCacheKey(targetLanguage, sourceNote); |
| | | if (cache.entries[directKey]) return cache.entries[directKey]; |
| | | |
| | | for (const equivalentLanguage of equivalentTargetLanguages.get(targetLanguage) ?? []) { |
| | | const equivalentKey = translationCacheKey(equivalentLanguage, sourceNote); |
| | | if (cache.entries[equivalentKey]) return cache.entries[equivalentKey]; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | async function translateMissingNotes(uniqueSourceNotes, supportedLanguages, cache) { |
| | | let translatedCount = 0; |
| | | let failedCount = 0; |
| | | let reusedEquivalentCount = 0; |
| | | let cacheHits = 0; |
| | | |
| | | for (const sourceNote of uniqueSourceNotes) { |
| | | cache.entries[translationCacheKey("zh-Hans", sourceNote)] = normalizeTranslatedNote(sourceNote); |
| | | } |
| | | |
| | | for (const targetLanguage of supportedLanguages) { |
| | | if (targetLanguage === "zh-Hans" || targetLanguage === "zh-Hant") continue; |
| | | |
| | | const missing = []; |
| | | for (const sourceNote of uniqueSourceNotes) { |
| | | const key = translationCacheKey(targetLanguage, sourceNote); |
| | | if (cache.entries[key]) { |
| | | cacheHits += 1; |
| | | continue; |
| | | } |
| | | const reusable = reusableTranslation(cache, targetLanguage, sourceNote); |
| | | if (reusable) { |
| | | cache.entries[key] = reusable; |
| | | reusedEquivalentCount += 1; |
| | | continue; |
| | | } |
| | | missing.push({ sourceNote, key }); |
| | | } |
| | | |
| | | if (missing.length > 0) { |
| | | console.warn(`Translating ${missing.length} source notes to ${targetLanguage}...`); |
| | | } |
| | | |
| | | for (let index = 0; index < missing.length; index += translationBatchSize) { |
| | | const batch = missing.slice(index, index + translationBatchSize); |
| | | try { |
| | | const translations = await translateNotesWithSplit( |
| | | batch.map((item) => item.sourceNote), |
| | | targetLanguage, |
| | | ); |
| | | translations.forEach((translation, translationIndex) => { |
| | | cache.entries[batch[translationIndex].key] = translation; |
| | | translatedCount += 1; |
| | | }); |
| | | } catch (error) { |
| | | failedCount += batch.length; |
| | | saveSourceNoteTranslationCache(cache); |
| | | throw new Error(`Batch failed for ${targetLanguage}; cached progress saved.\n${error.message}`); |
| | | } |
| | | saveSourceNoteTranslationCache(cache); |
| | | await sleep(translationPauseMS); |
| | | } |
| | | } |
| | | |
| | | saveSourceNoteTranslationCache(cache); |
| | | return { translatedCount, failedCount, reusedEquivalentCount, cacheHits }; |
| | | } |
| | | |
| | | function convertSimplifiedNotesToTraditional(sourceNotes) { |
| | | const notes = sourceNotes.map(normalizeTranslatedNote).filter(Boolean); |
| | | if (notes.length === 0) return new Map(); |
| | | |
| | | const swiftCode = ` |
| | | import Foundation |
| | | |
| | | let inputData = FileHandle.standardInput.readDataToEndOfFile() |
| | | let notes = try JSONDecoder().decode([String].self, from: inputData) |
| | | let converted = notes.map { note -> String in |
| | | let mutable = NSMutableString(string: note) |
| | | CFStringTransform(mutable, nil, "Simplified-Traditional" as CFString, false) |
| | | return mutable as String |
| | | } |
| | | let outputData = try JSONEncoder().encode(converted) |
| | | FileHandle.standardOutput.write(outputData) |
| | | `; |
| | | const result = spawnSync("/usr/bin/swift", ["-e", swiftCode], { |
| | | input: JSON.stringify(notes), |
| | | encoding: "utf8", |
| | | maxBuffer: 64 * 1024 * 1024, |
| | | }); |
| | | |
| | | if (result.error) { |
| | | throw result.error; |
| | | } |
| | | if (result.status !== 0) { |
| | | throw new Error(`zh-Hant conversion failed:\n${result.stderr}`); |
| | | } |
| | | |
| | | const converted = JSON.parse(result.stdout); |
| | | if (!Array.isArray(converted) || converted.length !== notes.length) { |
| | | throw new Error("zh-Hant conversion returned an unexpected result shape"); |
| | | } |
| | | |
| | | return new Map(notes.map((note, index) => [note, normalizeTranslatedNote(converted[index])])); |
| | | } |
| | | |
| | | function buildLocalizedNotes(sourceNote, supportedLanguages, cache, traditionalNote) { |
| | | const cleanSourceNote = normalizeTranslatedNote(sourceNote); |
| | | if (!cleanSourceNote) return {}; |
| | | |
| | | const notes = {}; |
| | | for (const languageCode of supportedLanguages) { |
| | | if (languageCode === "zh-Hans") { |
| | | notes[languageCode] = cleanSourceNote; |
| | | continue; |
| | | } |
| | | if (languageCode === "zh-Hant") { |
| | | const cleanTraditionalNote = normalizeTranslatedNote(traditionalNote); |
| | | if (cleanTraditionalNote) notes[languageCode] = cleanTraditionalNote; |
| | | continue; |
| | | } |
| | | const translatedNote = normalizeTranslatedNote(cache.entries[translationCacheKey(languageCode, cleanSourceNote)]); |
| | | if (translatedNote) notes[languageCode] = translatedNote; |
| | | } |
| | | return Object.fromEntries( |
| | | Object.entries(notes).sort(([left], [right]) => left.localeCompare(right)), |
| | | ); |
| | | } |
| | | |
| | | function writeJSON(filePath, value) { |
| | | const text = `${JSON.stringify(value, null, 2)}\n`; |
| | | fs.writeFileSync(filePath, text); |
| | | return { |
| | | file: path.basename(filePath), |
| | | sha256: sha256(text), |
| | | bytes: Buffer.byteLength(text), |
| | | }; |
| | | } |
| | | |
| | | function buildSplitResources({ finalRows, supportedLanguages, generatedAt, noteLimit }) { |
| | | const baseResource = { |
| | | resourceFormatVersion, |
| | | catalogContentVersion, |
| | | generatedAt, |
| | | noteLimit, |
| | | supportedLanguages, |
| | | fallbackLanguages: requiredFallbackLanguages, |
| | | entries: finalRows.map((row) => ({ |
| | | entryID: row.entryID, |
| | | rank: row.rank, |
| | | name: row.name, |
| | | normalizedName: row.normalizedName, |
| | | bundleIdentifier: row.bundleIdentifier, |
| | | defaultTag: row.tags, |
| | | sourceEvidence: row.sourceEvidence, |
| | | })), |
| | | }; |
| | | const baseInfo = writeJSON(outputs.runtimeBaseJSON, baseResource); |
| | | |
| | | const notesResources = {}; |
| | | for (const languageCode of supportedLanguages) { |
| | | const notesResource = { |
| | | resourceFormatVersion, |
| | | catalogContentVersion, |
| | | notesVersion, |
| | | generatedAt, |
| | | language: languageCode, |
| | | entries: finalRows |
| | | .map((row) => ({ |
| | | entryID: row.entryID, |
| | | note: row.notes[languageCode], |
| | | })) |
| | | .filter((entry) => entry.note), |
| | | }; |
| | | const notesPath = smartStartNotesPath(languageCode); |
| | | notesResources[languageCode] = { |
| | | ...writeJSON(notesPath, notesResource), |
| | | count: notesResource.entries.length, |
| | | }; |
| | | } |
| | | |
| | | const manifest = { |
| | | resourceFormatVersion, |
| | | catalogContentVersion, |
| | | notesVersion, |
| | | generatedAt, |
| | | noteLimit, |
| | | supportedLanguages, |
| | | fallbackLanguages: requiredFallbackLanguages, |
| | | baseResource: { |
| | | ...baseInfo, |
| | | count: baseResource.entries.length, |
| | | }, |
| | | notesResources, |
| | | }; |
| | | const manifestInfo = writeJSON(outputs.runtimeManifestJSON, manifest); |
| | | |
| | | return { |
| | | baseInfo, |
| | | manifestInfo, |
| | | notesResources, |
| | | }; |
| | | } |
| | | |
| | |
| | | const tags = []; |
| | | const unknownTokens = []; |
| | | for (const token of splitTags(value)) { |
| | | const mapped = tagerToStableTags.get(token); |
| | | if (!mapped) { |
| | | if (!stableTags.has(token)) { |
| | | unknownTokens.push(token); |
| | | continue; |
| | | } |
| | | tags.push(...mapped); |
| | | tags.push(token); |
| | | } |
| | | |
| | | return { |
| | | tags: orderedTags(tags.length > 0 ? tags : ["other"]), |
| | | tags: orderedTags(tags), |
| | | unknownTokens, |
| | | }; |
| | | } |
| | |
| | | return filtered; |
| | | } |
| | | |
| | | function build() { |
| | | async function build() { |
| | | fs.mkdirSync(outputDir, { recursive: true }); |
| | | const translations = loadCategoryTranslations(); |
| | | const supportedLanguages = loadSupportedLanguageCodes(); |
| | | const runtimeSmartCategoryIDs = loadRuntimeSmartCategoryIDs(); |
| | | const missingRuntimeCategories = stableTagOrder.filter((tag) => !runtimeSmartCategoryIDs.has(tag)); |
| | | if (missingRuntimeCategories.length > 0) { |
| | | throw new Error( |
| | | `SmartCategoryID is missing generated catalog tags: ${missingRuntimeCategories.join(", ")}`, |
| | | ); |
| | | } |
| | | for (const language of requiredFallbackLanguages) { |
| | | if (!supportedLanguages.includes(language)) { |
| | | throw new Error(`required fallback language is missing from Localization: ${language}`); |
| | | } |
| | | } |
| | | const appleNotes = parseAppleNotes(); |
| | | const previousRuntimeEntries = fs.existsSync(outputs.runtimeJSON) |
| | | ? new Map( |
| | |
| | | bundleChangedVsPrevious: 0, |
| | | missingLocalizedNotes: 0, |
| | | }; |
| | | const curatedRows = dedupeCuratedRows(rawCuratedRows, stats); |
| | | const finalRows = curatedRows.map((row, index) => { |
| | | const curatedRows = rawCuratedRows; |
| | | const preparedRows = curatedRows.map((row, index) => { |
| | | const name = String(row.Name ?? "").trim(); |
| | | const normalizedName = String(row.normalizedName ?? "").trim() || normalizeName(name); |
| | | const bundleIdentifier = normalizeBundle(row.bundleIdentifier); |
| | | const legacyTags = orderedTags(splitTags(row.defaultTag)); |
| | | const mapping = mapCuratedTags(row.tager); |
| | | for (const token of mapping.unknownTokens) unknownTagerTokens.add(token); |
| | | |
| | |
| | | stats.rowsWithSourceNotes += 1; |
| | | } |
| | | |
| | | const notes = buildLocalizedNotes(noteZH); |
| | | const previous = previousRuntimeEntries.get(normalizedName); |
| | | if (previous) { |
| | | if (JSON.stringify(previous.defaultTag ?? []) !== JSON.stringify(mapping.tags)) { |
| | |
| | | normalizedName, |
| | | bundleIdentifier, |
| | | tags: mapping.tags, |
| | | legacyTags, |
| | | noteZH, |
| | | notes, |
| | | sourceEvidence: usedAppleFallback |
| | | ? ["curated_tager_catalog", "apple_default_notes"] |
| | | : ["curated_tager_catalog"], |
| | | }; |
| | | }); |
| | | |
| | | const uniqueSourceNotes = [ |
| | | ...new Set(preparedRows.map((row) => normalizeTranslatedNote(row.noteZH)).filter(Boolean)), |
| | | ].sort((left, right) => left.localeCompare(right)); |
| | | const sourceFingerprint = sourceNotesFingerprint(uniqueSourceNotes); |
| | | const translationCache = loadSourceNoteTranslationCache(sourceFingerprint); |
| | | const traditionalNotesBySource = convertSimplifiedNotesToTraditional(uniqueSourceNotes); |
| | | for (const [sourceNote, traditionalNote] of traditionalNotesBySource.entries()) { |
| | | translationCache.entries[translationCacheKey("zh-Hant", sourceNote)] = traditionalNote; |
| | | } |
| | | const translationStats = await translateMissingNotes(uniqueSourceNotes, supportedLanguages, translationCache); |
| | | if (translationStats.failedCount > 0) { |
| | | throw new Error(`translation failed for ${translationStats.failedCount} source-note/language pairs; JSON generation blocked`); |
| | | } |
| | | |
| | | const finalRows = preparedRows.map((row) => ({ |
| | | ...row, |
| | | entryID: entryIDFor(row), |
| | | notes: buildLocalizedNotes( |
| | | row.noteZH, |
| | | supportedLanguages, |
| | | translationCache, |
| | | traditionalNotesBySource.get(normalizeTranslatedNote(row.noteZH)), |
| | | ), |
| | | })); |
| | | |
| | | const invalidTagRows = finalRows.filter((row) => row.tags.some((tag) => !stableTags.has(tag))); |
| | | const runtimeInvalidTagRows = finalRows.filter((row) => row.tags.some((tag) => !runtimeSmartCategoryIDs.has(tag))); |
| | | const entryIDCounts = new Map(); |
| | | for (const row of finalRows) { |
| | | entryIDCounts.set(row.entryID, (entryIDCounts.get(row.entryID) ?? 0) + 1); |
| | | } |
| | | const duplicateEntryIDs = [...entryIDCounts.entries()].filter(([, count]) => count > 1); |
| | | const emptyNormalizedRows = finalRows.filter((row) => !row.normalizedName); |
| | | const exactOtherRows = finalRows.filter((row) => row.tags.length === 1 && row.tags[0] === "other"); |
| | | const mixedOtherRows = finalRows.filter((row) => row.tags.length > 1 && row.tags.includes("other")); |
| | | const noteRows = finalRows.filter((row) => row.noteZH); |
| | | const actualLocalizedNoteCount = noteRows.reduce( |
| | | (total, row) => total + Object.keys(row.notes).filter((code) => supportedLanguages.includes(code)).length, |
| | | 0, |
| | | ); |
| | | const missingLocalizedNotes = noteRows.length * supportedLanguages.length - actualLocalizedNoteCount; |
| | | const missingLocalizedNoteRows = noteRows.filter( |
| | | (row) => supportedLanguages.some((code) => !row.notes[code]), |
| | | ); |
| | | if (runtimeInvalidTagRows.length > 0) { |
| | | throw new Error( |
| | | `runtime SmartCategoryID cannot decode ${runtimeInvalidTagRows.length} catalog rows`, |
| | | ); |
| | | } |
| | | if (duplicateEntryIDs.length > 0) { |
| | | throw new Error( |
| | | `duplicate Smart Start entryID values: ${duplicateEntryIDs.map(([id]) => id).slice(0, 10).join(", ")}`, |
| | | ); |
| | | } |
| | | const noteQualityIssues = []; |
| | | for (const row of noteRows) { |
| | | for (const code of supportedLanguages) { |
| | | if (!row.notes[code]) { |
| | | noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "missing_translation" }); |
| | | } |
| | | } |
| | | for (const [code, note] of Object.entries(row.notes)) { |
| | | if (charLength(note) > noteLimit) { |
| | | noteQualityIssues.push({ name: row.name, code, issue: `over_limit:${charLength(note)}` }); |
| | | noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: `over_limit:${charLength(note)}` }); |
| | | } else if (stripTerminalPunctuation(note) != note) { |
| | | noteQualityIssues.push({ name: row.name, code, issue: "trailing_punctuation" }); |
| | | noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "trailing_punctuation" }); |
| | | } else if (stripLeadingPunctuation(note) != note) { |
| | | noteQualityIssues.push({ name: row.name, code, issue: "leading_punctuation" }); |
| | | noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "leading_punctuation" }); |
| | | } else if (noteRepeatsAppName(note, row.name)) { |
| | | noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "repeats_app_name" }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | const cleanedCuratedRows = [ |
| | | ["Name", "normalizedName", "defaultTag", "tager", "bundleIdentifier", "defaultNote-ZH"], |
| | | ...curatedRows.map((row) => [ |
| | | String(row.Name ?? "").trim(), |
| | | String(row.normalizedName ?? "").trim() || normalizeName(row.Name ?? ""), |
| | | String(row.defaultTag ?? "").trim(), |
| | | String(row.tager ?? "").trim(), |
| | | normalizeBundle(row.bundleIdentifier) ?? "null", |
| | | truncateNote(row["defaultNote-ZH"]), |
| | | ]), |
| | | ]; |
| | | fs.writeFileSync(outputs.reviewCSV, `${stringifyCSV(cleanedCuratedRows)}\n`); |
| | | |
| | | const runtime = { |
| | | version: 2, |
| | | generatedAt: new Date().toISOString(), |
| | | noteLimit, |
| | | supportedLanguages: [...translations.keys()].sort(), |
| | | supportedLanguages, |
| | | entries: finalRows.map((row) => ({ |
| | | entryID: row.entryID, |
| | | rank: row.rank, |
| | | name: row.name, |
| | | normalizedName: row.normalizedName, |
| | |
| | | })), |
| | | }; |
| | | fs.writeFileSync(outputs.runtimeJSON, `${JSON.stringify(runtime, null, 2)}\n`); |
| | | const splitResources = buildSplitResources({ |
| | | finalRows, |
| | | supportedLanguages, |
| | | generatedAt: runtime.generatedAt, |
| | | noteLimit, |
| | | }); |
| | | |
| | | const duplicateRows = [ |
| | | ["name", "normalizedName", "tager", "legacyDefaultTag", "runtimeDefaultTag", "bundleIdentifier", "defaultNote-ZH"], |
| | | ["name", "normalizedName", "previousRuntimeDefaultTag", "runtimeDefaultTag", "bundleIdentifier", "defaultNote-ZH"], |
| | | ...finalRows |
| | | .filter((row) => row.legacyTags.join("|") !== row.tags.join("|")) |
| | | .filter((row) => { |
| | | const previous = previousRuntimeEntries.get(row.normalizedName); |
| | | return previous && JSON.stringify(previous.defaultTag ?? []) !== JSON.stringify(row.tags); |
| | | }) |
| | | .map((row) => [ |
| | | row.name, |
| | | row.normalizedName, |
| | | (curatedRows[row.rank - 1]?.tager ?? ""), |
| | | row.legacyTags.join("|"), |
| | | (previousRuntimeEntries.get(row.normalizedName)?.defaultTag ?? []).join("|"), |
| | | row.tags.join("|"), |
| | | row.bundleIdentifier ?? "null", |
| | | row.noteZH ?? "", |
| | |
| | | const sourceLines = [ |
| | | "- curated CSV: `Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv`", |
| | | `- curated rows: ${stats.curatedRows}`, |
| | | `- curated rows after alias dedupe: ${curatedRows.length}`, |
| | | `- alias duplicates removed: ${stats.aliasDuplicatesRemoved}`, |
| | | `- curated rows used for JSON: ${curatedRows.length}`, |
| | | `- unknown tager tokens: ${unknownTagerTokens.size}`, |
| | | ].join("\n"); |
| | | const topTagLines = stableTagOrder |
| | |
| | | |
| | | - Review CSV: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv\` |
| | | - Runtime JSON: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json\` |
| | | - Split base JSON: \`Research/SmartStart/UltimateDefaultCatalog/${splitResources.baseInfo.file}\` |
| | | - Split manifest JSON: \`Research/SmartStart/UltimateDefaultCatalog/${splitResources.manifestInfo.file}\` |
| | | - Split notes JSON files: ${Object.keys(splitResources.notesResources).length} |
| | | - Duplicate review: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_DuplicateReview.csv\` |
| | | - Translation QA: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_TranslationQA.md\` |
| | | - Source-note translation cache: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_SourceNoteTranslationCache.json\` |
| | | |
| | | ## Summary |
| | | |
| | |
| | | - Rows with Chinese default notes: ${noteRows.length} |
| | | - Rows with source Chinese notes from curated CSV: ${stats.rowsWithSourceNotes} |
| | | - Rows with Apple note fallback attached: ${stats.appleNotesAttached} |
| | | - Supported languages: ${supportedLanguages.length} |
| | | - Unique source notes: ${uniqueSourceNotes.length} |
| | | - Expected localized notes: ${noteRows.length * supportedLanguages.length} |
| | | - Actual localized notes: ${actualLocalizedNoteCount} |
| | | - Missing localized notes: ${missingLocalizedNotes} |
| | | - Rows missing at least one localized note: ${missingLocalizedNoteRows.length} |
| | | - Source note fingerprint: ${sourceFingerprint} |
| | | - Existing source-note cache ignored: ${translationCache.ignoredExistingCache ? "yes" : "no"} |
| | | - Translation provider: ${translationProvider} |
| | | - Translation backend: ${translationBackend} |
| | | - Translation cache hits before run: ${translationStats.cacheHits} |
| | | - Equivalent-language cache reuses: ${translationStats.reusedEquivalentCount} |
| | | - New non-Chinese translations generated: ${translationStats.translatedCount} |
| | | - Translation failures: ${translationStats.failedCount} |
| | | - zh-Hant generation: macOS CFStringTransform Simplified-Traditional from zh-Hans source notes |
| | | - Invalid tag rows: ${invalidTagRows.length} |
| | | - Runtime-invalid tag rows: ${runtimeInvalidTagRows.length} |
| | | - Duplicate entryID values: ${duplicateEntryIDs.length} |
| | | - Empty normalizedName rows: ${emptyNormalizedRows.length} |
| | | - Exact \`other\` rows: ${exactOtherRows.length} |
| | | - Mixed \`other\` rows after cleanup: ${mixedOtherRows.length} |
| | |
| | | |
| | | ## Notes |
| | | |
| | | - Runtime tags are generated from the curated CSV \`tager\` column, not the legacy \`defaultTag\` column. |
| | | - Runtime notes are generated only from real source notes. The generator must not synthesize notes from app names or category labels. |
| | | - Missing translations are intentionally omitted until a real translation pipeline or reviewed translation table provides them. |
| | | - Runtime JSON is generated strictly from the curated CSV. The generator does not rewrite the source CSV. |
| | | - Runtime tags are copied directly from the curated CSV \`tager\` column. |
| | | - Runtime notes are generated only from curated CSV \`defaultNote-ZH\`: \`zh-Hans\` is the source note, \`zh-Hant\` is converted from it, and all other supported languages are translated from the same source note. |
| | | - The source-note translation cache is only a fingerprint-validated acceleration artifact. Localized tag-summary notes are not valid sources for runtime notes. |
| | | `; |
| | | fs.writeFileSync(outputs.report, report); |
| | | |
| | |
| | | - Supported languages: ${runtime.supportedLanguages.length} |
| | | - Rows with source Chinese notes: ${noteRows.length} |
| | | - Source notes: ${noteRows.length} |
| | | - Required machine/reviewed translations: ${noteRows.length * (runtime.supportedLanguages.length - 1)} |
| | | - Required zh-Hant conversions: ${noteRows.length} |
| | | - Required non-Chinese machine/reviewed translations: ${noteRows.length * (runtime.supportedLanguages.length - 2)} |
| | | - Unique source notes: ${uniqueSourceNotes.length} |
| | | - Expected localized notes including source language: ${noteRows.length * runtime.supportedLanguages.length} |
| | | - Actual localized notes: ${actualLocalizedNoteCount} |
| | | - Missing localized notes: ${missingLocalizedNotes} |
| | | - Rows missing at least one localized note: ${missingLocalizedNoteRows.length} |
| | | - Source note fingerprint: ${sourceFingerprint} |
| | | - Existing source-note cache ignored: ${translationCache.ignoredExistingCache ? "yes" : "no"} |
| | | - Translation provider: ${translationProvider} |
| | | - Translation backend: ${translationBackend} |
| | | - Translation cache hits before run: ${translationStats.cacheHits} |
| | | - Equivalent-language cache reuses: ${translationStats.reusedEquivalentCount} |
| | | - New non-Chinese translations generated: ${translationStats.translatedCount} |
| | | - Translation failures: ${translationStats.failedCount} |
| | | - zh-Hant generation: macOS CFStringTransform Simplified-Traditional from zh-Hans source notes |
| | | - Generated placeholder translations: 0 |
| | | - Note quality issues: ${noteQualityIssues.length} |
| | | - Note limit: ${noteLimit} |
| | |
| | | return { |
| | | finalRows: finalRows.length, |
| | | noteRows: noteRows.length, |
| | | splitBaseBytes: splitResources.baseInfo.bytes, |
| | | splitNotesFiles: Object.keys(splitResources.notesResources).length, |
| | | changedVsPrevious: stats.tagChangedVsPrevious, |
| | | invalidTagRows: invalidTagRows.length, |
| | | runtimeInvalidTagRows: runtimeInvalidTagRows.length, |
| | | duplicateEntryIDs: duplicateEntryIDs.length, |
| | | emptyNormalizedRows: emptyNormalizedRows.length, |
| | | exactOtherRows: exactOtherRows.length, |
| | | mixedOtherRows: mixedOtherRows.length, |
| | | supportedLanguages: supportedLanguages.length, |
| | | uniqueSourceNotes: uniqueSourceNotes.length, |
| | | expectedLocalizedNotes: noteRows.length * supportedLanguages.length, |
| | | actualLocalizedNotes: actualLocalizedNoteCount, |
| | | missingLocalizedNotes, |
| | | sourceFingerprint, |
| | | sourceNoteCacheIgnoredExisting: translationCache.ignoredExistingCache, |
| | | translationCacheHitsBeforeRun: translationStats.cacheHits, |
| | | equivalentLanguageCacheReuses: translationStats.reusedEquivalentCount, |
| | | newNonChineseTranslationsGenerated: translationStats.translatedCount, |
| | | translationFailures: translationStats.failedCount, |
| | | noteQualityIssues: noteQualityIssues.length, |
| | | unknownTagerTokens: [...unknownTagerTokens].sort(), |
| | | outputDir, |
| | | }; |
| | | } |
| | | |
| | | console.log(JSON.stringify(build(), null, 2)); |
| | | console.log(JSON.stringify(await build(), null, 2)); |
| New file |
| | |
| | | { |
| | | "resourceFormatVersion": 1, |
| | | "catalogContentVersion": 2, |
| | | "notesVersion": 1, |
| | | "generatedAt": "2026-05-21T12:52:12.272Z", |
| | | "noteLimit": 80, |
| | | "supportedLanguages": [ |
| | | "ar", |
| | | "ar-Najdi", |
| | | "cs", |
| | | "da", |
| | | "de", |
| | | "en", |
| | | "es", |
| | | "fr", |
| | | "id", |
| | | "it", |
| | | "ja", |
| | | "ko", |
| | | "ms", |
| | | "nb", |
| | | "nl", |
| | | "nn", |
| | | "no", |
| | | "pl", |
| | | "pt-BR", |
| | | "ro", |
| | | "ru", |
| | | "sr-Cyrl", |
| | | "sv", |
| | | "th", |
| | | "tr", |
| | | "uk", |
| | | "vi", |
| | | "zh-Hans", |
| | | "zh-Hant" |
| | | ], |
| | | "fallbackLanguages": [ |
| | | "en", |
| | | "zh-Hans", |
| | | "zh-Hant" |
| | | ], |
| | | "baseResource": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.base.json", |
| | | "sha256": "650dcfba5bbf35816aea987baf6c29cfcab827ce72313a53b1aeb592a02949da", |
| | | "bytes": 1025647, |
| | | "count": 3530 |
| | | }, |
| | | "notesResources": { |
| | | "ar": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ar.json", |
| | | "sha256": "aa62181dfc8e7fa725a38697c3ff04e62afbd3db348c8dc2d29c792db4299c9e", |
| | | "bytes": 642963, |
| | | "count": 3530 |
| | | }, |
| | | "ar-Najdi": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ar-Najdi.json", |
| | | "sha256": "2a179b7f3301d68ac11baf5b24ea4f352001854767e77c2a007a81c126215b4f", |
| | | "bytes": 642969, |
| | | "count": 3530 |
| | | }, |
| | | "cs": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.cs.json", |
| | | "sha256": "ee279e32fdf309a54de26f2a8e6020c73b6b8d622e21bc9ea338b980b68005cc", |
| | | "bytes": 526452, |
| | | "count": 3530 |
| | | }, |
| | | "da": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.da.json", |
| | | "sha256": "a49338d929a60eae3621ff6c3a5d1113588cbdd7c29abc71fb9dc236708e6ecb", |
| | | "bytes": 508571, |
| | | "count": 3530 |
| | | }, |
| | | "de": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.de.json", |
| | | "sha256": "651c9f73a184ae64fb4d48c33ab2957bbc9bc599bc4ebb7030d577adbb1e47f6", |
| | | "bytes": 507531, |
| | | "count": 3530 |
| | | }, |
| | | "en": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.en.json", |
| | | "sha256": "70066831bb47b3db8a128586ab3718002c33a67776b86df35ee40df9be8c1a03", |
| | | "bytes": 490288, |
| | | "count": 3530 |
| | | }, |
| | | "es": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.es.json", |
| | | "sha256": "2850d6155b85466f7ec7fe12043566f1e33a17c9a0b4a7fea42ff3d9e5eb75db", |
| | | "bytes": 520116, |
| | | "count": 3530 |
| | | }, |
| | | "fr": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.fr.json", |
| | | "sha256": "5f73f2825962be92ff8f9832a9ccb6e6be80f8e798e9e820edab84c894094bd3", |
| | | "bytes": 517955, |
| | | "count": 3530 |
| | | }, |
| | | "id": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.id.json", |
| | | "sha256": "e267035ac1bd3ff41f4527f8a095b6477a1d3b30ae2918ac778565590e9ec971", |
| | | "bytes": 498449, |
| | | "count": 3530 |
| | | }, |
| | | "it": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.it.json", |
| | | "sha256": "bc17e00af5b73e64735e6a3fe99f82378c70ece1d61b82443e3331b5a535b3a9", |
| | | "bytes": 514420, |
| | | "count": 3530 |
| | | }, |
| | | "ja": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ja.json", |
| | | "sha256": "fe60e5986e5c71d2e0ab9c9e191b143caa5444e04b0c448c641921ec5a97b1b9", |
| | | "bytes": 593411, |
| | | "count": 3530 |
| | | }, |
| | | "ko": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ko.json", |
| | | "sha256": "a33d9c6a38f2c6005e3d12cc856229d6f76a3cc48cf44ca3aeeb107c91d51baa", |
| | | "bytes": 530870, |
| | | "count": 3530 |
| | | }, |
| | | "ms": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ms.json", |
| | | "sha256": "b52eb038e8824651f23687fde98776e3e972b6874c1708243e25c3fa1d43e8f4", |
| | | "bytes": 499338, |
| | | "count": 3530 |
| | | }, |
| | | "nb": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.nb.json", |
| | | "sha256": "e48d0f31ce453d73a3bb471738f1efaf37089e449054331983fc895dc3e87730", |
| | | "bytes": 503605, |
| | | "count": 3530 |
| | | }, |
| | | "nl": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.nl.json", |
| | | "sha256": "fceecd6c12f9192ba37ac5a2946389750d49c6bec5da225fb421878502c6ccf8", |
| | | "bytes": 501730, |
| | | "count": 3530 |
| | | }, |
| | | "nn": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.nn.json", |
| | | "sha256": "5f65ede5cbcfc257baf4e9fb216b0b9e777868d6e2b8f07bdff3b5cc4ae59f1f", |
| | | "bytes": 503605, |
| | | "count": 3530 |
| | | }, |
| | | "no": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.no.json", |
| | | "sha256": "643f2c80f8d81b351aaf43bf7d1fcea1004b38c8bd2b4e981ad6b685c59c1fc6", |
| | | "bytes": 503605, |
| | | "count": 3530 |
| | | }, |
| | | "pl": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.pl.json", |
| | | "sha256": "5e5dfd0e7bcae767cb7012983e703e8f78845291186849031e5a4005a3adeed8", |
| | | "bytes": 522662, |
| | | "count": 3530 |
| | | }, |
| | | "pt-BR": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.pt-BR.json", |
| | | "sha256": "92574cce7fd2c9f2b4ca557562c43f496fe7e1adf087aad30dc5d540197434af", |
| | | "bytes": 519826, |
| | | "count": 3530 |
| | | }, |
| | | "ro": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ro.json", |
| | | "sha256": "33a7fba4c5173537b7728e31b999996354644bbc51b1e62946c7b7f59999b7a5", |
| | | "bytes": 521453, |
| | | "count": 3530 |
| | | }, |
| | | "ru": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.ru.json", |
| | | "sha256": "627ee961ab1a7a97b03dcaaba9f1d580007a60ea2485921b00f47cf38bd99588", |
| | | "bytes": 723763, |
| | | "count": 3530 |
| | | }, |
| | | "sr-Cyrl": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.sr-Cyrl.json", |
| | | "sha256": "8a29563ab8cef2544b39dadf43079d82b72bfdb07ccd2aeebe507dfb66345fd0", |
| | | "bytes": 702212, |
| | | "count": 3530 |
| | | }, |
| | | "sv": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.sv.json", |
| | | "sha256": "a2c45d3fe12c5624b90356cce76b2b8a6b12482c0619c2b7095c1409e6882ba0", |
| | | "bytes": 506811, |
| | | "count": 3530 |
| | | }, |
| | | "th": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.th.json", |
| | | "sha256": "938d00696a97e52c1d45bdcdd006ac281ce5bcb2d022b43f990e7cbb62c43bf4", |
| | | "bytes": 902458, |
| | | "count": 3530 |
| | | }, |
| | | "tr": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.tr.json", |
| | | "sha256": "2c9edd6f4720963e1b957fc2f2ab1135751420330b6aed3ee86dc750355b10da", |
| | | "bytes": 522673, |
| | | "count": 3530 |
| | | }, |
| | | "uk": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.uk.json", |
| | | "sha256": "b6dd043d1d3b0ce30c7b60a895426edcb6da2753f81210a14f3daddebef27c6f", |
| | | "bytes": 713776, |
| | | "count": 3530 |
| | | }, |
| | | "vi": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.vi.json", |
| | | "sha256": "ad93b020bd5815ee3da00cfa1003fb34f81d656c19125e122b2117b015405771", |
| | | "bytes": 569123, |
| | | "count": 3530 |
| | | }, |
| | | "zh-Hans": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.zh-Hans.json", |
| | | "sha256": "d4497072e0d45792da7aef44fa32a98eb98594d9c8833828d250981ab6832490", |
| | | "bytes": 456457, |
| | | "count": 3530 |
| | | }, |
| | | "zh-Hant": { |
| | | "file": "SmartStart_UltimateDefaultCatalog.notes.zh-Hant.json", |
| | | "sha256": "61c3e6e71382c1107259ff3746a99b35731f66df04a2bdaccb20d03adbadad9c", |
| | | "bytes": 456457, |
| | | "count": 3530 |
| | | } |
| | | } |
| | | } |
| | |
| | | -framework AppKit \ |
| | | -framework SwiftUI \ |
| | | -framework Carbon \ |
| | | -framework CoreServices \ |
| | | -lcompression \ |
| | | -sdk "$SDK_PATH" \ |
| | | -target "$TARGET" \ |
| | | -Osize \ |
| | |
| | | cp -r "$SWIFT_DIR/Localization" "$RESOURCES_DIR/Localization" |
| | | |
| | | echo "==> Copying Smart Start catalog..." |
| | | if [ -f "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json" ]; then |
| | | cp "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.json" |
| | | SMARTSTART_SOURCE_DIR="$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog" |
| | | for required in \ |
| | | "SmartStart_UltimateDefaultCatalog.base.json" \ |
| | | "SmartStart_UltimateDefaultCatalog.manifest.json" \ |
| | | "SmartStart_UltimateDefaultCatalog.notes.en.json" \ |
| | | "SmartStart_UltimateDefaultCatalog.notes.zh-Hans.json"; do |
| | | if [ ! -f "$SMARTSTART_SOURCE_DIR/$required" ]; then |
| | | echo "❌ Missing Smart Start split resource: $required" |
| | | echo " Run: node Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs" |
| | | exit 1 |
| | | fi |
| | | if [ -f "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv" ]; then |
| | | cp "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.csv" |
| | | fi |
| | | if [ -f "$PROJECT_DIR/Research/SmartStart/AppDefaultTags_Review.csv" ]; then |
| | | cp "$PROJECT_DIR/Research/SmartStart/AppDefaultTags_Review.csv" "$RESOURCES_DIR/SmartStartAppDefaultTags.csv" |
| | | done |
| | | python3 - "$SMARTSTART_SOURCE_DIR/SmartStart_UltimateDefaultCatalog.base.json" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.base.json" <<'PY' |
| | | import json |
| | | import sys |
| | | source, destination = sys.argv[1], sys.argv[2] |
| | | with open(source, "r", encoding="utf-8") as f: |
| | | data = json.load(f) |
| | | with open(destination, "w", encoding="utf-8") as f: |
| | | json.dump(data, f, ensure_ascii=False, separators=(",", ":")) |
| | | PY |
| | | python3 - "$SMARTSTART_SOURCE_DIR/SmartStart_UltimateDefaultCatalog.manifest.json" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.manifest.json" <<'PY' |
| | | import json |
| | | import sys |
| | | source, destination = sys.argv[1], sys.argv[2] |
| | | with open(source, "r", encoding="utf-8") as f: |
| | | data = json.load(f) |
| | | with open(destination, "w", encoding="utf-8") as f: |
| | | json.dump(data, f, ensure_ascii=False, separators=(",", ":")) |
| | | PY |
| | | for notes_file in "$SMARTSTART_SOURCE_DIR"/SmartStart_UltimateDefaultCatalog.notes.*.json; do |
| | | [ -e "$notes_file" ] || continue |
| | | output_name="$(basename "$notes_file" | sed 's/SmartStart_UltimateDefaultCatalog/SmartStartUltimateDefaultCatalog/').deflate" |
| | | python3 - "$notes_file" "$RESOURCES_DIR/$output_name" <<'PY' |
| | | import json |
| | | import sys |
| | | import zlib |
| | | source, destination = sys.argv[1], sys.argv[2] |
| | | with open(source, "r", encoding="utf-8") as f: |
| | | data = json.load(f) |
| | | payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8") |
| | | compressor = zlib.compressobj(level=9, wbits=-15) |
| | | compressed = compressor.compress(payload) + compressor.flush() |
| | | with open(destination, "wb") as f: |
| | | f.write(compressed) |
| | | PY |
| | | done |
| | | |
| | | if find "$RESOURCES_DIR" -maxdepth 1 -type f \( \ |
| | | -name '*TranslationCache*' -o \ |
| | | -name '*invalid*' -o \ |
| | | -name 'SmartStartUltimateDefaultCatalog.json' -o \ |
| | | -name 'SmartStartUltimateDefaultCatalog.csv' -o \ |
| | | -name 'SmartStartUltimateDefaultCatalog.notes.*.json' \ |
| | | \) | grep -q .; then |
| | | echo "❌ Forbidden Smart Start artifact copied into app bundle" |
| | | find "$RESOURCES_DIR" -maxdepth 1 -type f \( \ |
| | | -name '*TranslationCache*' -o \ |
| | | -name '*invalid*' -o \ |
| | | -name 'SmartStartUltimateDefaultCatalog.json' -o \ |
| | | -name 'SmartStartUltimateDefaultCatalog.csv' -o \ |
| | | -name 'SmartStartUltimateDefaultCatalog.notes.*.json' \ |
| | | \) -print |
| | | exit 1 |
| | | fi |
| | | |
| | | echo "==> Embedding PkgInfo..." |
| | |
| | | #!/usr/bin/env python3 |
| | | """Generate Apptag app icon — two overlapping tags (red + blue) on macOS squircle.""" |
| | | """Generate TagLauncher app icon — two overlapping tags (red + blue) on macOS squircle.""" |
| | | from PIL import Image, ImageDraw, ImageFilter |
| | | import math, os, subprocess, tempfile |
| | | |