Archive TagLauncher 4.0.7
13 files modified
1 files added
| | |
| | | // MARK: - Application Entry Point |
| | | |
| | | @main |
| | | struct ApptagApp: App { |
| | | struct TagLauncherApp: App { |
| | | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate |
| | | |
| | | var body: some Scene { |
| | |
| | | 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 |
| | |
| | | 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 |
| | | } |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | /// 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 |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | |
| | | _ = 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") |
| | |
| | | .frame(width: 128, height: 128) |
| | | } |
| | | VStack(alignment: .leading, spacing: 4) { |
| | | Text("Apptag") |
| | | Text("TagLauncher") |
| | | .font(.title2) |
| | | .fontWeight(.semibold) |
| | | Text(tr("app.description")) |
| | |
| | | // MARK: - Notification for manual re-index |
| | | |
| | | extension Notification.Name { |
| | | static let apptagEditModeChanged = Notification.Name("ApptagEditModeChanged") |
| | | static let tagLauncherEditModeChanged = Notification.Name("TagLauncherEditModeChanged") |
| | | } |
| | | |
| | | // MARK: - Edit Phase |
| | |
| | | .onChange(of: editPhase) { _, newPhase in |
| | | let active = newPhase != .none |
| | | NotificationCenter.default.post( |
| | | name: .apptagEditModeChanged, |
| | | name: .tagLauncherEditModeChanged, |
| | | object: nil, |
| | | userInfo: ["active": active] |
| | | ) |
| | |
| | | /// 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() |
| | |
| | | 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]) |
| | | } |
| | | } |
| | | } |
| | |
| | | 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) { |
| | |
| | | .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) |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | 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)) |
| | |
| | | .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) |
| | |
| | | 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) |
| | |
| | | } |
| | | .contentShape(RoundedRectangle(cornerRadius: 14)) |
| | | .onTapGesture { |
| | | if isColorlessFilled { |
| | | filledColorlessContainer = nil |
| | | if isColorlessGrid { |
| | | toggleColorlessFill(group.name) |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | 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 |
| | | } |
| | | |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | { |
| | | "menu.show": "Show Apptag", |
| | | "menu.show": "Show TagLauncher", |
| | | "menu.preferences": "Preferences…", |
| | | "menu.quit": "Quit", |
| | | "menu.language": "Language", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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" |
| | | } |
| | |
| | | { |
| | | "menu.show": "Mostrar Apptag", |
| | | "menu.show": "Mostrar TagLauncher", |
| | | "menu.preferences": "Preferencias…", |
| | | "menu.quit": "Salir", |
| | | "menu.language": "Idioma", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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" |
| | | } |
| | |
| | | { |
| | | "menu.show": "Afficher Apptag", |
| | | "menu.show": "Afficher TagLauncher", |
| | | "menu.preferences": "Préférences…", |
| | | "menu.quit": "Quitter", |
| | | "menu.language": "Langue", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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" |
| | | } |
| | |
| | | { |
| | | "menu.show": "Mostra Apptag", |
| | | "menu.show": "Mostra TagLauncher", |
| | | "menu.preferences": "Preferenze…", |
| | | "menu.quit": "Esci", |
| | | "menu.language": "Lingua", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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" |
| | | } |
| | |
| | | { |
| | | "menu.show": "Apptagを表示", |
| | | "menu.show": "TagLauncherを表示", |
| | | "menu.preferences": "設定…", |
| | | "menu.quit": "終了", |
| | | "menu.language": "言語", |
| | |
| | | "settings.appListStyle": "アプリ一覧スタイル:", |
| | | "settings.flat": "フラット", |
| | | "settings.container": "無色コンテナ", |
| | | "settings.flatDesc": "\"フラット\"はアプリを直接表示します。\"無色コンテナ\"と\"カラーコンテナ\"はコンテナを滝状に配置します。\"無色グリッドコンテナ\"と\"カラーグリッドコンテナ\"は等高のグリッド行でアプリアイコンを揃えます。", |
| | | "settings.flatDesc": "\"フラット\"はアプリを直接表示します。\"無色コンテナ\"と\"カラーコンテナ\"はコンテナを滝状に配置します。\"無色グリッド\"と\"カラーグリッド\"は等高のグリッド行でアプリアイコンを揃えます。", |
| | | "settings.hideAppNames": "アプリ名を非表示", |
| | | "settings.tagPosition": "タグ位置:", |
| | | "settings.left": "左", |
| | |
| | | "tag.cancel": "キャンセル", |
| | | "tag.add": "追加", |
| | | "tag.newName": "新しいタグ名", |
| | | "app.name": "Apptag", |
| | | "app.name": "TagLauncher", |
| | | "app.description": "タグベースのアプリランチャー", |
| | | "app.version": "バージョン", |
| | | "app.build": "ビルド", |
| | |
| | | "tag.system": "システム", |
| | | "tag.productivity": "オフィス", |
| | | "settings.coloredContainer": "カラーコンテナ", |
| | | "settings.gridContainer": "無色グリッドコンテナ", |
| | | "settings.coloredGridContainer": "カラーグリッドコンテナ" |
| | | "settings.gridContainer": "無色グリッド", |
| | | "settings.coloredGridContainer": "カラーグリッド" |
| | | } |
| | |
| | | { |
| | | "menu.show": "Apptag 보기", |
| | | "menu.show": "TagLauncher 보기", |
| | | "menu.preferences": "환경설정…", |
| | | "menu.quit": "종료", |
| | | "menu.language": "언어", |
| | |
| | | "settings.appListStyle": "앱 목록 스타일:", |
| | | "settings.flat": "플랫", |
| | | "settings.container": "무색 컨테이너", |
| | | "settings.flatDesc": "\"플랫\"은 앱을 직접 표시합니다. \"무색 컨테이너\"와 \"색상 컨테이너\"는 컨테이너를 폭포식으로 배치합니다. \"무색 그리드 컨테이너\"와 \"색상 그리드 컨테이너\"는 같은 높이의 그리드 행으로 앱 아이콘을 정렬합니다.", |
| | | "settings.flatDesc": "\"플랫\"은 앱을 직접 표시합니다. \"무색 컨테이너\"와 \"색상 컨테이너\"는 컨테이너를 폭포식으로 배치합니다. \"무색 그리드\"와 \"색상 그리드\"는 같은 높이의 그리드 행으로 앱 아이콘을 정렬합니다.", |
| | | "settings.hideAppNames": "앱 이름 숨기기", |
| | | "settings.tagPosition": "태그 위치:", |
| | | "settings.left": "왼쪽", |
| | |
| | | "tag.cancel": "취소", |
| | | "tag.add": "추가", |
| | | "tag.newName": "새 태그 이름", |
| | | "app.name": "Apptag", |
| | | "app.name": "TagLauncher", |
| | | "app.description": "태그 기반 앱 런처", |
| | | "app.version": "버전", |
| | | "app.build": "빌드", |
| | |
| | | "tag.system": "시스템", |
| | | "tag.productivity": "오피스", |
| | | "settings.coloredContainer": "색상 컨테이너", |
| | | "settings.gridContainer": "무색 그리드 컨테이너", |
| | | "settings.coloredGridContainer": "색상 그리드 컨테이너" |
| | | "settings.gridContainer": "무색 그리드", |
| | | "settings.coloredGridContainer": "색상 그리드" |
| | | } |
| | |
| | | { |
| | | "menu.show": "Показать Apptag", |
| | | "menu.show": "Показать TagLauncher", |
| | | "menu.preferences": "Настройки…", |
| | | "menu.quit": "Выйти", |
| | | "menu.language": "Язык", |
| | |
| | | "settings.appListStyle": "Стиль списка приложений:", |
| | | "settings.flat": "Плоский", |
| | | "settings.container": "Бесцветный контейнер", |
| | | "settings.flatDesc": "\"Плоский\" показывает приложения напрямую. \"Бесцветный контейнер\" и \"Цветной контейнер\" используют каскадные контейнеры. \"Бесцветный сеточный контейнер\" и \"Цветной сеточный контейнер\" выравнивают значки приложений в строках сетки одинаковой высоты.", |
| | | "settings.flatDesc": "\"Плоский\" показывает приложения напрямую. \"Бесцветный контейнер\" и \"Цветной контейнер\" используют каскадные контейнеры. \"Бесцветная сетка\" и \"Цветная сетка\" выравнивают значки приложений в строках сетки одинаковой высоты.", |
| | | "settings.hideAppNames": "Скрыть названия приложений", |
| | | "settings.tagPosition": "Положение тегов:", |
| | | "settings.left": "Слева", |
| | |
| | | "tag.cancel": "Отмена", |
| | | "tag.add": "Добавить", |
| | | "tag.newName": "Новое название тега", |
| | | "app.name": "Apptag", |
| | | "app.name": "TagLauncher", |
| | | "app.description": "Лаунчер приложений на основе тегов", |
| | | "app.version": "Версия", |
| | | "app.build": "Сборка", |
| | |
| | | "tag.system": "Система", |
| | | "tag.productivity": "Офис", |
| | | "settings.coloredContainer": "Цветной контейнер", |
| | | "settings.gridContainer": "Бесцветный сеточный контейнер", |
| | | "settings.coloredGridContainer": "Цветной сеточный контейнер" |
| | | "settings.gridContainer": "Бесцветная сетка", |
| | | "settings.coloredGridContainer": "Цветная сетка" |
| | | } |
| | |
| | | { |
| | | "menu.show": "显示 Apptag", |
| | | "menu.show": "显示 TagLauncher", |
| | | "menu.preferences": "偏好设置…", |
| | | "menu.quit": "退出", |
| | | "menu.language": "语言", |
| | |
| | | "settings.appListStyle": "应用列表样式:", |
| | | "settings.flat": "平铺", |
| | | "settings.container": "无色容器", |
| | | "settings.flatDesc": "\"平铺\"直接显示应用。\"无色容器\"和\"彩色容器\"使用瀑布流容器。\"无色网格容器\"和\"彩色网格容器\"会按等高网格行对齐应用图标。", |
| | | "settings.flatDesc": "\"平铺\"直接显示应用。\"无色容器\"和\"彩色容器\"使用瀑布流容器。\"无色网格\"和\"彩色网格\"会按等高网格行对齐应用图标。", |
| | | "settings.hideAppNames": "隐藏应用名称", |
| | | "settings.tagPosition": "标签位置:", |
| | | "settings.left": "左侧", |
| | |
| | | "tag.cancel": "取消", |
| | | "tag.add": "添加", |
| | | "tag.newName": "新标签名称", |
| | | "app.name": "Apptag", |
| | | "app.name": "TagLauncher", |
| | | "app.description": "基于标签的应用启动器", |
| | | "app.version": "版本", |
| | | "app.build": "构建", |
| | |
| | | "tag.system": "系统优化", |
| | | "tag.productivity": "办公", |
| | | "settings.coloredContainer": "彩色容器", |
| | | "settings.gridContainer": "无色网格容器", |
| | | "settings.coloredGridContainer": "彩色网格容器" |
| | | "settings.gridContainer": "无色网格", |
| | | "settings.coloredGridContainer": "彩色网格" |
| | | } |
| | |
| | | { |
| | | "menu.show": "顯示 Apptag", |
| | | "menu.show": "顯示 TagLauncher", |
| | | "menu.preferences": "偏好設定…", |
| | | "menu.quit": "退出", |
| | | "menu.language": "語言", |
| | |
| | | "settings.appListStyle": "應用程式列表樣式:", |
| | | "settings.flat": "平鋪", |
| | | "settings.container": "無色容器", |
| | | "settings.flatDesc": "\"平鋪\"直接顯示應用程式。\"無色容器\"和\"彩色容器\"使用瀑布流容器。\"無色網格容器\"和\"彩色網格容器\"會按等高網格列對齊應用程式圖示。", |
| | | "settings.flatDesc": "\"平鋪\"直接顯示應用程式。\"無色容器\"和\"彩色容器\"使用瀑布流容器。\"無色網格\"和\"彩色網格\"會按等高網格列對齊應用程式圖示。", |
| | | "settings.hideAppNames": "隱藏應用程式名稱", |
| | | "settings.tagPosition": "標籤位置:", |
| | | "settings.left": "左側", |
| | |
| | | "tag.cancel": "取消", |
| | | "tag.add": "新增", |
| | | "tag.newName": "新標籤名稱", |
| | | "app.name": "Apptag", |
| | | "app.name": "TagLauncher", |
| | | "app.description": "基於標籤的應用程式啟動器", |
| | | "app.version": "版本", |
| | | "app.build": "建置", |
| | |
| | | "tag.system": "系統優化", |
| | | "tag.productivity": "辦公", |
| | | "settings.coloredContainer": "彩色容器", |
| | | "settings.gridContainer": "無色網格容器", |
| | | "settings.coloredGridContainer": "彩色網格容器" |
| | | "settings.gridContainer": "無色網格", |
| | | "settings.coloredGridContainer": "彩色網格" |
| | | } |
| | |
| | | #!/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" |
| | |
| | | |
| | | 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" |
| | |
| | | -framework Carbon \ |
| | | -sdk "$SDK_PATH" \ |
| | | -target "$TARGET" \ |
| | | -Osize \ |
| | | "$SWIFT_DIR"/*.swift |
| | | |
| | | echo "==> Copying Info.plist..." |
| | |
| | | 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 |
| New file |
| | |
| | | #!/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" |