Ariver
2026-05-11 4445b715434d503cedd62b7ebd2c01fe6c700e0c
Archive TagLauncher 4.0.7
13 files modified
1 files added
258 ■■■■■ changed files
Apptag/ApptagApp.swift 20 ●●●● patch | view | raw | blame | history
Apptag/ContentView.swift 47 ●●●● patch | view | raw | blame | history
Apptag/Info.plist 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/en.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/es.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/fr.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/it.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/ja.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/ko.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/ru.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/zh-Hans.json 10 ●●●● patch | view | raw | blame | history
Apptag/Localization/zh-Hant.json 10 ●●●● patch | view | raw | blame | history
build.sh 54 ●●●●● patch | view | raw | blame | history
make_dmg.sh 37 ●●●●● patch | view | raw | blame | history
Apptag/ApptagApp.swift
@@ -5,7 +5,7 @@
// MARK: - Application Entry Point
@main
struct ApptagApp: App {
struct TagLauncherApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
@@ -45,7 +45,7 @@
        setupLaunchAtLogin()
    }
    /// Dock icon click → show overlay (same as menubar "Show Apptag")
    /// Dock icon click → show overlay (same as menubar "Show TagLauncher")
    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
        showOverlay()
        return false  // Suppress default "unhide all windows" behavior
@@ -137,9 +137,9 @@
        if let button = statusItem.button {
            button.image = NSImage(
                systemSymbolName: "tag.fill",
                accessibilityDescription: "Apptag"
                accessibilityDescription: "TagLauncher"
            )
            button.toolTip = "Apptag — Tag-based app launcher"
            button.toolTip = "TagLauncher — Tag-based app launcher"
            button.action = #selector(toggleOverlay)
            button.target = self
        }
@@ -297,7 +297,7 @@
        hotkeyRef = ref
        if status != noErr {
            print("[Apptag] Hotkey registration failed: \(status). Falling back to menu bar only.")
            print("[TagLauncher] Hotkey registration failed: \(status). Falling back to menu bar only.")
            return
        }
@@ -385,7 +385,7 @@
    /// Track whether the overlay is in edit mode to suppress auto-dismiss.
    private func observeEditMode() {
        NotificationCenter.default.addObserver(
            forName: .apptagEditModeChanged,
            forName: .tagLauncherEditModeChanged,
            object: nil,
            queue: .main
        ) { [weak self] notification in
@@ -515,14 +515,14 @@
    private func exportTags() {
        let panel = NSSavePanel()
        panel.title = tr("settings.export")
        panel.nameFieldStringValue = "Apptag-tags.json"
        panel.nameFieldStringValue = "TagLauncher-tags.json"
        panel.allowedContentTypes = [.json]
        panel.begin { response in
            guard response == .OK, let url = panel.url else { return }
            do {
                try TagDatabase.exportTo(url)
            } catch {
                fputs("[Apptag] Export failed: \(error)\n", stderr)
                fputs("[TagLauncher] Export failed: \(error)\n", stderr)
            }
        }
    }
@@ -538,7 +538,7 @@
                _ = try TagDatabase.importFrom(url)
                scanApps()
            } catch {
                fputs("[Apptag] Import failed: \(error)\n", stderr)
                fputs("[TagLauncher] Import failed: \(error)\n", stderr)
                // Show alert on failure
                let alert = NSAlert()
                alert.messageText = tr("settings.importFailed")
@@ -694,7 +694,7 @@
                                .frame(width: 128, height: 128)
                        }
                        VStack(alignment: .leading, spacing: 4) {
                            Text("Apptag")
                            Text("TagLauncher")
                                .font(.title2)
                                .fontWeight(.semibold)
                            Text(tr("app.description"))
Apptag/ContentView.swift
@@ -4,7 +4,7 @@
// MARK: - Notification for manual re-index
extension Notification.Name {
    static let apptagEditModeChanged = Notification.Name("ApptagEditModeChanged")
    static let tagLauncherEditModeChanged = Notification.Name("TagLauncherEditModeChanged")
}
// MARK: - Edit Phase
@@ -267,7 +267,7 @@
        .onChange(of: editPhase) { _, newPhase in
            let active = newPhase != .none
            NotificationCenter.default.post(
                name: .apptagEditModeChanged,
                name: .tagLauncherEditModeChanged,
                object: nil,
                userInfo: ["active": active]
            )
@@ -277,7 +277,7 @@
    /// Set edit phase with synchronous notification BEFORE state change.
    func setEditPhase(_ phase: EditPhase) {
        if phase != .none {
            NotificationCenter.default.post(name: .apptagEditModeChanged, object: nil, userInfo: ["active": true])
            NotificationCenter.default.post(name: .tagLauncherEditModeChanged, object: nil, userInfo: ["active": true])
            NSApp.activate(ignoringOtherApps: true)
            // Always sync tag list from database when entering edit mode
            let store = TagDatabase.load()
@@ -287,7 +287,7 @@
        editPhase = phase
        if phase == .none {
            DispatchQueue.main.async {
                NotificationCenter.default.post(name: .apptagEditModeChanged, object: nil, userInfo: ["active": false])
                NotificationCenter.default.post(name: .tagLauncherEditModeChanged, object: nil, userInfo: ["active": false])
            }
        }
    }
@@ -482,6 +482,7 @@
        let isColorless = isColorlessContainerMode
        let isColorlessFilled = isColorless && filledColorlessContainer == group.name
        let isHovered = hoveredContainer == group.name
        let isColorlessActive = isColorless && (isColorlessFilled || isHovered)
        let tagColor = Color(nsColor: TagColor.nsColor(for: tagColors[group.name] ?? 0))
        return VStack(alignment: .leading, spacing: 6) {
            HStack(spacing: 0) {
@@ -511,7 +512,7 @@
        .padding(16)
        .background(
            RoundedRectangle(cornerRadius: 14)
                .fill((isColored || isColorlessFilled) ? tagColor.opacity(0.30) : Color.clear)
                .fill((isColored || isColorlessActive) ? tagColor.opacity(0.30) : Color.clear)
                .background(
                    RoundedRectangle(cornerRadius: 14)
                        .fill(.ultraThinMaterial)
@@ -521,22 +522,21 @@
            RoundedRectangle(cornerRadius: 14)
                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
        )
        .shadow(color: .black.opacity(isColored && isHovered ? 0.22 : 0),
                radius: isColored && isHovered ? 18 : 0,
                y: isColored && isHovered ? 10 : 0)
        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessActive ? 0.22 : 0),
                radius: (isColored && isHovered) || isColorlessActive ? 18 : 0,
                y: (isColored && isHovered) || isColorlessActive ? 10 : 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)
        .onHover { hovering in
            if isColored {
            if isColored || isColorless {
                hoveredContainer = hovering ? group.name : nil
            } else if hovering {
                fillColorlessContainer(group.name)
            }
        }
        .contentShape(RoundedRectangle(cornerRadius: 14))
        .onTapGesture {
            if isColorlessFilled {
                filledColorlessContainer = nil
            if isColorless {
                toggleColorlessFill(group.name)
            }
        }
    }
@@ -689,9 +689,11 @@
    private func gridContainerCard(_ group: TagGroup, width: CGFloat, fixedRows: Int) -> some View {
        let isColored = displayMode == "coloredGridContainer"
        let isColorlessGrid = displayMode == "gridContainer"
        let isColorless = isColorlessContainerMode
        let isColorlessFilled = isColorless && filledColorlessContainer == group.name
        let isHovered = hoveredContainer == group.name
        let isColorlessGridActive = isColorlessGrid && (isColorlessFilled || isHovered)
        let cols = iconColumns(width: width)
        let contentWidth = max(1, width - 32)
        let cellWidth = max(1, (contentWidth - 6 * CGFloat(cols - 1)) / CGFloat(cols))
@@ -739,7 +741,7 @@
        .padding(16)
        .background(
            RoundedRectangle(cornerRadius: 14)
                .fill((isColored || isColorlessFilled) ? tagColor.opacity(0.30) : Color.clear)
                .fill((isColored || isColorlessGridActive) ? tagColor.opacity(0.30) : Color.clear)
                .background(
                    RoundedRectangle(cornerRadius: 14)
                        .fill(.ultraThinMaterial)
@@ -749,13 +751,14 @@
            RoundedRectangle(cornerRadius: 14)
                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
        )
        .shadow(color: .black.opacity(isColored && isHovered ? 0.22 : 0),
                radius: isColored && isHovered ? 18 : 0,
                y: isColored && isHovered ? 10 : 0)
        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessGridActive ? 0.22 : 0),
                radius: (isColored && isHovered) || isColorlessGridActive ? 18 : 0,
                y: (isColored && isHovered) || isColorlessGridActive ? 10 : 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)
        .onHover { hovering in
            if isColored {
            if isColored || isColorlessGrid {
                hoveredContainer = hovering ? group.name : nil
            } else if hovering {
                fillColorlessContainer(group.name)
@@ -763,8 +766,8 @@
        }
        .contentShape(RoundedRectangle(cornerRadius: 14))
        .onTapGesture {
            if isColorlessFilled {
                filledColorlessContainer = nil
            if isColorlessGrid {
                toggleColorlessFill(group.name)
            }
        }
    }
@@ -1019,12 +1022,12 @@
    }
    private func fillColorlessContainer(_ id: String) {
        guard displayMode == "container" else { return }
        guard isColorlessContainerMode else { return }
        filledColorlessContainer = id
    }
    private func toggleColorlessFill(_ id: String) {
        guard displayMode == "container" else { return }
        guard isColorlessContainerMode else { return }
        filledColorlessContainer = (filledColorlessContainer == id) ? nil : id
    }
Apptag/Info.plist
@@ -5,9 +5,9 @@
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleDisplayName</key>
    <string>Apptag</string>
    <string>TagLauncher</string>
    <key>CFBundleExecutable</key>
    <string>Apptag</string>
    <string>TagLauncher</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon</string>
    <key>CFBundleIdentifier</key>
@@ -15,13 +15,13 @@
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>Apptag</string>
    <string>TagLauncher</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>4.0.6</string>
    <string>4.0.7</string>
    <key>CFBundleVersion</key>
    <string>7</string>
    <string>8</string>
    <key>LSMinimumSystemVersion</key>
    <string>26.0</string>
    <key>NSHighResolutionCapable</key>
Apptag/Localization/en.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Show Apptag",
  "menu.show": "Show TagLauncher",
  "menu.preferences": "Preferences…",
  "menu.quit": "Quit",
  "menu.language": "Language",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "App list style:",
  "settings.flat": "Flat",
  "settings.container": "Colorless Container",
  "settings.flatDesc": "\"Flat\" shows apps directly. \"Colorless Container\" and \"Colored Container\" use masonry containers. \"Colorless Grid Container\" and \"Colored Grid Container\" align app icons in equal-height grid rows.",
  "settings.flatDesc": "\"Flat\" shows apps directly. \"Colorless Container\" and \"Colored Container\" use masonry containers. \"Colorless Grid\" and \"Colored Grid\" align app icons in equal-height grid rows.",
  "settings.hideAppNames": "Hide app names",
  "settings.tagPosition": "Tag position:",
  "settings.left": "Left",
@@ -43,7 +43,7 @@
  "tag.cancel": "Cancel",
  "tag.add": "Add",
  "tag.newName": "New tag name",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "Tag-based app launcher",
  "app.version": "Version",
  "app.build": "Build",
@@ -57,6 +57,6 @@
  "tag.system": "System",
  "tag.productivity": "Productivity",
  "settings.coloredContainer": "Colored Container",
  "settings.gridContainer": "Colorless Grid Container",
  "settings.coloredGridContainer": "Colored Grid Container"
  "settings.gridContainer": "Colorless Grid",
  "settings.coloredGridContainer": "Colored Grid"
}
Apptag/Localization/es.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Mostrar Apptag",
  "menu.show": "Mostrar TagLauncher",
  "menu.preferences": "Preferencias…",
  "menu.quit": "Salir",
  "menu.language": "Idioma",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "Estilo de lista de apps:",
  "settings.flat": "Plano",
  "settings.container": "Contenedor sin color",
  "settings.flatDesc": "\"Plano\" muestra las apps directamente. \"Contenedor sin color\" y \"Contenedor coloreado\" usan contenedores en cascada. \"Contenedor de cuadrícula sin color\" y \"Contenedor de cuadrícula coloreado\" alinean los iconos en filas de cuadrícula de igual altura.",
  "settings.flatDesc": "\"Plano\" muestra las apps directamente. \"Contenedor sin color\" y \"Contenedor coloreado\" usan contenedores en cascada. \"Cuadrícula sin color\" y \"Cuadrícula coloreada\" alinean los iconos en filas de cuadrícula de igual altura.",
  "settings.hideAppNames": "Ocultar nombres de apps",
  "settings.tagPosition": "Posición de etiquetas:",
  "settings.left": "Izquierda",
@@ -43,7 +43,7 @@
  "tag.cancel": "Cancelar",
  "tag.add": "Añadir",
  "tag.newName": "Nuevo nombre de etiqueta",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "Lanzador de apps basado en etiquetas",
  "app.version": "Versión",
  "app.build": "Build",
@@ -57,6 +57,6 @@
  "tag.system": "Sistema",
  "tag.productivity": "Oficina",
  "settings.coloredContainer": "Contenedor coloreado",
  "settings.gridContainer": "Contenedor de cuadrícula sin color",
  "settings.coloredGridContainer": "Contenedor de cuadrícula coloreado"
  "settings.gridContainer": "Cuadrícula sin color",
  "settings.coloredGridContainer": "Cuadrícula coloreada"
}
Apptag/Localization/fr.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Afficher Apptag",
  "menu.show": "Afficher TagLauncher",
  "menu.preferences": "Préférences…",
  "menu.quit": "Quitter",
  "menu.language": "Langue",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "Style de liste d'applications :",
  "settings.flat": "Liste",
  "settings.container": "Conteneur sans couleur",
  "settings.flatDesc": "\"Liste\" affiche les apps directement. \"Conteneur sans couleur\" et \"Conteneur coloré\" utilisent des conteneurs en cascade. \"Conteneur grille sans couleur\" et \"Conteneur grille coloré\" alignent les icônes dans des lignes de grille de hauteur égale.",
  "settings.flatDesc": "\"Liste\" affiche les apps directement. \"Conteneur sans couleur\" et \"Conteneur coloré\" utilisent des conteneurs en cascade. \"Grille sans couleur\" et \"Grille colorée\" alignent les icônes dans des lignes de grille de hauteur égale.",
  "settings.hideAppNames": "Masquer les noms d'apps",
  "settings.tagPosition": "Position des étiquettes :",
  "settings.left": "Gauche",
@@ -43,7 +43,7 @@
  "tag.cancel": "Annuler",
  "tag.add": "Ajouter",
  "tag.newName": "Nouveau nom d'étiquette",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "Lanceur d'applications par étiquettes",
  "app.version": "Version",
  "app.build": "Build",
@@ -57,6 +57,6 @@
  "tag.system": "Système",
  "tag.productivity": "Bureau",
  "settings.coloredContainer": "Conteneur coloré",
  "settings.gridContainer": "Conteneur grille sans couleur",
  "settings.coloredGridContainer": "Conteneur grille coloré"
  "settings.gridContainer": "Grille sans couleur",
  "settings.coloredGridContainer": "Grille colorée"
}
Apptag/Localization/it.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Mostra Apptag",
  "menu.show": "Mostra TagLauncher",
  "menu.preferences": "Preferenze…",
  "menu.quit": "Esci",
  "menu.language": "Lingua",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "Stile elenco app:",
  "settings.flat": "Elenco",
  "settings.container": "Contenitore senza colore",
  "settings.flatDesc": "\"Elenco\" mostra le app direttamente. \"Contenitore senza colore\" e \"Contenitore colorato\" usano contenitori a cascata. \"Contenitore griglia senza colore\" e \"Contenitore griglia colorato\" allineano le icone in righe griglia di uguale altezza.",
  "settings.flatDesc": "\"Elenco\" mostra le app direttamente. \"Contenitore senza colore\" e \"Contenitore colorato\" usano contenitori a cascata. \"Griglia senza colore\" e \"Griglia colorata\" allineano le icone in righe griglia di uguale altezza.",
  "settings.hideAppNames": "Nascondi nomi app",
  "settings.tagPosition": "Posizione tag:",
  "settings.left": "Sinistra",
@@ -43,7 +43,7 @@
  "tag.cancel": "Annulla",
  "tag.add": "Aggiungi",
  "tag.newName": "Nuovo nome tag",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "Launcher di app basato su tag",
  "app.version": "Versione",
  "app.build": "Build",
@@ -57,6 +57,6 @@
  "tag.system": "Sistema",
  "tag.productivity": "Ufficio",
  "settings.coloredContainer": "Contenitore colorato",
  "settings.gridContainer": "Contenitore griglia senza colore",
  "settings.coloredGridContainer": "Contenitore griglia colorato"
  "settings.gridContainer": "Griglia senza colore",
  "settings.coloredGridContainer": "Griglia colorata"
}
Apptag/Localization/ja.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Apptagを表示",
  "menu.show": "TagLauncherを表示",
  "menu.preferences": "設定…",
  "menu.quit": "終了",
  "menu.language": "言語",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "アプリ一覧スタイル:",
  "settings.flat": "フラット",
  "settings.container": "無色コンテナ",
  "settings.flatDesc": "\"フラット\"はアプリを直接表示します。\"無色コンテナ\"と\"カラーコンテナ\"はコンテナを滝状に配置します。\"無色グリッドコンテナ\"と\"カラーグリッドコンテナ\"は等高のグリッド行でアプリアイコンを揃えます。",
  "settings.flatDesc": "\"フラット\"はアプリを直接表示します。\"無色コンテナ\"と\"カラーコンテナ\"はコンテナを滝状に配置します。\"無色グリッド\"と\"カラーグリッド\"は等高のグリッド行でアプリアイコンを揃えます。",
  "settings.hideAppNames": "アプリ名を非表示",
  "settings.tagPosition": "タグ位置:",
  "settings.left": "左",
@@ -43,7 +43,7 @@
  "tag.cancel": "キャンセル",
  "tag.add": "追加",
  "tag.newName": "新しいタグ名",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "タグベースのアプリランチャー",
  "app.version": "バージョン",
  "app.build": "ビルド",
@@ -57,6 +57,6 @@
  "tag.system": "システム",
  "tag.productivity": "オフィス",
  "settings.coloredContainer": "カラーコンテナ",
  "settings.gridContainer": "無色グリッドコンテナ",
  "settings.coloredGridContainer": "カラーグリッドコンテナ"
  "settings.gridContainer": "無色グリッド",
  "settings.coloredGridContainer": "カラーグリッド"
}
Apptag/Localization/ko.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Apptag 보기",
  "menu.show": "TagLauncher 보기",
  "menu.preferences": "환경설정…",
  "menu.quit": "종료",
  "menu.language": "언어",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "앱 목록 스타일:",
  "settings.flat": "플랫",
  "settings.container": "무색 컨테이너",
  "settings.flatDesc": "\"플랫\"은 앱을 직접 표시합니다. \"무색 컨테이너\"와 \"색상 컨테이너\"는 컨테이너를 폭포식으로 배치합니다. \"무색 그리드 컨테이너\"와 \"색상 그리드 컨테이너\"는 같은 높이의 그리드 행으로 앱 아이콘을 정렬합니다.",
  "settings.flatDesc": "\"플랫\"은 앱을 직접 표시합니다. \"무색 컨테이너\"와 \"색상 컨테이너\"는 컨테이너를 폭포식으로 배치합니다. \"무색 그리드\"와 \"색상 그리드\"는 같은 높이의 그리드 행으로 앱 아이콘을 정렬합니다.",
  "settings.hideAppNames": "앱 이름 숨기기",
  "settings.tagPosition": "태그 위치:",
  "settings.left": "왼쪽",
@@ -43,7 +43,7 @@
  "tag.cancel": "취소",
  "tag.add": "추가",
  "tag.newName": "새 태그 이름",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "태그 기반 앱 런처",
  "app.version": "버전",
  "app.build": "빌드",
@@ -57,6 +57,6 @@
  "tag.system": "시스템",
  "tag.productivity": "오피스",
  "settings.coloredContainer": "색상 컨테이너",
  "settings.gridContainer": "무색 그리드 컨테이너",
  "settings.coloredGridContainer": "색상 그리드 컨테이너"
  "settings.gridContainer": "무색 그리드",
  "settings.coloredGridContainer": "색상 그리드"
}
Apptag/Localization/ru.json
@@ -1,5 +1,5 @@
{
  "menu.show": "Показать Apptag",
  "menu.show": "Показать TagLauncher",
  "menu.preferences": "Настройки…",
  "menu.quit": "Выйти",
  "menu.language": "Язык",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "Стиль списка приложений:",
  "settings.flat": "Плоский",
  "settings.container": "Бесцветный контейнер",
  "settings.flatDesc": "\"Плоский\" показывает приложения напрямую. \"Бесцветный контейнер\" и \"Цветной контейнер\" используют каскадные контейнеры. \"Бесцветный сеточный контейнер\" и \"Цветной сеточный контейнер\" выравнивают значки приложений в строках сетки одинаковой высоты.",
  "settings.flatDesc": "\"Плоский\" показывает приложения напрямую. \"Бесцветный контейнер\" и \"Цветной контейнер\" используют каскадные контейнеры. \"Бесцветная сетка\" и \"Цветная сетка\" выравнивают значки приложений в строках сетки одинаковой высоты.",
  "settings.hideAppNames": "Скрыть названия приложений",
  "settings.tagPosition": "Положение тегов:",
  "settings.left": "Слева",
@@ -43,7 +43,7 @@
  "tag.cancel": "Отмена",
  "tag.add": "Добавить",
  "tag.newName": "Новое название тега",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "Лаунчер приложений на основе тегов",
  "app.version": "Версия",
  "app.build": "Сборка",
@@ -57,6 +57,6 @@
  "tag.system": "Система",
  "tag.productivity": "Офис",
  "settings.coloredContainer": "Цветной контейнер",
  "settings.gridContainer": "Бесцветный сеточный контейнер",
  "settings.coloredGridContainer": "Цветной сеточный контейнер"
  "settings.gridContainer": "Бесцветная сетка",
  "settings.coloredGridContainer": "Цветная сетка"
}
Apptag/Localization/zh-Hans.json
@@ -1,5 +1,5 @@
{
  "menu.show": "显示 Apptag",
  "menu.show": "显示 TagLauncher",
  "menu.preferences": "偏好设置…",
  "menu.quit": "退出",
  "menu.language": "语言",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "应用列表样式:",
  "settings.flat": "平铺",
  "settings.container": "无色容器",
  "settings.flatDesc": "\"平铺\"直接显示应用。\"无色容器\"和\"彩色容器\"使用瀑布流容器。\"无色网格容器\"和\"彩色网格容器\"会按等高网格行对齐应用图标。",
  "settings.flatDesc": "\"平铺\"直接显示应用。\"无色容器\"和\"彩色容器\"使用瀑布流容器。\"无色网格\"和\"彩色网格\"会按等高网格行对齐应用图标。",
  "settings.hideAppNames": "隐藏应用名称",
  "settings.tagPosition": "标签位置:",
  "settings.left": "左侧",
@@ -43,7 +43,7 @@
  "tag.cancel": "取消",
  "tag.add": "添加",
  "tag.newName": "新标签名称",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "基于标签的应用启动器",
  "app.version": "版本",
  "app.build": "构建",
@@ -57,6 +57,6 @@
  "tag.system": "系统优化",
  "tag.productivity": "办公",
  "settings.coloredContainer": "彩色容器",
  "settings.gridContainer": "无色网格容器",
  "settings.coloredGridContainer": "彩色网格容器"
  "settings.gridContainer": "无色网格",
  "settings.coloredGridContainer": "彩色网格"
}
Apptag/Localization/zh-Hant.json
@@ -1,5 +1,5 @@
{
  "menu.show": "顯示 Apptag",
  "menu.show": "顯示 TagLauncher",
  "menu.preferences": "偏好設定…",
  "menu.quit": "退出",
  "menu.language": "語言",
@@ -13,7 +13,7 @@
  "settings.appListStyle": "應用程式列表樣式:",
  "settings.flat": "平鋪",
  "settings.container": "無色容器",
  "settings.flatDesc": "\"平鋪\"直接顯示應用程式。\"無色容器\"和\"彩色容器\"使用瀑布流容器。\"無色網格容器\"和\"彩色網格容器\"會按等高網格列對齊應用程式圖示。",
  "settings.flatDesc": "\"平鋪\"直接顯示應用程式。\"無色容器\"和\"彩色容器\"使用瀑布流容器。\"無色網格\"和\"彩色網格\"會按等高網格列對齊應用程式圖示。",
  "settings.hideAppNames": "隱藏應用程式名稱",
  "settings.tagPosition": "標籤位置:",
  "settings.left": "左側",
@@ -43,7 +43,7 @@
  "tag.cancel": "取消",
  "tag.add": "新增",
  "tag.newName": "新標籤名稱",
  "app.name": "Apptag",
  "app.name": "TagLauncher",
  "app.description": "基於標籤的應用程式啟動器",
  "app.version": "版本",
  "app.build": "建置",
@@ -57,6 +57,6 @@
  "tag.system": "系統優化",
  "tag.productivity": "辦公",
  "settings.coloredContainer": "彩色容器",
  "settings.gridContainer": "無色網格容器",
  "settings.coloredGridContainer": "彩色網格容器"
  "settings.gridContainer": "無色網格",
  "settings.coloredGridContainer": "彩色網格"
}
build.sh
@@ -1,7 +1,7 @@
#!/bin/bash
set -euo pipefail
APP_NAME="Apptag"
APP_NAME="TagLauncher"
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$PROJECT_DIR/build"
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
@@ -11,6 +11,20 @@
SDK_PATH=$(xcrun --sdk macosx --show-sdk-path)
TARGET="arm64-apple-macosx26.0"
# --- Optional: App Store / Sandbox signing ---
# Set CODESIGN_IDENTITY to your "Apple Distribution" or "Mac Developer" cert name.
# Example: CODESIGN_IDENTITY="Apple Distribution: Your Name (TEAMID)"
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:-}"
ENTITLEMENTS="$SWIFT_DIR/TagLauncher.entitlements"
APP_STORE_MODE=false
if [ -n "${APP_STORE:-}" ] && [ "$APP_STORE" = "1" ]; then
    APP_STORE_MODE=true
    if [ -z "$CODESIGN_IDENTITY" ]; then
        echo "⚠️  APP_STORE=1 but CODESIGN_IDENTITY not set. Will ad-hoc sign with entitlements."
    fi
fi
echo "==> Cleaning..."
rm -rf "$BUILD_DIR"
@@ -27,6 +41,7 @@
    -framework Carbon \
    -sdk "$SDK_PATH" \
    -target "$TARGET" \
    -Osize \
    "$SWIFT_DIR"/*.swift
echo "==> Copying Info.plist..."
@@ -42,9 +57,46 @@
echo "==> Copying Localization files..."
cp -r "$SWIFT_DIR/Localization" "$RESOURCES_DIR/Localization"
echo "==> Embedding PkgInfo..."
echo -n "APPL????" > "$APP_BUNDLE/Contents/PkgInfo"
echo "==> Setting permissions..."
chmod +x "$MACOS_DIR/$APP_NAME"
echo "==> Stripping debug symbols..."
strip -x "$MACOS_DIR/$APP_NAME"
# --- Signing ---
if [ "$APP_STORE_MODE" = true ] || [ -n "$CODESIGN_IDENTITY" ]; then
    echo "==> Signing with entitlements..."
    if [ -n "$CODESIGN_IDENTITY" ]; then
        CODESIGN_ARGS="--sign \"$CODESIGN_IDENTITY\""
    else
        CODESIGN_ARGS="--sign -"
    fi
    if [ -f "$ENTITLEMENTS" ]; then
        eval codesign --force --deep $CODESIGN_ARGS --entitlements "$ENTITLEMENTS" "$APP_BUNDLE"
        echo "   Signed with entitlements: $ENTITLEMENTS"
    else
        eval codesign --force --deep $CODESIGN_ARGS "$APP_BUNDLE"
        echo "   Signed (no entitlements file found)"
    fi
else
    # Default: ad-hoc sign without entitlements (local dev)
    echo "==> Ad-hoc signing..."
    codesign --force --deep --sign - "$APP_BUNDLE"
fi
echo ""
echo "✅ App built at: $APP_BUNDLE"
echo "   Run: open $APP_BUNDLE"
# --- App Store mode: remind about next steps ---
if [ "$APP_STORE_MODE" = true ]; then
    echo ""
    echo "📦 App Store build ready. Next steps:"
    echo "   1. Verify sandbox: spctl -a -v \"$APP_BUNDLE\""
    echo "   2. Test in sandbox: sandbox-exec -f <(echo '(version 1)(allow default)') open \"$APP_BUNDLE\""
    echo "   3. Upload via Transporter or 'xcrun altool --upload-app'"
fi
make_dmg.sh
New file
@@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_NAME="TagLauncher"
BUILD_DIR="$PROJECT_DIR/build"
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
DMG_NAME="$APP_NAME.dmg"
DMG_PATH="$BUILD_DIR/$DMG_NAME"
STAGING="$BUILD_DIR/dmg_staging"
echo "==> Preparing DMG staging..."
rm -rf "$STAGING"
mkdir -p "$STAGING"
echo "==> Copying $APP_NAME.app..."
cp -R "$APP_BUNDLE" "$STAGING/"
echo "==> Creating Applications symlink..."
ln -s /Applications "$STAGING/Applications"
echo "==> Creating DMG..."
hdiutil create \
    -volname "$APP_NAME" \
    -srcfolder "$STAGING" \
    -ov \
    -format UDZO \
    -imagekey zlib-level=9 \
    "$DMG_PATH"
echo "==> Cleaning up staging..."
rm -rf "$STAGING"
SIZE=$(du -sh "$DMG_PATH" | awk '{print $1}')
echo ""
echo "✅ DMG created: $DMG_PATH ($SIZE)"
echo "   Open: open $DMG_PATH"