diff --git a/.github/workflows/release-developer-id.yml b/.github/workflows/release-developer-id.yml index a9570f2..acb75ec 100644 --- a/.github/workflows/release-developer-id.yml +++ b/.github/workflows/release-developer-id.yml @@ -88,8 +88,17 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + version="${GITHUB_REF_NAME#v}" + awk -v heading="## [$version]" ' + index($0, heading) == 1 { found = 1; next } + found && /^## \[/ { exit } + found { print } + END { if (!found) exit 1 } + ' CHANGELOG.md > release-notes.md + test -s release-notes.md + gh release create "$GITHUB_REF_NAME" \ dist/developer-id/*.dmg \ dist/developer-id/*.sha256 \ --title "MousePortal $GITHUB_REF_NAME" \ - --notes "Signed and notarized MousePortal release." + --notes-file release-notes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bd00679..a6f410d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,17 +14,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Improved portal visual editor - Configuration profile import/export -## [1.1.0] - 2026-08-31 +## [1.1.0] - 2026-09-02 ### Added -- Window layout snapshots and lock-screen recovery for standard application windows. -- Display-canvas previews with switchable automatic and manually saved layouts. +- Manual and automatic window-layout snapshots for each display arrangement. +- Display-canvas previews with switchable saved layouts and visible window placement. +- Apply, rename, delete, and automatic-to-manual promotion actions for saved layouts. +- Independent controls for global window recovery, sleep/lock capture, and automatic wake restoration. +- Searchable ignored-application settings for excluding apps from future snapshots. +- System, Light, and Dark application themes across all 20 supported languages. + +### Changed +- Moved display controls into a clearer top toolbar and each display card. +- Persisted the resizable display-layout sidebar width. +- Kept four portal rows visible before scrolling. + +### Fixed +- Prevented wake handling from scanning displays or moving windows when automatic restoration is disabled. +- Improved matching for multiple windows from the same application and preserved layouts with saved windows. +- Limited restoration to standard, non-minimized, non-full-screen windows and documented unsupported macOS window modes. ## [1.0.1] - 2026-07-24 ### Fixed - Prevented a portal's two lines from overlapping or sharing an endpoint on the same display edge. +## [1.0.0] - 2026-07-19 + +### Added +- First public release of Hotkey Jump and bidirectional edge portals. +- Automatic display-arrangement detection, named layouts, and display visualization. +- Global shortcut and portal configuration with menu bar and launch-at-login support. +- Localized interface in 20 languages. + ## [0.1.2] - 2026-06-15 ### Changed diff --git a/MousePortal/AppDelegate.swift b/MousePortal/AppDelegate.swift index b44c7d9..66cf26d 100644 --- a/MousePortal/AppDelegate.swift +++ b/MousePortal/AppDelegate.swift @@ -32,6 +32,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // 设置单例引用 AppDelegate.shared = self + AppTheme.stored().apply() terminateOtherInstancesIfNeeded() // 确保应用作为正规前台应用运行(对于 swift run 方式很重要) diff --git a/MousePortal/Resources/ar.lproj/Localizable.strings b/MousePortal/Resources/ar.lproj/Localizable.strings index 78d7104..f541315 100644 --- a/MousePortal/Resources/ar.lproj/Localizable.strings +++ b/MousePortal/Resources/ar.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* الإعدادات - عام */ "settings.app_behavior" = "سلوك التطبيق"; "settings.language" = "اللغة"; +"settings.theme" = "المظهر"; +"theme.system" = "اتّباع النظام"; +"theme.light" = "فاتح"; +"theme.dark" = "داكن"; "settings.show_menubar_icon" = "إظهار الأيقونة في شريط القوائم"; "settings.launch_at_login" = "التشغيل عند تسجيل الدخول"; "settings.permissions" = "الأذونات"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "تذكّر النوافذ قبل الإسبات أو القفل"; "window_recovery.automatic" = "الاستعادة تلقائياً بعد تنبيه الشاشات"; "window_recovery.behavior" = "الاستعادة بعد التنبيه"; -"window_recovery.save" = "حفظ التخطيط الحالي"; +"window_recovery.save" = "حفظ تخطيط النوافذ"; "window_recovery.ignored_apps" = "التطبيقات المتجاهلة"; "window_recovery.search_apps" = "البحث عن التطبيقات"; "window_recovery.ignored_apps_description" = "فعّل تطبيقاً لاستبعاده من اللقطات المستقبلية."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "إعادة تسمية تخطيط النوافذ"; "window_layout.name_placeholder" = "أدخل اسم تخطيط النوافذ"; "window_recovery.enable_in_settings" = "فعّل استعادة النوافذ أولاً من الإعدادات."; -"window_recovery.save_help" = "حفظ مواضع النوافذ الحالية."; +"window_recovery.save_help" = "حفظ مواضع النوافذ وأحجامها الحالية."; diff --git a/MousePortal/Resources/de.lproj/Localizable.strings b/MousePortal/Resources/de.lproj/Localizable.strings index 486e023..f25ea39 100644 --- a/MousePortal/Resources/de.lproj/Localizable.strings +++ b/MousePortal/Resources/de.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* Einstellungen - Allgemein */ "settings.app_behavior" = "App-Verhalten"; "settings.language" = "Sprache"; +"settings.theme" = "Design"; +"theme.system" = "Systemeinstellung"; +"theme.light" = "Hell"; +"theme.dark" = "Dunkel"; "settings.show_menubar_icon" = "Symbol in Menüleiste anzeigen"; "settings.launch_at_login" = "Bei Anmeldung starten"; "settings.permissions" = "Berechtigungen"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "Fenster vor Ruhezustand oder Sperren merken"; "window_recovery.automatic" = "Nach dem Aufwachen der Displays automatisch wiederherstellen"; "window_recovery.behavior" = "Wiederherstellung nach Ruhezustand"; -"window_recovery.save" = "Aktuelles Layout sichern"; +"window_recovery.save" = "Fensterlayout speichern"; "window_recovery.ignored_apps" = "Ignorierte Apps"; "window_recovery.search_apps" = "Apps suchen"; "window_recovery.ignored_apps_description" = "Aktivieren Sie eine App, um sie aus zukünftigen Schnappschüssen auszuschließen."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "Fensterlayout umbenennen"; "window_layout.name_placeholder" = "Name des Fensterlayouts eingeben"; "window_recovery.enable_in_settings" = "Aktiviere zuerst die Fensterwiederherstellung in den Einstellungen."; -"window_recovery.save_help" = "Aktuelle Fensterpositionen speichern."; +"window_recovery.save_help" = "Aktuelle Fensterpositionen und -größen speichern."; diff --git a/MousePortal/Resources/en.lproj/Localizable.strings b/MousePortal/Resources/en.lproj/Localizable.strings index 5aab23f..02722df 100644 --- a/MousePortal/Resources/en.lproj/Localizable.strings +++ b/MousePortal/Resources/en.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* Settings - General */ "settings.app_behavior" = "App Behavior"; "settings.language" = "Language"; +"settings.theme" = "Theme"; +"theme.system" = "Follow System"; +"theme.light" = "Light"; +"theme.dark" = "Dark"; "settings.show_menubar_icon" = "Show menu bar icon"; "settings.launch_at_login" = "Launch at login"; "settings.permissions" = "Permissions"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "Remember windows before sleep or lock"; "window_recovery.automatic" = "Restore automatically after displays wake"; "window_recovery.behavior" = "Wake Recovery"; -"window_recovery.save" = "Save Current Layout"; +"window_recovery.save" = "Save Window Layout"; "window_recovery.ignored_apps" = "Ignored Applications"; "window_recovery.search_apps" = "Search applications"; "window_recovery.ignored_apps_description" = "Turn on an app to exclude it from future snapshots."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "Rename Window Layout"; "window_layout.name_placeholder" = "Enter window layout name"; "window_recovery.enable_in_settings" = "Enable Window Recovery in Settings first."; -"window_recovery.save_help" = "Save the current window positions."; +"window_recovery.save_help" = "Save the current window positions and sizes."; diff --git a/MousePortal/Resources/es.lproj/Localizable.strings b/MousePortal/Resources/es.lproj/Localizable.strings index f1e5fcb..6d69128 100644 --- a/MousePortal/Resources/es.lproj/Localizable.strings +++ b/MousePortal/Resources/es.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* Ajustes - General */ "settings.app_behavior" = "Comportamiento de la app"; "settings.language" = "Idioma"; +"settings.theme" = "Tema"; +"theme.system" = "Seguir el sistema"; +"theme.light" = "Claro"; +"theme.dark" = "Oscuro"; "settings.show_menubar_icon" = "Mostrar icono en la barra de menús"; "settings.launch_at_login" = "Iniciar al arrancar"; "settings.permissions" = "Permisos"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "Recordar ventanas antes del reposo o bloqueo"; "window_recovery.automatic" = "Restaurar automáticamente al activarse las pantallas"; "window_recovery.behavior" = "Recuperación al despertar"; -"window_recovery.save" = "Guardar diseño actual"; +"window_recovery.save" = "Guardar diseño de ventanas"; "window_recovery.ignored_apps" = "Aplicaciones ignoradas"; "window_recovery.search_apps" = "Buscar aplicaciones"; "window_recovery.ignored_apps_description" = "Activa una aplicación para excluirla de futuras instantáneas."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "Renombrar disposición de ventanas"; "window_layout.name_placeholder" = "Introduce el nombre de la disposición"; "window_recovery.enable_in_settings" = "Activa primero la restauración de ventanas en Ajustes."; -"window_recovery.save_help" = "Guarda las posiciones actuales de las ventanas."; +"window_recovery.save_help" = "Guarda las posiciones y tamaños actuales de las ventanas."; diff --git a/MousePortal/Resources/fr.lproj/Localizable.strings b/MousePortal/Resources/fr.lproj/Localizable.strings index 684038b..1bbd5ec 100644 --- a/MousePortal/Resources/fr.lproj/Localizable.strings +++ b/MousePortal/Resources/fr.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* Réglages - Général */ "settings.app_behavior" = "Comportement de l'app"; "settings.language" = "Langue"; +"settings.theme" = "Thème"; +"theme.system" = "Suivre le système"; +"theme.light" = "Clair"; +"theme.dark" = "Sombre"; "settings.show_menubar_icon" = "Afficher l'icône dans la barre des menus"; "settings.launch_at_login" = "Lancer au démarrage"; "settings.permissions" = "Autorisations"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "Mémoriser les fenêtres avant la veille ou le verrouillage"; "window_recovery.automatic" = "Restaurer automatiquement au réveil des écrans"; "window_recovery.behavior" = "Récupération au réveil"; -"window_recovery.save" = "Enregistrer la disposition actuelle"; +"window_recovery.save" = "Enregistrer la disposition des fenêtres"; "window_recovery.ignored_apps" = "Applications ignorées"; "window_recovery.search_apps" = "Rechercher des applications"; "window_recovery.ignored_apps_description" = "Activez une application pour l’exclure des prochains instantanés."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "Renommer la disposition des fenêtres"; "window_layout.name_placeholder" = "Saisissez le nom de la disposition"; "window_recovery.enable_in_settings" = "Activez d’abord la restauration des fenêtres dans Réglages."; -"window_recovery.save_help" = "Enregistrer les positions actuelles des fenêtres."; +"window_recovery.save_help" = "Enregistrer les positions et tailles actuelles des fenêtres."; diff --git a/MousePortal/Resources/hi.lproj/Localizable.strings b/MousePortal/Resources/hi.lproj/Localizable.strings index 2f1d94f..6a425e4 100644 --- a/MousePortal/Resources/hi.lproj/Localizable.strings +++ b/MousePortal/Resources/hi.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* सेटिंग्स - सामान्य */ "settings.app_behavior" = "ऐप व्यवहार"; "settings.language" = "भाषा"; +"settings.theme" = "थीम"; +"theme.system" = "सिस्टम के अनुसार"; +"theme.light" = "हल्की"; +"theme.dark" = "गहरी"; "settings.show_menubar_icon" = "मेनू बार में आइकन दिखाएं"; "settings.launch_at_login" = "लॉगिन पर लॉन्च करें"; "settings.permissions" = "अनुमतियां"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "स्लीप या लॉक से पहले विंडो याद रखें"; "window_recovery.automatic" = "डिस्प्ले जागने के बाद अपने आप पुनर्स्थापित करें"; "window_recovery.behavior" = "जागने पर पुनर्प्राप्ति"; -"window_recovery.save" = "वर्तमान लेआउट सहेजें"; +"window_recovery.save" = "विंडो लेआउट सहेजें"; "window_recovery.ignored_apps" = "अनदेखे ऐप"; "window_recovery.search_apps" = "ऐप खोजें"; "window_recovery.ignored_apps_description" = "किसी ऐप को भविष्य के स्नैपशॉट से बाहर रखने के लिए उसे चालू करें।"; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "विंडो लेआउट का नाम बदलें"; "window_layout.name_placeholder" = "विंडो लेआउट नाम दर्ज करें"; "window_recovery.enable_in_settings" = "पहले सेटिंग्स में विंडो रिकवरी चालू करें।"; -"window_recovery.save_help" = "मौजूदा विंडो स्थितियाँ सहेजें।"; +"window_recovery.save_help" = "मौजूदा विंडो की स्थितियाँ और आकार सहेजें।"; diff --git a/MousePortal/Resources/id.lproj/Localizable.strings b/MousePortal/Resources/id.lproj/Localizable.strings index e29831f..292280c 100644 --- a/MousePortal/Resources/id.lproj/Localizable.strings +++ b/MousePortal/Resources/id.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Pengaturan - Umum */ "settings.app_behavior" = "Perilaku Aplikasi"; "settings.language" = "Bahasa"; +"settings.theme" = "Tema"; +"theme.system" = "Ikuti sistem"; +"theme.light" = "Terang"; +"theme.dark" = "Gelap"; "settings.show_menubar_icon" = "Tampilkan ikon di bilah menu"; "settings.launch_at_login" = "Jalankan saat login"; "settings.permissions" = "Izin"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Ingat jendela sebelum tidur atau terkunci"; "window_recovery.automatic" = "Pulihkan otomatis setelah layar aktif"; "window_recovery.behavior" = "Pemulihan setelah aktif"; -"window_recovery.save" = "Simpan tata letak saat ini"; +"window_recovery.save" = "Simpan tata letak jendela"; "window_recovery.ignored_apps" = "Aplikasi yang diabaikan"; "window_recovery.search_apps" = "Cari aplikasi"; "window_recovery.ignored_apps_description" = "Aktifkan aplikasi untuk mengecualikannya dari cuplikan berikutnya."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Ubah nama tata letak jendela"; "window_layout.name_placeholder" = "Masukkan nama tata letak jendela"; "window_recovery.enable_in_settings" = "Aktifkan Pemulihan Jendela di Pengaturan terlebih dahulu."; -"window_recovery.save_help" = "Simpan posisi jendela saat ini."; +"window_recovery.save_help" = "Simpan posisi dan ukuran jendela saat ini."; diff --git a/MousePortal/Resources/it.lproj/Localizable.strings b/MousePortal/Resources/it.lproj/Localizable.strings index 150e5eb..767780a 100644 --- a/MousePortal/Resources/it.lproj/Localizable.strings +++ b/MousePortal/Resources/it.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Impostazioni - Generali */ "settings.app_behavior" = "Comportamento app"; "settings.language" = "Lingua"; +"settings.theme" = "Tema"; +"theme.system" = "Segui il sistema"; +"theme.light" = "Chiaro"; +"theme.dark" = "Scuro"; "settings.show_menubar_icon" = "Mostra icona nella barra dei menu"; "settings.launch_at_login" = "Avvia al login"; "settings.permissions" = "Permessi"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Ricorda le finestre prima dello stop o del blocco"; "window_recovery.automatic" = "Ripristina automaticamente al riavvio degli schermi"; "window_recovery.behavior" = "Ripristino al riavvio"; -"window_recovery.save" = "Salva disposizione attuale"; +"window_recovery.save" = "Salva layout finestre"; "window_recovery.ignored_apps" = "App ignorate"; "window_recovery.search_apps" = "Cerca app"; "window_recovery.ignored_apps_description" = "Attiva un’app per escluderla dalle istantanee future."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Rinomina disposizione finestre"; "window_layout.name_placeholder" = "Inserisci il nome della disposizione"; "window_recovery.enable_in_settings" = "Abilita prima il ripristino finestre nelle Impostazioni."; -"window_recovery.save_help" = "Salva le posizioni attuali delle finestre."; +"window_recovery.save_help" = "Salva le posizioni e le dimensioni attuali delle finestre."; diff --git a/MousePortal/Resources/ja.lproj/Localizable.strings b/MousePortal/Resources/ja.lproj/Localizable.strings index 3451913..efa744e 100644 --- a/MousePortal/Resources/ja.lproj/Localizable.strings +++ b/MousePortal/Resources/ja.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* 設定 - 一般 */ "settings.app_behavior" = "アプリの動作"; "settings.language" = "言語"; +"settings.theme" = "テーマ"; +"theme.system" = "システム設定に従う"; +"theme.light" = "ライト"; +"theme.dark" = "ダーク"; "settings.show_menubar_icon" = "メニューバーにアイコンを表示"; "settings.launch_at_login" = "ログイン時に起動"; "settings.permissions" = "権限"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "スリープまたはロック前にウインドウを記憶"; "window_recovery.automatic" = "ディスプレイ復帰後に自動復元"; "window_recovery.behavior" = "スリープ解除時の復元"; -"window_recovery.save" = "現在の配置を保存"; +"window_recovery.save" = "ウインドウ配置を保存"; "window_recovery.ignored_apps" = "除外するアプリ"; "window_recovery.search_apps" = "アプリを検索"; "window_recovery.ignored_apps_description" = "オンにしたアプリは今後のスナップショットから除外されます。"; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "ウインドウ配置の名前を変更"; "window_layout.name_placeholder" = "ウインドウ配置名を入力"; "window_recovery.enable_in_settings" = "先に設定でウインドウ復元を有効にしてください。"; -"window_recovery.save_help" = "現在のウインドウ位置を保存します。"; +"window_recovery.save_help" = "現在のウインドウの位置とサイズを保存します。"; diff --git a/MousePortal/Resources/ko.lproj/Localizable.strings b/MousePortal/Resources/ko.lproj/Localizable.strings index 79633ee..ab6610b 100644 --- a/MousePortal/Resources/ko.lproj/Localizable.strings +++ b/MousePortal/Resources/ko.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* 설정 - 일반 */ "settings.app_behavior" = "앱 동작"; "settings.language" = "언어"; +"settings.theme" = "테마"; +"theme.system" = "시스템 설정 따르기"; +"theme.light" = "라이트"; +"theme.dark" = "다크"; "settings.show_menubar_icon" = "메뉴 막대에 아이콘 표시"; "settings.launch_at_login" = "로그인 시 시작"; "settings.permissions" = "권한"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "잠자기 또는 잠금 전에 윈도우 기억"; "window_recovery.automatic" = "디스플레이가 깨어난 후 자동 복원"; "window_recovery.behavior" = "깨우기 복구"; -"window_recovery.save" = "현재 레이아웃 저장"; +"window_recovery.save" = "창 레이아웃 저장"; "window_recovery.ignored_apps" = "무시할 앱"; "window_recovery.search_apps" = "앱 검색"; "window_recovery.ignored_apps_description" = "앱을 켜면 이후 스냅샷에서 제외됩니다."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "윈도우 배치 이름 변경"; "window_layout.name_placeholder" = "윈도우 배치 이름 입력"; "window_recovery.enable_in_settings" = "먼저 설정에서 윈도우 복원을 켜세요."; -"window_recovery.save_help" = "현재 윈도우 위치를 저장합니다."; +"window_recovery.save_help" = "현재 창의 위치와 크기를 저장합니다."; diff --git a/MousePortal/Resources/ms.lproj/Localizable.strings b/MousePortal/Resources/ms.lproj/Localizable.strings index 993439f..6a3304c 100644 --- a/MousePortal/Resources/ms.lproj/Localizable.strings +++ b/MousePortal/Resources/ms.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Tetapan - Umum */ "settings.app_behavior" = "Tingkah Laku Aplikasi"; "settings.language" = "Bahasa"; +"settings.theme" = "Tema"; +"theme.system" = "Ikut sistem"; +"theme.light" = "Cerah"; +"theme.dark" = "Gelap"; "settings.show_menubar_icon" = "Tunjukkan ikon di bar menu"; "settings.launch_at_login" = "Lancar semasa log masuk"; "settings.permissions" = "Kebenaran"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Ingat tetingkap sebelum tidur atau dikunci"; "window_recovery.automatic" = "Pulihkan secara automatik selepas paparan bangun"; "window_recovery.behavior" = "Pemulihan selepas bangun"; -"window_recovery.save" = "Simpan susun atur semasa"; +"window_recovery.save" = "Simpan susun atur tetingkap"; "window_recovery.ignored_apps" = "Aplikasi diabaikan"; "window_recovery.search_apps" = "Cari aplikasi"; "window_recovery.ignored_apps_description" = "Hidupkan aplikasi untuk mengecualikannya daripada petikan akan datang."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Namakan semula susun atur tetingkap"; "window_layout.name_placeholder" = "Masukkan nama susun atur tetingkap"; "window_recovery.enable_in_settings" = "Hidupkan Pemulihan Tetingkap dalam Tetapan dahulu."; -"window_recovery.save_help" = "Simpan kedudukan tetingkap semasa."; +"window_recovery.save_help" = "Simpan kedudukan dan saiz tetingkap semasa."; diff --git a/MousePortal/Resources/nl.lproj/Localizable.strings b/MousePortal/Resources/nl.lproj/Localizable.strings index 44a5c6e..a1c001d 100644 --- a/MousePortal/Resources/nl.lproj/Localizable.strings +++ b/MousePortal/Resources/nl.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Instellingen - Algemeen */ "settings.app_behavior" = "App-gedrag"; "settings.language" = "Taal"; +"settings.theme" = "Thema"; +"theme.system" = "Systeem volgen"; +"theme.light" = "Licht"; +"theme.dark" = "Donker"; "settings.show_menubar_icon" = "Toon pictogram in menubalk"; "settings.launch_at_login" = "Start bij inloggen"; "settings.permissions" = "Machtigingen"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Vensters onthouden voor sluimerstand of vergrendeling"; "window_recovery.automatic" = "Automatisch herstellen nadat beeldschermen ontwaken"; "window_recovery.behavior" = "Herstel na ontwaken"; -"window_recovery.save" = "Huidige indeling bewaren"; +"window_recovery.save" = "Vensterindeling bewaren"; "window_recovery.ignored_apps" = "Genegeerde apps"; "window_recovery.search_apps" = "Apps zoeken"; "window_recovery.ignored_apps_description" = "Schakel een app in om deze uit toekomstige momentopnamen uit te sluiten."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Vensterindeling hernoemen"; "window_layout.name_placeholder" = "Voer de naam van de vensterindeling in"; "window_recovery.enable_in_settings" = "Schakel eerst Vensterherstel in bij Instellingen."; -"window_recovery.save_help" = "Sla de huidige vensterposities op."; +"window_recovery.save_help" = "Sla de huidige vensterposities en -formaten op."; diff --git a/MousePortal/Resources/pl.lproj/Localizable.strings b/MousePortal/Resources/pl.lproj/Localizable.strings index 1f1bbc0..8008c62 100644 --- a/MousePortal/Resources/pl.lproj/Localizable.strings +++ b/MousePortal/Resources/pl.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Ustawienia - Ogólne */ "settings.app_behavior" = "Zachowanie aplikacji"; "settings.language" = "Język"; +"settings.theme" = "Motyw"; +"theme.system" = "Zgodnie z systemem"; +"theme.light" = "Jasny"; +"theme.dark" = "Ciemny"; "settings.show_menubar_icon" = "Pokaż ikonę na pasku menu"; "settings.launch_at_login" = "Uruchom przy logowaniu"; "settings.permissions" = "Uprawnienia"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Zapamiętaj okna przed uśpieniem lub blokadą"; "window_recovery.automatic" = "Przywracaj automatycznie po wybudzeniu ekranów"; "window_recovery.behavior" = "Przywracanie po wybudzeniu"; -"window_recovery.save" = "Zapisz bieżący układ"; +"window_recovery.save" = "Zapisz układ okien"; "window_recovery.ignored_apps" = "Ignorowane aplikacje"; "window_recovery.search_apps" = "Szukaj aplikacji"; "window_recovery.ignored_apps_description" = "Włącz aplikację, aby wykluczyć ją z przyszłych migawek."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Zmień nazwę układu okien"; "window_layout.name_placeholder" = "Wpisz nazwę układu okien"; "window_recovery.enable_in_settings" = "Najpierw włącz przywracanie okien w Ustawieniach."; -"window_recovery.save_help" = "Zapisz bieżące położenie okien."; +"window_recovery.save_help" = "Zapisz bieżące położenie i rozmiary okien."; diff --git a/MousePortal/Resources/pt-BR.lproj/Localizable.strings b/MousePortal/Resources/pt-BR.lproj/Localizable.strings index adc6847..1ad7ad2 100644 --- a/MousePortal/Resources/pt-BR.lproj/Localizable.strings +++ b/MousePortal/Resources/pt-BR.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* Ajustes - Geral */ "settings.app_behavior" = "Comportamento do app"; "settings.language" = "Idioma"; +"settings.theme" = "Tema"; +"theme.system" = "Seguir o sistema"; +"theme.light" = "Claro"; +"theme.dark" = "Escuro"; "settings.show_menubar_icon" = "Mostrar ícone na barra de menus"; "settings.launch_at_login" = "Iniciar ao fazer login"; "settings.permissions" = "Permissões"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "Lembrar janelas antes de repousar ou bloquear"; "window_recovery.automatic" = "Restaurar automaticamente quando as telas despertarem"; "window_recovery.behavior" = "Recuperação ao despertar"; -"window_recovery.save" = "Salvar layout atual"; +"window_recovery.save" = "Salvar layout das janelas"; "window_recovery.ignored_apps" = "Aplicativos ignorados"; "window_recovery.search_apps" = "Buscar aplicativos"; "window_recovery.ignored_apps_description" = "Ative um aplicativo para excluí-lo dos próximos instantâneos."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "Renomear layout das janelas"; "window_layout.name_placeholder" = "Digite o nome do layout"; "window_recovery.enable_in_settings" = "Ative primeiro a restauração de janelas nos Ajustes."; -"window_recovery.save_help" = "Salva as posições atuais das janelas."; +"window_recovery.save_help" = "Salva as posições e os tamanhos atuais das janelas."; diff --git a/MousePortal/Resources/ru.lproj/Localizable.strings b/MousePortal/Resources/ru.lproj/Localizable.strings index cf8c3fb..0155b72 100644 --- a/MousePortal/Resources/ru.lproj/Localizable.strings +++ b/MousePortal/Resources/ru.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* Настройки - Основные */ "settings.app_behavior" = "Поведение приложения"; "settings.language" = "Язык"; +"settings.theme" = "Тема"; +"theme.system" = "Как в системе"; +"theme.light" = "Светлая"; +"theme.dark" = "Тёмная"; "settings.show_menubar_icon" = "Показывать значок в строке меню"; "settings.launch_at_login" = "Запускать при входе"; "settings.permissions" = "Разрешения"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "Запоминать окна перед сном или блокировкой"; "window_recovery.automatic" = "Автоматически восстанавливать после пробуждения дисплеев"; "window_recovery.behavior" = "Восстановление после сна"; -"window_recovery.save" = "Сохранить текущее расположение"; +"window_recovery.save" = "Сохранить раскладку окон"; "window_recovery.ignored_apps" = "Игнорируемые приложения"; "window_recovery.search_apps" = "Поиск приложений"; "window_recovery.ignored_apps_description" = "Включите приложение, чтобы исключить его из будущих снимков."; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "Переименовать схему окон"; "window_layout.name_placeholder" = "Введите название схемы окон"; "window_recovery.enable_in_settings" = "Сначала включите восстановление окон в настройках."; -"window_recovery.save_help" = "Сохранить текущие позиции окон."; +"window_recovery.save_help" = "Сохранить текущие позиции и размеры окон."; diff --git a/MousePortal/Resources/th.lproj/Localizable.strings b/MousePortal/Resources/th.lproj/Localizable.strings index d5f701e..2e10616 100644 --- a/MousePortal/Resources/th.lproj/Localizable.strings +++ b/MousePortal/Resources/th.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* การตั้งค่า - ทั่วไป */ "settings.app_behavior" = "พฤติกรรมแอป"; "settings.language" = "ภาษา"; +"settings.theme" = "ธีม"; +"theme.system" = "ตามระบบ"; +"theme.light" = "สว่าง"; +"theme.dark" = "มืด"; "settings.show_menubar_icon" = "แสดงไอคอนในแถบเมนู"; "settings.launch_at_login" = "เปิดเมื่อเข้าสู่ระบบ"; "settings.permissions" = "สิทธิ์"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "จำหน้าต่างก่อนพักเครื่องหรือล็อก"; "window_recovery.automatic" = "คืนค่าอัตโนมัติหลังจอภาพตื่น"; "window_recovery.behavior" = "การกู้คืนหลังตื่น"; -"window_recovery.save" = "บันทึกเค้าโครงปัจจุบัน"; +"window_recovery.save" = "บันทึกเค้าโครงหน้าต่าง"; "window_recovery.ignored_apps" = "แอปที่ละเว้น"; "window_recovery.search_apps" = "ค้นหาแอป"; "window_recovery.ignored_apps_description" = "เปิดสวิตช์แอปเพื่อไม่รวมแอปนั้นจากสแนปช็อตในอนาคต"; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "เปลี่ยนชื่อเค้าโครงหน้าต่าง"; "window_layout.name_placeholder" = "ป้อนชื่อเค้าโครงหน้าต่าง"; "window_recovery.enable_in_settings" = "โปรดเปิดการคืนค่าหน้าต่างในการตั้งค่าก่อน"; -"window_recovery.save_help" = "บันทึกตำแหน่งหน้าต่างปัจจุบัน"; +"window_recovery.save_help" = "บันทึกตำแหน่งและขนาดของหน้าต่างปัจจุบัน"; diff --git a/MousePortal/Resources/tr.lproj/Localizable.strings b/MousePortal/Resources/tr.lproj/Localizable.strings index 1c0f188..b5462ba 100644 --- a/MousePortal/Resources/tr.lproj/Localizable.strings +++ b/MousePortal/Resources/tr.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Ayarlar - Genel */ "settings.app_behavior" = "Uygulama Davranışı"; "settings.language" = "Dil"; +"settings.theme" = "Tema"; +"theme.system" = "Sistemi takip et"; +"theme.light" = "Açık"; +"theme.dark" = "Koyu"; "settings.show_menubar_icon" = "Menü çubuğunda simge göster"; "settings.launch_at_login" = "Girişte başlat"; "settings.permissions" = "İzinler"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Uyku veya kilitlemeden önce pencereleri hatırla"; "window_recovery.automatic" = "Ekranlar uyandığında otomatik geri yükle"; "window_recovery.behavior" = "Uyanma sonrası kurtarma"; -"window_recovery.save" = "Geçerli düzeni kaydet"; +"window_recovery.save" = "Pencere düzenini kaydet"; "window_recovery.ignored_apps" = "Yoksayılan uygulamalar"; "window_recovery.search_apps" = "Uygulama ara"; "window_recovery.ignored_apps_description" = "Bir uygulamayı gelecekteki anlık görüntülerden çıkarmak için açın."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Pencere düzenini yeniden adlandır"; "window_layout.name_placeholder" = "Pencere düzeni adını girin"; "window_recovery.enable_in_settings" = "Önce Ayarlar’dan Pencere Kurtarma’yı açın."; -"window_recovery.save_help" = "Geçerli pencere konumlarını kaydeder."; +"window_recovery.save_help" = "Geçerli pencere konumlarını ve boyutlarını kaydeder."; diff --git a/MousePortal/Resources/vi.lproj/Localizable.strings b/MousePortal/Resources/vi.lproj/Localizable.strings index e540fee..ace75c3 100644 --- a/MousePortal/Resources/vi.lproj/Localizable.strings +++ b/MousePortal/Resources/vi.lproj/Localizable.strings @@ -39,6 +39,10 @@ /* Cài đặt - Chung */ "settings.app_behavior" = "Hành vi ứng dụng"; "settings.language" = "Ngôn ngữ"; +"settings.theme" = "Giao diện"; +"theme.system" = "Theo hệ thống"; +"theme.light" = "Sáng"; +"theme.dark" = "Tối"; "settings.show_menubar_icon" = "Hiển thị biểu tượng trên thanh menu"; "settings.launch_at_login" = "Khởi chạy khi đăng nhập"; "settings.permissions" = "Quyền"; @@ -161,7 +165,7 @@ "window_recovery.remember_before_sleep" = "Ghi nhớ cửa sổ trước khi ngủ hoặc khóa"; "window_recovery.automatic" = "Tự động khôi phục sau khi màn hình thức dậy"; "window_recovery.behavior" = "Khôi phục khi thức dậy"; -"window_recovery.save" = "Lưu bố cục hiện tại"; +"window_recovery.save" = "Lưu bố cục cửa sổ"; "window_recovery.ignored_apps" = "Ứng dụng bị bỏ qua"; "window_recovery.search_apps" = "Tìm ứng dụng"; "window_recovery.ignored_apps_description" = "Bật một ứng dụng để loại ứng dụng đó khỏi các ảnh chụp nhanh sau này."; @@ -178,4 +182,4 @@ "window_layout.rename_title" = "Đổi tên bố cục cửa sổ"; "window_layout.name_placeholder" = "Nhập tên bố cục cửa sổ"; "window_recovery.enable_in_settings" = "Trước tiên hãy bật Khôi phục cửa sổ trong Cài đặt."; -"window_recovery.save_help" = "Lưu vị trí cửa sổ hiện tại."; +"window_recovery.save_help" = "Lưu vị trí và kích thước cửa sổ hiện tại."; diff --git a/MousePortal/Resources/zh-Hans.lproj/Localizable.strings b/MousePortal/Resources/zh-Hans.lproj/Localizable.strings index 7a54470..8de8463 100644 --- a/MousePortal/Resources/zh-Hans.lproj/Localizable.strings +++ b/MousePortal/Resources/zh-Hans.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* 设置 - 通用 */ "settings.app_behavior" = "应用行为"; "settings.language" = "语言"; +"settings.theme" = "主题"; +"theme.system" = "跟随系统"; +"theme.light" = "浅色"; +"theme.dark" = "深色"; "settings.show_menubar_icon" = "在菜单栏显示图标"; "settings.launch_at_login" = "登录时启动"; "settings.permissions" = "权限"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "睡眠或锁屏前记住窗口"; "window_recovery.automatic" = "显示器唤醒后自动恢复"; "window_recovery.behavior" = "唤醒恢复"; -"window_recovery.save" = "保存当前布局"; +"window_recovery.save" = "保存窗口布局"; "window_recovery.ignored_apps" = "忽略的应用"; "window_recovery.search_apps" = "搜索应用"; "window_recovery.ignored_apps_description" = "打开应用右侧开关,可在之后的快照中排除它。"; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "重命名窗口布局"; "window_layout.name_placeholder" = "输入窗口布局名称"; "window_recovery.enable_in_settings" = "请先在设置中打开窗口恢复。"; -"window_recovery.save_help" = "保存当前窗口位置。"; +"window_recovery.save_help" = "保存当前窗口的位置和大小。"; diff --git a/MousePortal/Resources/zh-Hant.lproj/Localizable.strings b/MousePortal/Resources/zh-Hant.lproj/Localizable.strings index 6aa3e71..6ff61e1 100644 --- a/MousePortal/Resources/zh-Hant.lproj/Localizable.strings +++ b/MousePortal/Resources/zh-Hant.lproj/Localizable.strings @@ -40,6 +40,10 @@ /* 設定 - 一般 */ "settings.app_behavior" = "應用程式行為"; "settings.language" = "語言"; +"settings.theme" = "主題"; +"theme.system" = "跟隨系統"; +"theme.light" = "淺色"; +"theme.dark" = "深色"; "settings.show_menubar_icon" = "在選單列顯示圖示"; "settings.launch_at_login" = "登入時啟動"; "settings.permissions" = "權限"; @@ -159,7 +163,7 @@ "window_recovery.remember_before_sleep" = "睡眠或鎖定前記住視窗"; "window_recovery.automatic" = "顯示器喚醒後自動還原"; "window_recovery.behavior" = "喚醒還原"; -"window_recovery.save" = "儲存目前佈局"; +"window_recovery.save" = "儲存視窗佈局"; "window_recovery.ignored_apps" = "忽略的應用程式"; "window_recovery.search_apps" = "搜尋應用程式"; "window_recovery.ignored_apps_description" = "開啟應用程式右側開關,可在之後的快照中排除它。"; @@ -176,4 +180,4 @@ "window_layout.rename_title" = "重新命名視窗佈局"; "window_layout.name_placeholder" = "輸入視窗佈局名稱"; "window_recovery.enable_in_settings" = "請先在設定中開啟視窗還原。"; -"window_recovery.save_help" = "儲存目前的視窗位置。"; +"window_recovery.save_help" = "儲存目前視窗的位置和大小。"; diff --git a/MousePortal/Utils/AppTheme.swift b/MousePortal/Utils/AppTheme.swift new file mode 100644 index 0000000..dd893ae --- /dev/null +++ b/MousePortal/Utils/AppTheme.swift @@ -0,0 +1,46 @@ +import AppKit + +enum AppTheme: String, CaseIterable, Identifiable { + static let storageKey = "appTheme" + + case system + case light + case dark + + var id: String { rawValue } + + var localizedName: String { + switch self { + case .system: + return L("theme.system") + case .light: + return L("theme.light") + case .dark: + return L("theme.dark") + } + } + + var appearanceName: NSAppearance.Name? { + switch self { + case .system: + return nil + case .light: + return .aqua + case .dark: + return .darkAqua + } + } + + static func stored(in defaults: UserDefaults = .standard) -> AppTheme { + guard let rawValue = defaults.string(forKey: storageKey), + let theme = AppTheme(rawValue: rawValue) else { + return .system + } + return theme + } + + @MainActor + func apply() { + NSApplication.shared.appearance = appearanceName.flatMap(NSAppearance.init(named:)) + } +} diff --git a/MousePortal/Views/SettingsView.swift b/MousePortal/Views/SettingsView.swift index 3082653..19c9b4e 100644 --- a/MousePortal/Views/SettingsView.swift +++ b/MousePortal/Views/SettingsView.swift @@ -123,6 +123,7 @@ struct GeneralSettingsView: View { @ObservedObject private var languageService = LanguageService.shared @ObservedObject private var launchAtLoginService = LaunchAtLoginService.shared @AppStorage("showMenuBarIcon") private var showMenuBarIcon = true + @AppStorage(AppTheme.storageKey) private var appTheme = AppTheme.system var body: some View { Form { @@ -138,6 +139,18 @@ struct GeneralSettingsView: View { } } + Picker(L("settings.theme"), selection: Binding( + get: { appTheme }, + set: { theme in + appTheme = theme + theme.apply() + } + )) { + ForEach(AppTheme.allCases) { theme in + Text(theme.localizedName).tag(theme) + } + } + Toggle(L("settings.show_menubar_icon"), isOn: Binding( get: { showMenuBarIcon }, set: { newValue in diff --git a/MousePortalTests/Utils/AppThemeTests.swift b/MousePortalTests/Utils/AppThemeTests.swift new file mode 100644 index 0000000..e9a0848 --- /dev/null +++ b/MousePortalTests/Utils/AppThemeTests.swift @@ -0,0 +1,47 @@ +import XCTest +import AppKit +@testable import MousePortal + +final class AppThemeTests: XCTestCase { + func testStoredThemeDefaultsToSystemForMissingOrInvalidValue() { + let suiteName = "AppThemeTests.\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + XCTAssertEqual(AppTheme.stored(in: defaults), .system) + + defaults.set("unknown", forKey: AppTheme.storageKey) + + XCTAssertEqual(AppTheme.stored(in: defaults), .system) + } + + func testStoredThemeRestoresSavedValue() { + let suiteName = "AppThemeTests.\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + defaults.set(AppTheme.light.rawValue, forKey: AppTheme.storageKey) + + XCTAssertEqual(AppTheme.stored(in: defaults), .light) + } + + func testThemeMapsToNativeAppearance() { + XCTAssertNil(AppTheme.system.appearanceName) + XCTAssertEqual(AppTheme.light.appearanceName, .aqua) + XCTAssertEqual(AppTheme.dark.appearanceName, .darkAqua) + } + + @MainActor + func testApplyUpdatesApplicationAppearance() { + let originalAppearance = NSApplication.shared.appearance + defer { NSApplication.shared.appearance = originalAppearance } + + AppTheme.light.apply() + XCTAssertEqual(NSApplication.shared.appearance?.name, .aqua) + + AppTheme.dark.apply() + XCTAssertEqual(NSApplication.shared.appearance?.name, .darkAqua) + + AppTheme.system.apply() + XCTAssertNil(NSApplication.shared.appearance) + } +} diff --git a/README.md b/README.md index 8a3919c..c063b88 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,11 @@ > **[中文版本](README.zh.md)** | [English](README.md) -MousePortal is a macOS utility for managing mouse cursor movement between multiple displays. It provides two main features: +MousePortal is a macOS utility for managing mouse cursor movement and window placement across multiple displays. It provides three core features: 1. **Hotkey Jump** - Global keyboard shortcuts to instantly teleport the cursor to specific displays 2. **Portals** - Custom edge-to-edge teleportation zones that warp the cursor when crossing defined screen boundaries +3. **Window Layouts** - Save, preview, and restore application window positions for each display arrangement

MousePortal preview @@ -42,6 +43,15 @@ MousePortal is a macOS utility for managing mouse cursor movement between multip - **Key-held** - Requires holding a modifier key (default: Option) - Visual editor for easy creation and adjustment of portal zones +### Window Layouts and Recovery +- Save multiple named window layouts for each display arrangement +- Capture an automatic **Before Lock** layout before sleep or screen lock +- Preview saved windows directly on the display canvas +- Apply, rename, delete, or promote an automatic layout to a manual layout +- Control global window recovery independently from sleep/lock capture and automatic wake restoration +- Search for applications and exclude them from future snapshots +- Restore standard, non-minimized, non-full-screen windows; Split View, Stage Manager groups, and Spaces are not restored + ### Multi-language Support Available in 20 languages: English, Simplified Chinese, Traditional Chinese, Japanese, Korean, German, French, Spanish, Portuguese (Brazil), Russian, Italian, Dutch, Polish, Turkish, Arabic, Hindi, Thai, Vietnamese, Indonesian, Malay. @@ -50,6 +60,17 @@ Available in 20 languages: English, Simplified Chinese, Traditional Chinese, Jap - Menu bar quick access - Configuration profile management (for different setups) - Display layout visualization +- System, Light, and Dark app themes + +## Screenshots + +| English · Dark | English · Light | +| --- | --- | +| ![MousePortal Window Layout in English and Dark theme](docs/images/window-layout-en-dark.webp) | ![MousePortal Window Layout in English and Light theme](docs/images/window-layout-en-light.webp) | + +| 简体中文 · 深色 | 简体中文 · 浅色 | +| --- | --- | +| ![MousePortal 窗口布局中文深色主题](docs/images/window-layout-zh-Hans-dark.webp) | ![MousePortal 窗口布局中文浅色主题](docs/images/window-layout-zh-Hans-light.webp) | ## System Requirements @@ -111,6 +132,14 @@ swift run 2. Assign a name to each layout (e.g., "Dual Display", "Triple Display") 3. Specify relative positions of each display +### Save and Restore Window Layouts + +1. Open **Settings > Window Recovery** and turn on **Enable Window Recovery** +2. Optionally enable **Remember windows before sleep or lock** and automatic restoration after displays wake +3. Use the ignored-app search to exclude applications that should not be captured +4. Return to **Display Layouts**, enable **Show Window Layout**, then click **Save Window Layout** +5. Manage layouts below the matching display arrangement: apply, rename, delete, or promote an automatic layout to manual + ## Architecture ### Core Services @@ -121,6 +150,7 @@ swift run | `HotkeyService` | Listens for global keyboard shortcuts via CGEvent | | `PermissionService` | Handles macOS Accessibility permission | | `DisplayService` | Fetches display info via `CGGetActiveDisplayList`/`CGDisplayBounds` | +| `WindowLayoutService` | Captures, previews, and restores application window layouts | | `LanguageService` | Handles dynamic language switching | | `LaunchAtLoginService` | Manages launch at login | @@ -129,6 +159,7 @@ swift run - **`PortalPair`** - Two `PortalLine` objects defining a bidirectional warp zone - **`HotkeyConfig`** - Maps a key+modifiers combo to a display layout - **`ConfigProfile`** - Named configuration snapshots for different setups +- **`WindowLayoutSnapshot`** - A manual or automatic snapshot of windows for one display topology ## Development diff --git a/README.zh.md b/README.zh.md index fe558f8..e8e0d8e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -18,10 +18,11 @@ > **[English Version](README.md)** | 中文 -MousePortal 是一个 macOS 实用工具,用于在多个显示器之间管理鼠标光标移动。它提供两种主要功能: +MousePortal 是一个 macOS 实用工具,用于在多个显示器之间管理鼠标光标移动和窗口位置。它提供三项核心功能: 1. **热键跳转** - 全局键盘快捷键,瞬间将光标移动到指定显示器中心 2. **传送门** - 自定义的边缘到边缘传送区域,当光标跨越定义的屏幕边界时自动传送 +3. **窗口布局** - 针对每种显示器排列保存、预览和恢复应用窗口的位置

MousePortal 预览 @@ -42,6 +43,15 @@ MousePortal 是一个 macOS 实用工具,用于在多个显示器之间管理 - **按键模式** - 需要按住修饰键(默认为 Option) - 可视化编辑器,可轻松创建和调整传送门区域 +### 窗口布局与恢复 +- 为每种显示器排列保存多个命名窗口布局 +- 在睡眠或锁屏前自动保存一个 **锁屏前布局** +- 在显示器画布上直接预览已保存的窗口 +- 支持切换、改名、删除布局,以及将自动布局提升为手动布局 +- 全局窗口恢复、睡眠/锁屏前记录和唤醒后自动恢复分别控制 +- 可搜索应用并将其加入忽略列表,后续快照不再记录 +- 仅恢复普通、非最小化、非全屏窗口;不恢复 Split View、Stage Manager 分组和 Spaces + ### 多语言支持 支持 20 种语言:英语、简体中文、繁体中文、日语、韩语、德语、法语、西班牙语、葡萄牙语(巴西)、俄语、意大利语、荷兰语、波兰语、土耳其语、阿拉伯语、印地语、泰语、越南语、印尼语、马来语。 @@ -50,6 +60,17 @@ MousePortal 是一个 macOS 实用工具,用于在多个显示器之间管理 - 菜单栏快捷访问 - 配置文件管理(支持不同场景的配置) - 显示器布局可视化 +- 跟随系统、浅色和深色三种应用主题 + +## 截图 + +| English · Dark | English · Light | +| --- | --- | +| ![MousePortal 窗口布局英文深色主题](docs/images/window-layout-en-dark.webp) | ![MousePortal 窗口布局英文浅色主题](docs/images/window-layout-en-light.webp) | + +| 简体中文 · 深色 | 简体中文 · 浅色 | +| --- | --- | +| ![MousePortal 窗口布局中文深色主题](docs/images/window-layout-zh-Hans-dark.webp) | ![MousePortal 窗口布局中文浅色主题](docs/images/window-layout-zh-Hans-light.webp) | ## 系统要求 @@ -111,6 +132,14 @@ swift run 2. 为每个布局指定一个名称(例如 "双显示器", "三显示器") 3. 指定每个显示器的相对位置 +### 保存与恢复窗口布局 + +1. 打开 **设置 > 窗口恢复**,打开 **启用窗口恢复** +2. 按需启用 **睡眠或锁屏前记住窗口** 和显示器唤醒后自动恢复 +3. 使用忽略应用搜索,排除不需要记录的应用 +4. 回到 **显示器排列**,打开 **显示窗口布局**,然后点击 **保存窗口布局** +5. 在对应显示器排列下管理布局:切换、改名、删除,或将自动布局提升为手动布局 + ## 架构 ### 核心服务 @@ -121,6 +150,7 @@ swift run | `HotkeyService` | 通过 CGEvent 监听全局键盘快捷键 | | `PermissionService` | 处理 macOS 辅助功能权限 | | `DisplayService` | 通过 `CGGetActiveDisplayList`/`CGDisplayBounds` 获取显示器信息 | +| `WindowLayoutService` | 捕获、预览和恢复应用窗口布局 | | `LanguageService` | 处理动态语言切换 | | `LaunchAtLoginService` | 管理开机自启动 | @@ -129,6 +159,7 @@ swift run - **`PortalPair`** - 定义双向传送区域的两个 `PortalLine` 对象 - **`HotkeyConfig`** - 将键 + 修饰键组合映射到显示器布局 - **`ConfigProfile`** - 用于不同设置的命名配置快照 +- **`WindowLayoutSnapshot`** - 针对一套显示器拓扑保存的手动或自动窗口快照 ## 开发 diff --git a/docs/images/window-layout-en-dark.webp b/docs/images/window-layout-en-dark.webp new file mode 100644 index 0000000..85568c1 Binary files /dev/null and b/docs/images/window-layout-en-dark.webp differ diff --git a/docs/images/window-layout-en-light.webp b/docs/images/window-layout-en-light.webp new file mode 100644 index 0000000..51d1c78 Binary files /dev/null and b/docs/images/window-layout-en-light.webp differ diff --git a/docs/images/window-layout-zh-Hans-dark.webp b/docs/images/window-layout-zh-Hans-dark.webp new file mode 100644 index 0000000..427d98f Binary files /dev/null and b/docs/images/window-layout-zh-Hans-dark.webp differ diff --git a/docs/images/window-layout-zh-Hans-light.webp b/docs/images/window-layout-zh-Hans-light.webp new file mode 100644 index 0000000..0ed43ee Binary files /dev/null and b/docs/images/window-layout-zh-Hans-light.webp differ