Release Apptag 6.0.5 build 610
| | |
| | | # Apptag — Mac App Store 提审资料 |
| | | |
| | | > 版本: 5.7.8 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-15 |
| | | > 版本: 6.0.5 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-17 |
| | | |
| | | --- |
| | | |
| | |
| | | | **App 名称** | Apptag | |
| | | | **副标题** | Tag-Based App Launcher | |
| | | | **Bundle ID** | com.apptag.launcher | |
| | | | **版本号** | 5.7.8 | |
| | | | **Build 号** | 583 | |
| | | | **版本号** | 6.0.5 | |
| | | | **Build 号** | 610 | |
| | | | **SKU** | apptag-mac-001 | |
| | | | **主要类别** | Utilities (工具) | |
| | | | **次要类别** | Productivity (效率) | |
| | |
| | | ## 8. What's New (本次版本更新说明) |
| | | |
| | | ``` |
| | | Apptag 5.7.8 |
| | | Apptag 6.0.5 |
| | | |
| | | - Fixed duplicate overlay keyboard event monitors that could accumulate after repeatedly opening TagLauncher |
| | | - Removed obsolete tag reorder fallback handlers that could interfere with click handling after drag sorting |
| | | - Reduced unnecessary Dock refreshes when unrelated preferences change |
| | | - Made the menu bar icon mandatory and removed the Show Menu Bar Icon preference across all localizations |
| | | - Removed unused macOS app-menu items and added a localized Show App List command with symbol-only shortcut glyphs |
| | | - Replaced the default macOS Services/Hide menu groups and made the menu bar status item use a fixed visible icon slot |
| | | - Replaced the menu bar SF Symbol with a code-drawn template icon so the status item remains visible even if symbol rendering fails |
| | | - Improved the General preferences layout for long localized text by using two-column style choices and aligned setting rows |
| | | - Refined the app-list style chooser so Flat spans the first row and the remaining four styles form a balanced 2x2 grid |
| | | - Reduced the app-list style option height and added compact visual glyphs for flat, masonry, and grid layouts |
| | | - Added a symbol-only shortcut hint on the Language, Data, and About preference tabs for Option + Shift + Space |
| | | - Fixed the menu bar icon visibility issue |
| | | - Fixed Preferences placement when opened above TagLauncher from a fullscreen app Space |
| | | - Fixed tag navigation reorder mode so clicking elsewhere exits the shaking state |
| | | - Expanded localization support to 29 language variants |
| | | - Updated the Language preferences tab to a two-column layout for better readability |
| | | - Added long-press drag sorting for tag navigation in normal app-list views |
| | | - Tag sorting now works from top, left, or right tag navigation positions across all 5 app-list styles |
| | | - Tag reorder animations now update both the tag navigation and matching app containers |
| | | - Added automatic migration for legacy non-sandbox tag databases into the App Store sandbox container |
| | | - Added a dedicated Language tab in Preferences with live language switching |
| | | - Preferences now opens centered above the active TagLauncher overlay, including multi-display setups |
| | | - Improved tag reordering in edit mode with live visual feedback and synchronized group layout updates |
| | | - Renamed backup actions to Export/Import Categories & Layout across all supported languages |
| | | - Added Mac App Store sandbox entitlements and App Store build validation |
| | | - Hid Launch at Login controls in sandboxed App Store builds |
| | | - Removed deprecated app launch API usage |
| | | - Version kept at 5.7.8, Build updated to 583 |
| | | - Added an Uncommon app marker for apps you are still learning, with optional hover notes |
| | | - New apps can be marked as Uncommon automatically and are cleared after enough launches from TagLauncher |
| | | - Added local note editing for Uncommon apps; notes stay in the exported JSON even if the marker is removed later |
| | | - Added an Uncommon bubble toggle in the Data preferences tab, enabled by default |
| | | - Reworked batch category editing into clear Add Tags and Remove Tags modes |
| | | - Fixed batch editing so selected apps no longer inherit unrelated tags from each other |
| | | - Added clear confirmation feedback showing the real number of tag links added or removed |
| | | - Improved Remove Tags mode with red delete indicators and safer label truncation |
| | | - Fixed edit-sidebar layout so long localized hints no longer stretch the page |
| | | - Removed the edit-page center title and gave more room to the action hint and confirmation button |
| | | - Improved shortcut hints in Language, Data, and About settings pages using symbol-only key graphics |
| | | - Fixed a crash caused by toolbar hint layout constraints in the edit screen |
| | | - Fixed Safari and other modern macOS system apps being missed when they live behind Cryptex system paths |
| | | - Kept duplicate app icons out by resolving symlinks for scanning deduplication |
| | | - Version updated to 6.0.5, Build updated to 610 |
| | | ``` |
| | | |
| | | --- |
| | |
| | | var sourceTag: String? = nil |
| | | var dragModeActive: Bool = false |
| | | var onDragModeChange: ((Bool) -> Void)? = nil |
| | | var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil |
| | | var onEditNote: ((AppInfo, CGRect) -> Void)? = nil |
| | | let onSelect: () -> Void |
| | | |
| | | @State private var isHovered = false |
| | | @State private var wiggle = false |
| | | @State private var globalFrame: CGRect = .zero |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = true |
| | | |
| | | static let hoverScale: CGFloat = 1.22 |
| | | static let labelHeight: CGFloat = 14 |
| | |
| | | .padding(.vertical, 8) |
| | | .padding(.horizontal, 4) |
| | | .frame(width: Self.stableWidth(iconSize: iconSize), height: Self.stableHeight(iconSize: iconSize)) |
| | | .background( |
| | | GeometryReader { proxy in |
| | | Color.clear |
| | | .onAppear { globalFrame = proxy.frame(in: .global) } |
| | | .onChange(of: proxy.frame(in: .global)) { _, newFrame in |
| | | globalFrame = newFrame |
| | | } |
| | | } |
| | | ) |
| | | .contentShape(Rectangle()) |
| | | .onHover { hovering in |
| | | isHovered = hovering |
| | | if showUncommonAppBubbles && app.isUncommon { |
| | | onBubbleHover?(app, globalFrame, hovering) |
| | | } |
| | | } |
| | | .contextMenu { |
| | | if app.isUncommon { |
| | | Button(tr("appNote.edit")) { |
| | | onEditNote?(app, globalFrame) |
| | | } |
| | | } |
| | | } |
| | | .rotationEffect(.degrees(dragModeActive ? (wiggle ? 2.0 : -2.0) : 0)) |
| | | .animation( |
| | |
| | | } |
| | | } |
| | | |
| | | enum BubblePlacement { |
| | | case above |
| | | case below |
| | | } |
| | | |
| | | struct AppNameBubble: View { |
| | | let appName: String |
| | | let note: String? |
| | | let isEditing: Bool |
| | | let placement: BubblePlacement |
| | | @Binding var draftNote: String |
| | | var noteFocused: FocusState<Bool>.Binding |
| | | let onCommit: () -> Void |
| | | let onCancel: () -> Void |
| | | |
| | | private var displayNote: String { |
| | | note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" |
| | | } |
| | | |
| | | var body: some View { |
| | | content |
| | | .onExitCommand(perform: onCancel) |
| | | } |
| | | |
| | | private var content: some View { |
| | | VStack(spacing: isEditing || !displayNote.isEmpty ? 8 : 0) { |
| | | Text(appName) |
| | | .font(.system(size: isEditing ? 22 : 24, weight: .bold)) |
| | | .foregroundStyle(.white) |
| | | .lineLimit(1) |
| | | .truncationMode(.middle) |
| | | .frame(maxWidth: .infinity) |
| | | |
| | | if isEditing { |
| | | VStack(alignment: .trailing, spacing: 4) { |
| | | TextField(tr("appNote.placeholder"), text: limitedDraft) |
| | | .textFieldStyle(.plain) |
| | | .font(.system(size: 15, weight: .medium)) |
| | | .foregroundStyle(.white) |
| | | .padding(.horizontal, 12) |
| | | .frame(height: 36) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 9) |
| | | .fill(Color.white.opacity(0.12)) |
| | | ) |
| | | .focused(noteFocused) |
| | | .onSubmit(onCommit) |
| | | Text("\(draftNote.count) / \(TagDatabase.maxAppNoteLength)") |
| | | .font(.system(size: 10, weight: .medium)) |
| | | .foregroundStyle(.white.opacity(0.35)) |
| | | } |
| | | } else if !displayNote.isEmpty { |
| | | Text(displayNote) |
| | | .font(.system(size: 14, weight: .medium)) |
| | | .lineSpacing(2) |
| | | .foregroundStyle(.white.opacity(0.78)) |
| | | .lineLimit(4) |
| | | .multilineTextAlignment(.center) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | .frame(maxWidth: .infinity) |
| | | } |
| | | } |
| | | .padding(.horizontal, isEditing ? 22 : 24) |
| | | .padding(.vertical, isEditing ? 18 : 16) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 18, style: .continuous) |
| | | .fill(Color.black.opacity(0.92)) |
| | | .shadow(color: .black.opacity(0.34), radius: 24, y: 16) |
| | | .shadow(color: .black.opacity(0.18), radius: 8, y: 3) |
| | | ) |
| | | } |
| | | |
| | | private var limitedDraft: Binding<String> { |
| | | Binding( |
| | | get: { draftNote }, |
| | | set: { draftNote = String($0.prefix(TagDatabase.maxAppNoteLength)) } |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private struct DraggableAppIconView: NSViewRepresentable { |
| | | let icon: NSImage |
| | | let iconSize: CGFloat |
| | |
| | | @AppStorage("hideAppNames") private var hideAppNames = false |
| | | @AppStorage("showDockIcon") private var showDockIcon = false |
| | | @AppStorage("launchAtLogin") private var launchAtLogin = true |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = true |
| | | @State private var selectedLanguage = L10n.currentCode |
| | | @State private var isRefreshingLanguage = false |
| | | @State private var allApps: [AppInfo] = [] |
| | |
| | | |
| | | // Tab 4: Data |
| | | VStack(spacing: 0) { |
| | | Spacer(minLength: 96) |
| | | Spacer(minLength: 54) |
| | | |
| | | VStack(alignment: .leading, spacing: 5) { |
| | | Toggle(tr("settings.uncommonBubble"), isOn: $showUncommonAppBubbles) |
| | | .font(.system(size: 13, weight: .medium)) |
| | | Text(tr("settings.uncommonBubbleDesc")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | } |
| | | .frame(width: 420, alignment: .leading) |
| | | |
| | | Spacer(minLength: 28) |
| | | |
| | | VStack(alignment: .leading, spacing: 8) { |
| | | Text(tr("settings.backup")) |
| | |
| | | case editingApps |
| | | } |
| | | |
| | | enum EditTagOperation { |
| | | case add |
| | | case remove |
| | | } |
| | | |
| | | // MARK: - Native NSTextField (avoids SwiftUI TextField event issues) |
| | | |
| | | /// Custom container that wraps NSTextField so that hitTest returns the container |
| | |
| | | |
| | | // MARK: - Full-Screen Overlay |
| | | |
| | | private struct AppBubbleContext { |
| | | let app: AppInfo |
| | | let frame: CGRect |
| | | } |
| | | |
| | | private struct EditActionFeedback: Identifiable { |
| | | let id = UUID() |
| | | let title: String |
| | | let message: String |
| | | } |
| | | |
| | | private struct EditActionFeedbackBubble: View { |
| | | let title: String |
| | | let message: String |
| | | let onClose: () -> Void |
| | | |
| | | var body: some View { |
| | | VStack(alignment: .leading, spacing: 14) { |
| | | HStack(alignment: .top, spacing: 12) { |
| | | Text(title) |
| | | .font(.system(size: 24, weight: .bold)) |
| | | .foregroundStyle(.white) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | |
| | | Button(action: onClose) { |
| | | Image(systemName: "xmark") |
| | | .font(.system(size: 12, weight: .bold)) |
| | | .foregroundStyle(.white.opacity(0.82)) |
| | | .frame(width: 26, height: 26) |
| | | .background( |
| | | Circle() |
| | | .fill(Color.white.opacity(0.12)) |
| | | ) |
| | | } |
| | | .buttonStyle(.plain) |
| | | } |
| | | |
| | | ScrollView(.vertical, showsIndicators: true) { |
| | | Text(message) |
| | | .font(.system(size: 15, weight: .medium)) |
| | | .lineSpacing(3) |
| | | .foregroundStyle(.white.opacity(0.84)) |
| | | .multilineTextAlignment(.leading) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | } |
| | | .frame(maxHeight: 220) |
| | | } |
| | | .padding(.horizontal, 24) |
| | | .padding(.vertical, 20) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 18, style: .continuous) |
| | | .fill(Color.black.opacity(0.92)) |
| | | .shadow(color: .black.opacity(0.34), radius: 24, y: 16) |
| | | .shadow(color: .black.opacity(0.18), radius: 8, y: 3) |
| | | ) |
| | | } |
| | | } |
| | | |
| | | struct ContentView: View { |
| | | let hideOverlay: () -> Void |
| | | |
| | |
| | | @State private var editPhase: EditPhase = .none |
| | | @State private var selectedAppPaths: Set<URL> = [] |
| | | @State private var selectedTagNames: Set<String> = [] |
| | | @State private var successToast: String? = nil |
| | | @State private var removableTagNames: Set<String> = [] |
| | | @State private var editTagOperation: EditTagOperation = .add |
| | | @State private var editActionFeedback: EditActionFeedback? = nil |
| | | @State private var draggedTagNames: [String] = [] // live drag order |
| | | @State private var dragItem: String? = nil // currently dragged tag |
| | | @State private var tagReorderFrames: [String: CGRect] = [:] |
| | |
| | | @State private var dropRefreshVisible = false |
| | | @State private var dropRefreshStartedAt: Date? = nil |
| | | @State private var layoutRefreshID = UUID() |
| | | @State private var hoveredBubble: AppBubbleContext? = nil |
| | | @State private var editingBubble: AppBubbleContext? = nil |
| | | @State private var bubbleDraftNote = "" |
| | | @FocusState private var bubbleNoteFocused: Bool |
| | | |
| | | // Configurable defaults |
| | | @AppStorage("defaultGroupName") private var defaultGroupName = "Other" |
| | |
| | | @State private var notchHeight: CGFloat = 0 |
| | | @AppStorage("displayMode") private var displayMode = "flat" |
| | | @AppStorage("hideAppNames") private var hideAppNames = false |
| | | |
| | | private let editSidebarWidth: CGFloat = 188 |
| | | private let editSidebarHorizontalInset: CGFloat = 12 |
| | | |
| | | private var isSideLayout: Bool { |
| | | tagPosition == "left" || tagPosition == "right" |
| | |
| | | case .editingApps: |
| | | editAppsView |
| | | } |
| | | |
| | | uncommonAppBubbleOverlay |
| | | editActionFeedbackOverlay |
| | | |
| | | if let message = dropWarningToast { |
| | | Text(message) |
| | |
| | | } |
| | | .onChange(of: editPhase) { _, newPhase in |
| | | let active = newPhase != .none |
| | | dismissAppBubble() |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherEditModeChanged, |
| | | object: nil, |
| | | userInfo: ["active": active] |
| | | ) |
| | | } |
| | | .onChange(of: bubbleNoteFocused) { _, focused in |
| | | if editingBubble != nil && !focused { |
| | | commitBubbleNote() |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | .id(layoutRefreshID) |
| | | } |
| | | |
| | | private var uncommonAppBubbleOverlay: some View { |
| | | GeometryReader { proxy in |
| | | if let context = editingBubble ?? hoveredBubble { |
| | | let rootFrame = proxy.frame(in: .global) |
| | | let editing = editingBubble != nil |
| | | let width = min(editing ? 440 : 520, max(260, proxy.size.width - 48)) |
| | | let placement = bubblePlacement(for: context.frame, rootFrame: rootFrame) |
| | | let metrics = bubbleMetrics( |
| | | for: context, |
| | | rootFrame: rootFrame, |
| | | rootSize: proxy.size, |
| | | width: width, |
| | | placement: placement, |
| | | isEditing: editing |
| | | ) |
| | | |
| | | AppNameBubble( |
| | | appName: context.app.name, |
| | | note: currentNote(for: context.app), |
| | | isEditing: editing, |
| | | placement: placement, |
| | | draftNote: $bubbleDraftNote, |
| | | noteFocused: $bubbleNoteFocused, |
| | | onCommit: commitBubbleNote, |
| | | onCancel: dismissAppBubble |
| | | ) |
| | | .frame(width: width) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | .position(x: metrics.centerX, y: metrics.centerY) |
| | | .transition(.scale(scale: 0.94).combined(with: .opacity)) |
| | | .zIndex(500) |
| | | } |
| | | } |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(editingBubble != nil) |
| | | } |
| | | |
| | | private var flatGrid: some View { |
| | | ScrollViewReader { proxy in |
| | | ScrollView { |
| | |
| | | showNames: !hideAppNames, |
| | | dragModeActive: appDragModeActive, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | onDropApp: { path, source, copy in |
| | | dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy) |
| | | } |
| | |
| | | sourceTag: group.name, |
| | | dragModeActive: appDragModeActive, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | onSelect: { openApp(app) } |
| | | ) |
| | | } |
| | |
| | | .shadow(color: .black.opacity((isColored && isHovered) || isColorlessActive ? 0.22 : 0), |
| | | radius: (isColored && isHovered) || isColorlessActive ? 18 : 0, |
| | | y: (isColored && isHovered) || isColorlessActive ? 10 : 0) |
| | | .zIndex(isHovered ? 50 : 0) |
| | | .scaleEffect(isColored && isHovered ? 1.015 : 1.0) |
| | | .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isHovered) |
| | | .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isColorlessFilled) |
| | |
| | | sourceTag: group.name, |
| | | dragModeActive: appDragModeActive, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onBubbleHover: handleBubbleHover, |
| | | onEditNote: beginEditingBubbleNote, |
| | | onSelect: { openApp(app) } |
| | | ) |
| | | .frame(width: cellWidth) |
| | |
| | | .shadow(color: .black.opacity((isColored && isHovered) || isColorlessGridActive ? 0.22 : 0), |
| | | radius: (isColored && isHovered) || isColorlessGridActive ? 18 : 0, |
| | | y: (isColored && isHovered) || isColorlessGridActive ? 10 : 0) |
| | | .zIndex(isHovered ? 50 : 0) |
| | | .scaleEffect(isColored && isHovered ? 1.015 : 1.0) |
| | | .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isHovered) |
| | | .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isColorlessFilled) |
| | |
| | | |
| | | private var editAppsView: some View { |
| | | VStack(spacing: 0) { |
| | | HStack { |
| | | Button { cancelEditApps(); setEditPhase(.none) } label: { |
| | | Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right") |
| | | .font(.system(size: 12)) |
| | | HStack(alignment: .center, spacing: 14) { |
| | | HStack(alignment: .center, spacing: 12) { |
| | | Button { cancelEditApps(); setEditPhase(.none) } label: { |
| | | Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right") |
| | | .font(.system(size: 12)) |
| | | } |
| | | .buttonStyle(.bordered) |
| | | |
| | | operationHeaderPicker |
| | | } |
| | | .buttonStyle(.bordered) |
| | | Spacer() |
| | | Text(tr("edit.title")).font(.headline) |
| | | Spacer() |
| | | Button(tr("edit.confirm")) { confirmAssign() } |
| | | |
| | | Spacer(minLength: 16) |
| | | |
| | | headerModeHintView |
| | | |
| | | Button(editConfirmTitle) { confirmAssign() } |
| | | .buttonStyle(.borderedProminent) |
| | | .disabled(selectedAppPaths.isEmpty) |
| | | .disabled(selectedAppPaths.isEmpty || pendingOperationTagNames.isEmpty) |
| | | .fixedSize(horizontal: true, vertical: false) |
| | | .layoutPriority(2) |
| | | } |
| | | .padding(.horizontal, 24) |
| | | .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20) |
| | |
| | | |
| | | HStack(spacing: 0) { |
| | | VStack(alignment: .leading, spacing: 4) { |
| | | Text(tr("edit.selectTags")).font(.caption).foregroundStyle(.secondary).padding(.bottom, 4) |
| | | Text(tr("edit.dragHint")).font(.caption2).foregroundStyle(.tertiary).padding(.bottom, 2) |
| | | VStack(alignment: .leading, spacing: 6) { |
| | | Text(tr("edit.selectTags")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .lineLimit(2) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | .multilineTextAlignment(.leading) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | |
| | | Text(tr("edit.dragHint")) |
| | | .font(.caption2) |
| | | .foregroundStyle(.tertiary) |
| | | .lineLimit(nil) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | .multilineTextAlignment(.leading) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | } |
| | | .padding(.horizontal, editSidebarHorizontalInset) |
| | | .padding(.top, 4) |
| | | .padding(.bottom, 4) |
| | | .frame(width: editSidebarWidth, alignment: .leading) |
| | | |
| | | ScrollView(.vertical, showsIndicators: false) { |
| | | VStack(spacing: 4) { |
| | | ForEach(sortedTagNames, id: \.self) { tagName in |
| | |
| | | tagReorderFrames = frames |
| | | } |
| | | } |
| | | .frame(width: 155) |
| | | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) |
| | | } |
| | | .frame(width: editSidebarWidth, alignment: .topLeading) |
| | | Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1) |
| | | |
| | | if allApps.isEmpty { |
| | |
| | | } |
| | | } |
| | | |
| | | if let msg = successToast { |
| | | Text(msg).font(.headline) |
| | | .padding(.horizontal, 20).padding(.vertical, 10) |
| | | .background(RoundedRectangle(cornerRadius: 10).fill(.ultraThickMaterial)) |
| | | .transition(.move(edge: .bottom).combined(with: .opacity)) |
| | | .onAppear { |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { |
| | | withAnimation { successToast = nil } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private var editAppsGrid: some View { |
| | | ScrollView { |
| | | LazyVStack(alignment: .leading, spacing: 24) { |
| | | ForEach(groups) { group in |
| | | ForEach(editGroups) { group in |
| | | editFlatGroup(group) |
| | | } |
| | | } |
| | | .padding(20) |
| | | } |
| | | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) |
| | | } |
| | | |
| | | private func editFlatGroup(_ group: TagGroup) -> some View { |
| | |
| | | ) { |
| | | ForEach(group.apps) { app in editableAppItem(app) } |
| | | } |
| | | .frame(minHeight: group.apps.isEmpty ? max(52, iconSize + 12) : 0) |
| | | } |
| | | } |
| | | |
| | | private func selectableTagItem(_ tagName: String) -> some View { |
| | | let isSelected = selectedTagNames.contains(tagName) |
| | | let isRemovableCandidate = removableTagNames.contains(tagName) |
| | | let isEnabled = editTagOperation == .add || isRemovableCandidate |
| | | let showsPendingRemoval = editTagOperation == .remove && isRemovableCandidate && !isSelected |
| | | let isUncommon = tagName == TagDatabase.uncommonTagKey |
| | | let colorIndex = isUncommon ? 0 : (tagColors[tagName] ?? 0) |
| | | return HStack(spacing: 6) { |
| | | Circle() |
| | | .fill(isSelected ? Color.accentColor : Color.secondary.opacity(0.3)) |
| | | .fill(isSelected ? Color.accentColor : Color.secondary.opacity(isEnabled ? 0.3 : 0.18)) |
| | | .frame(width: 16, height: 16) |
| | | .overlay(isSelected ? Image(systemName: "checkmark").font(.system(size: 8, weight: .bold)).foregroundStyle(.white) : nil) |
| | | Text(tagName).font(.system(size: 13, weight: .medium)).foregroundStyle(.primary) |
| | | Text(displayTagName(tagName)) |
| | | .font(.system(size: 13, weight: isUncommon ? .semibold : .medium)) |
| | | .foregroundStyle(isEnabled ? Color.primary : Color.secondary.opacity(0.66)) |
| | | .lineLimit(1) |
| | | .truncationMode(.tail) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | HStack(spacing: 4) { |
| | | if isUncommon { |
| | | Image(systemName: "questionmark.bubble.fill") |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(isEnabled ? Color.secondary : Color.secondary.opacity(0.55)) |
| | | .frame(width: 16, height: 16) |
| | | } |
| | | if showsPendingRemoval { |
| | | Image(systemName: "trash.fill") |
| | | .font(.system(size: 11, weight: .bold)) |
| | | .foregroundStyle(.red) |
| | | .frame(width: 16, height: 16) |
| | | .transition(.scale(scale: 0.85).combined(with: .opacity)) |
| | | } else if editTagOperation == .remove { |
| | | Color.clear.frame(width: 16, height: 16) |
| | | } |
| | | } |
| | | .frame( |
| | | minWidth: editTagOperation == .remove |
| | | ? (isUncommon ? 36 : 16) |
| | | : (isUncommon ? 16 : 0), |
| | | alignment: .trailing |
| | | ) |
| | | .layoutPriority(1) |
| | | } |
| | | .padding(.horizontal, 10).padding(.vertical, 5) |
| | | .padding(.horizontal, 10) |
| | | .frame(height: 27) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | .background(RoundedRectangle(cornerRadius: 6) |
| | | .fill(Color(nsColor: TagColor.nsColor(for: tagColors[tagName] ?? 0).withAlphaComponent(0.3)))) |
| | | .fill(Color(nsColor: TagColor.nsColor(for: colorIndex).withAlphaComponent(isEnabled ? (isUncommon ? 0.22 : 0.3) : 0.12)))) |
| | | .contentShape(RoundedRectangle(cornerRadius: 6)) |
| | | .onTapGesture { |
| | | guard isEnabled else { return } |
| | | if isSelected { selectedTagNames.remove(tagName) } else { selectedTagNames.insert(tagName) } |
| | | } |
| | | .opacity(isEnabled ? 1.0 : 0.58) |
| | | .animation(.easeInOut(duration: 0.15), value: showsPendingRemoval) |
| | | } |
| | | |
| | | private func tagReorderGesture(for tagName: String) -> some Gesture { |
| | | DragGesture(minimumDistance: 4, coordinateSpace: .named("tagReorderList")) |
| | | .onChanged { value in |
| | | guard canReorderTag(tagName) else { return } |
| | | if dragItem == nil { |
| | | dragItem = tagName |
| | | } |
| | |
| | | reorderDraggedTag(at: value.location) |
| | | } |
| | | .onEnded { _ in |
| | | guard canReorderTag(tagName) else { return } |
| | | dragItem = nil |
| | | TagEditor.reorderTags(draggedTagNames) |
| | | } |
| | |
| | | } |
| | | |
| | | private func confirmAssign() { |
| | | guard !selectedAppPaths.isEmpty else { return } |
| | | let paths = selectedAppPaths.map { $0.path } |
| | | let tags = sortedTagNames.filter { selectedTagNames.contains($0) } |
| | | TagEditor.setTags(tags, to: paths) |
| | | selectedAppPaths = []; selectedTagNames = [] |
| | | let selectedApps = allApps.filter { selectedAppPaths.contains($0.path) } |
| | | let tags = pendingOperationTagNames |
| | | guard !selectedApps.isEmpty, !tags.isEmpty else { return } |
| | | |
| | | let paths = selectedApps.map { $0.path.path } |
| | | let feedback = buildEditActionFeedback(for: selectedApps, tags: tags) |
| | | |
| | | switch editTagOperation { |
| | | case .add: |
| | | TagEditor.appendTags(tags, to: paths) |
| | | case .remove: |
| | | TagEditor.removeTags(tags, from: paths) |
| | | } |
| | | selectedAppPaths = [] |
| | | selectedTagNames = [] |
| | | removableTagNames = [] |
| | | refreshApps() |
| | | withAnimation { successToast = tr("edit.success") } |
| | | showEditActionFeedback(feedback) |
| | | } |
| | | |
| | | private func cancelEditApps() { |
| | | selectedAppPaths = []; selectedTagNames = [] |
| | | selectedAppPaths = [] |
| | | selectedTagNames = [] |
| | | removableTagNames = [] |
| | | editTagOperation = .add |
| | | } |
| | | |
| | | private func editableAppItem(_ app: AppInfo) -> some View { |
| | |
| | | } |
| | | |
| | | private func syncSelectedTagsFromSelectedApps() { |
| | | let selectedApps = allApps.filter { selectedAppPaths.contains($0.path) } |
| | | guard !selectedApps.isEmpty else { |
| | | guard !selectedAppPaths.isEmpty else { |
| | | selectedTagNames = [] |
| | | removableTagNames = [] |
| | | return |
| | | } |
| | | |
| | | let editableTags = Set(sortedTagNames) |
| | | if selectedApps.count == 1 { |
| | | selectedTagNames = Set(selectedApps[0].tags.filter { editableTags.contains($0) }) |
| | | guard editTagOperation == .remove else { |
| | | removableTagNames = [] |
| | | return |
| | | } |
| | | |
| | | selectedTagNames = selectedApps.reduce(into: Set<String>()) { result, app in |
| | | result.formUnion(app.tags.filter { editableTags.contains($0) }) |
| | | let selectedApps = allApps.filter { selectedAppPaths.contains($0.path) } |
| | | let autoSelectedTags = selectedApps.reduce(into: Set<String>()) { result, app in |
| | | result.formUnion(app.tags) |
| | | if app.isUncommon { |
| | | result.insert(TagDatabase.uncommonTagKey) |
| | | } |
| | | } |
| | | removableTagNames = autoSelectedTags |
| | | selectedTagNames = autoSelectedTags |
| | | } |
| | | |
| | | private var operationHeaderPicker: some View { |
| | | HStack(spacing: 8) { |
| | | Text(tr("edit.operationLabel")) |
| | | .font(.system(size: 12, weight: .medium)) |
| | | .foregroundStyle(.secondary) |
| | | |
| | | HStack(spacing: 6) { |
| | | operationModeButton(.add, titleKey: "edit.modeAdd", systemImage: "plus.circle.fill") |
| | | operationModeButton(.remove, titleKey: "edit.modeRemove", systemImage: "minus.circle.fill") |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func operationModeButton(_ mode: EditTagOperation, titleKey: String, systemImage: String) -> some View { |
| | | let isActive = editTagOperation == mode |
| | | return Button { |
| | | setEditTagOperation(mode) |
| | | } label: { |
| | | Label(tr(titleKey), systemImage: systemImage) |
| | | .font(.system(size: 12, weight: .semibold)) |
| | | .padding(.horizontal, 12) |
| | | .padding(.vertical, 6) |
| | | .foregroundStyle(isActive ? Color.white : Color.primary) |
| | | .background( |
| | | Capsule(style: .continuous) |
| | | .fill(isActive ? Color.accentColor : Color.secondary.opacity(0.12)) |
| | | ) |
| | | } |
| | | .buttonStyle(.plain) |
| | | } |
| | | |
| | | private var editConfirmTitle: String { |
| | | switch editTagOperation { |
| | | case .add: |
| | | return tr("edit.confirmAdd") |
| | | case .remove: |
| | | return tr("edit.confirmRemove") |
| | | } |
| | | } |
| | | |
| | | private func setEditTagOperation(_ mode: EditTagOperation) { |
| | | guard editTagOperation != mode else { return } |
| | | editTagOperation = mode |
| | | if mode == .add { |
| | | selectedTagNames = [] |
| | | removableTagNames = [] |
| | | } else { |
| | | syncSelectedTagsFromSelectedApps() |
| | | } |
| | | } |
| | | |
| | | private var pendingOperationTagNames: [String] { |
| | | switch editTagOperation { |
| | | case .add: |
| | | return sortedTagNames.filter { selectedTagNames.contains($0) } |
| | | case .remove: |
| | | return sortedTagNames.filter { removableTagNames.contains($0) && !selectedTagNames.contains($0) } |
| | | } |
| | | } |
| | | |
| | | private var editModeHintText: String { |
| | | editTagOperation == .remove ? tr("edit.removeHint") : tr("edit.modeHint") |
| | | } |
| | | |
| | | private var headerModeHintView: some View { |
| | | Text(editModeHintText) |
| | | .font(.system(size: 12, weight: .medium)) |
| | | .foregroundStyle(.secondary) |
| | | .lineLimit(1) |
| | | .truncationMode(.middle) |
| | | .frame(minWidth: 0, maxWidth: 760, alignment: .trailing) |
| | | .layoutPriority(1) |
| | | .help(editModeHintText) |
| | | } |
| | | |
| | | private var editActionFeedbackOverlay: some View { |
| | | GeometryReader { proxy in |
| | | if let feedback = editActionFeedback { |
| | | ZStack { |
| | | Color.black.opacity(0.08) |
| | | .ignoresSafeArea() |
| | | |
| | | EditActionFeedbackBubble( |
| | | title: feedback.title, |
| | | message: feedback.message, |
| | | onClose: dismissEditActionFeedback |
| | | ) |
| | | .frame(width: min(760, max(360, proxy.size.width - 120))) |
| | | .position(x: proxy.size.width / 2, y: proxy.size.height / 2) |
| | | .transition(.scale(scale: 0.94).combined(with: .opacity)) |
| | | } |
| | | .zIndex(700) |
| | | } |
| | | } |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(editActionFeedback != nil) |
| | | } |
| | | |
| | | private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback { |
| | | let appNames = selectedApps.map(\.name) |
| | | .joined(separator: localizedListSeparator) |
| | | |
| | | switch editTagOperation { |
| | | case .add: |
| | | let actualAddCount = selectedApps.reduce(into: 0) { count, app in |
| | | count += tags.reduce(into: 0) { subtotal, tagName in |
| | | if !appHasTag(app, tagName: tagName) { |
| | | subtotal += 1 |
| | | } |
| | | } |
| | | } |
| | | |
| | | return EditActionFeedback( |
| | | title: tr("edit.confirmAdd"), |
| | | message: formattedFeedbackMessage( |
| | | forKey: "edit.feedbackAddFormat", |
| | | replacements: [ |
| | | "%appCount%": "\(selectedApps.count)", |
| | | "%appNames%": appNames, |
| | | "%tagCount%": "\(actualAddCount)" |
| | | ] |
| | | ) |
| | | ) |
| | | |
| | | case .remove: |
| | | let actualRemovedTagNames = tags.filter { tagName in |
| | | selectedApps.contains(where: { appHasTag($0, tagName: tagName) }) |
| | | } |
| | | let actualRemoveCount = selectedApps.reduce(into: 0) { count, app in |
| | | count += tags.reduce(into: 0) { subtotal, tagName in |
| | | if appHasTag(app, tagName: tagName) { |
| | | subtotal += 1 |
| | | } |
| | | } |
| | | } |
| | | |
| | | return EditActionFeedback( |
| | | title: tr("edit.confirmRemove"), |
| | | message: formattedFeedbackMessage( |
| | | forKey: "edit.feedbackRemoveFormat", |
| | | replacements: [ |
| | | "%appCount%": "\(selectedApps.count)", |
| | | "%appNames%": appNames, |
| | | "%tagCount%": "\(actualRemoveCount)", |
| | | "%tagNames%": actualRemovedTagNames |
| | | .map(displayTagName) |
| | | .joined(separator: localizedListSeparator) |
| | | ] |
| | | ) |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private func appHasTag(_ app: AppInfo, tagName: String) -> Bool { |
| | | if tagName == TagDatabase.uncommonTagKey { |
| | | return app.isUncommon |
| | | } |
| | | return app.tags.contains(tagName) |
| | | } |
| | | |
| | | private func formattedFeedbackMessage(forKey key: String, replacements: [String: String]) -> String { |
| | | replacements.reduce(tr(key)) { partial, replacement in |
| | | partial.replacingOccurrences(of: replacement.key, with: replacement.value) |
| | | } |
| | | } |
| | | |
| | | private func showEditActionFeedback(_ feedback: EditActionFeedback) { |
| | | let feedbackID = feedback.id |
| | | withAnimation(.spring(response: 0.24, dampingFraction: 0.82)) { |
| | | editActionFeedback = feedback |
| | | } |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 2.6) { |
| | | guard editActionFeedback?.id == feedbackID else { return } |
| | | dismissEditActionFeedback() |
| | | } |
| | | } |
| | | |
| | | private func dismissEditActionFeedback() { |
| | | withAnimation(.easeOut(duration: 0.18)) { |
| | | editActionFeedback = nil |
| | | } |
| | | } |
| | | |
| | | private var localizedListSeparator: String { |
| | | switch L10n.currentCode { |
| | | case "zh-Hans", "zh-Hant", "ja": |
| | | return "、" |
| | | case "ar", "ar-Najdi": |
| | | return "، " |
| | | default: |
| | | return ", " |
| | | } |
| | | } |
| | | |
| | |
| | | // Use user-defined order, fall back to alpha |
| | | let ordered = draggedTagNames.filter { filtered.contains($0) } |
| | | let remaining = filtered.filter { !ordered.contains($0) }.sorted() |
| | | return ordered + remaining |
| | | return ordered + remaining + [TagDatabase.uncommonTagKey] |
| | | } |
| | | |
| | | private func displayTagName(_ tagName: String) -> String { |
| | | tagName == TagDatabase.uncommonTagKey ? tr("group.uncommon") : tagName |
| | | } |
| | | |
| | | private var tagLabels: [TagLabel] { |
| | |
| | | } |
| | | } |
| | | |
| | | private var editGroups: [TagGroup] { |
| | | groups + [ |
| | | TagGroup( |
| | | name: tr("group.uncommon"), |
| | | apps: allApps.filter(\.isUncommon).sorted { |
| | | $0.name.localizedStandardCompare($1.name) == .orderedAscending |
| | | } |
| | | ) |
| | | ] |
| | | } |
| | | |
| | | // MARK: - Actions |
| | | |
| | | private func handleBubbleHover(app: AppInfo, frame: CGRect, hovering: Bool) { |
| | | guard editingBubble == nil else { return } |
| | | if hovering { |
| | | hoveredBubble = AppBubbleContext(app: app, frame: frame) |
| | | } else if hoveredBubble?.app.path == app.path { |
| | | hoveredBubble = nil |
| | | } |
| | | } |
| | | |
| | | private func beginEditingBubbleNote(app: AppInfo, frame: CGRect) { |
| | | bubbleDraftNote = currentNote(for: app) |
| | | hoveredBubble = nil |
| | | editingBubble = AppBubbleContext(app: app, frame: frame) |
| | | DispatchQueue.main.async { |
| | | bubbleNoteFocused = true |
| | | } |
| | | } |
| | | |
| | | private func commitBubbleNote() { |
| | | guard let context = editingBubble else { return } |
| | | let limited = String(bubbleDraftNote.prefix(TagDatabase.maxAppNoteLength)) |
| | | .trimmingCharacters(in: .whitespacesAndNewlines) |
| | | TagEditor.setAppNote(limited, for: context.app.path.path) |
| | | bubbleDraftNote = limited |
| | | editingBubble = nil |
| | | bubbleNoteFocused = false |
| | | refreshApps() |
| | | } |
| | | |
| | | private func dismissAppBubble() { |
| | | hoveredBubble = nil |
| | | editingBubble = nil |
| | | bubbleNoteFocused = false |
| | | } |
| | | |
| | | private func currentNote(for app: AppInfo) -> String { |
| | | if let latest = allApps.first(where: { $0.path == app.path })?.note { |
| | | return latest |
| | | } |
| | | return app.note ?? "" |
| | | } |
| | | |
| | | private func bubblePlacement(for frame: CGRect, rootFrame: CGRect) -> BubblePlacement { |
| | | frame.minY - rootFrame.minY < 170 ? .below : .above |
| | | } |
| | | |
| | | private func bubbleMetrics( |
| | | for context: AppBubbleContext, |
| | | rootFrame: CGRect, |
| | | rootSize: CGSize, |
| | | width: CGFloat, |
| | | placement: BubblePlacement, |
| | | isEditing: Bool |
| | | ) -> (centerX: CGFloat, centerY: CGFloat) { |
| | | let appCenterX = context.frame.midX - rootFrame.minX |
| | | let halfWidth = width / 2 |
| | | let centerX = min(max(appCenterX, halfWidth + 24), max(halfWidth + 24, rootSize.width - halfWidth - 24)) |
| | | let estimatedHeight = estimatedBubbleHeight(for: context.app, width: width, isEditing: isEditing) |
| | | let topY: CGFloat |
| | | if placement == .below { |
| | | topY = context.frame.maxY - rootFrame.minY + 10 |
| | | } else { |
| | | topY = context.frame.minY - rootFrame.minY - estimatedHeight - 10 |
| | | } |
| | | let clampedTopY = min(max(topY, 14), max(14, rootSize.height - estimatedHeight - 14)) |
| | | return (centerX, clampedTopY + estimatedHeight / 2) |
| | | } |
| | | |
| | | private func estimatedBubbleHeight(for app: AppInfo, width: CGFloat, isEditing: Bool) -> CGFloat { |
| | | if isEditing { |
| | | return 120 |
| | | } |
| | | let note = currentNote(for: app).trimmingCharacters(in: .whitespacesAndNewlines) |
| | | guard !note.isEmpty else { return 68 } |
| | | let contentWidth = max(160, width - 48) |
| | | let approxCharsPerLine = max(16, Int(contentWidth / 14)) |
| | | let lines = min(4, max(1, Int(ceil(Double(note.count) / Double(approxCharsPerLine))))) |
| | | return 70 + CGFloat(lines) * 20 |
| | | } |
| | | |
| | | func scrollTo(_ id: String) { |
| | | withAnimation(.easeInOut(duration: 0.25)) { scrollProxy?.scrollTo(id, anchor: .top) } |
| | |
| | | |
| | | func refreshApps(forceLayoutRefresh: Bool = false) { |
| | | DispatchQueue.global(qos: .userInitiated).async { |
| | | var apps = AppIndexer.scan() |
| | | let store = TagDatabase.load() |
| | | apps = TagEditor.annotate(apps: apps) |
| | | let scannedApps = AppIndexer.scan() |
| | | let store = TagEditor.reconcileScannedApps(scannedApps) |
| | | let apps = TagEditor.annotate(apps: scannedApps, store: store) |
| | | let colors = store.tags.mapValues { $0.color } |
| | | let order = TagEditor.orderedTagNames() |
| | | DispatchQueue.main.async { |
| | |
| | | appDragModeActive = false |
| | | endTagNavReorder() |
| | | hideOverlay() |
| | | DispatchQueue.global(qos: .utility).async { |
| | | TagEditor.recordLauncherOpen(for: app.path.path) |
| | | } |
| | | let configuration = NSWorkspace.OpenConfiguration() |
| | | NSWorkspace.shared.openApplication(at: app.path, configuration: configuration) |
| | | } |
| | |
| | | let tags: [String] |
| | | let bundleIdentifier: String? |
| | | let icon: NSImage // Pre-loaded during background scan |
| | | var isUncommon: Bool = false |
| | | var note: String? = nil |
| | | |
| | | /// True if this is an Apple pre-installed app. |
| | | var isAppleApp: Bool { |
| | | if let bid = bundleIdentifier, bid.hasPrefix("com.apple.") { return true } |
| | | return path.path.hasPrefix("/System/Applications/") |
| | | return AppIndexer.isSystemAppPath(path.path) |
| | | } |
| | | |
| | | func hash(into hasher: inout Hasher) { hasher.combine(path) } |
| | |
| | | static let searchPaths: [URL] = [ |
| | | URL(fileURLWithPath: "/Applications"), |
| | | URL(fileURLWithPath: "/System/Applications"), |
| | | URL(fileURLWithPath: "/System/Cryptexes/App/System/Applications"), |
| | | URL(fileURLWithPath: "/System/Volumes/Preboot/Cryptexes/App/System/Applications"), |
| | | FileManager.default.homeDirectoryForCurrentUser |
| | | .appendingPathComponent("Applications") |
| | | ] |
| | | |
| | | private static let systemAppPathPrefixes = [ |
| | | "/System/Applications/", |
| | | "/System/Cryptexes/App/System/Applications/", |
| | | "/System/Volumes/Preboot/Cryptexes/App/System/Applications/" |
| | | ] |
| | | |
| | | static func isSystemAppPath(_ path: String) -> Bool { |
| | | systemAppPathPrefixes.contains { path.hasPrefix($0) } |
| | | } |
| | | |
| | | /// Scan all standard locations. Tags are annotated from TagDatabase by the caller. |
| | | static func scan() -> [AppInfo] { |
| | | var seen = Set<URL>() |
| | | var seenResolvedPaths = Set<String>() |
| | | var apps: [AppInfo] = [] |
| | | |
| | | for baseURL in searchPaths { |
| | | scanDirectChildren( |
| | | in: baseURL, |
| | | apps: &apps, |
| | | seenResolvedPaths: &seenResolvedPaths |
| | | ) |
| | | |
| | | guard let enumerator = FileManager.default.enumerator( |
| | | at: baseURL, |
| | | includingPropertiesForKeys: [.isDirectoryKey], |
| | | includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey], |
| | | options: [.skipsHiddenFiles, .skipsPackageDescendants] |
| | | ) else { continue } |
| | | |
| | | for case let url as URL in enumerator { |
| | | guard url.pathExtension == "app" else { continue } |
| | | guard !seen.contains(url) else { continue } |
| | | seen.insert(url) |
| | | |
| | | let name = url.deletingPathExtension().lastPathComponent |
| | | let icon = NSWorkspace.shared.icon(forFile: url.path) |
| | | icon.size = NSSize(width: 96, height: 96) |
| | | var bundleId: String? = nil |
| | | if let bundle = Bundle(url: url) { |
| | | bundleId = bundle.bundleIdentifier |
| | | } |
| | | |
| | | apps.append(AppInfo( |
| | | name: name, path: url, tags: [], |
| | | bundleIdentifier: bundleId, icon: icon |
| | | )) |
| | | appendAppIfNeeded( |
| | | at: url, |
| | | apps: &apps, |
| | | seenResolvedPaths: &seenResolvedPaths |
| | | ) |
| | | } |
| | | } |
| | | |
| | | return apps.sorted { |
| | | $0.name.localizedStandardCompare($1.name) == .orderedAscending |
| | | } |
| | | } |
| | | |
| | | private static func scanDirectChildren( |
| | | in baseURL: URL, |
| | | apps: inout [AppInfo], |
| | | seenResolvedPaths: inout Set<String> |
| | | ) { |
| | | guard let names = try? FileManager.default.contentsOfDirectory(atPath: baseURL.path) else { return } |
| | | |
| | | for name in names where !name.hasPrefix(".") { |
| | | appendAppIfNeeded( |
| | | at: baseURL.appendingPathComponent(name), |
| | | apps: &apps, |
| | | seenResolvedPaths: &seenResolvedPaths |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private static func appendAppIfNeeded( |
| | | at url: URL, |
| | | apps: inout [AppInfo], |
| | | seenResolvedPaths: inout Set<String> |
| | | ) { |
| | | guard url.pathExtension.lowercased() == "app" else { return } |
| | | |
| | | let displayURL = url.standardizedFileURL |
| | | let resolvedURL = displayURL.resolvingSymlinksInPath().standardizedFileURL |
| | | guard seenResolvedPaths.insert(resolvedURL.path).inserted else { return } |
| | | |
| | | let name = displayURL.deletingPathExtension().lastPathComponent |
| | | 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 |
| | | |
| | | apps.append(AppInfo( |
| | | name: name, |
| | | path: displayURL, |
| | | tags: [], |
| | | bundleIdentifier: bundleId, |
| | | icon: icon |
| | | )) |
| | | } |
| | | |
| | | /// Group apps by their tags (from TagDatabase). |
| | |
| | | } |
| | | |
| | | enum TagDatabase { |
| | | static let uncommonTagKey = "__system.uncommon" |
| | | static let maxAppNoteLength = 80 |
| | | static let autoUncommonOpenThreshold = 100 |
| | | |
| | | enum UncommonSource: String, Codable { |
| | | case auto |
| | | case manual |
| | | } |
| | | |
| | | // MARK: Storage types |
| | | |
| | |
| | | var tags: [String: TagDef] = [:] |
| | | var appTags: [String: [String]] = [:] // path → tag names |
| | | var tagOrder: [String] = [] // display order; empty → alpha sort |
| | | var uncommonAppPaths: [String] = [] // special marker; does not affect normal groups |
| | | var uncommonSources: [String: UncommonSource] = [:] // current uncommon source: auto/manual |
| | | var appOpenCounts: [String: Int] = [:] // 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 |
| | | |
| | | enum CodingKeys: String, CodingKey { |
| | | case version |
| | | case tags |
| | | case appTags |
| | | case tagOrder |
| | | case uncommonAppPaths |
| | | case uncommonSources |
| | | case appOpenCounts |
| | | case knownAppPaths |
| | | case appNotes |
| | | } |
| | | |
| | | init() {} |
| | | |
| | | init(from decoder: Decoder) throws { |
| | | let container = try decoder.container(keyedBy: CodingKeys.self) |
| | | version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1 |
| | | tags = try container.decodeIfPresent([String: TagDef].self, forKey: .tags) ?? [:] |
| | | appTags = try container.decodeIfPresent([String: [String]].self, forKey: .appTags) ?? [:] |
| | | tagOrder = try container.decodeIfPresent([String].self, forKey: .tagOrder) ?? [] |
| | | uncommonAppPaths = try container.decodeIfPresent([String].self, forKey: .uncommonAppPaths) ?? [] |
| | | uncommonSources = try container.decodeIfPresent([String: UncommonSource].self, forKey: .uncommonSources) ?? [:] |
| | | appOpenCounts = try container.decodeIfPresent([String: Int].self, forKey: .appOpenCounts) ?? [:] |
| | | knownAppPaths = try container.decodeIfPresent([String].self, forKey: .knownAppPaths) ?? [] |
| | | appNotes = try container.decodeIfPresent([String: String].self, forKey: .appNotes) ?? [:] |
| | | |
| | | for path in uncommonAppPaths where uncommonSources[path] == nil { |
| | | uncommonSources[path] = .manual |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: Paths |
| | |
| | | } |
| | | |
| | | static func save(_ store: Store) { |
| | | guard let data = try? JSONEncoder().encode(store) else { return } |
| | | let encoder = JSONEncoder() |
| | | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] |
| | | guard let data = try? encoder.encode(store) else { return } |
| | | try? data.write(to: storeURL, options: .atomic) |
| | | } |
| | | |
| | |
| | | |
| | | /// Annotate scanned apps with tags from the database. |
| | | static func annotate(apps: [AppInfo]) -> [AppInfo] { |
| | | let store = TagDatabase.load() |
| | | annotate(apps: apps, store: TagDatabase.load()) |
| | | } |
| | | |
| | | static func annotate(apps: [AppInfo], store: TagDatabase.Store) -> [AppInfo] { |
| | | let uncommonPaths = Set(store.uncommonAppPaths) |
| | | return apps.map { app in |
| | | 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, icon: app.icon, |
| | | isUncommon: uncommonPaths.contains(app.path.path), |
| | | note: store.appNotes[app.path.path] |
| | | ) |
| | | } |
| | | } |
| | |
| | | /// Replace the full editable tag set for multiple apps. |
| | | static func setTags(_ tags: [String], to paths: [String]) { |
| | | var store = TagDatabase.load() |
| | | let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey) |
| | | let validTags = tags.filter { store.tags[$0] != nil } |
| | | var uncommonPaths = Set(store.uncommonAppPaths) |
| | | for path in paths { |
| | | if validTags.isEmpty { |
| | | store.appTags.removeValue(forKey: path) |
| | | } else { |
| | | store.appTags[path] = validTags |
| | | } |
| | | if selectedUncommon { |
| | | uncommonPaths.insert(path) |
| | | store.uncommonSources[path] = .manual |
| | | } else { |
| | | uncommonPaths.remove(path) |
| | | store.uncommonSources.removeValue(forKey: path) |
| | | } |
| | | } |
| | | store.uncommonAppPaths = uncommonPaths.sorted() |
| | | TagDatabase.save(store) |
| | | } |
| | | |
| | | /// Append tags to multiple apps without disturbing their existing tag sets. |
| | | static func appendTags(_ tags: [String], to paths: [String]) { |
| | | guard !tags.isEmpty, !paths.isEmpty else { return } |
| | | |
| | | var store = TagDatabase.load() |
| | | let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey) |
| | | let validTags = tags.filter { store.tags[$0] != nil } |
| | | var uncommonPaths = Set(store.uncommonAppPaths) |
| | | |
| | | for path in paths { |
| | | var current = store.appTags[path] ?? [] |
| | | for tag in validTags where !current.contains(tag) { |
| | | current.append(tag) |
| | | } |
| | | if !current.isEmpty { |
| | | store.appTags[path] = current |
| | | } |
| | | |
| | | if selectedUncommon { |
| | | uncommonPaths.insert(path) |
| | | store.uncommonSources[path] = .manual |
| | | } |
| | | } |
| | | |
| | | store.uncommonAppPaths = uncommonPaths.sorted() |
| | | TagDatabase.save(store) |
| | | } |
| | | |
| | | /// Remove tags from multiple apps while preserving every unrelated tag. |
| | | static func removeTags(_ tags: [String], from paths: [String]) { |
| | | guard !tags.isEmpty, !paths.isEmpty else { return } |
| | | |
| | | var store = TagDatabase.load() |
| | | let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey) |
| | | let validTags = Set(tags.filter { store.tags[$0] != nil }) |
| | | var uncommonPaths = Set(store.uncommonAppPaths) |
| | | |
| | | for path in paths { |
| | | var current = store.appTags[path] ?? [] |
| | | current.removeAll { validTags.contains($0) } |
| | | |
| | | if current.isEmpty { |
| | | store.appTags.removeValue(forKey: path) |
| | | } else { |
| | | store.appTags[path] = current |
| | | } |
| | | |
| | | if selectedUncommon { |
| | | uncommonPaths.remove(path) |
| | | store.uncommonSources.removeValue(forKey: path) |
| | | } |
| | | } |
| | | |
| | | store.uncommonAppPaths = uncommonPaths.sorted() |
| | | TagDatabase.save(store) |
| | | } |
| | | |
| | | static func reconcileScannedApps(_ apps: [AppInfo]) -> TagDatabase.Store { |
| | | var store = TagDatabase.load() |
| | | let scannedPaths = Set(apps.map { $0.path.path }) |
| | | let knownPaths = Set(store.knownAppPaths) |
| | | |
| | | if store.knownAppPaths.isEmpty { |
| | | store.knownAppPaths = scannedPaths.sorted() |
| | | if apps.isEmpty == false { |
| | | TagDatabase.save(store) |
| | | } |
| | | return store |
| | | } |
| | | |
| | | let newPaths = scannedPaths.subtracting(knownPaths) |
| | | guard !newPaths.isEmpty else { return store } |
| | | |
| | | var uncommonPaths = Set(store.uncommonAppPaths) |
| | | for path in newPaths { |
| | | uncommonPaths.insert(path) |
| | | store.uncommonSources[path] = .auto |
| | | if store.appOpenCounts[path] == nil { |
| | | store.appOpenCounts[path] = 0 |
| | | } |
| | | } |
| | | |
| | | store.uncommonAppPaths = uncommonPaths.sorted() |
| | | store.knownAppPaths = knownPaths.union(newPaths).sorted() |
| | | TagDatabase.save(store) |
| | | return store |
| | | } |
| | | |
| | | static func recordLauncherOpen(for path: String) { |
| | | var store = TagDatabase.load() |
| | | store.appOpenCounts[path] = (store.appOpenCounts[path] ?? 0) + 1 |
| | | |
| | | if store.uncommonSources[path] == .auto, |
| | | store.appOpenCounts[path, default: 0] >= TagDatabase.autoUncommonOpenThreshold { |
| | | var uncommonPaths = Set(store.uncommonAppPaths) |
| | | uncommonPaths.remove(path) |
| | | store.uncommonAppPaths = uncommonPaths.sorted() |
| | | store.uncommonSources.removeValue(forKey: path) |
| | | } |
| | | |
| | | TagDatabase.save(store) |
| | | } |
| | | |
| | | static func setAppNote(_ note: String, for path: String) { |
| | | var store = TagDatabase.load() |
| | | let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines) |
| | | let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength)) |
| | | if limited.isEmpty { |
| | | store.appNotes.removeValue(forKey: path) |
| | | } else { |
| | | store.appNotes[path] = limited |
| | | } |
| | | TagDatabase.save(store) |
| | | } |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>APPL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>5.7.8</string> |
| | | <string>6.0.5</string> |
| | | <key>CFBundleVersion</key> |
| | | <string>583</string> |
| | | <string>610</string> |
| | | <key>LSMinimumSystemVersion</key> |
| | | <string>15.0</string> |
| | | <key>NSHighResolutionCapable</key> |
| | |
| | | "settings.exportFailed": "فشل التصدير", |
| | | "settings.import": "استيراد التصنيفات والتخطيط…", |
| | | "settings.backupDesc": "التصدير يحفظ التصنيفات والتخطيط بملف JSON. الاستيراد يستبدل الإعداد الحالي بملف.", |
| | | "settings.uncommonBubble": "فقاعات للتطبيقات القليلة الاستخدام", |
| | | "settings.uncommonBubbleDesc": "ورّ فقاعة كبيرة للاسم والملاحظة بس للتطبيقات المحددة كقليلة الاستخدام.", |
| | | "settings.importFailed": "فشل الاستيراد", |
| | | "edit.exit": "إنهاء التحرير", |
| | | "edit.tags": "تحرير الوسوم", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "غير مصنف", |
| | | "group.appleBuiltIn": "مدمج", |
| | | "group.uncommon": "ما ينستخدم واجد", |
| | | "drop.systemDefaultWarning": "لا تسحب التطبيقات لتصنيفات النظام الافتراضية", |
| | | "tag.design": "تصميم", |
| | | "tag.development": "تطوير", |
| | |
| | | "tag.productivity": "إنتاجية", |
| | | "settings.coloredContainer": "حاوية ملونة", |
| | | "settings.gridContainer": "شبكة بدون لون", |
| | | "settings.coloredGridContainer": "شبكة ملونة" |
| | | "settings.coloredGridContainer": "شبكة ملونة", |
| | | "appNote.edit": "عدّل ملاحظة الغرض…", |
| | | "appNote.placeholder": "ملاحظة غرض التطبيق", |
| | | "edit.modeAdd": "إضافة وسوم", |
| | | "edit.modeRemove": "إزالة وسوم", |
| | | "edit.modeHint": "علامات التحديد في اليسار تنطبق على هالعملية فقط.", |
| | | "edit.currentTags": "الوسوم الحالية", |
| | | "edit.commonTags": "الوسوم المشتركة", |
| | | "edit.selectionHint": "اختر التطبيقات من اليمين، ثم اختر من اليسار الوسوم اللي تبي تضيفها أو تزيلها.", |
| | | "edit.noAssignedTags": "التطبيق المحدد ما عليه وسوم للحين.", |
| | | "edit.noCommonTags": "ما فيه وسوم مشتركة بين التطبيقات المحددة.", |
| | | "edit.mixedTagsHint": "باقي الوسوم تختلف بين التطبيقات المحددة.", |
| | | "edit.operationLabel": "هالاجراء:", |
| | | "edit.confirmAdd": "تأكيد الإضافة", |
| | | "edit.confirmRemove": "تأكيد الإزالة", |
| | | "edit.feedbackAddFormat": "هالعملية اضافت %tagCount% من الوسوم على %appCount% تطبيقات (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "هالعملية شالت %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم اللي انشالت: %tagNames%.", |
| | | "edit.removeHint": "هنا ما تقدر تشيل الصح الا من الوسوم الموجودة من قبل. اي وسم يطلع معه رمز حذف احمر بينشال بعد التاكيد." |
| | | } |
| | |
| | | "settings.exportFailed": "فشل التصدير", |
| | | "settings.import": "استيراد التصنيفات والتخطيط…", |
| | | "settings.backupDesc": "يحفظ التصدير التصنيفات والتخطيط في ملف JSON. يستبدل الاستيراد الإعداد الحالي بملف.", |
| | | "settings.uncommonBubble": "فقاعات التطبيقات قليلة الاستخدام", |
| | | "settings.uncommonBubbleDesc": "اعرض فقاعة كبيرة للاسم والملاحظة فقط للتطبيقات المحددة كقليلة الاستخدام.", |
| | | "settings.importFailed": "فشل الاستيراد", |
| | | "edit.exit": "إنهاء التحرير", |
| | | "edit.tags": "تحرير الوسوم", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "غير مصنف", |
| | | "group.appleBuiltIn": "مدمج", |
| | | "group.uncommon": "غير مستخدم غالبًا", |
| | | "drop.systemDefaultWarning": "لا تسحب التطبيقات إلى التصنيفات الافتراضية للنظام", |
| | | "tag.design": "تصميم", |
| | | "tag.development": "تطوير", |
| | |
| | | "tag.productivity": "إنتاجية", |
| | | "settings.coloredContainer": "حاوية ملونة", |
| | | "settings.gridContainer": "شبكة بلا لون", |
| | | "settings.coloredGridContainer": "شبكة ملونة" |
| | | "settings.coloredGridContainer": "شبكة ملونة", |
| | | "appNote.edit": "تحرير ملاحظة الغرض…", |
| | | "appNote.placeholder": "ملاحظة غرض التطبيق", |
| | | "edit.modeAdd": "إضافة وسوم", |
| | | "edit.modeRemove": "إزالة وسوم", |
| | | "edit.modeHint": "علامات التحديد على اليسار تنطبق فقط على هذه العملية.", |
| | | "edit.currentTags": "الوسوم الحالية", |
| | | "edit.commonTags": "الوسوم المشتركة", |
| | | "edit.selectionHint": "اختر التطبيقات من اليمين، ثم اختر من اليسار الوسوم التي تريد إضافتها أو إزالتها.", |
| | | "edit.noAssignedTags": "التطبيق المحدد لا يملك وسوما بعد.", |
| | | "edit.noCommonTags": "لا توجد وسوم مشتركة بين التطبيقات المحددة.", |
| | | "edit.mixedTagsHint": "الوسوم الأخرى تختلف بين التطبيقات المحددة.", |
| | | "edit.operationLabel": "هذا الاجراء:", |
| | | "edit.confirmAdd": "تأكيد الإضافة", |
| | | "edit.confirmRemove": "تأكيد الإزالة", |
| | | "edit.feedbackAddFormat": "اضافت هذه العملية %tagCount% من الوسوم عبر %appCount% تطبيقات (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "ازالت هذه العملية %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم التي تمت ازالتها: %tagNames%.", |
| | | "edit.removeHint": "هنا يمكنك الغاء تحديد الوسوم الموجودة بالفعل فقط. الوسوم التي يظهر بجانبها رمز حذف احمر ستزال بعد التاكيد." |
| | | } |
| | |
| | | "settings.exportFailed": "Export se nezdařil", |
| | | "settings.import": "Importovat kategorie a rozvržení…", |
| | | "settings.backupDesc": "Export uloží kategorie a rozvržení do souboru JSON. Import nahradí aktuální nastavení souborem.", |
| | | "settings.uncommonBubble": "Bubliny pro málo používané aplikace", |
| | | "settings.uncommonBubbleDesc": "Zobrazí velké jméno a poznámku jen u aplikací označených jako málo používané.", |
| | | "settings.importFailed": "Import se nezdařil", |
| | | "edit.exit": "Ukončit úpravy", |
| | | "edit.tags": "Upravit štítky", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Nezařazeno", |
| | | "group.appleBuiltIn": "Vestavěné", |
| | | "group.uncommon": "Málo používané", |
| | | "drop.systemDefaultWarning": "Nepřetahujte aplikace do výchozích systémových kategorií", |
| | | "tag.design": "Design", |
| | | "tag.development": "Vývoj", |
| | |
| | | "tag.productivity": "Produktivita", |
| | | "settings.coloredContainer": "Barevný kontejner", |
| | | "settings.gridContainer": "Bezbarvá mřížka", |
| | | "settings.coloredGridContainer": "Barevná mřížka" |
| | | "settings.coloredGridContainer": "Barevná mřížka", |
| | | "appNote.edit": "Upravit poznámku k účelu…", |
| | | "appNote.placeholder": "Poznámka k účelu aplikace", |
| | | "edit.modeAdd": "Pridat stitky", |
| | | "edit.modeRemove": "Odebrat stitky", |
| | | "edit.modeHint": "Zatrzeni vlevo plati jen pro tuto operaci.", |
| | | "edit.currentTags": "Aktualni stitky", |
| | | "edit.commonTags": "Spolecne stitky", |
| | | "edit.selectionHint": "Vyberte aplikace vpravo a pak vlevo zvolte stitky, ktere chcete pridat nebo odebrat.", |
| | | "edit.noAssignedTags": "Vybrana aplikace zatim nema zadne stitky.", |
| | | "edit.noCommonTags": "Vybrane aplikace nemaji zadne spolecne stitky.", |
| | | "edit.mixedTagsHint": "Ostatni stitky se mezi vybranymi aplikacemi lisi.", |
| | | "edit.operationLabel": "Akce:", |
| | | "edit.confirmAdd": "Potvrdit pridani", |
| | | "edit.confirmRemove": "Potvrdit odebrani", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Eksport mislykkedes", |
| | | "settings.import": "Importer kategorier og layout…", |
| | | "settings.backupDesc": "Eksport gemmer kategorier og layout i en JSON-fil. Import erstatter den aktuelle opsætning med en fil.", |
| | | "settings.uncommonBubble": "Bobler for sjældent brugte apps", |
| | | "settings.uncommonBubbleDesc": "Viser stor navne- og noteboble kun for apps markeret som sjældent brugt.", |
| | | "settings.importFailed": "Import mislykkedes", |
| | | "edit.exit": "Afslut redigering", |
| | | "edit.tags": "Rediger tags", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Ukategoriseret", |
| | | "group.appleBuiltIn": "Indbygget", |
| | | "group.uncommon": "Sjældent brugt", |
| | | "drop.systemDefaultWarning": "Træk ikke apps til systemets standardkategorier", |
| | | "tag.design": "Design", |
| | | "tag.development": "Udvikling", |
| | |
| | | "tag.productivity": "Produktivitet", |
| | | "settings.coloredContainer": "Farvet beholder", |
| | | "settings.gridContainer": "Farveløst gitter", |
| | | "settings.coloredGridContainer": "Farvet gitter" |
| | | "settings.coloredGridContainer": "Farvet gitter", |
| | | "appNote.edit": "Rediger formålsnote…", |
| | | "appNote.placeholder": "Formålsnote for app", |
| | | "edit.modeAdd": "Tilfoj tags", |
| | | "edit.modeRemove": "Fjern tags", |
| | | "edit.modeHint": "Afkrydsningerne til venstre gaelder kun for denne handling.", |
| | | "edit.currentTags": "Nuvaerende tags", |
| | | "edit.commonTags": "Faelles tags", |
| | | "edit.selectionHint": "Vaelg apps til hojre, og vaelg derefter til venstre de tags, du vil tilfoje eller fjerne.", |
| | | "edit.noAssignedTags": "Den valgte app har ingen tags endnu.", |
| | | "edit.noCommonTags": "De valgte apps har ingen faelles tags.", |
| | | "edit.mixedTagsHint": "Andre tags varierer mellem de valgte apps.", |
| | | "edit.operationLabel": "Handling:", |
| | | "edit.confirmAdd": "Bekraeft tilfojelse", |
| | | "edit.confirmRemove": "Bekraeft fjernelse", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Export fehlgeschlagen", |
| | | "settings.import": "Kategorien & Layout importieren…", |
| | | "settings.backupDesc": "Der Export speichert Kategorien und Layout als JSON-Datei. Der Import ersetzt die aktuelle Konfiguration durch eine Datei.", |
| | | "settings.uncommonBubble": "Hinweisblasen fuer seltene Apps", |
| | | "settings.uncommonBubbleDesc": "Zeigt Name und Notiz nur fuer Apps, die als selten genutzt markiert sind.", |
| | | "settings.importFailed": "Import fehlgeschlagen", |
| | | "edit.exit": "Bearbeitung beenden", |
| | | "edit.tags": "Tags bearbeiten", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Nicht kategorisiert", |
| | | "group.appleBuiltIn": "Integriert", |
| | | "group.uncommon": "Selten genutzt", |
| | | "drop.systemDefaultWarning": "Ziehe keine Apps in die Standard-Systemkategorien", |
| | | "tag.design": "Design", |
| | | "tag.development": "Entwicklung", |
| | |
| | | "tag.productivity": "Produktivität", |
| | | "settings.coloredContainer": "Farbiger Container", |
| | | "settings.gridContainer": "Farbloses Raster", |
| | | "settings.coloredGridContainer": "Farbiges Raster" |
| | | "settings.coloredGridContainer": "Farbiges Raster", |
| | | "appNote.edit": "Verwendungsnotiz bearbeiten…", |
| | | "appNote.placeholder": "Verwendungsnotiz zur App", |
| | | "edit.modeAdd": "Tags hinzufuegen", |
| | | "edit.modeRemove": "Tags entfernen", |
| | | "edit.modeHint": "Die Haken links gelten nur fuer diesen Vorgang.", |
| | | "edit.currentTags": "Aktuelle Tags", |
| | | "edit.commonTags": "Gemeinsame Tags", |
| | | "edit.selectionHint": "Waehlen Sie rechts Apps aus und links die Tags, die in diesem Schritt hinzugefuegt oder entfernt werden sollen.", |
| | | "edit.noAssignedTags": "Die ausgewaehlte App hat noch keine Tags.", |
| | | "edit.noCommonTags": "Die ausgewaehlten Apps haben keine gemeinsamen Tags.", |
| | | "edit.mixedTagsHint": "Weitere Tags unterscheiden sich zwischen den ausgewaehlten Apps.", |
| | | "edit.operationLabel": "Aktion:", |
| | | "edit.confirmAdd": "Hinzufuegen bestaetigen", |
| | | "edit.confirmRemove": "Entfernen bestaetigen", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Export Failed", |
| | | "settings.import": "Import Categories & Layout…", |
| | | "settings.backupDesc": "Export saves your categories and layout to a JSON file. Import replaces the current setup with one from a file.", |
| | | "settings.uncommonBubble": "Uncommon app bubble tips", |
| | | "settings.uncommonBubbleDesc": "Show a large name and note bubble only for apps marked as Uncommon.", |
| | | "settings.importFailed": "Import Failed", |
| | | "edit.exit": "Exit editing", |
| | | "edit.tags": "Edit Tags", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Uncategorized", |
| | | "group.appleBuiltIn": "Built-in", |
| | | "group.uncommon": "Uncommon", |
| | | "drop.systemDefaultWarning": "Do not drag apps into system default categories", |
| | | "tag.design": "Design", |
| | | "tag.development": "Development", |
| | |
| | | "tag.productivity": "Productivity", |
| | | "settings.coloredContainer": "Colored Container", |
| | | "settings.gridContainer": "Colorless Grid", |
| | | "settings.coloredGridContainer": "Colored Grid" |
| | | "settings.coloredGridContainer": "Colored Grid", |
| | | "appNote.edit": "Edit purpose note…", |
| | | "appNote.placeholder": "App purpose note", |
| | | "edit.modeAdd": "Add tags", |
| | | "edit.modeRemove": "Remove tags", |
| | | "edit.modeHint": "Left-side checkmarks apply only to this operation.", |
| | | "edit.currentTags": "Current tags", |
| | | "edit.commonTags": "Shared tags", |
| | | "edit.selectionHint": "Select apps on the right, then choose the tags to add or remove on the left.", |
| | | "edit.noAssignedTags": "The selected app has no tags yet.", |
| | | "edit.noCommonTags": "These selected apps have no shared tags.", |
| | | "edit.mixedTagsHint": "Other tags differ between the selected apps.", |
| | | "edit.operationLabel": "Action:", |
| | | "edit.confirmAdd": "Confirm Add", |
| | | "edit.confirmRemove": "Confirm Remove", |
| | | "edit.feedbackAddFormat": "This operation added %tagCount% tags across %appCount% apps (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "This operation removed %tagCount% tags across %appCount% apps (%appNames%). Removed tags: %tagNames%.", |
| | | "edit.removeHint": "Only existing tags can be unchecked here. Tags showing a red delete icon will be removed after confirmation." |
| | | } |
| | |
| | | "settings.exportFailed": "Error de exportación", |
| | | "settings.import": "Importar categorías y diseño…", |
| | | "settings.backupDesc": "La exportación guarda las categorías y el diseño en un archivo JSON. La importación reemplaza la configuración actual.", |
| | | "settings.uncommonBubble": "Burbujas para apps poco usadas", |
| | | "settings.uncommonBubbleDesc": "Muestra una burbuja grande con nombre y nota solo para apps marcadas como poco usadas.", |
| | | "settings.importFailed": "Error de importación", |
| | | "edit.exit": "Salir de edición", |
| | | "edit.tags": "Editar etiquetas", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Sin categorizar", |
| | | "group.appleBuiltIn": "Integradas", |
| | | "group.uncommon": "Poco usadas", |
| | | "drop.systemDefaultWarning": "No arrastres a las categorías predeterminadas del sistema", |
| | | "tag.design": "Diseño", |
| | | "tag.development": "Desarrollo", |
| | |
| | | "tag.productivity": "Oficina", |
| | | "settings.coloredContainer": "Contenedor coloreado", |
| | | "settings.gridContainer": "Cuadrícula sin color", |
| | | "settings.coloredGridContainer": "Cuadrícula coloreada" |
| | | "settings.coloredGridContainer": "Cuadrícula coloreada", |
| | | "appNote.edit": "Editar nota de uso…", |
| | | "appNote.placeholder": "Nota de uso de la app", |
| | | "edit.modeAdd": "Anadir etiquetas", |
| | | "edit.modeRemove": "Quitar etiquetas", |
| | | "edit.modeHint": "Las marcas de la izquierda solo se aplican a esta operacion.", |
| | | "edit.currentTags": "Etiquetas actuales", |
| | | "edit.commonTags": "Etiquetas compartidas", |
| | | "edit.selectionHint": "Seleccione aplicaciones a la derecha y luego elija a la izquierda las etiquetas que desea anadir o quitar.", |
| | | "edit.noAssignedTags": "La aplicacion seleccionada aun no tiene etiquetas.", |
| | | "edit.noCommonTags": "Las aplicaciones seleccionadas no tienen etiquetas en comun.", |
| | | "edit.mixedTagsHint": "Las demas etiquetas difieren entre las aplicaciones seleccionadas.", |
| | | "edit.operationLabel": "Accion:", |
| | | "edit.confirmAdd": "Confirmar agregado", |
| | | "edit.confirmRemove": "Confirmar eliminacion", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Échec de l'export", |
| | | "settings.import": "Importer catégories et disposition…", |
| | | "settings.backupDesc": "L'export sauvegarde les catégories et la disposition dans un fichier JSON. L'import remplace la configuration actuelle.", |
| | | "settings.uncommonBubble": "Bulles pour apps peu utilisées", |
| | | "settings.uncommonBubbleDesc": "Affiche une grande bulle avec le nom et une note uniquement pour les apps marquées comme peu utilisées.", |
| | | "settings.importFailed": "Échec de l'import", |
| | | "edit.exit": "Quitter l'édition", |
| | | "edit.tags": "Modifier les étiquettes", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Non classé", |
| | | "group.appleBuiltIn": "Intégrées", |
| | | "group.uncommon": "Peu utilisé", |
| | | "drop.systemDefaultWarning": "Ne déposez pas dans les catégories système par défaut", |
| | | "tag.design": "Design", |
| | | "tag.development": "Développement", |
| | |
| | | "tag.productivity": "Bureau", |
| | | "settings.coloredContainer": "Conteneur coloré", |
| | | "settings.gridContainer": "Grille sans couleur", |
| | | "settings.coloredGridContainer": "Grille colorée" |
| | | "settings.coloredGridContainer": "Grille colorée", |
| | | "appNote.edit": "Modifier la note d’usage…", |
| | | "appNote.placeholder": "Note d’usage de l’app", |
| | | "edit.modeAdd": "Ajouter des etiquettes", |
| | | "edit.modeRemove": "Retirer des etiquettes", |
| | | "edit.modeHint": "Les coches a gauche ne s'appliquent qu'a cette operation.", |
| | | "edit.currentTags": "Etiquettes actuelles", |
| | | "edit.commonTags": "Etiquettes communes", |
| | | "edit.selectionHint": "Selectionnez des apps a droite, puis choisissez a gauche les etiquettes a ajouter ou a retirer.", |
| | | "edit.noAssignedTags": "L'app selectionnee n'a pas encore d'etiquette.", |
| | | "edit.noCommonTags": "Les apps selectionnees n'ont aucune etiquette commune.", |
| | | "edit.mixedTagsHint": "Les autres etiquettes different selon les apps selectionnees.", |
| | | "edit.operationLabel": "Action :", |
| | | "edit.confirmAdd": "Confirmer ajout", |
| | | "edit.confirmRemove": "Confirmer retrait", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Ekspor gagal", |
| | | "settings.import": "Impor kategori & tata letak…", |
| | | "settings.backupDesc": "Ekspor menyimpan kategori dan tata letak ke file JSON. Impor mengganti pengaturan saat ini dengan file.", |
| | | "settings.uncommonBubble": "Gelembung app jarang dipakai", |
| | | "settings.uncommonBubbleDesc": "Tampilkan gelembung nama besar dan catatan hanya untuk app yang ditandai jarang dipakai.", |
| | | "settings.importFailed": "Impor gagal", |
| | | "edit.exit": "Keluar dari pengeditan", |
| | | "edit.tags": "Edit tag", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Tanpa kategori", |
| | | "group.appleBuiltIn": "Bawaan", |
| | | "group.uncommon": "Jarang dipakai", |
| | | "drop.systemDefaultWarning": "Jangan seret app ke kategori bawaan sistem", |
| | | "tag.design": "Desain", |
| | | "tag.development": "Pengembangan", |
| | |
| | | "tag.productivity": "Produktivitas", |
| | | "settings.coloredContainer": "Kontainer berwarna", |
| | | "settings.gridContainer": "Grid tanpa warna", |
| | | "settings.coloredGridContainer": "Grid berwarna" |
| | | "settings.coloredGridContainer": "Grid berwarna", |
| | | "appNote.edit": "Edit catatan kegunaan…", |
| | | "appNote.placeholder": "Catatan kegunaan app", |
| | | "edit.modeAdd": "Tambahkan label", |
| | | "edit.modeRemove": "Hapus label", |
| | | "edit.modeHint": "Centang di kiri hanya berlaku untuk operasi ini.", |
| | | "edit.currentTags": "Label saat ini", |
| | | "edit.commonTags": "Label bersama", |
| | | "edit.selectionHint": "Pilih app di kanan, lalu pilih di kiri label yang ingin ditambahkan atau dihapus.", |
| | | "edit.noAssignedTags": "App yang dipilih belum memiliki label.", |
| | | "edit.noCommonTags": "App yang dipilih tidak memiliki label bersama.", |
| | | "edit.mixedTagsHint": "Label lainnya berbeda di antara app yang dipilih.", |
| | | "edit.operationLabel": "Aksi:", |
| | | "edit.confirmAdd": "Konfirmasi tambah", |
| | | "edit.confirmRemove": "Konfirmasi hapus", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Esportazione fallita", |
| | | "settings.import": "Importa categorie e layout…", |
| | | "settings.backupDesc": "L'esportazione salva categorie e layout in un file JSON. L'importazione sostituisce la configurazione attuale.", |
| | | "settings.uncommonBubble": "Bolle per app poco usate", |
| | | "settings.uncommonBubbleDesc": "Mostra una grande bolla con nome e nota solo per le app segnate come poco usate.", |
| | | "settings.importFailed": "Importazione fallita", |
| | | "edit.exit": "Esci dalla modifica", |
| | | "edit.tags": "Modifica tag", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Non categorizzato", |
| | | "group.appleBuiltIn": "Integrate", |
| | | "group.uncommon": "Poco usate", |
| | | "drop.systemDefaultWarning": "Non trascinare nelle categorie di sistema predefinite", |
| | | "tag.design": "Design", |
| | | "tag.development": "Sviluppo", |
| | |
| | | "tag.productivity": "Ufficio", |
| | | "settings.coloredContainer": "Contenitore colorato", |
| | | "settings.gridContainer": "Griglia senza colore", |
| | | "settings.coloredGridContainer": "Griglia colorata" |
| | | "settings.coloredGridContainer": "Griglia colorata", |
| | | "appNote.edit": "Modifica nota d’uso…", |
| | | "appNote.placeholder": "Nota d’uso dell’app", |
| | | "edit.modeAdd": "Aggiungi tag", |
| | | "edit.modeRemove": "Rimuovi tag", |
| | | "edit.modeHint": "I segni di spunta a sinistra valgono solo per questa operazione.", |
| | | "edit.currentTags": "Tag attuali", |
| | | "edit.commonTags": "Tag condivisi", |
| | | "edit.selectionHint": "Seleziona le app a destra, poi scegli a sinistra i tag da aggiungere o rimuovere.", |
| | | "edit.noAssignedTags": "L'app selezionata non ha ancora tag.", |
| | | "edit.noCommonTags": "Le app selezionate non hanno tag in comune.", |
| | | "edit.mixedTagsHint": "Gli altri tag variano tra le app selezionate.", |
| | | "edit.operationLabel": "Azione:", |
| | | "edit.confirmAdd": "Conferma aggiunta", |
| | | "edit.confirmRemove": "Conferma rimozione", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "エクスポート失敗", |
| | | "settings.import": "カテゴリとレイアウトをインポート…", |
| | | "settings.backupDesc": "エクスポートはカテゴリとレイアウトをJSONファイルに保存します。インポートは現在の設定をファイルの内容で置き換えます。", |
| | | "settings.uncommonBubble": "あまり使わないアプリの吹き出し", |
| | | "settings.uncommonBubbleDesc": "あまり使わないとマークしたアプリだけに、大きな名前と用途メモを表示します。", |
| | | "settings.importFailed": "インポート失敗", |
| | | "edit.exit": "編集を終了", |
| | | "edit.tags": "タグを編集", |
| | |
| | | "app.build": "ビルド", |
| | | "group.uncategorized": "未分類", |
| | | "group.appleBuiltIn": "Mac内蔵", |
| | | "group.uncommon": "あまり使わない", |
| | | "drop.systemDefaultWarning": "システム既定カテゴリにはドラッグできません", |
| | | "tag.design": "デザイン", |
| | | "tag.development": "プログラミング", |
| | |
| | | "tag.productivity": "オフィス", |
| | | "settings.coloredContainer": "カラーコンテナ", |
| | | "settings.gridContainer": "無色グリッド", |
| | | "settings.coloredGridContainer": "カラーグリッド" |
| | | "settings.coloredGridContainer": "カラーグリッド", |
| | | "appNote.edit": "用途メモを編集…", |
| | | "appNote.placeholder": "アプリの用途メモ", |
| | | "edit.modeAdd": "タグを追加", |
| | | "edit.modeRemove": "タグを削除", |
| | | "edit.modeHint": "左側のチェックは今回の操作にのみ適用されます。", |
| | | "edit.currentTags": "現在のタグ", |
| | | "edit.commonTags": "共通タグ", |
| | | "edit.selectionHint": "右側でアプリを選択し、左側で今回追加または削除するタグを選んでください。", |
| | | "edit.noAssignedTags": "選択したアプリにはまだタグがありません。", |
| | | "edit.noCommonTags": "選択したアプリに共通するタグはありません。", |
| | | "edit.mixedTagsHint": "そのほかのタグは選択したアプリごとに異なります。", |
| | | "edit.operationLabel": "今回の操作:", |
| | | "edit.confirmAdd": "追加を確認", |
| | | "edit.confirmRemove": "削除を確認", |
| | | "edit.feedbackAddFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)に%tagCount%個のタグを追加しました。", |
| | | "edit.feedbackRemoveFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)から%tagCount%個のタグを削除しました。削除したタグ: %tagNames%。", |
| | | "edit.removeHint": "ここでは既存のタグだけチェックを外せます。赤い削除アイコンが表示されたタグは、確認後に削除されます。" |
| | | } |
| | |
| | | "settings.exportFailed": "내보내기 실패", |
| | | "settings.import": "분류와 레이아웃 가져오기…", |
| | | "settings.backupDesc": "내보내기는 분류와 레이아웃을 JSON 파일로 저장합니다. 가져오기는 파일의 내용으로 현재 설정을 대체합니다.", |
| | | "settings.uncommonBubble": "자주 쓰지 않는 앱 말풍선", |
| | | "settings.uncommonBubbleDesc": "자주 쓰지 않음으로 표시한 앱에만 큰 이름과 메모 말풍선을 표시합니다.", |
| | | "settings.importFailed": "가져오기 실패", |
| | | "edit.exit": "편집 종료", |
| | | "edit.tags": "태그 편집", |
| | |
| | | "app.build": "빌드", |
| | | "group.uncategorized": "미분류", |
| | | "group.appleBuiltIn": "Mac 내장", |
| | | "group.uncommon": "자주 쓰지 않음", |
| | | "drop.systemDefaultWarning": "시스템 기본 분류로 드래그하지 마세요", |
| | | "tag.design": "디자인", |
| | | "tag.development": "프로그래밍", |
| | |
| | | "tag.productivity": "오피스", |
| | | "settings.coloredContainer": "색상 컨테이너", |
| | | "settings.gridContainer": "무색 그리드", |
| | | "settings.coloredGridContainer": "색상 그리드" |
| | | "settings.coloredGridContainer": "색상 그리드", |
| | | "appNote.edit": "용도 메모 편집…", |
| | | "appNote.placeholder": "앱 용도 메모", |
| | | "edit.modeAdd": "태그 추가", |
| | | "edit.modeRemove": "태그 제거", |
| | | "edit.modeHint": "왼쪽 체크는 이번 작업에만 적용됩니다.", |
| | | "edit.currentTags": "현재 태그", |
| | | "edit.commonTags": "공통 태그", |
| | | "edit.selectionHint": "오른쪽에서 앱을 선택한 뒤, 왼쪽에서 이번에 추가하거나 제거할 태그를 고르세요.", |
| | | "edit.noAssignedTags": "선택한 앱에는 아직 태그가 없습니다.", |
| | | "edit.noCommonTags": "선택한 앱들 사이에 공통 태그가 없습니다.", |
| | | "edit.mixedTagsHint": "다른 태그는 선택한 앱마다 서로 다릅니다.", |
| | | "edit.operationLabel": "이번 작업:", |
| | | "edit.confirmAdd": "추가 확인", |
| | | "edit.confirmRemove": "제거 확인", |
| | | "edit.feedbackAddFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에 태그 %tagCount%개가 새로 추가되었습니다.", |
| | | "edit.feedbackRemoveFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에서 태그 %tagCount%개가 제거되었습니다. 제거된 태그: %tagNames%.", |
| | | "edit.removeHint": "여기서는 이미 있는 태그만 체크 해제할 수 있습니다. 빨간 삭제 아이콘이 보이는 태그는 확인 후 제거됩니다." |
| | | } |
| | |
| | | "settings.exportFailed": "Ekspor gagal", |
| | | "settings.import": "Import kategori & susun atur…", |
| | | "settings.backupDesc": "Eksport menyimpan kategori dan susun atur ke fail JSON. Import menggantikan tetapan semasa dengan fail.", |
| | | "settings.uncommonBubble": "Gelembung app jarang digunakan", |
| | | "settings.uncommonBubbleDesc": "Tunjukkan gelembung nama besar dan nota hanya untuk app yang ditanda jarang digunakan.", |
| | | "settings.importFailed": "Impor gagal", |
| | | "edit.exit": "Keluar dari pengeditan", |
| | | "edit.tags": "Edit tag", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Tidak berkategori", |
| | | "group.appleBuiltIn": "Terbina dalam", |
| | | "group.uncommon": "Jarang digunakan", |
| | | "drop.systemDefaultWarning": "Jangan seret app ke kategori bawaan sistem", |
| | | "tag.design": "Desain", |
| | | "tag.development": "Pengembangan", |
| | |
| | | "tag.productivity": "Produktivitas", |
| | | "settings.coloredContainer": "Kontainer berwarna", |
| | | "settings.gridContainer": "Grid tanpa warna", |
| | | "settings.coloredGridContainer": "Grid berwarna" |
| | | "settings.coloredGridContainer": "Grid berwarna", |
| | | "appNote.edit": "Edit nota kegunaan…", |
| | | "appNote.placeholder": "Nota kegunaan app", |
| | | "edit.modeAdd": "Tambah tag", |
| | | "edit.modeRemove": "Buang tag", |
| | | "edit.modeHint": "Tanda di sebelah kiri hanya digunakan untuk operasi ini.", |
| | | "edit.currentTags": "Tag semasa", |
| | | "edit.commonTags": "Tag bersama", |
| | | "edit.selectionHint": "Pilih app di sebelah kanan, kemudian pilih di sebelah kiri tag yang mahu ditambah atau dibuang.", |
| | | "edit.noAssignedTags": "App yang dipilih belum mempunyai tag.", |
| | | "edit.noCommonTags": "App yang dipilih tidak mempunyai tag bersama.", |
| | | "edit.mixedTagsHint": "Tag lain berbeza antara app yang dipilih.", |
| | | "edit.operationLabel": "Tindakan:", |
| | | "edit.confirmAdd": "Sahkan tambah", |
| | | "edit.confirmRemove": "Sahkan buang", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Eksport mislyktes", |
| | | "settings.import": "Importer kategorier og layout…", |
| | | "settings.backupDesc": "Eksport lagrer kategorier og layout i en JSON-fil. Import erstatter gjeldende oppsett med en fil.", |
| | | "settings.uncommonBubble": "Bobler for lite brukte apper", |
| | | "settings.uncommonBubbleDesc": "Viser stor navn- og notatboble bare for apper merket som lite brukt.", |
| | | "settings.importFailed": "Import mislyktes", |
| | | "edit.exit": "Avslutt redigering", |
| | | "edit.tags": "Rediger etiketter", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Ukategorisert", |
| | | "group.appleBuiltIn": "Innebygd", |
| | | "group.uncommon": "Lite brukt", |
| | | "drop.systemDefaultWarning": "Ikke dra apper til systemets standardkategorier", |
| | | "tag.design": "Design", |
| | | "tag.development": "Utvikling", |
| | |
| | | "tag.productivity": "Produktivitet", |
| | | "settings.coloredContainer": "Farget beholder", |
| | | "settings.gridContainer": "Fargeløst rutenett", |
| | | "settings.coloredGridContainer": "Farget rutenett" |
| | | "settings.coloredGridContainer": "Farget rutenett", |
| | | "appNote.edit": "Rediger bruksnotat…", |
| | | "appNote.placeholder": "Bruksnotat for app", |
| | | "edit.modeAdd": "Legg til tagger", |
| | | "edit.modeRemove": "Fjern tagger", |
| | | "edit.modeHint": "Avkrysningene til venstre gjelder bare for denne handlingen.", |
| | | "edit.currentTags": "Gjeldende tagger", |
| | | "edit.commonTags": "Felles tagger", |
| | | "edit.selectionHint": "Velg apper til hoyre, og velg deretter til venstre hvilke tagger som skal legges til eller fjernes.", |
| | | "edit.noAssignedTags": "Den valgte appen har ingen tagger ennå.", |
| | | "edit.noCommonTags": "De valgte appene har ingen felles tagger.", |
| | | "edit.mixedTagsHint": "Andre tagger varierer mellom de valgte appene.", |
| | | "edit.operationLabel": "Handling:", |
| | | "edit.confirmAdd": "Bekreft legg til", |
| | | "edit.confirmRemove": "Bekreft fjern", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Export mislukt", |
| | | "settings.import": "Categorieën en lay-out importeren…", |
| | | "settings.backupDesc": "Export slaat categorieën en lay-out op in een JSON-bestand. Import vervangt de huidige instelling door een bestand.", |
| | | "settings.uncommonBubble": "Bubbels voor weinig gebruikte apps", |
| | | "settings.uncommonBubbleDesc": "Toont een grote naam- en notitiebubbel alleen voor apps die als weinig gebruikt zijn gemarkeerd.", |
| | | "settings.importFailed": "Import mislukt", |
| | | "edit.exit": "Bewerken afsluiten", |
| | | "edit.tags": "Tags bewerken", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Niet gecategoriseerd", |
| | | "group.appleBuiltIn": "Ingebouwd", |
| | | "group.uncommon": "Weinig gebruikt", |
| | | "drop.systemDefaultWarning": "Sleep apps niet naar standaard systeemcategorieën", |
| | | "tag.design": "Ontwerp", |
| | | "tag.development": "Ontwikkeling", |
| | |
| | | "tag.productivity": "Productiviteit", |
| | | "settings.coloredContainer": "Gekleurde container", |
| | | "settings.gridContainer": "Kleurloos raster", |
| | | "settings.coloredGridContainer": "Gekleurd raster" |
| | | "settings.coloredGridContainer": "Gekleurd raster", |
| | | "appNote.edit": "Gebruiksnotitie bewerken…", |
| | | "appNote.placeholder": "Gebruiksnotitie voor app", |
| | | "edit.modeAdd": "Tags toevoegen", |
| | | "edit.modeRemove": "Tags verwijderen", |
| | | "edit.modeHint": "De vinkjes links gelden alleen voor deze bewerking.", |
| | | "edit.currentTags": "Huidige tags", |
| | | "edit.commonTags": "Gedeelde tags", |
| | | "edit.selectionHint": "Selecteer rechts apps en kies links de tags die je wilt toevoegen of verwijderen.", |
| | | "edit.noAssignedTags": "De geselecteerde app heeft nog geen tags.", |
| | | "edit.noCommonTags": "De geselecteerde apps hebben geen gedeelde tags.", |
| | | "edit.mixedTagsHint": "Andere tags verschillen per geselecteerde app.", |
| | | "edit.operationLabel": "Actie:", |
| | | "edit.confirmAdd": "Toevoegen bevestigen", |
| | | "edit.confirmRemove": "Verwijderen bevestigen", |
| | | "edit.feedbackAddFormat": "Deze actie heeft %tagCount% tags toegevoegd aan %appCount% apps (%appNames%).", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Eksport mislyktes", |
| | | "settings.import": "Importer kategorier og layout…", |
| | | "settings.backupDesc": "Eksport lagrer kategorier og layout i en JSON-fil. Import erstatter gjeldende oppsett med en fil.", |
| | | "settings.uncommonBubble": "Bobler for lite brukte appar", |
| | | "settings.uncommonBubbleDesc": "Viser stor namn- og notatboble berre for appar merkte som lite brukt.", |
| | | "settings.importFailed": "Import mislyktes", |
| | | "edit.exit": "Avslutt redigering", |
| | | "edit.tags": "Rediger etiketter", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Ukategorisert", |
| | | "group.appleBuiltIn": "Innebygd", |
| | | "group.uncommon": "Lite brukt", |
| | | "drop.systemDefaultWarning": "Ikke dra apper til systemets standardkategorier", |
| | | "tag.design": "Design", |
| | | "tag.development": "Utvikling", |
| | |
| | | "tag.productivity": "Produktivitet", |
| | | "settings.coloredContainer": "Farget beholder", |
| | | "settings.gridContainer": "Fargeløst rutenett", |
| | | "settings.coloredGridContainer": "Farget rutenett" |
| | | "settings.coloredGridContainer": "Farget rutenett", |
| | | "appNote.edit": "Rediger bruksnotat…", |
| | | "appNote.placeholder": "Bruksnotat for app", |
| | | "edit.modeAdd": "Legg til taggar", |
| | | "edit.modeRemove": "Fjern taggar", |
| | | "edit.modeHint": "Avkryssingane til venstre gjeld berre for denne handlinga.", |
| | | "edit.currentTags": "Noverande taggar", |
| | | "edit.commonTags": "Felles taggar", |
| | | "edit.selectionHint": "Vel appar til hogre, og vel deretter til venstre kva taggar som skal leggjast til eller fjernast.", |
| | | "edit.noAssignedTags": "Den valde appen har ingen taggar enno.", |
| | | "edit.noCommonTags": "Dei valde appane har ingen felles taggar.", |
| | | "edit.mixedTagsHint": "Andre taggar varierer mellom dei valde appane.", |
| | | "edit.operationLabel": "Handling:", |
| | | "edit.confirmAdd": "Stadfest legg til", |
| | | "edit.confirmRemove": "Stadfest fjern", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Eksport mislyktes", |
| | | "settings.import": "Importer kategorier og layout…", |
| | | "settings.backupDesc": "Eksport lagrer kategorier og layout i en JSON-fil. Import erstatter gjeldende oppsett med en fil.", |
| | | "settings.uncommonBubble": "Bobler for lite brukte apper", |
| | | "settings.uncommonBubbleDesc": "Viser stor navn- og notatboble bare for apper merket som lite brukt.", |
| | | "settings.importFailed": "Import mislyktes", |
| | | "edit.exit": "Avslutt redigering", |
| | | "edit.tags": "Rediger etiketter", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Ukategorisert", |
| | | "group.appleBuiltIn": "Innebygd", |
| | | "group.uncommon": "Lite brukt", |
| | | "drop.systemDefaultWarning": "Ikke dra apper til systemets standardkategorier", |
| | | "tag.design": "Design", |
| | | "tag.development": "Utvikling", |
| | |
| | | "tag.productivity": "Produktivitet", |
| | | "settings.coloredContainer": "Farget beholder", |
| | | "settings.gridContainer": "Fargeløst rutenett", |
| | | "settings.coloredGridContainer": "Farget rutenett" |
| | | "settings.coloredGridContainer": "Farget rutenett", |
| | | "appNote.edit": "Rediger bruksnotat…", |
| | | "appNote.placeholder": "Bruksnotat for app", |
| | | "edit.modeAdd": "Legg til tagger", |
| | | "edit.modeRemove": "Fjern tagger", |
| | | "edit.modeHint": "Avkrysningene til venstre gjelder bare for denne handlingen.", |
| | | "edit.currentTags": "Gjeldende tagger", |
| | | "edit.commonTags": "Felles tagger", |
| | | "edit.selectionHint": "Velg apper til hoyre, og velg deretter til venstre hvilke tagger som skal legges til eller fjernes.", |
| | | "edit.noAssignedTags": "Den valgte appen har ingen tagger ennå.", |
| | | "edit.noCommonTags": "De valgte appene har ingen felles tagger.", |
| | | "edit.mixedTagsHint": "Andre tagger varierer mellom de valgte appene.", |
| | | "edit.operationLabel": "Handling:", |
| | | "edit.confirmAdd": "Bekreft legg til", |
| | | "edit.confirmRemove": "Bekreft fjern", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Eksport nieudany", |
| | | "settings.import": "Importuj kategorie i układ…", |
| | | "settings.backupDesc": "Eksport zapisuje kategorie i układ do pliku JSON. Import zastępuje bieżącą konfigurację plikiem.", |
| | | "settings.uncommonBubble": "Dymki dla rzadko używanych aplikacji", |
| | | "settings.uncommonBubbleDesc": "Pokazuje dużą nazwę i notatkę tylko dla aplikacji oznaczonych jako rzadko używane.", |
| | | "settings.importFailed": "Import nieudany", |
| | | "edit.exit": "Zakończ edycję", |
| | | "edit.tags": "Edytuj tagi", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Bez kategorii", |
| | | "group.appleBuiltIn": "Wbudowane", |
| | | "group.uncommon": "Rzadko używane", |
| | | "drop.systemDefaultWarning": "Nie przeciągaj aplikacji do domyślnych kategorii systemu", |
| | | "tag.design": "Projektowanie", |
| | | "tag.development": "Programowanie", |
| | |
| | | "tag.productivity": "Produktywność", |
| | | "settings.coloredContainer": "Kolorowy kontener", |
| | | "settings.gridContainer": "Bezbarwna siatka", |
| | | "settings.coloredGridContainer": "Kolorowa siatka" |
| | | "settings.coloredGridContainer": "Kolorowa siatka", |
| | | "appNote.edit": "Edytuj notatkę o przeznaczeniu…", |
| | | "appNote.placeholder": "Notatka o przeznaczeniu aplikacji", |
| | | "edit.modeAdd": "Dodaj tagi", |
| | | "edit.modeRemove": "Usun tagi", |
| | | "edit.modeHint": "Zaznaczenia po lewej dotycza tylko tej operacji.", |
| | | "edit.currentTags": "Biezace tagi", |
| | | "edit.commonTags": "Wspolne tagi", |
| | | "edit.selectionHint": "Wybierz aplikacje po prawej, a nastepnie po lewej wybierz tagi do dodania lub usuniecia.", |
| | | "edit.noAssignedTags": "Wybrana aplikacja nie ma jeszcze tagow.", |
| | | "edit.noCommonTags": "Wybrane aplikacje nie maja wspolnych tagow.", |
| | | "edit.mixedTagsHint": "Pozostale tagi roznia sie miedzy wybranymi aplikacjami.", |
| | | "edit.operationLabel": "Dzialanie:", |
| | | "edit.confirmAdd": "Potwierdz dodanie", |
| | | "edit.confirmRemove": "Potwierdz usuniecie", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Falha ao exportar", |
| | | "settings.import": "Importar categorias e layout…", |
| | | "settings.backupDesc": "A exportação salva categorias e layout em um arquivo JSON. A importação substitui a configuração atual por um arquivo.", |
| | | "settings.uncommonBubble": "Balões para apps pouco usados", |
| | | "settings.uncommonBubbleDesc": "Mostra um balão grande com nome e nota apenas para apps marcados como pouco usados.", |
| | | "settings.importFailed": "Falha ao importar", |
| | | "edit.exit": "Sair da edição", |
| | | "edit.tags": "Editar etiquetas", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Sem categoria", |
| | | "group.appleBuiltIn": "Integrados", |
| | | "group.uncommon": "Pouco usados", |
| | | "drop.systemDefaultWarning": "Não arraste apps para categorias padrão do sistema", |
| | | "tag.design": "Design", |
| | | "tag.development": "Desenvolvimento", |
| | |
| | | "tag.productivity": "Produtividade", |
| | | "settings.coloredContainer": "Contêiner colorido", |
| | | "settings.gridContainer": "Grade sem cor", |
| | | "settings.coloredGridContainer": "Grade colorida" |
| | | "settings.coloredGridContainer": "Grade colorida", |
| | | "appNote.edit": "Editar nota de uso…", |
| | | "appNote.placeholder": "Nota de uso do app", |
| | | "edit.modeAdd": "Adicionar tags", |
| | | "edit.modeRemove": "Remover tags", |
| | | "edit.modeHint": "As marcacoes a esquerda valem apenas para esta operacao.", |
| | | "edit.currentTags": "Tags atuais", |
| | | "edit.commonTags": "Tags em comum", |
| | | "edit.selectionHint": "Selecione apps a direita e depois escolha a esquerda as tags para adicionar ou remover.", |
| | | "edit.noAssignedTags": "O app selecionado ainda nao tem tags.", |
| | | "edit.noCommonTags": "Os apps selecionados nao tem tags em comum.", |
| | | "edit.mixedTagsHint": "As outras tags diferem entre os apps selecionados.", |
| | | "edit.operationLabel": "Acao:", |
| | | "edit.confirmAdd": "Confirmar adicao", |
| | | "edit.confirmRemove": "Confirmar remocao", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Export eșuat", |
| | | "settings.import": "Importă categorii și aspect…", |
| | | "settings.backupDesc": "Exportul salvează categoriile și aspectul într-un fișier JSON. Importul înlocuiește configurația curentă cu un fișier.", |
| | | "settings.uncommonBubble": "Bule pentru aplicații folosite rar", |
| | | "settings.uncommonBubbleDesc": "Afișează o bulă mare cu nume și notă doar pentru aplicațiile marcate ca folosite rar.", |
| | | "settings.importFailed": "Import eșuat", |
| | | "edit.exit": "Ieși din editare", |
| | | "edit.tags": "Editează etichete", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Necategorizat", |
| | | "group.appleBuiltIn": "Integrat", |
| | | "group.uncommon": "Folosit rar", |
| | | "drop.systemDefaultWarning": "Nu trage aplicații în categoriile implicite ale sistemului", |
| | | "tag.design": "Design", |
| | | "tag.development": "Dezvoltare", |
| | |
| | | "tag.productivity": "Productivitate", |
| | | "settings.coloredContainer": "Container colorat", |
| | | "settings.gridContainer": "Grilă fără culoare", |
| | | "settings.coloredGridContainer": "Grilă colorată" |
| | | "settings.coloredGridContainer": "Grilă colorată", |
| | | "appNote.edit": "Editează nota de utilizare…", |
| | | "appNote.placeholder": "Notă de utilizare a aplicației", |
| | | "edit.modeAdd": "Adauga etichete", |
| | | "edit.modeRemove": "Elimina etichete", |
| | | "edit.modeHint": "Bifele din stanga se aplica doar acestei operatii.", |
| | | "edit.currentTags": "Etichete curente", |
| | | "edit.commonTags": "Etichete comune", |
| | | "edit.selectionHint": "Selecteaza aplicatii in dreapta, apoi alege in stanga etichetele pe care vrei sa le adaugi sau sa le elimini.", |
| | | "edit.noAssignedTags": "Aplicatia selectata nu are inca etichete.", |
| | | "edit.noCommonTags": "Aplicatiile selectate nu au etichete comune.", |
| | | "edit.mixedTagsHint": "Celelalte etichete difera intre aplicatiile selectate.", |
| | | "edit.operationLabel": "Actiune:", |
| | | "edit.confirmAdd": "Confirma adaugarea", |
| | | "edit.confirmRemove": "Confirma eliminarea", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Ошибка экспорта", |
| | | "settings.import": "Импорт категорий и макета…", |
| | | "settings.backupDesc": "Экспорт сохраняет категории и макет в JSON-файл. Импорт заменяет текущие настройки данными из файла.", |
| | | "settings.uncommonBubble": "Подсказки для редких приложений", |
| | | "settings.uncommonBubbleDesc": "Показывает большое имя и заметку только для приложений, отмеченных как редко используемые.", |
| | | "settings.importFailed": "Ошибка импорта", |
| | | "edit.exit": "Выйти из редактирования", |
| | | "edit.tags": "Редактировать теги", |
| | |
| | | "app.build": "Сборка", |
| | | "group.uncategorized": "Без категории", |
| | | "group.appleBuiltIn": "Встроенные", |
| | | "group.uncommon": "Редко используется", |
| | | "drop.systemDefaultWarning": "Не перетаскивайте в системные категории по умолчанию", |
| | | "tag.design": "Дизайн", |
| | | "tag.development": "Разработка", |
| | |
| | | "tag.productivity": "Офис", |
| | | "settings.coloredContainer": "Цветной контейнер", |
| | | "settings.gridContainer": "Бесцветная сетка", |
| | | "settings.coloredGridContainer": "Цветная сетка" |
| | | "settings.coloredGridContainer": "Цветная сетка", |
| | | "appNote.edit": "Изменить заметку о назначении…", |
| | | "appNote.placeholder": "Заметка о назначении приложения", |
| | | "edit.modeAdd": "Добавить теги", |
| | | "edit.modeRemove": "Удалить теги", |
| | | "edit.modeHint": "Отметки слева применяются только к этой операции.", |
| | | "edit.currentTags": "Текущие теги", |
| | | "edit.commonTags": "Общие теги", |
| | | "edit.selectionHint": "Выберите приложения справа, затем слева отметьте теги, которые нужно добавить или удалить.", |
| | | "edit.noAssignedTags": "У выбранного приложения пока нет тегов.", |
| | | "edit.noCommonTags": "У выбранных приложений нет общих тегов.", |
| | | "edit.mixedTagsHint": "Остальные теги различаются у выбранных приложений.", |
| | | "edit.operationLabel": "Действие:", |
| | | "edit.confirmAdd": "Подтвердить добавление", |
| | | "edit.confirmRemove": "Подтвердить удаление", |
| | | "edit.feedbackAddFormat": "Эта операция добавила %tagCount% тегов для %appCount% приложений (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Эта операция удалила %tagCount% тегов у %appCount% приложений (%appNames%). Удаленные теги: %tagNames%.", |
| | | "edit.removeHint": "Здесь можно снять отметку только с уже существующих тегов. Теги с красным значком удаления будут удалены после подтверждения." |
| | | } |
| | |
| | | "settings.exportFailed": "Извоз није успео", |
| | | "settings.import": "Увези категорије и распоред…", |
| | | "settings.backupDesc": "Извоз чува категорије и распоред у JSON датотеку. Увоз замењује тренутна подешавања датотеком.", |
| | | "settings.uncommonBubble": "Облачићи за ретке апликације", |
| | | "settings.uncommonBubbleDesc": "Приказује велико име и белешку само за апликације означене као ретко коришћене.", |
| | | "settings.importFailed": "Увоз није успео", |
| | | "edit.exit": "Изађи из уређивања", |
| | | "edit.tags": "Уреди ознаке", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Некатегорисано", |
| | | "group.appleBuiltIn": "Уграђено", |
| | | "group.uncommon": "Ретко коришћено", |
| | | "drop.systemDefaultWarning": "Не превлачите апликације у подразумеване системске категорије", |
| | | "tag.design": "Дизајн", |
| | | "tag.development": "Развој", |
| | |
| | | "tag.productivity": "Продуктивност", |
| | | "settings.coloredContainer": "Обојени контејнер", |
| | | "settings.gridContainer": "Безбојна мрежа", |
| | | "settings.coloredGridContainer": "Обојена мрежа" |
| | | "settings.coloredGridContainer": "Обојена мрежа", |
| | | "appNote.edit": "Уреди белешку о намени…", |
| | | "appNote.placeholder": "Белешка о намени апликације", |
| | | "edit.modeAdd": "Додај ознаке", |
| | | "edit.modeRemove": "Уклони ознаке", |
| | | "edit.modeHint": "Означавање лево важи само за ову радњу.", |
| | | "edit.currentTags": "Тренутне ознаке", |
| | | "edit.commonTags": "Заједничке ознаке", |
| | | "edit.selectionHint": "Изаберите апликације десно, па лево изаберите ознаке које желите да додате или уклоните.", |
| | | "edit.noAssignedTags": "Изабрана апликација још нема ознаке.", |
| | | "edit.noCommonTags": "Изабране апликације немају заједничке ознаке.", |
| | | "edit.mixedTagsHint": "Остале ознаке се разликују између изабраних апликација.", |
| | | "edit.operationLabel": "Радња:", |
| | | "edit.confirmAdd": "Потврди додавање", |
| | | "edit.confirmRemove": "Потврди уклањање", |
| | | "edit.feedbackAddFormat": "Ова радња је додала %tagCount% ознака за %appCount% апликација (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Ова радња је уклонила %tagCount% ознака са %appCount% апликација (%appNames%). Уклоњене ознаке: %tagNames%.", |
| | | "edit.removeHint": "Овде можете одчекирати само постојеће ознаке. Ознаке са црвеном иконицом брисања биће уклоњене након потврде." |
| | | } |
| | |
| | | "settings.exportFailed": "Export misslyckades", |
| | | "settings.import": "Importera kategorier och layout…", |
| | | "settings.backupDesc": "Export sparar kategorier och layout till en JSON-fil. Import ersätter den aktuella konfigurationen med en fil.", |
| | | "settings.uncommonBubble": "Bubblor för sällan använda appar", |
| | | "settings.uncommonBubbleDesc": "Visar en stor namn- och anteckningsbubbla endast för appar markerade som sällan använda.", |
| | | "settings.importFailed": "Import misslyckades", |
| | | "edit.exit": "Avsluta redigering", |
| | | "edit.tags": "Redigera taggar", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Okategoriserat", |
| | | "group.appleBuiltIn": "Inbyggt", |
| | | "group.uncommon": "Sällan använd", |
| | | "drop.systemDefaultWarning": "Dra inte appar till systemets standardkategorier", |
| | | "tag.design": "Design", |
| | | "tag.development": "Utveckling", |
| | |
| | | "tag.productivity": "Produktivitet", |
| | | "settings.coloredContainer": "Färgad behållare", |
| | | "settings.gridContainer": "Färglöst rutnät", |
| | | "settings.coloredGridContainer": "Färgat rutnät" |
| | | "settings.coloredGridContainer": "Färgat rutnät", |
| | | "appNote.edit": "Redigera användningsanteckning…", |
| | | "appNote.placeholder": "Användningsanteckning för app", |
| | | "edit.modeAdd": "Lagg till taggar", |
| | | "edit.modeRemove": "Ta bort taggar", |
| | | "edit.modeHint": "Markeringarna till vanster galler bara for den har atgarden.", |
| | | "edit.currentTags": "Nuvarande taggar", |
| | | "edit.commonTags": "Gemensamma taggar", |
| | | "edit.selectionHint": "Valj appar till hoger och valj sedan till vanster vilka taggar som ska laggas till eller tas bort.", |
| | | "edit.noAssignedTags": "Den valda appen har inga taggar an.", |
| | | "edit.noCommonTags": "De valda apparna har inga gemensamma taggar.", |
| | | "edit.mixedTagsHint": "Ovriga taggar skiljer sig mellan de valda apparna.", |
| | | "edit.operationLabel": "Atgard:", |
| | | "edit.confirmAdd": "Bekrafta lagg till", |
| | | "edit.confirmRemove": "Bekrafta ta bort", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "ส่งออกไม่สำเร็จ", |
| | | "settings.import": "นำเข้าหมวดหมู่และเลย์เอาต์…", |
| | | "settings.backupDesc": "การส่งออกจะบันทึกหมวดหมู่และเลย์เอาต์เป็นไฟล์ JSON การนำเข้าจะแทนที่การตั้งค่าปัจจุบันด้วยไฟล์", |
| | | "settings.uncommonBubble": "บับเบิลสำหรับแอปที่ไม่ค่อยใช้", |
| | | "settings.uncommonBubbleDesc": "แสดงชื่อขนาดใหญ่และบันทึกเฉพาะแอปที่ทำเครื่องหมายว่าไม่ค่อยใช้", |
| | | "settings.importFailed": "นำเข้าไม่สำเร็จ", |
| | | "edit.exit": "ออกจากการแก้ไข", |
| | | "edit.tags": "แก้ไขแท็ก", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "ไม่จัดหมวดหมู่", |
| | | "group.appleBuiltIn": "ในตัว", |
| | | "group.uncommon": "ไม่ค่อยใช้", |
| | | "drop.systemDefaultWarning": "อย่าลากแอปไปยังหมวดหมู่เริ่มต้นของระบบ", |
| | | "tag.design": "ออกแบบ", |
| | | "tag.development": "พัฒนา", |
| | |
| | | "tag.productivity": "งาน", |
| | | "settings.coloredContainer": "คอนเทนเนอร์มีสี", |
| | | "settings.gridContainer": "กริดไม่มีสี", |
| | | "settings.coloredGridContainer": "กริดมีสี" |
| | | "settings.coloredGridContainer": "กริดมีสี", |
| | | "appNote.edit": "แก้ไขบันทึกการใช้งาน…", |
| | | "appNote.placeholder": "บันทึกการใช้งานแอป", |
| | | "edit.modeAdd": "เพิ่มแท็ก", |
| | | "edit.modeRemove": "ลบแท็ก", |
| | | "edit.modeHint": "เครื่องหมายทางซ้ายมีผลกับการทำงานครั้งนี้เท่านั้น", |
| | | "edit.currentTags": "แท็กปัจจุบัน", |
| | | "edit.commonTags": "แท็กร่วม", |
| | | "edit.selectionHint": "เลือกแอปทางขวา แล้วเลือกทางซ้ายว่าแท็กใดจะเพิ่มหรือลบในการทำครั้งนี้", |
| | | "edit.noAssignedTags": "แอปที่เลือกยังไม่มีแท็ก", |
| | | "edit.noCommonTags": "แอปที่เลือกไม่มีแท็กร่วมกัน", |
| | | "edit.mixedTagsHint": "แท็กอื่น ๆ ของแอปที่เลือกยังแตกต่างกัน", |
| | | "edit.operationLabel": "การทำงานนี้:", |
| | | "edit.confirmAdd": "ยืนยันการเพิ่ม", |
| | | "edit.confirmRemove": "ยืนยันการลบ", |
| | | "edit.feedbackAddFormat": "การทำงานครั้งนี้ได้เพิ่มแท็ก %tagCount% รายการให้กับแอป %appCount% แอป (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "การทำงานครั้งนี้ได้ลบแท็ก %tagCount% รายการออกจากแอป %appCount% แอป (%appNames%). แท็กที่ลบ: %tagNames%.", |
| | | "edit.removeHint": "ที่นี่สามารถยกเลิกเครื่องหมายได้เฉพาะแท็กที่มีอยู่แล้วเท่านั้น แท็กที่แสดงไอคอนลบสีแดงจะถูกลบหลังจากยืนยัน" |
| | | } |
| | |
| | | "settings.exportFailed": "Dışa aktarma başarısız", |
| | | "settings.import": "Kategorileri ve düzeni içe aktar…", |
| | | "settings.backupDesc": "Dışa aktarma kategorileri ve düzeni JSON dosyasına kaydeder. İçe aktarma mevcut kurulumu dosyadakiyle değiştirir.", |
| | | "settings.uncommonBubble": "Az kullanılan app balonları", |
| | | "settings.uncommonBubbleDesc": "Yalnızca az kullanılan olarak işaretlenen app’lerde büyük ad ve not balonu gösterir.", |
| | | "settings.importFailed": "İçe aktarma başarısız", |
| | | "edit.exit": "Düzenlemeden çık", |
| | | "edit.tags": "Etiketleri düzenle", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Kategorisiz", |
| | | "group.appleBuiltIn": "Yerleşik", |
| | | "group.uncommon": "Az kullanılan", |
| | | "drop.systemDefaultWarning": "Uygulamaları sistem varsayılan kategorilerine sürüklemeyin", |
| | | "tag.design": "Tasarım", |
| | | "tag.development": "Geliştirme", |
| | |
| | | "tag.productivity": "Verimlilik", |
| | | "settings.coloredContainer": "Renkli Kapsayıcı", |
| | | "settings.gridContainer": "Renksiz Izgara", |
| | | "settings.coloredGridContainer": "Renkli Izgara" |
| | | "settings.coloredGridContainer": "Renkli Izgara", |
| | | "appNote.edit": "Kullanım notunu düzenle…", |
| | | "appNote.placeholder": "App kullanım notu", |
| | | "edit.modeAdd": "Etiket ekle", |
| | | "edit.modeRemove": "Etiket kaldir", |
| | | "edit.modeHint": "Soldaki isaretler yalnizca bu islem icin gecerlidir.", |
| | | "edit.currentTags": "Guncel etiketler", |
| | | "edit.commonTags": "Ortak etiketler", |
| | | "edit.selectionHint": "Saga taraftan uygulamalari secin, sonra soldan eklemek veya kaldirmak istediginiz etiketleri secin.", |
| | | "edit.noAssignedTags": "Secilen uygulamanin henuz etiketi yok.", |
| | | "edit.noCommonTags": "Secilen uygulamalarin ortak etiketi yok.", |
| | | "edit.mixedTagsHint": "Diger etiketler secilen uygulamalar arasinda farklilik gosterir.", |
| | | "edit.operationLabel": "Islem:", |
| | | "edit.confirmAdd": "Eklemeyi onayla", |
| | | "edit.confirmRemove": "Kaldirmayi onayla", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "Помилка експорту", |
| | | "settings.import": "Імпорт категорій і макета…", |
| | | "settings.backupDesc": "Експорт зберігає категорії та макет у JSON-файл. Імпорт замінює поточні налаштування даними з файлу.", |
| | | "settings.uncommonBubble": "Підказки для рідкісних програм", |
| | | "settings.uncommonBubbleDesc": "Показує велику назву й нотатку лише для програм, позначених як рідко використовувані.", |
| | | "settings.importFailed": "Помилка імпорту", |
| | | "edit.exit": "Вийти з редагування", |
| | | "edit.tags": "Редагувати теги", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Без категорії", |
| | | "group.appleBuiltIn": "Вбудовані", |
| | | "group.uncommon": "Рідко використовується", |
| | | "drop.systemDefaultWarning": "Не перетягуйте застосунки в системні категорії за замовчуванням", |
| | | "tag.design": "Дизайн", |
| | | "tag.development": "Розробка", |
| | |
| | | "tag.productivity": "Продуктивність", |
| | | "settings.coloredContainer": "Кольоровий контейнер", |
| | | "settings.gridContainer": "Безбарвна сітка", |
| | | "settings.coloredGridContainer": "Кольорова сітка" |
| | | "settings.coloredGridContainer": "Кольорова сітка", |
| | | "appNote.edit": "Редагувати нотатку про призначення…", |
| | | "appNote.placeholder": "Нотатка про призначення програми", |
| | | "edit.modeAdd": "Додати теги", |
| | | "edit.modeRemove": "Вилучити теги", |
| | | "edit.modeHint": "Позначки ліворуч застосовуються лише до цієї дії.", |
| | | "edit.currentTags": "Поточні теги", |
| | | "edit.commonTags": "Спільні теги", |
| | | "edit.selectionHint": "Виберіть застосунки праворуч, а потім ліворуч позначте теги, які треба додати або вилучити.", |
| | | "edit.noAssignedTags": "У вибраного застосунку ще немає тегів.", |
| | | "edit.noCommonTags": "У вибраних застосунків немає спільних тегів.", |
| | | "edit.mixedTagsHint": "Інші теги відрізняються між вибраними застосунками.", |
| | | "edit.operationLabel": "Дія:", |
| | | "edit.confirmAdd": "Підтвердити додавання", |
| | | "edit.confirmRemove": "Підтвердити вилучення", |
| | | "edit.feedbackAddFormat": "Ця дія додала %tagCount% тегів для %appCount% застосунків (%appNames%).", |
| | | "edit.feedbackRemoveFormat": "Ця дія вилучила %tagCount% тегів у %appCount% застосунків (%appNames%). Вилучені теги: %tagNames%.", |
| | | "edit.removeHint": "Тут можна зняти позначку лише з наявних тегів. Теги з червоною іконкою видалення буде вилучено після підтвердження." |
| | | } |
| | |
| | | "settings.exportFailed": "Xuất thất bại", |
| | | "settings.import": "Nhập danh mục & bố cục…", |
| | | "settings.backupDesc": "Xuất sẽ lưu danh mục và bố cục thành tệp JSON. Nhập sẽ thay thế thiết lập hiện tại bằng một tệp.", |
| | | "settings.uncommonBubble": "Bong bóng cho app ít dùng", |
| | | "settings.uncommonBubbleDesc": "Chỉ hiển thị bong bóng tên lớn và ghi chú cho app được đánh dấu là ít dùng.", |
| | | "settings.importFailed": "Nhập thất bại", |
| | | "edit.exit": "Thoát chỉnh sửa", |
| | | "edit.tags": "Sửa thẻ", |
| | |
| | | "app.build": "Build", |
| | | "group.uncategorized": "Chưa phân loại", |
| | | "group.appleBuiltIn": "Tích hợp", |
| | | "group.uncommon": "Ít dùng", |
| | | "drop.systemDefaultWarning": "Không kéo ứng dụng vào danh mục mặc định của hệ thống", |
| | | "tag.design": "Thiết kế", |
| | | "tag.development": "Phát triển", |
| | |
| | | "tag.productivity": "Năng suất", |
| | | "settings.coloredContainer": "Vùng chứa có màu", |
| | | "settings.gridContainer": "Lưới không màu", |
| | | "settings.coloredGridContainer": "Lưới có màu" |
| | | "settings.coloredGridContainer": "Lưới có màu", |
| | | "appNote.edit": "Sửa ghi chú mục đích…", |
| | | "appNote.placeholder": "Ghi chú mục đích của app", |
| | | "edit.modeAdd": "Them nhan", |
| | | "edit.modeRemove": "Xoa nhan", |
| | | "edit.modeHint": "Cac dau tich ben trai chi ap dung cho thao tac nay.", |
| | | "edit.currentTags": "Nhan hien tai", |
| | | "edit.commonTags": "Nhan chung", |
| | | "edit.selectionHint": "Chon app o ben phai, sau do chon o ben trai cac nhan can them hoac xoa.", |
| | | "edit.noAssignedTags": "App da chon chua co nhan nao.", |
| | | "edit.noCommonTags": "Cac app da chon khong co nhan chung.", |
| | | "edit.mixedTagsHint": "Cac nhan khac khac nhau giua cac app da chon.", |
| | | "edit.operationLabel": "Tac vu nay:", |
| | | "edit.confirmAdd": "Xac nhan them", |
| | | "edit.confirmRemove": "Xac nhan xoa", |
| | | "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." |
| | | } |
| | |
| | | "settings.exportFailed": "导出失败", |
| | | "settings.import": "导入分类与布局…", |
| | | "settings.backupDesc": "导出将分类与布局保存为 JSON 文件。导入用一个文件替换当前设置。", |
| | | "settings.uncommonBubble": "不常用 App 气泡提示", |
| | | "settings.uncommonBubbleDesc": "仅对标记为“不常用”的 App 显示大号名称与用途备忘。", |
| | | "settings.importFailed": "导入失败", |
| | | "edit.exit": "退出编辑", |
| | | "edit.tags": "编辑标签", |
| | |
| | | "app.build": "构建", |
| | | "group.uncategorized": "未分类", |
| | | "group.appleBuiltIn": "Mac自带", |
| | | "group.uncommon": "不常用", |
| | | "drop.systemDefaultWarning": "请勿拖动至系统默认分类", |
| | | "tag.design": "设计", |
| | | "tag.development": "编程", |
| | |
| | | "tag.productivity": "办公", |
| | | "settings.coloredContainer": "彩色容器", |
| | | "settings.gridContainer": "无色网格", |
| | | "settings.coloredGridContainer": "彩色网格" |
| | | "settings.coloredGridContainer": "彩色网格", |
| | | "appNote.edit": "编辑用途备忘…", |
| | | "appNote.placeholder": "APP 用途备忘", |
| | | "edit.modeAdd": "添加标签", |
| | | "edit.modeRemove": "移除标签", |
| | | "edit.modeHint": "左侧勾选只作用于本次操作。", |
| | | "edit.currentTags": "当前标签", |
| | | "edit.commonTags": "共同标签", |
| | | "edit.selectionHint": "先在右侧选择应用,再在左侧勾选本次要添加或移除的标签。", |
| | | "edit.noAssignedTags": "当前选中的应用还没有标签。", |
| | | "edit.noCommonTags": "当前选中的这些应用没有共同标签。", |
| | | "edit.mixedTagsHint": "这些应用的其他标签彼此不同。", |
| | | "edit.operationLabel": "本轮操作:", |
| | | "edit.confirmAdd": "确认添加", |
| | | "edit.confirmRemove": "确认移除", |
| | | "edit.feedbackAddFormat": "本次针对%appCount%个应用(%appNames%)新增了%tagCount%个标签。", |
| | | "edit.feedbackRemoveFormat": "本次针对%appCount%个应用(%appNames%)移除了%tagCount%个标签(%tagNames%)。", |
| | | "edit.removeHint": "仅可取消已有标签的勾选;出现红色删除图标的标签会在确认后被移除。" |
| | | } |
| | |
| | | "settings.exportFailed": "匯出失敗", |
| | | "settings.import": "匯入分類與版面配置…", |
| | | "settings.backupDesc": "匯出將分類與版面配置儲存為 JSON 檔案。匯入用一個檔案取代目前設定。", |
| | | "settings.uncommonBubble": "不常用 App 氣泡提示", |
| | | "settings.uncommonBubbleDesc": "僅對標記為「不常用」的 App 顯示大號名稱與用途備忘。", |
| | | "settings.importFailed": "匯入失敗", |
| | | "edit.exit": "結束編輯", |
| | | "edit.tags": "編輯標籤", |
| | |
| | | "app.build": "建置", |
| | | "group.uncategorized": "未分類", |
| | | "group.appleBuiltIn": "Mac內建", |
| | | "group.uncommon": "不常用", |
| | | "drop.systemDefaultWarning": "請勿拖動至系統預設分類", |
| | | "tag.design": "設計", |
| | | "tag.development": "程式設計", |
| | |
| | | "tag.productivity": "辦公", |
| | | "settings.coloredContainer": "彩色容器", |
| | | "settings.gridContainer": "無色網格", |
| | | "settings.coloredGridContainer": "彩色網格" |
| | | "settings.coloredGridContainer": "彩色網格", |
| | | "appNote.edit": "編輯用途備忘…", |
| | | "appNote.placeholder": "APP 用途備忘", |
| | | "edit.modeAdd": "新增標籤", |
| | | "edit.modeRemove": "移除標籤", |
| | | "edit.modeHint": "左側勾選只作用於本次操作。", |
| | | "edit.currentTags": "目前標籤", |
| | | "edit.commonTags": "共同標籤", |
| | | "edit.selectionHint": "先在右側選取應用,再在左側勾選本次要新增或移除的標籤。", |
| | | "edit.noAssignedTags": "目前選取的應用尚未有標籤。", |
| | | "edit.noCommonTags": "目前選取的這些應用沒有共同標籤。", |
| | | "edit.mixedTagsHint": "這些應用的其他標籤彼此不同。", |
| | | "edit.operationLabel": "本輪操作:", |
| | | "edit.confirmAdd": "確認新增", |
| | | "edit.confirmRemove": "確認移除", |
| | | "edit.feedbackAddFormat": "本次針對%appCount%個應用(%appNames%)新增了%tagCount%個標籤。", |
| | | "edit.feedbackRemoveFormat": "本次針對%appCount%個應用(%appNames%)移除了%tagCount%個標籤(%tagNames%)。", |
| | | "edit.removeHint": "僅可取消既有標籤的勾選;出現紅色刪除圖示的標籤會在確認後被移除。" |
| | | } |
| | |
| | | var showNames: Bool = true |
| | | var dragModeActive: Bool = false |
| | | var onDragModeChange: ((Bool) -> Void)? = nil |
| | | var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil |
| | | var onEditNote: ((AppInfo, CGRect) -> Void)? = nil |
| | | var onDropApp: ((String, String, Bool) -> Void)? = nil |
| | | |
| | | /// Adaptive columns — auto-fit based on icon size and available width. |
| | |
| | | sourceTag: group.name, |
| | | dragModeActive: dragModeActive, |
| | | onDragModeChange: onDragModeChange, |
| | | onBubbleHover: onBubbleHover, |
| | | onEditNote: onEditNote, |
| | | onSelect: { onSelectApp(app) } |
| | | ) |
| | | } |
| | |
| | | # Apptag Changelog |
| | | |
| | | ## [6.0.5] — 2026-05-16 |
| | | |
| | | - 修正应用分类编辑页的批量操作逻辑:添加模式只追加手动勾选的标签,移除模式会先自动勾选右侧已选 App 现有的标签合集,未命中的标签置灰不可点 |
| | | - 移除模式下,取消某个已有标签的勾选后,会在标签右侧显示鲜红删除图标;重新勾回后,删除图标立即消失 |
| | | - 优化移除模式的左侧提示文案,明确说明只有已有标签可取消勾选,出现红色删除图标的标签会在确认后被移除 |
| | | - 新增居中全屏反馈提示:点击「确认添加 / 确认移除」后,会复用“不常用 App”气泡矩形样式显示本次操作结果,并支持自动消失和右上角关闭 |
| | | - 反馈统计改为真实增减计数:逐个 App 计算实际新增/移除的标签关系数,避免把原本已存在或不存在的标签重复计入 |
| | | - 调整编辑页顶部排版:将本轮操作说明移到确认按钮同一行,并为长文案提供省略与悬浮完整提示;左侧“选择标签 / 拖动排序”提示改为与标签列表左边缘对齐 |
| | | - 将英文标题文案由 `This edit:` 收敛为更自然的 `Action:`,并同步检查、收紧其他语言的对应表述 |
| | | - 修复编辑分类页左侧 sidebar 被说明文字横向撑开的布局问题,固定左栏宽度并恢复右侧应用区紧贴分隔线展开 |
| | | - 左侧 `Select tags / Drag to reorder` 提示现在严格约束在标签列表宽度内,超长时换行,不再越出左侧标签列表边界 |
| | | - 去掉编辑分类页顶部居中的标题,把空间让给右侧模式说明;右侧说明现在会优先尽量完整显示,只有左右按钮区确实排不下时才从中间省略 |
| | | - 修复顶部模式说明使用无限宽度参与窗口约束计算时可能触发 AppKit 约束更新异常并闪退的问题 |
| | | - 修复新版 macOS 上 Safari 等系统 App 可能被扫描遗漏的问题:扫描器现在会识别搜索目录的直接 `.app` 子级、补充 Cryptex 系统应用目录,并用真实路径去重但保留稳定入口路径 |
| | | - 补齐编辑模式相关新增文案到全部语言文件,避免切换语言后出现缺失键 |
| | | - 版本号更新为 `6.0.5`,Build 更新为 `610` |
| | | |
| | | ## [6.0.0] — 2026-05-16 |
| | | |
| | | - 新增“不常用”特殊系统标记:只在 App 分类编辑模式中显示,不作为普通分类出现在日常应用列表中 |
| | | - “不常用”不改变 App 原有分类归属;App 仍留在原分类中,只额外获得低频标记 |
| | | - 新增“不常用 App 气泡提示”开关,默认开启,放在设置页“数据”页签最上方 |
| | | - 对已标记为“不常用”的 App,在开关开启后 hover 显示大号 App 名称 Bubble;有用途备注时同步显示备注 |
| | | - 支持右键“不常用”App 编辑用途备忘,备注最多 80 个 Unicode 字符 |
| | | - 备注数据进入分类与布局 JSON,并与“不常用”标记状态解耦:移除“不常用”标记后备注仍保留 |
| | | - 编辑模式中的批量标签操作改为“添加标签 / 移除标签”双模式;右侧选中 App 不再自动反向勾选左侧标签,避免批量污染原有分类 |
| | | - 为编辑模式新增只读标签状态区:单选显示当前标签,多选显示共同标签,并提示其他标签可能不同 |
| | | - 新扫描到的 App 会自动标记为“不常用”,并开始统计从 TagLauncher 打开的次数;达到 100 次后,只有系统自动加上的“不常用”才会被自动移除 |
| | | - 为新增功能补齐全部 29 个语言版本文案 |
| | | - 版本号更新为 `6.0.0`,Build 更新为 `600` |
| | | |
| | | ## [5.7.8] — 2026-05-15 |
| | | |
| | | - 修复 TagLauncher 浮层每次打开都会重复添加 Escape 键本地监听的问题,关闭浮层时会同步释放监听器,避免事件监听累积 |