From beebaf7b68b3749f4a1bc4d916e23906b8c16d7f Mon Sep 17 00:00:00 2001
From: Ariver <ar@MacBook-Air.local>
Date: Wed, 13 May 2026 23:31:23 +0800
Subject: [PATCH] Release TagLauncher 5.6.0
---
Apptag/ApptagApp.swift | 16 ++-
AppStore_Submission.md | 19 ++-
Apptag/Localization/en.json | 1
Apptag/Localization/zh-Hant.json | 1
Apptag/TagLauncher.entitlements | 6 +
Apptag/Localization/ja.json | 1
Apptag/Info.plist | 4
Apptag/Localization/fr.json | 1
Apptag/Localization/ko.json | 1
Apptag/Localization/zh-Hans.json | 1
Apptag/ContentView.swift | 173 ++++++++++++++++++++++++++++++----
Apptag/DataLayer.swift | 49 +++++++++
Apptag/Localization/it.json | 1
Apptag/Localization/es.json | 1
Apptag/Localization/ru.json | 1
15 files changed, 242 insertions(+), 34 deletions(-)
diff --git a/AppStore_Submission.md b/AppStore_Submission.md
index e11b6a2..b0335d1 100644
--- a/AppStore_Submission.md
+++ b/AppStore_Submission.md
@@ -1,6 +1,6 @@
# Apptag — Mac App Store 提审资料
-> 版本: 5.5.0 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-11
+> 版本: 5.6.0 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-13
---
@@ -11,8 +11,8 @@
| **App 名称** | Apptag |
| **副标题** | Tag-Based App Launcher |
| **Bundle ID** | com.apptag.launcher |
-| **版本号** | 5.5.0 |
-| **Build 号** | 550 |
+| **版本号** | 5.6.0 |
+| **Build 号** | 560 |
| **SKU** | apptag-mac-001 |
| **主要类别** | Utilities (工具) |
| **次要类别** | Productivity (效率) |
@@ -187,8 +187,12 @@
## 8. What's New (本次版本更新说明)
```
-Apptag 5.5.0
+Apptag 5.6.0
+- 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
@@ -196,7 +200,7 @@
- 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 updated to 5.5.0 (Build 550)
+- Version updated to 5.6.0 (Build 560)
```
---
@@ -220,13 +224,18 @@
**已完成配置**:
1. **Entitlements 文件**: `Apptag/TagLauncher.entitlements` 已创建,包含:
- `com.apple.security.app-sandbox`
+ - `com.apple.security.files.user-selected.read-write`,用于用户主动选择位置后的导入/导出 JSON
+ - `com.apple.security.temporary-exception.files.home-relative-path.read-only`,用于一次性读取旧版非沙盒数据库 `~/Library/Application Support/Apptag/`
2. **build.sh 已更新**:
- `APP_STORE=1` 时强制检查 entitlements 文件存在
- `APP_STORE=1 CODESIGN_IDENTITY="..." bash build.sh` 使用 entitlements 签名
3. **代码处理**:
+ - App Store 沙盒环境下会优先迁移旧版非沙盒 `tags.json`,避免升级后标签和排序看起来丢失
- 本地非沙盒版本保留 LaunchAgent 登录启动
- App Store 沙盒环境下隐藏 Launch at Login 设置项,并跳过 LaunchAgent 写入
+**审核说明建议**: 临时 home-relative-path 读取权限仅用于从旧版非沙盒存储位置迁移用户已有标签、分类和排序数据到 sandbox 容器,不用于持续访问用户文件。
+
**仍需验证**: TestFlight 中确认沙盒下 Carbon `RegisterEventHotKey` 是否可正常工作。
### 🟢 已处理: Launch at Login 与 Sandbox
diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index 9698519..76f0549 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -588,6 +588,7 @@
try TagDatabase.exportTo(url)
} catch {
fputs("[TagLauncher] Export failed: \(error)\n", stderr)
+ showDataAlert(title: tr("settings.exportFailed"), message: error.localizedDescription)
}
}
}
@@ -604,16 +605,19 @@
scanApps()
} catch {
fputs("[TagLauncher] Import failed: \(error)\n", stderr)
- // Show alert on failure
- let alert = NSAlert()
- alert.messageText = tr("settings.importFailed")
- alert.informativeText = error.localizedDescription
- alert.alertStyle = .warning
- alert.runModal()
+ showDataAlert(title: tr("settings.importFailed"), message: error.localizedDescription)
}
}
}
+ private func showDataAlert(title: String, message: String) {
+ let alert = NSAlert()
+ alert.messageText = title
+ alert.informativeText = message
+ alert.alertStyle = .warning
+ alert.runModal()
+ }
+
private var appVersion: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
}
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 843f892..3430c2e 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -110,7 +110,10 @@
struct TagPill: View {
let name: String
let colorIndex: Int
+ var dragModeActive: Bool = false
+ var isDragging: Bool = false
let action: () -> Void
+ @State private var wiggle = false
private var bgColor: Color {
Color(nsColor: TagColor.nsColor(for: colorIndex))
@@ -124,16 +127,28 @@
}
var body: some View {
- Button(action: action) {
- Text(name)
- .font(.system(size: 13, weight: .medium))
- .foregroundStyle(textColor)
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .background(RoundedRectangle(cornerRadius: 7).fill(bgColor))
- .shadow(color: .black.opacity(0.2), radius: 3, y: 1)
+ Text(name)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundStyle(textColor)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(RoundedRectangle(cornerRadius: 7).fill(bgColor))
+ .shadow(color: .black.opacity(isDragging ? 0.34 : 0.2), radius: isDragging ? 8 : 3, y: isDragging ? 4 : 1)
+ .scaleEffect(isDragging ? 1.05 : 1.0)
+ .rotationEffect(.degrees(dragModeActive ? (wiggle ? 1.8 : -1.8) : 0))
+ .animation(
+ dragModeActive
+ ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
+ : .default,
+ value: wiggle
+ )
+ .onChange(of: dragModeActive) { _, active in
+ wiggle = active
+ }
+ .contentShape(RoundedRectangle(cornerRadius: 7))
+ .onTapGesture {
+ if !dragModeActive { action() }
}
- .buttonStyle(.plain)
}
}
@@ -142,7 +157,10 @@
struct SideTagPill: View {
let name: String
let colorIndex: Int
+ var dragModeActive: Bool = false
+ var isDragging: Bool = false
let action: () -> Void
+ @State private var wiggle = false
private var bgColor: Color {
Color(nsColor: TagColor.nsColor(for: colorIndex))
@@ -152,16 +170,28 @@
}
var body: some View {
- Button(action: action) {
- Text(name)
- .font(.system(size: 13, weight: .medium))
- .foregroundStyle(textColor)
- .padding(.horizontal, 10).padding(.vertical, 5)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(RoundedRectangle(cornerRadius: 6).fill(bgColor))
- .shadow(color: .black.opacity(0.2), radius: 3, y: 1)
+ Text(name)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundStyle(textColor)
+ .padding(.horizontal, 10).padding(.vertical, 5)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(RoundedRectangle(cornerRadius: 6).fill(bgColor))
+ .shadow(color: .black.opacity(isDragging ? 0.34 : 0.2), radius: isDragging ? 8 : 3, y: isDragging ? 4 : 1)
+ .scaleEffect(isDragging ? 1.03 : 1.0)
+ .rotationEffect(.degrees(dragModeActive ? (wiggle ? 1.6 : -1.6) : 0))
+ .animation(
+ dragModeActive
+ ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
+ : .default,
+ value: wiggle
+ )
+ .onChange(of: dragModeActive) { _, active in
+ wiggle = active
+ }
+ .contentShape(RoundedRectangle(cornerRadius: 6))
+ .onTapGesture {
+ if !dragModeActive { action() }
}
- .buttonStyle(.plain)
}
}
@@ -205,6 +235,9 @@
@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 tagNavDragModeActive = false
+ @State private var tagNavDragItem: String? = nil
+ @State private var tagNavReorderFrames: [String: CGRect] = [:]
@State private var hoveredContainer: String? = nil // colored container lift
// Fixed interaction for "Colorless Container": hover fills persistently; click clears.
@State private var filledColorlessContainer: String? = nil
@@ -388,12 +421,17 @@
HStack(spacing: 8) {
ForEach(tagLabels) { tag in
TagPill(name: tag.name, colorIndex: tag.colorIndex,
+ dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
+ isDragging: tagNavDragItem == tag.name,
action: {
if isColorlessContainerMode {
toggleColorlessFill(tag.id)
}
scrollTo(tag.id)
})
+ .background(tagNavFrameReader(for: tag.name))
+ .zIndex(tagNavDragItem == tag.name ? 1 : 0)
+ .highPriorityGesture(tagNavReorderGesture(for: tag.name))
.onHover { hovering in
if hovering {
fillColorlessContainer(tag.id)
@@ -401,7 +439,12 @@
}
}
}
- }.padding(.horizontal, 24)
+ }
+ .padding(.horizontal, 24)
+ .coordinateSpace(name: "tagNavReorder")
+ .onPreferenceChange(TagNavReorderFramePreferenceKey.self) { frames in
+ tagNavReorderFrames = frames
+ }
}
}
@@ -410,12 +453,17 @@
VStack(spacing: 6) {
ForEach(tagLabels) { tag in
SideTagPill(name: tag.name, colorIndex: tag.colorIndex,
+ dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
+ isDragging: tagNavDragItem == tag.name,
action: {
if isColorlessContainerMode {
toggleColorlessFill(tag.id)
}
scrollTo(tag.id)
})
+ .background(tagNavFrameReader(for: tag.name))
+ .zIndex(tagNavDragItem == tag.name ? 1 : 0)
+ .highPriorityGesture(tagNavReorderGesture(for: tag.name))
.onHover { hovering in
if hovering {
fillColorlessContainer(tag.id)
@@ -423,7 +471,12 @@
}
}
}
- }.padding(12)
+ }
+ .padding(12)
+ .coordinateSpace(name: "tagNavReorder")
+ .onPreferenceChange(TagNavReorderFramePreferenceKey.self) { frames in
+ tagNavReorderFrames = frames
+ }
}.frame(width: 135)
}
@@ -1179,6 +1232,78 @@
filledColorlessContainer = (filledColorlessContainer == id) ? nil : id
}
+ private func canReorderTag(_ name: String) -> Bool {
+ tagColors[name] != nil && name != "Mac自带" && name != defaultGroupName
+ }
+
+ private func tagNavFrameReader(for tagName: String) -> some View {
+ GeometryReader { proxy in
+ Color.clear.preference(
+ key: TagNavReorderFramePreferenceKey.self,
+ value: canReorderTag(tagName)
+ ? [tagName: proxy.frame(in: .named("tagNavReorder"))]
+ : [:]
+ )
+ }
+ }
+
+ private func tagNavReorderGesture(for tagName: String) -> some Gesture {
+ LongPressGesture(minimumDuration: 0.5)
+ .sequenced(before: DragGesture(minimumDistance: 3, coordinateSpace: .named("tagNavReorder")))
+ .onChanged { value in
+ guard canReorderTag(tagName) else { return }
+ switch value {
+ case .first(true):
+ beginTagNavReorder(tagName)
+ case .second(true, let drag?):
+ if tagNavDragItem == nil {
+ beginTagNavReorder(tagName)
+ }
+ reorderTagNavItem(at: drag.location)
+ default:
+ break
+ }
+ }
+ .onEnded { value in
+ guard canReorderTag(tagName) else { return }
+ if case .second(true, let drag?) = value {
+ reorderTagNavItem(at: drag.location)
+ }
+ endTagNavReorder()
+ }
+ }
+
+ private func beginTagNavReorder(_ tagName: String) {
+ guard canReorderTag(tagName) else { return }
+ if tagNavDragItem == nil {
+ tagNavDragItem = tagName
+ }
+ tagNavDragModeActive = true
+ }
+
+ private func endTagNavReorder() {
+ if tagNavDragModeActive {
+ TagEditor.reorderTags(draggedTagNames)
+ }
+ tagNavDragModeActive = false
+ tagNavDragItem = nil
+ }
+
+ private func reorderTagNavItem(at location: CGPoint) {
+ guard let fromName = tagNavDragItem,
+ let targetName = tagNavReorderFrames.first(where: { $0.value.contains(location) })?.key,
+ fromName != targetName,
+ let fromIndex = draggedTagNames.firstIndex(of: fromName),
+ let toIndex = draggedTagNames.firstIndex(of: targetName)
+ else { return }
+
+ withAnimation(.spring(response: 0.22, dampingFraction: 0.82)) {
+ let destination = toIndex > fromIndex ? toIndex + 1 : toIndex
+ draggedTagNames.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: destination)
+ }
+ TagEditor.reorderTags(draggedTagNames)
+ }
+
private func handleAppDrop(_ providers: [NSItemProvider], targetTag: String) -> Bool {
guard let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) }) else {
return false
@@ -1315,6 +1440,14 @@
}
}
+private struct TagNavReorderFramePreferenceKey: PreferenceKey {
+ static var defaultValue: [String: CGRect] = [:]
+
+ static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) {
+ value.merge(nextValue(), uniquingKeysWith: { _, new in new })
+ }
+}
+
private struct TagLabel: Identifiable {
var id: String { name }
let name: String
diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index 6147cac..2eb3d99 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -165,9 +165,25 @@
static var storeURL: URL { storeDir.appendingPathComponent("tags.json") }
+ private static var legacyStoreURL: URL? {
+ guard ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil else {
+ return nil
+ }
+
+ let home = FileManager.default.homeDirectoryForCurrentUser
+ let bundleID = Bundle.main.bundleIdentifier ?? "com.apptag.launcher"
+ let marker = "/Library/Containers/\(bundleID)/Data"
+ guard let range = home.path.range(of: marker) else { return nil }
+
+ let realHomePath = String(home.path[..<range.lowerBound])
+ return URL(fileURLWithPath: realHomePath)
+ .appendingPathComponent("Library/Application Support/Apptag/tags.json")
+ }
+
// MARK: Load / Save
static func load() -> Store {
+ migrateLegacyStoreIfNeeded()
guard let data = try? Data(contentsOf: storeURL),
let store = try? JSONDecoder().decode(Store.self, from: data)
else { return Store() }
@@ -179,10 +195,40 @@
try? data.write(to: storeURL, options: .atomic)
}
+ /// App Store sandbox builds read Application Support inside the app container.
+ /// Older non-sandbox builds stored the same database directly under ~/Library.
+ /// On first sandbox launch, migrate the richer legacy database before seeding defaults.
+ static func migrateLegacyStoreIfNeeded() {
+ let fm = FileManager.default
+ guard let legacyURL = legacyStoreURL,
+ fm.fileExists(atPath: legacyURL.path)
+ else { return }
+
+ guard let legacyData = try? Data(contentsOf: legacyURL),
+ let legacyStore = try? JSONDecoder().decode(Store.self, from: legacyData)
+ else { return }
+
+ if let currentData = try? Data(contentsOf: storeURL),
+ let currentStore = try? JSONDecoder().decode(Store.self, from: currentData),
+ storeScore(currentStore) >= storeScore(legacyStore) {
+ return
+ }
+
+ try? fm.createDirectory(at: storeDir, withIntermediateDirectories: true)
+ try? legacyData.write(to: storeURL, options: .atomic)
+ }
+
+ private static func storeScore(_ store: Store) -> Int {
+ store.appTags.count * 100 + store.tagOrder.count * 10 + store.tags.count
+ }
+
// MARK: Export / Import
static func exportTo(_ url: URL) throws {
- try FileManager.default.copyItem(at: storeURL, to: url)
+ migrateLegacyStoreIfNeeded()
+ let store = load()
+ let data = try JSONEncoder().encode(store)
+ try data.write(to: url, options: .atomic)
}
static func importFrom(_ url: URL) throws -> Store {
@@ -195,6 +241,7 @@
/// Seed default tags on first launch. Only runs if store doesn't exist yet.
/// Tag names are loaded from the current language's localization.
static func seedDefaultTags() {
+ migrateLegacyStoreIfNeeded()
guard !FileManager.default.fileExists(atPath: storeURL.path) else { return }
let keys = [
diff --git a/Apptag/Info.plist b/Apptag/Info.plist
index 6755fc9..a9c9d12 100644
--- a/Apptag/Info.plist
+++ b/Apptag/Info.plist
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>5.5.0</string>
+ <string>5.6.0</string>
<key>CFBundleVersion</key>
- <string>550</string>
+ <string>560</string>
<key>LSMinimumSystemVersion</key>
<string>15.0</string>
<key>NSHighResolutionCapable</key>
diff --git a/Apptag/Localization/en.json b/Apptag/Localization/en.json
index bf25473..c8b7bc7 100644
--- a/Apptag/Localization/en.json
+++ b/Apptag/Localization/en.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "Icon display size. Grid columns adjust automatically.",
"settings.backup": "Backup & Restore",
"settings.export": "Export Categories & Layout…",
+ "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.importFailed": "Import Failed",
diff --git a/Apptag/Localization/es.json b/Apptag/Localization/es.json
index 0ce4851..6e0907f 100644
--- a/Apptag/Localization/es.json
+++ b/Apptag/Localization/es.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "Tamaño de visualización de iconos. Las columnas se ajustan automáticamente.",
"settings.backup": "Copia de seguridad",
"settings.export": "Exportar categorías y diseño…",
+ "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.importFailed": "Error de importación",
diff --git a/Apptag/Localization/fr.json b/Apptag/Localization/fr.json
index f0a757c..6c9d00e 100644
--- a/Apptag/Localization/fr.json
+++ b/Apptag/Localization/fr.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "Taille d'affichage des icônes. Les colonnes s'ajustent automatiquement.",
"settings.backup": "Sauvegarde et restauration",
"settings.export": "Exporter catégories et disposition…",
+ "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.importFailed": "Échec de l'import",
diff --git a/Apptag/Localization/it.json b/Apptag/Localization/it.json
index 38ac781..44dbb93 100644
--- a/Apptag/Localization/it.json
+++ b/Apptag/Localization/it.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "Dimensione di visualizzazione delle icone. Le colonne si adattano automaticamente.",
"settings.backup": "Backup e ripristino",
"settings.export": "Esporta categorie e layout…",
+ "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.importFailed": "Importazione fallita",
diff --git a/Apptag/Localization/ja.json b/Apptag/Localization/ja.json
index 8f024d4..74e87a1 100644
--- a/Apptag/Localization/ja.json
+++ b/Apptag/Localization/ja.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "アイコン表示サイズ。グリッド列は自動調整されます。",
"settings.backup": "バックアップと復元",
"settings.export": "カテゴリとレイアウトをエクスポート…",
+ "settings.exportFailed": "エクスポート失敗",
"settings.import": "カテゴリとレイアウトをインポート…",
"settings.backupDesc": "エクスポートはカテゴリとレイアウトをJSONファイルに保存します。インポートは現在の設定をファイルの内容で置き換えます。",
"settings.importFailed": "インポート失敗",
diff --git a/Apptag/Localization/ko.json b/Apptag/Localization/ko.json
index ce0a436..909b4f7 100644
--- a/Apptag/Localization/ko.json
+++ b/Apptag/Localization/ko.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "아이콘 표시 크기. 그리드 열은 자동 조정됩니다.",
"settings.backup": "백업 및 복원",
"settings.export": "분류와 레이아웃 내보내기…",
+ "settings.exportFailed": "내보내기 실패",
"settings.import": "분류와 레이아웃 가져오기…",
"settings.backupDesc": "내보내기는 분류와 레이아웃을 JSON 파일로 저장합니다. 가져오기는 파일의 내용으로 현재 설정을 대체합니다.",
"settings.importFailed": "가져오기 실패",
diff --git a/Apptag/Localization/ru.json b/Apptag/Localization/ru.json
index d259399..fedb682 100644
--- a/Apptag/Localization/ru.json
+++ b/Apptag/Localization/ru.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "Размер отображения значков. Колонки сетки подстраиваются автоматически.",
"settings.backup": "Резервное копирование",
"settings.export": "Экспорт категорий и макета…",
+ "settings.exportFailed": "Ошибка экспорта",
"settings.import": "Импорт категорий и макета…",
"settings.backupDesc": "Экспорт сохраняет категории и макет в JSON-файл. Импорт заменяет текущие настройки данными из файла.",
"settings.importFailed": "Ошибка импорта",
diff --git a/Apptag/Localization/zh-Hans.json b/Apptag/Localization/zh-Hans.json
index 516e047..5e96d76 100644
--- a/Apptag/Localization/zh-Hans.json
+++ b/Apptag/Localization/zh-Hans.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "图标显示尺寸。网格列数自动调整。",
"settings.backup": "备份与恢复",
"settings.export": "导出分类与布局…",
+ "settings.exportFailed": "导出失败",
"settings.import": "导入分类与布局…",
"settings.backupDesc": "导出将分类与布局保存为 JSON 文件。导入用一个文件替换当前设置。",
"settings.importFailed": "导入失败",
diff --git a/Apptag/Localization/zh-Hant.json b/Apptag/Localization/zh-Hant.json
index d0a6bd7..abd3a21 100644
--- a/Apptag/Localization/zh-Hant.json
+++ b/Apptag/Localization/zh-Hant.json
@@ -29,6 +29,7 @@
"settings.iconSizeDesc": "圖示顯示尺寸。網格列數自動調整。",
"settings.backup": "備份與還原",
"settings.export": "匯出分類與版面配置…",
+ "settings.exportFailed": "匯出失敗",
"settings.import": "匯入分類與版面配置…",
"settings.backupDesc": "匯出將分類與版面配置儲存為 JSON 檔案。匯入用一個檔案取代目前設定。",
"settings.importFailed": "匯入失敗",
diff --git a/Apptag/TagLauncher.entitlements b/Apptag/TagLauncher.entitlements
index 852fa1a..e10e91e 100644
--- a/Apptag/TagLauncher.entitlements
+++ b/Apptag/TagLauncher.entitlements
@@ -4,5 +4,11 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
+ <key>com.apple.security.files.user-selected.read-write</key>
+ <true/>
+ <key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
+ <array>
+ <string>/Library/Application Support/Apptag/</string>
+ </array>
</dict>
</plist>
--
Gitblit v1.9.3