mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
✨ feat(electron): enhance native module handling and improve desktop features (#11867)
* 🔧 refactor: streamline theme handling and title bar overlay * ✨ feat(titlebar): integrate theme update handling in SimpleTitleBar component * 🔧 chore: move `node-mac-permissions` to optionalDependencies and add TypeScript module declaration * ✨ feat(electron): implement connection drawer state management and enhance auth modal functionality * 🐛 fix(ci): fix Windows PowerShell Start-Job working directory issue Start-Job runs in a separate process with default user directory, causing npm install-isolated to fail. Fixed by setting correct working directory in each job using $using:workingDir. * 🐛 fix(ci): use Start-Process instead of Start-Job for Windows parallel install Start-Job runs in isolated PowerShell process without inheriting PATH, causing pnpm/npm commands to fail. Start-Process inherits environment and provides proper exit code handling. * 🐛 fix(ci): use desktop-build-setup action for Windows build Use the same composite action as other desktop workflows instead of custom PowerShell parallel install which has environment issues. * ✨ feat(menu): enhance context menu with additional options for image and link handling * 🔧 fix(auth-modal): prevent modal from opening during desktop onboarding * ✨ feat(electron): enhance native module handling and improve localization resource loading resolves LOBE-4370 - Added `copyNativeModulesToSource` function to resolve pnpm symlinks for native modules before packaging. - Introduced `getNativeModulesFilesConfig` to explicitly include native modules in the build process. - Updated `electron-builder` configuration to utilize the new functions for better native module management. - Enhanced localization resource loading by splitting JSON files by namespace. * 🐛 fix(lint): use slice instead of substring * 🐛 fix(desktop): include global.d.ts in tsconfig for node-mac-permissions types * 🐛 fix(desktop): add ts-ignore for optional node-mac-permissions module * fix: update ui
This commit is contained in:
parent
ba0fab13a1
commit
e3c80d53ce
52 changed files with 1025 additions and 175 deletions
12
.github/workflows/manual-build-desktop.yml
vendored
12
.github/workflows/manual-build-desktop.yml
vendored
|
|
@ -184,18 +184,10 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node & pnpm
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/desktop-build-setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: 'false'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
$job1 = Start-Job -ScriptBlock { pnpm install --node-linker=hoisted }
|
||||
$job2 = Start-Job -ScriptBlock { npm run install-isolated --prefix=./apps/desktop }
|
||||
$job1, $job2 | Wait-Job | Receive-Job
|
||||
|
||||
- name: Set package version
|
||||
run: npm run workflow:set-desktop-version ${{ needs.version.outputs.version }} ${{ inputs.channel }}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import { fileURLToPath } from 'node:url';
|
|||
|
||||
import {
|
||||
copyNativeModules,
|
||||
copyNativeModulesToSource,
|
||||
getAsarUnpackPatterns,
|
||||
getFilesPatterns,
|
||||
getNativeModulesFilesConfig,
|
||||
} from './native-deps.config.mjs';
|
||||
|
||||
dotenv.config();
|
||||
|
|
@ -89,6 +90,13 @@ const getIconFileName = () => {
|
|||
* @see https://www.electron.build/configuration
|
||||
*/
|
||||
const config = {
|
||||
/**
|
||||
* BeforePack hook to resolve pnpm symlinks for native modules.
|
||||
* This ensures native modules are properly included in the asar archive.
|
||||
*/
|
||||
beforePack: async () => {
|
||||
await copyNativeModulesToSource();
|
||||
},
|
||||
/**
|
||||
* AfterPack hook for post-processing:
|
||||
* 1. Copy native modules to asar.unpacked (resolving pnpm symlinks)
|
||||
|
|
@ -204,10 +212,10 @@ const config = {
|
|||
'!dist/next/packages',
|
||||
'!dist/next/.next/server/app/sitemap',
|
||||
'!dist/next/.next/static/media',
|
||||
// Exclude node_modules from packaging (except native modules)
|
||||
// Exclude all node_modules first
|
||||
'!node_modules',
|
||||
// Include native modules (defined in native-deps.config.mjs)
|
||||
...getFilesPatterns(),
|
||||
// Then explicitly include native modules using object form (handles pnpm symlinks)
|
||||
...getNativeModulesFilesConfig(),
|
||||
],
|
||||
generateUpdatesFilesForAllChannels: true,
|
||||
linux: {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ export default defineConfig({
|
|||
if (id.includes('node_modules/debug')) {
|
||||
return 'vendor-debug';
|
||||
}
|
||||
|
||||
// Split i18n json resources by namespace (ns), not by locale.
|
||||
// Example: ".../resources/locales/zh-CN/common.json?import" -> "locales-common"
|
||||
const normalizedId = id.replaceAll('\\', '/').split('?')[0];
|
||||
const match = normalizedId.match(/\/locales\/[^/]+\/([^/]+)\.json$/);
|
||||
|
||||
if (match?.[1]) return `locales-${match[1]}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -112,6 +112,19 @@ export function getFilesPatterns() {
|
|||
return getAllDependencies().map((dep) => `node_modules/${dep}/**/*`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate files config objects for electron-builder to explicitly copy native modules.
|
||||
* This uses object form to ensure scoped packages with pnpm symlinks are properly copied.
|
||||
* @returns {Array<{from: string, to: string, filter: string[]}>}
|
||||
*/
|
||||
export function getNativeModulesFilesConfig() {
|
||||
return getAllDependencies().map((dep) => ({
|
||||
filter: ['**/*'],
|
||||
from: `node_modules/${dep}`,
|
||||
to: `node_modules/${dep}`,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate glob patterns for electron-builder asarUnpack config
|
||||
* @returns {string[]} Array of glob patterns
|
||||
|
|
@ -128,6 +141,47 @@ export function getExternalDependencies() {
|
|||
return getAllDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy native modules to source node_modules, resolving pnpm symlinks.
|
||||
* This is used in beforePack hook to ensure native modules are properly
|
||||
* included in the asar archive (electron-builder glob doesn't follow symlinks).
|
||||
*/
|
||||
export async function copyNativeModulesToSource() {
|
||||
const fsPromises = await import('node:fs/promises');
|
||||
const deps = getAllDependencies();
|
||||
const sourceNodeModules = path.join(__dirname, 'node_modules');
|
||||
|
||||
console.log(`📦 Resolving ${deps.length} native module symlinks for packaging...`);
|
||||
|
||||
for (const dep of deps) {
|
||||
const modulePath = path.join(sourceNodeModules, dep);
|
||||
|
||||
try {
|
||||
const stat = await fsPromises.lstat(modulePath);
|
||||
|
||||
if (stat.isSymbolicLink()) {
|
||||
// Resolve the symlink to get the real path
|
||||
const realPath = await fsPromises.realpath(modulePath);
|
||||
console.log(` 📎 ${dep} (resolving symlink)`);
|
||||
|
||||
// Remove the symlink
|
||||
await fsPromises.rm(modulePath, { force: true, recursive: true });
|
||||
|
||||
// Create parent directory if needed (for scoped packages like @napi-rs)
|
||||
await fsPromises.mkdir(path.dirname(modulePath), { recursive: true });
|
||||
|
||||
// Copy the actual directory content in place of the symlink
|
||||
await copyDir(realPath, modulePath);
|
||||
}
|
||||
} catch (err) {
|
||||
// Module might not exist (optional dependency for different platform)
|
||||
console.log(` ⏭️ ${dep} (skipped: ${err.code || err.message})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Native module symlinks resolved`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy native modules to destination, resolving symlinks
|
||||
* This is used in afterPack hook to handle pnpm symlinks correctly
|
||||
|
|
|
|||
|
|
@ -40,13 +40,16 @@
|
|||
"update-server": "sh scripts/update-test/run-test.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "^0.1.70",
|
||||
"electron-updater": "^6.6.2",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"get-port-please": "^3.2.0",
|
||||
"node-mac-permissions": "^2.5.0",
|
||||
"superjson": "^2.2.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-mac-permissions": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||
|
|
@ -102,6 +105,7 @@
|
|||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@napi-rs/canvas",
|
||||
"electron",
|
||||
"electron-builder",
|
||||
"node-mac-permissions"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "التحقق من التحديثات...",
|
||||
"context.copyImage": "نسخ الصورة",
|
||||
"context.copyImageAddress": "نسخ عنوان الصورة",
|
||||
"context.copyLink": "نسخ الرابط",
|
||||
"context.inspectElement": "فحص العنصر",
|
||||
"context.openLink": "فتح الرابط",
|
||||
"context.saveImage": "حفظ الصورة",
|
||||
"context.saveImageAs": "حفظ الصورة باسم…",
|
||||
"context.searchWithGoogle": "البحث باستخدام جوجل",
|
||||
"dev.devPanel": "لوحة المطور",
|
||||
"dev.devTools": "أدوات المطور",
|
||||
"dev.forceReload": "إعادة تحميل قسري",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "نسخ",
|
||||
"edit.cut": "قص",
|
||||
"edit.delete": "حذف",
|
||||
"edit.lookUp": "البحث",
|
||||
"edit.paste": "لصق",
|
||||
"edit.redo": "إعادة",
|
||||
"edit.selectAll": "تحديد الكل",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Проверка за актуализации...",
|
||||
"context.copyImage": "Копирай изображение",
|
||||
"context.copyImageAddress": "Копирай адреса на изображението",
|
||||
"context.copyLink": "Копирай връзката",
|
||||
"context.inspectElement": "Инспектиране на елемент",
|
||||
"context.openLink": "Отвори връзката",
|
||||
"context.saveImage": "Запази изображението",
|
||||
"context.saveImageAs": "Запази изображението като…",
|
||||
"context.searchWithGoogle": "Търси с Google",
|
||||
"dev.devPanel": "Панел на разработчика",
|
||||
"dev.devTools": "Инструменти за разработчици",
|
||||
"dev.forceReload": "Принудително презареждане",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Копиране",
|
||||
"edit.cut": "Изрязване",
|
||||
"edit.delete": "Изтрий",
|
||||
"edit.lookUp": "Потърси",
|
||||
"edit.paste": "Поставяне",
|
||||
"edit.redo": "Повторно",
|
||||
"edit.selectAll": "Избери всичко",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Überprüfen Sie auf Updates...",
|
||||
"context.copyImage": "Bild kopieren",
|
||||
"context.copyImageAddress": "Bildadresse kopieren",
|
||||
"context.copyLink": "Link kopieren",
|
||||
"context.inspectElement": "Element untersuchen",
|
||||
"context.openLink": "Link öffnen",
|
||||
"context.saveImage": "Bild speichern",
|
||||
"context.saveImageAs": "Bild speichern unter…",
|
||||
"context.searchWithGoogle": "Mit Google suchen",
|
||||
"dev.devPanel": "Entwicklerpanel",
|
||||
"dev.devTools": "Entwicklerwerkzeuge",
|
||||
"dev.forceReload": "Erzwinge Neuladen",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Kopieren",
|
||||
"edit.cut": "Ausschneiden",
|
||||
"edit.delete": "Löschen",
|
||||
"edit.lookUp": "Nachschlagen",
|
||||
"edit.paste": "Einfügen",
|
||||
"edit.redo": "Wiederherstellen",
|
||||
"edit.selectAll": "Alles auswählen",
|
||||
|
|
|
|||
|
|
@ -23,4 +23,4 @@
|
|||
"status.loading": "Loading",
|
||||
"status.success": "Success",
|
||||
"status.warning": "Warning"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"update.newVersion": "New Version Found",
|
||||
"update.newVersionAvailable": "New version: {{version}}",
|
||||
"update.skipThisVersion": "Skip This Version"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Check for updates...",
|
||||
"context.copyImage": "Copy Image",
|
||||
"context.copyImageAddress": "Copy Image Address",
|
||||
"context.copyLink": "Copy Link",
|
||||
"context.inspectElement": "Inspect Element",
|
||||
"context.openLink": "Open Link",
|
||||
"context.saveImage": "Save Image",
|
||||
"context.saveImageAs": "Save Image As…",
|
||||
"context.searchWithGoogle": "Search with Google",
|
||||
"dev.devPanel": "Developer Panel",
|
||||
"dev.devTools": "Developer Tools",
|
||||
"dev.forceReload": "Force Reload",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Copy",
|
||||
"edit.cut": "Cut",
|
||||
"edit.delete": "Delete",
|
||||
"edit.lookUp": "Look Up",
|
||||
"edit.paste": "Paste",
|
||||
"edit.redo": "Redo",
|
||||
"edit.selectAll": "Select All",
|
||||
|
|
@ -70,4 +79,4 @@
|
|||
"window.title": "Window",
|
||||
"window.toggleFullscreen": "Toggle Fullscreen",
|
||||
"window.zoom": "Zoom"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Comprobando actualizaciones...",
|
||||
"context.copyImage": "Copiar imagen",
|
||||
"context.copyImageAddress": "Copiar dirección de la imagen",
|
||||
"context.copyLink": "Copiar enlace",
|
||||
"context.inspectElement": "Inspeccionar elemento",
|
||||
"context.openLink": "Abrir enlace",
|
||||
"context.saveImage": "Guardar imagen",
|
||||
"context.saveImageAs": "Guardar imagen como…",
|
||||
"context.searchWithGoogle": "Buscar con Google",
|
||||
"dev.devPanel": "Panel de desarrollador",
|
||||
"dev.devTools": "Herramientas de desarrollador",
|
||||
"dev.forceReload": "Recargar forzosamente",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Copiar",
|
||||
"edit.cut": "Cortar",
|
||||
"edit.delete": "Eliminar",
|
||||
"edit.lookUp": "Buscar",
|
||||
"edit.paste": "Pegar",
|
||||
"edit.redo": "Rehacer",
|
||||
"edit.selectAll": "Seleccionar todo",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "بررسی بهروزرسانی...",
|
||||
"context.copyImage": "کپی تصویر",
|
||||
"context.copyImageAddress": "کپی آدرس تصویر",
|
||||
"context.copyLink": "کپی لینک",
|
||||
"context.inspectElement": "بازرسی عنصر",
|
||||
"context.openLink": "باز کردن لینک",
|
||||
"context.saveImage": "ذخیره تصویر",
|
||||
"context.saveImageAs": "ذخیره تصویر به عنوان…",
|
||||
"context.searchWithGoogle": "جستجو با گوگل",
|
||||
"dev.devPanel": "پنل توسعهدهنده",
|
||||
"dev.devTools": "ابزارهای توسعهدهنده",
|
||||
"dev.forceReload": "بارگذاری اجباری",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "کپی",
|
||||
"edit.cut": "برش",
|
||||
"edit.delete": "حذف",
|
||||
"edit.lookUp": "جستجو",
|
||||
"edit.paste": "چسباندن",
|
||||
"edit.redo": "انجام مجدد",
|
||||
"edit.selectAll": "انتخاب همه",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Vérifier les mises à jour...",
|
||||
"context.copyImage": "Copier l'image",
|
||||
"context.copyImageAddress": "Copier l'adresse de l'image",
|
||||
"context.copyLink": "Copier le lien",
|
||||
"context.inspectElement": "Inspecter l'élément",
|
||||
"context.openLink": "Ouvrir le lien",
|
||||
"context.saveImage": "Enregistrer l'image",
|
||||
"context.saveImageAs": "Enregistrer l'image sous…",
|
||||
"context.searchWithGoogle": "Rechercher avec Google",
|
||||
"dev.devPanel": "Panneau de développement",
|
||||
"dev.devTools": "Outils de développement",
|
||||
"dev.forceReload": "Recharger de force",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Copier",
|
||||
"edit.cut": "Couper",
|
||||
"edit.delete": "Supprimer",
|
||||
"edit.lookUp": "Rechercher",
|
||||
"edit.paste": "Coller",
|
||||
"edit.redo": "Rétablir",
|
||||
"edit.selectAll": "Tout sélectionner",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Controlla aggiornamenti...",
|
||||
"context.copyImage": "Copia immagine",
|
||||
"context.copyImageAddress": "Copia indirizzo immagine",
|
||||
"context.copyLink": "Copia link",
|
||||
"context.inspectElement": "Ispeziona elemento",
|
||||
"context.openLink": "Apri link",
|
||||
"context.saveImage": "Salva immagine",
|
||||
"context.saveImageAs": "Salva immagine come…",
|
||||
"context.searchWithGoogle": "Cerca con Google",
|
||||
"dev.devPanel": "Pannello sviluppatore",
|
||||
"dev.devTools": "Strumenti per sviluppatori",
|
||||
"dev.forceReload": "Ricarica forzata",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Copia",
|
||||
"edit.cut": "Taglia",
|
||||
"edit.delete": "Elimina",
|
||||
"edit.lookUp": "Cerca",
|
||||
"edit.paste": "Incolla",
|
||||
"edit.redo": "Ripeti",
|
||||
"edit.selectAll": "Seleziona tutto",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "更新を確認しています...",
|
||||
"context.copyImage": "画像をコピー",
|
||||
"context.copyImageAddress": "画像のアドレスをコピー",
|
||||
"context.copyLink": "リンクをコピー",
|
||||
"context.inspectElement": "要素を検証",
|
||||
"context.openLink": "リンクを開く",
|
||||
"context.saveImage": "画像を保存",
|
||||
"context.saveImageAs": "名前を付けて画像を保存…",
|
||||
"context.searchWithGoogle": "Googleで検索",
|
||||
"dev.devPanel": "開発者パネル",
|
||||
"dev.devTools": "開発者ツール",
|
||||
"dev.forceReload": "強制再読み込み",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "コピー",
|
||||
"edit.cut": "切り取り",
|
||||
"edit.delete": "削除",
|
||||
"edit.lookUp": "調べる",
|
||||
"edit.paste": "貼り付け",
|
||||
"edit.redo": "やり直し",
|
||||
"edit.selectAll": "すべて選択",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "업데이트 확인 중...",
|
||||
"context.copyImage": "이미지 복사",
|
||||
"context.copyImageAddress": "이미지 주소 복사",
|
||||
"context.copyLink": "링크 복사",
|
||||
"context.inspectElement": "요소 검사",
|
||||
"context.openLink": "링크 열기",
|
||||
"context.saveImage": "이미지 저장",
|
||||
"context.saveImageAs": "다른 이름으로 이미지 저장…",
|
||||
"context.searchWithGoogle": "Google로 검색",
|
||||
"dev.devPanel": "개발자 패널",
|
||||
"dev.devTools": "개발자 도구",
|
||||
"dev.forceReload": "강제 새로 고침",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "복사",
|
||||
"edit.cut": "잘라내기",
|
||||
"edit.delete": "삭제",
|
||||
"edit.lookUp": "찾아보기",
|
||||
"edit.paste": "붙여넣기",
|
||||
"edit.redo": "다시 실행",
|
||||
"edit.selectAll": "모두 선택",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Updates controleren...",
|
||||
"context.copyImage": "Afbeelding kopiëren",
|
||||
"context.copyImageAddress": "Afbeeldingsadres kopiëren",
|
||||
"context.copyLink": "Link kopiëren",
|
||||
"context.inspectElement": "Element inspecteren",
|
||||
"context.openLink": "Link openen",
|
||||
"context.saveImage": "Afbeelding opslaan",
|
||||
"context.saveImageAs": "Afbeelding opslaan als…",
|
||||
"context.searchWithGoogle": "Zoeken met Google",
|
||||
"dev.devPanel": "Ontwikkelaarspaneel",
|
||||
"dev.devTools": "Ontwikkelaarstools",
|
||||
"dev.forceReload": "Forceer herladen",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Kopiëren",
|
||||
"edit.cut": "Knippen",
|
||||
"edit.delete": "Verwijderen",
|
||||
"edit.lookUp": "Opzoeken",
|
||||
"edit.paste": "Plakken",
|
||||
"edit.redo": "Opnieuw doen",
|
||||
"edit.selectAll": "Alles selecteren",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Sprawdzanie aktualizacji...",
|
||||
"context.copyImage": "Kopiuj obraz",
|
||||
"context.copyImageAddress": "Kopiuj adres obrazu",
|
||||
"context.copyLink": "Kopiuj link",
|
||||
"context.inspectElement": "Zbadaj element",
|
||||
"context.openLink": "Otwórz link",
|
||||
"context.saveImage": "Zapisz obraz",
|
||||
"context.saveImageAs": "Zapisz obraz jako…",
|
||||
"context.searchWithGoogle": "Szukaj w Google",
|
||||
"dev.devPanel": "Panel dewelopera",
|
||||
"dev.devTools": "Narzędzia dewelopera",
|
||||
"dev.forceReload": "Wymuś ponowne załadowanie",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Kopiuj",
|
||||
"edit.cut": "Wytnij",
|
||||
"edit.delete": "Usuń",
|
||||
"edit.lookUp": "Wyszukaj",
|
||||
"edit.paste": "Wklej",
|
||||
"edit.redo": "Ponów",
|
||||
"edit.selectAll": "Zaznacz wszystko",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Verificando atualizações...",
|
||||
"context.copyImage": "Copiar Imagem",
|
||||
"context.copyImageAddress": "Copiar Endereço da Imagem",
|
||||
"context.copyLink": "Copiar Link",
|
||||
"context.inspectElement": "Inspecionar Elemento",
|
||||
"context.openLink": "Abrir Link",
|
||||
"context.saveImage": "Salvar Imagem",
|
||||
"context.saveImageAs": "Salvar Imagem Como…",
|
||||
"context.searchWithGoogle": "Pesquisar com o Google",
|
||||
"dev.devPanel": "Painel do Desenvolvedor",
|
||||
"dev.devTools": "Ferramentas do Desenvolvedor",
|
||||
"dev.forceReload": "Recarregar Forçadamente",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Copiar",
|
||||
"edit.cut": "Cortar",
|
||||
"edit.delete": "Excluir",
|
||||
"edit.lookUp": "Pesquisar",
|
||||
"edit.paste": "Colar",
|
||||
"edit.redo": "Refazer",
|
||||
"edit.selectAll": "Selecionar Tudo",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Проверка обновлений...",
|
||||
"context.copyImage": "Копировать изображение",
|
||||
"context.copyImageAddress": "Копировать адрес изображения",
|
||||
"context.copyLink": "Копировать ссылку",
|
||||
"context.inspectElement": "Просмотреть элемент",
|
||||
"context.openLink": "Открыть ссылку",
|
||||
"context.saveImage": "Сохранить изображение",
|
||||
"context.saveImageAs": "Сохранить изображение как…",
|
||||
"context.searchWithGoogle": "Искать с помощью Google",
|
||||
"dev.devPanel": "Панель разработчика",
|
||||
"dev.devTools": "Инструменты разработчика",
|
||||
"dev.forceReload": "Принудительная перезагрузка",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Копировать",
|
||||
"edit.cut": "Вырезать",
|
||||
"edit.delete": "Удалить",
|
||||
"edit.lookUp": "Поиск",
|
||||
"edit.paste": "Вставить",
|
||||
"edit.redo": "Повторить",
|
||||
"edit.selectAll": "Выбрать все",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Güncellemeleri kontrol et...",
|
||||
"context.copyImage": "Resmi Kopyala",
|
||||
"context.copyImageAddress": "Resim Adresini Kopyala",
|
||||
"context.copyLink": "Bağlantıyı Kopyala",
|
||||
"context.inspectElement": "Öğeyi İncele",
|
||||
"context.openLink": "Bağlantıyı Aç",
|
||||
"context.saveImage": "Resmi Kaydet",
|
||||
"context.saveImageAs": "Resmi Farklı Kaydet…",
|
||||
"context.searchWithGoogle": "Google ile Ara",
|
||||
"dev.devPanel": "Geliştirici Paneli",
|
||||
"dev.devTools": "Geliştirici Araçları",
|
||||
"dev.forceReload": "Zorla Yenile",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Kopyala",
|
||||
"edit.cut": "Kes",
|
||||
"edit.delete": "Sil",
|
||||
"edit.lookUp": "Ara",
|
||||
"edit.paste": "Yapıştır",
|
||||
"edit.redo": "Yinele",
|
||||
"edit.selectAll": "Tümünü Seç",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "Kiểm tra cập nhật...",
|
||||
"context.copyImage": "Sao chép hình ảnh",
|
||||
"context.copyImageAddress": "Sao chép địa chỉ hình ảnh",
|
||||
"context.copyLink": "Sao chép liên kết",
|
||||
"context.inspectElement": "Kiểm tra phần tử",
|
||||
"context.openLink": "Mở liên kết",
|
||||
"context.saveImage": "Lưu hình ảnh",
|
||||
"context.saveImageAs": "Lưu hình ảnh thành…",
|
||||
"context.searchWithGoogle": "Tìm kiếm với Google",
|
||||
"dev.devPanel": "Bảng điều khiển nhà phát triển",
|
||||
"dev.devTools": "Công cụ phát triển",
|
||||
"dev.forceReload": "Tải lại cưỡng bức",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "Sao chép",
|
||||
"edit.cut": "Cắt",
|
||||
"edit.delete": "Xóa",
|
||||
"edit.lookUp": "Tra cứu",
|
||||
"edit.paste": "Dán",
|
||||
"edit.redo": "Làm lại",
|
||||
"edit.selectAll": "Chọn tất cả",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "检查更新…",
|
||||
"context.copyImage": "复制图片",
|
||||
"context.copyImageAddress": "复制图片地址",
|
||||
"context.copyLink": "复制链接",
|
||||
"context.inspectElement": "检查元素",
|
||||
"context.openLink": "打开链接",
|
||||
"context.saveImage": "保存图片",
|
||||
"context.saveImageAs": "图片另存为…",
|
||||
"context.searchWithGoogle": "使用谷歌搜索",
|
||||
"dev.devPanel": "开发者面板",
|
||||
"dev.devTools": "开发者工具",
|
||||
"dev.forceReload": "强制重新加载",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "复制",
|
||||
"edit.cut": "剪切",
|
||||
"edit.delete": "删除",
|
||||
"edit.lookUp": "查找",
|
||||
"edit.paste": "粘贴",
|
||||
"edit.redo": "重做",
|
||||
"edit.selectAll": "全选",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"common.checkUpdates": "檢查更新...",
|
||||
"context.copyImage": "複製圖片",
|
||||
"context.copyImageAddress": "複製圖片位址",
|
||||
"context.copyLink": "複製連結",
|
||||
"context.inspectElement": "檢查元素",
|
||||
"context.openLink": "開啟連結",
|
||||
"context.saveImage": "儲存圖片",
|
||||
"context.saveImageAs": "圖片另存新檔…",
|
||||
"context.searchWithGoogle": "使用 Google 搜尋",
|
||||
"dev.devPanel": "開發者面板",
|
||||
"dev.devTools": "開發者工具",
|
||||
"dev.forceReload": "強制重新載入",
|
||||
|
|
@ -24,6 +32,7 @@
|
|||
"edit.copy": "複製",
|
||||
"edit.cut": "剪下",
|
||||
"edit.delete": "刪除",
|
||||
"edit.lookUp": "查詢",
|
||||
"edit.paste": "貼上",
|
||||
"edit.redo": "重做",
|
||||
"edit.selectAll": "全選",
|
||||
|
|
|
|||
|
|
@ -203,12 +203,9 @@ export default class SystemController extends ControllerModule {
|
|||
async updateThemeModeHandler(themeMode: ThemeMode) {
|
||||
this.app.storeManager.set('themeMode', themeMode);
|
||||
this.app.browserManager.broadcastToAllWindows('themeChanged', { themeMode });
|
||||
|
||||
// Apply visual effects to all browser windows when theme mode changes
|
||||
this.app.browserManager.handleAppThemeChange();
|
||||
// Set app theme mode to the system theme mode
|
||||
|
||||
this.setSystemThemeMode(themeMode);
|
||||
this.app.browserManager.handleAppThemeChange();
|
||||
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-c
|
|||
import {
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
Menu,
|
||||
session as electronSession,
|
||||
ipcMain,
|
||||
screen,
|
||||
|
|
@ -12,7 +11,7 @@ import console from 'node:console';
|
|||
import { join } from 'node:path';
|
||||
|
||||
import { preloadDir, resourcesDir } from '@/const/dir';
|
||||
import { isDev, isMac } from '@/const/env';
|
||||
import { isMac } from '@/const/env';
|
||||
import { ELECTRON_BE_PROTOCOL_SCHEME } from '@/const/protocol';
|
||||
import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
|
||||
import { backendProxyProtocolManager } from '@/core/infrastructure/BackendProxyProtocolManager';
|
||||
|
|
@ -121,9 +120,7 @@ export default class Browser {
|
|||
logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
|
||||
logger.debug(`[${this.identifier}] Resolved window state: ${JSON.stringify(resolvedState)}`);
|
||||
|
||||
// Calculate traffic light position to center vertically in title bar
|
||||
// Traffic light buttons are approximately 12px tall
|
||||
const trafficLightY = Math.round((TITLE_BAR_HEIGHT - 12) / 2);
|
||||
|
||||
|
||||
return new BrowserWindow({
|
||||
...rest,
|
||||
|
|
@ -134,7 +131,7 @@ export default class Browser {
|
|||
height: resolvedState.height,
|
||||
show: false,
|
||||
title,
|
||||
trafficLightPosition: isMac ? { x: 12, y: trafficLightY } : undefined,
|
||||
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
webPreferences: {
|
||||
|
|
@ -192,7 +189,7 @@ export default class Browser {
|
|||
this.setupCloseListener(browserWindow);
|
||||
this.setupFocusListener(browserWindow);
|
||||
this.setupWillPreventUnloadListener(browserWindow);
|
||||
this.setupDevContextMenu(browserWindow);
|
||||
this.setupContextMenu(browserWindow);
|
||||
}
|
||||
|
||||
private setupWillPreventUnloadListener(browserWindow: BrowserWindow): void {
|
||||
|
|
@ -239,39 +236,25 @@ export default class Browser {
|
|||
}
|
||||
|
||||
/**
|
||||
* Setup context menu with "Inspect Element" option in development mode
|
||||
* Setup context menu with platform-specific features
|
||||
* Delegates to MenuManager for consistent platform behavior
|
||||
*/
|
||||
private setupDevContextMenu(browserWindow: BrowserWindow): void {
|
||||
if (!isDev) return;
|
||||
|
||||
logger.debug(`[${this.identifier}] Setting up dev context menu.`);
|
||||
private setupContextMenu(browserWindow: BrowserWindow): void {
|
||||
logger.debug(`[${this.identifier}] Setting up context menu.`);
|
||||
|
||||
browserWindow.webContents.on('context-menu', (_event, params) => {
|
||||
const { x, y } = params;
|
||||
const { x, y, selectionText, linkURL, srcURL, mediaType, isEditable } = params;
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
click: () => {
|
||||
browserWindow.webContents.inspectElement(x, y);
|
||||
},
|
||||
label: 'Inspect Element',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
click: () => {
|
||||
browserWindow.webContents.openDevTools();
|
||||
},
|
||||
label: 'Open DevTools',
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
browserWindow.webContents.reload();
|
||||
},
|
||||
label: 'Reload',
|
||||
},
|
||||
]);
|
||||
|
||||
menu.popup({ window: browserWindow });
|
||||
// Use the platform menu system with full context data
|
||||
this.app.menuManager.showContextMenu('default', {
|
||||
isEditable,
|
||||
linkURL: linkURL || undefined,
|
||||
mediaType: mediaType as any,
|
||||
selectionText: selectionText || undefined,
|
||||
srcURL: srcURL || undefined,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
||||
import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, nativeTheme } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { buildDir } from '@/const/dir';
|
||||
import { isDev, isWindows } from '@/const/env';
|
||||
import { isDev, isMac, isWindows } from '@/const/env';
|
||||
import {
|
||||
BACKGROUND_DARK,
|
||||
BACKGROUND_LIGHT,
|
||||
SYMBOL_COLOR_DARK,
|
||||
SYMBOL_COLOR_LIGHT,
|
||||
THEME_CHANGE_DELAY,
|
||||
} from '@/const/theme';
|
||||
} from '../../const/theme';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
const logger = createLogger('core:WindowThemeManager');
|
||||
|
|
@ -40,6 +40,15 @@ export class WindowThemeManager {
|
|||
this.boundHandleThemeChange = this.handleThemeChange.bind(this);
|
||||
}
|
||||
|
||||
private getWindowsTitleBarOverlay(isDarkMode: boolean): WindowsThemeConfig['titleBarOverlay'] {
|
||||
return {
|
||||
color: '#00000000',
|
||||
// Reduce 2px to prevent blocking the container border edge
|
||||
height: TITLE_BAR_HEIGHT - 2,
|
||||
symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
/**
|
||||
|
|
@ -75,10 +84,18 @@ export class WindowThemeManager {
|
|||
/**
|
||||
* Get platform-specific theme configuration for window creation
|
||||
*/
|
||||
getPlatformConfig(): Partial<WindowsThemeConfig> {
|
||||
getPlatformConfig(): Partial<BrowserWindowConstructorOptions> {
|
||||
if (isWindows) {
|
||||
return this.getWindowsConfig(this.isDarkMode);
|
||||
}
|
||||
if (isMac) {
|
||||
// Calculate traffic light position to center vertically in title bar
|
||||
// Traffic light buttons are approximately 12px tall
|
||||
const trafficLightY = Math.round((TITLE_BAR_HEIGHT - 12) / 2);
|
||||
return {
|
||||
trafficLightPosition: { x: 12, y: trafficLightY },
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -89,12 +106,7 @@ export class WindowThemeManager {
|
|||
return {
|
||||
backgroundColor: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
|
||||
icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
|
||||
titleBarOverlay: {
|
||||
color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
|
||||
// Reduce 2px to prevent blocking the container border edge
|
||||
height: TITLE_BAR_HEIGHT - 2,
|
||||
symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
|
||||
},
|
||||
titleBarOverlay: this.getWindowsTitleBarOverlay(isDarkMode),
|
||||
titleBarStyle: 'hidden',
|
||||
};
|
||||
}
|
||||
|
|
@ -123,11 +135,41 @@ export class WindowThemeManager {
|
|||
logger.debug(`[${this.identifier}] App theme mode changed, reapplying visual effects.`);
|
||||
setTimeout(() => {
|
||||
this.applyVisualEffects();
|
||||
this.applyWindowsTitleBarOverlay();
|
||||
}, THEME_CHANGE_DELAY);
|
||||
}
|
||||
|
||||
// ==================== Visual Effects ====================
|
||||
|
||||
private resolveWindowsIsDarkModeFromElectron(): boolean {
|
||||
if (nativeTheme.themeSource === 'dark') return true;
|
||||
if (nativeTheme.themeSource === 'light') return false;
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Windows title bar overlay based on Electron theme mode.
|
||||
* Mirror the structure of `applyVisualEffects`, but only updates title bar overlay.
|
||||
*/
|
||||
private applyWindowsTitleBarOverlay(): void {
|
||||
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;
|
||||
|
||||
logger.debug(`[${this.identifier}] Applying Windows title bar overlay`);
|
||||
const isDarkMode = this.resolveWindowsIsDarkModeFromElectron();
|
||||
|
||||
try {
|
||||
if (!isWindows) return;
|
||||
|
||||
this.browserWindow.setTitleBarOverlay(this.getWindowsTitleBarOverlay(isDarkMode));
|
||||
|
||||
logger.debug(
|
||||
`[${this.identifier}] Windows title bar overlay applied successfully (dark mode: ${isDarkMode})`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`[${this.identifier}] Failed to apply Windows title bar overlay:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply visual effects based on current theme
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ vi.mock('@/const/env', () => ({
|
|||
isWindows: true,
|
||||
}));
|
||||
|
||||
vi.mock('@/const/theme', () => ({
|
||||
vi.mock('../../../const/theme', () => ({
|
||||
BACKGROUND_DARK: '#1a1a1a',
|
||||
BACKGROUND_LIGHT: '#ffffff',
|
||||
SYMBOL_COLOR_DARK: '#ffffff',
|
||||
|
|
@ -332,7 +332,7 @@ describe('Browser', () => {
|
|||
expect.objectContaining({
|
||||
backgroundColor: '#1a1a1a',
|
||||
titleBarOverlay: expect.objectContaining({
|
||||
color: '#1a1a1a',
|
||||
color: '#00000000',
|
||||
symbolColor: '#ffffff',
|
||||
}),
|
||||
}),
|
||||
|
|
@ -346,7 +346,7 @@ describe('Browser', () => {
|
|||
expect.objectContaining({
|
||||
backgroundColor: '#ffffff',
|
||||
titleBarOverlay: expect.objectContaining({
|
||||
color: '#ffffff',
|
||||
color: '#00000000',
|
||||
symbolColor: '#000000',
|
||||
}),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ vi.mock('@lobechat/desktop-bridge', () => ({
|
|||
TITLE_BAR_HEIGHT: 38,
|
||||
}));
|
||||
|
||||
vi.mock('@/const/theme', () => ({
|
||||
vi.mock('../../../const/theme', () => ({
|
||||
BACKGROUND_DARK: '#1a1a1a',
|
||||
BACKGROUND_LIGHT: '#ffffff',
|
||||
SYMBOL_COLOR_DARK: '#ffffff',
|
||||
|
|
@ -91,7 +91,7 @@ describe('WindowThemeManager', () => {
|
|||
backgroundColor: '#1a1a1a',
|
||||
icon: undefined,
|
||||
titleBarOverlay: {
|
||||
color: '#1a1a1a',
|
||||
color: '#00000000',
|
||||
height: 36,
|
||||
symbolColor: '#ffffff',
|
||||
},
|
||||
|
|
@ -108,7 +108,7 @@ describe('WindowThemeManager', () => {
|
|||
backgroundColor: '#ffffff',
|
||||
icon: undefined,
|
||||
titleBarOverlay: {
|
||||
color: '#ffffff',
|
||||
color: '#00000000',
|
||||
height: 36,
|
||||
symbolColor: '#000000',
|
||||
},
|
||||
|
|
@ -185,7 +185,7 @@ describe('WindowThemeManager', () => {
|
|||
|
||||
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
|
||||
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
|
||||
color: '#1a1a1a',
|
||||
color: '#00000000',
|
||||
height: 36,
|
||||
symbolColor: '#ffffff',
|
||||
});
|
||||
|
|
@ -197,7 +197,7 @@ describe('WindowThemeManager', () => {
|
|||
|
||||
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#ffffff');
|
||||
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
|
||||
color: '#ffffff',
|
||||
color: '#00000000',
|
||||
height: 36,
|
||||
symbolColor: '#000000',
|
||||
});
|
||||
|
|
|
|||
31
apps/desktop/src/main/global.d.ts
vendored
31
apps/desktop/src/main/global.d.ts
vendored
|
|
@ -1,3 +1,34 @@
|
|||
import 'vite/client';
|
||||
|
||||
/**
|
||||
* `node-mac-permissions` is a macOS-only native module.
|
||||
*
|
||||
* In Windows/Linux environments the dependency may be omitted (installed as an optional dependency),
|
||||
* but we still need a module declaration so TypeScript can compile.
|
||||
*/
|
||||
declare module 'node-mac-permissions' {
|
||||
export type AuthStatus = 'authorized' | 'denied' | 'not determined' | 'restricted';
|
||||
|
||||
export type AuthType =
|
||||
| 'accessibility'
|
||||
| 'calendar'
|
||||
| 'camera'
|
||||
| 'contacts'
|
||||
| 'full-disk-access'
|
||||
| 'input-monitoring'
|
||||
| 'location'
|
||||
| 'microphone'
|
||||
| 'reminders'
|
||||
| 'screen'
|
||||
| 'speech-recognition';
|
||||
|
||||
export function getAuthStatus(type: AuthType): AuthStatus;
|
||||
|
||||
export function askForAccessibilityAccess(): void;
|
||||
export function askForMicrophoneAccess(): Promise<AuthStatus>;
|
||||
export function askForCameraAccess(): Promise<AuthStatus>;
|
||||
export function askForScreenCaptureAccess(openPreferences?: boolean): void;
|
||||
export function askForFullDiskAccess(): void;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
const menu = {
|
||||
'common.checkUpdates': 'Check for updates...',
|
||||
'context.copyImage': 'Copy Image',
|
||||
'context.copyImageAddress': 'Copy Image Address',
|
||||
'context.copyLink': 'Copy Link',
|
||||
'context.inspectElement': 'Inspect Element',
|
||||
'context.openLink': 'Open Link',
|
||||
'context.saveImage': 'Save Image',
|
||||
'context.saveImageAs': 'Save Image As…',
|
||||
'context.searchWithGoogle': 'Search with Google',
|
||||
'dev.devPanel': 'Developer Panel',
|
||||
'dev.devTools': 'Developer Tools',
|
||||
'dev.forceReload': 'Force Reload',
|
||||
|
|
@ -24,6 +32,7 @@ const menu = {
|
|||
'edit.copy': 'Copy',
|
||||
'edit.cut': 'Cut',
|
||||
'edit.delete': 'Delete',
|
||||
'edit.lookUp': 'Look Up',
|
||||
'edit.paste': 'Paste',
|
||||
'edit.redo': 'Redo',
|
||||
'edit.selectAll': 'Select All',
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ describe('LinuxMenu', () => {
|
|||
});
|
||||
|
||||
it('should pass data to context menu', () => {
|
||||
const data = { selection: 'text' };
|
||||
const data = { selectionText: 'test selection', x: 100, y: 200 };
|
||||
linuxMenu.buildContextMenu('chat', data);
|
||||
|
||||
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Menu, MenuItemConstructorOptions, app, dialog, shell } from 'electron';
|
||||
/* eslint-disable unicorn/no-array-push-push */
|
||||
import { Menu, MenuItemConstructorOptions, app, clipboard, dialog, shell } from 'electron';
|
||||
|
||||
import { isDev } from '@/const/env';
|
||||
|
||||
import type { IMenuPlatform, MenuOptions } from '../types';
|
||||
import type { ContextMenuData, IMenuPlatform, MenuOptions } from '../types';
|
||||
import { BaseMenuPlatform } from './BaseMenuPlatform';
|
||||
|
||||
export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
|
||||
|
|
@ -16,7 +17,7 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
return this.appMenu;
|
||||
}
|
||||
|
||||
buildContextMenu(type: string, data?: any): Menu {
|
||||
buildContextMenu(type: string, data?: ContextMenuData): Menu {
|
||||
let template: MenuItemConstructorOptions[];
|
||||
switch (type) {
|
||||
case 'chat': {
|
||||
|
|
@ -28,7 +29,7 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
template = this.getDefaultContextMenuTemplate();
|
||||
template = this.getDefaultContextMenuTemplate(data);
|
||||
}
|
||||
}
|
||||
return Menu.buildFromTemplate(template);
|
||||
|
|
@ -198,35 +199,175 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
return template;
|
||||
}
|
||||
|
||||
private getDefaultContextMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
private getDefaultContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
const hasLink = Boolean(data?.linkURL);
|
||||
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Link actions
|
||||
if (hasLink) {
|
||||
template.push({
|
||||
click: () => shell.openExternal(data!.linkURL!),
|
||||
label: t('context.openLink'),
|
||||
});
|
||||
template.push({
|
||||
click: () => clipboard.writeText(data!.linkURL!),
|
||||
label: t('context.copyLink'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Image actions
|
||||
if (hasImage) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.downloadURL(data!.srcURL!);
|
||||
},
|
||||
label: t('context.saveImage'),
|
||||
});
|
||||
template.push({
|
||||
click: () => {
|
||||
clipboard.writeText(data!.srcURL!);
|
||||
},
|
||||
label: t('context.copyImageAddress'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions
|
||||
template.push(
|
||||
{ label: t('edit.cut'), role: 'cut' },
|
||||
{ label: t('edit.copy'), role: 'copy' },
|
||||
{ label: t('edit.paste'), role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.selectAll'), role: 'selectAll' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private getChatContextMenuTemplate(data?: any): MenuItemConstructorOptions[] {
|
||||
console.log(data);
|
||||
private getChatContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
const hasLink = Boolean(data?.linkURL);
|
||||
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Link actions
|
||||
if (hasLink) {
|
||||
template.push({
|
||||
click: () => shell.openExternal(data!.linkURL!),
|
||||
label: t('context.openLink'),
|
||||
});
|
||||
template.push({
|
||||
click: () => clipboard.writeText(data!.linkURL!),
|
||||
label: t('context.copyLink'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Image actions
|
||||
if (hasImage) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.downloadURL(data!.srcURL!);
|
||||
},
|
||||
label: t('context.saveImage'),
|
||||
});
|
||||
template.push({
|
||||
click: () => {
|
||||
clipboard.writeText(data!.srcURL!);
|
||||
},
|
||||
label: t('context.copyImageAddress'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions for chat
|
||||
template.push(
|
||||
{ accelerator: 'Ctrl+C', label: t('edit.copy'), role: 'copy' },
|
||||
{ accelerator: 'Ctrl+V', label: t('edit.paste'), role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.selectAll'), role: 'selectAll' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private getEditorContextMenuTemplate(_data?: any): MenuItemConstructorOptions[] {
|
||||
private getEditorContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions for editor
|
||||
template.push(
|
||||
{ accelerator: 'Ctrl+X', label: t('edit.cut'), role: 'cut' },
|
||||
{ accelerator: 'Ctrl+C', label: t('edit.copy'), role: 'copy' },
|
||||
{ accelerator: 'Ctrl+V', label: t('edit.paste'), role: 'paste' },
|
||||
|
|
@ -234,7 +375,21 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
{ accelerator: 'Ctrl+A', label: t('edit.selectAll'), role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.delete'), role: 'delete' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private getTrayMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ describe('MacOSMenu', () => {
|
|||
});
|
||||
|
||||
it('should pass data to chat context menu', () => {
|
||||
const data = { messageId: '123' };
|
||||
const data = { selectionText: 'test selection', x: 100, y: 200 };
|
||||
macOSMenu.buildContextMenu('chat', data);
|
||||
|
||||
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { Menu, MenuItemConstructorOptions, app, shell } from 'electron';
|
||||
/* eslint-disable unicorn/no-array-push-push */
|
||||
import { Menu, MenuItemConstructorOptions, app, clipboard, shell } from 'electron';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { isDev } from '@/const/env';
|
||||
import NotificationCtr from '@/controllers/NotificationCtr';
|
||||
import SystemController from '@/controllers/SystemCtr';
|
||||
|
||||
import type { IMenuPlatform, MenuOptions } from '../types';
|
||||
import type { ContextMenuData, IMenuPlatform, MenuOptions } from '../types';
|
||||
import { BaseMenuPlatform } from './BaseMenuPlatform';
|
||||
|
||||
export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
||||
|
|
@ -22,7 +23,7 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
return this.appMenu;
|
||||
}
|
||||
|
||||
buildContextMenu(type: string, data?: any): Menu {
|
||||
buildContextMenu(type: string, data?: ContextMenuData): Menu {
|
||||
let template: MenuItemConstructorOptions[];
|
||||
switch (type) {
|
||||
case 'chat': {
|
||||
|
|
@ -34,7 +35,7 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
template = this.getDefaultContextMenuTemplate();
|
||||
template = this.getDefaultContextMenuTemplate(data);
|
||||
}
|
||||
}
|
||||
return Menu.buildFromTemplate(template);
|
||||
|
|
@ -370,35 +371,210 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
return template;
|
||||
}
|
||||
|
||||
private getDefaultContextMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
private getDefaultContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
const hasLink = Boolean(data?.linkURL);
|
||||
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Look Up (macOS only) - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.showDefinitionForSelection();
|
||||
},
|
||||
label: t('edit.lookUp'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Link actions
|
||||
if (hasLink) {
|
||||
template.push({
|
||||
click: () => shell.openExternal(data!.linkURL!),
|
||||
label: t('context.openLink'),
|
||||
});
|
||||
template.push({
|
||||
click: () => clipboard.writeText(data!.linkURL!),
|
||||
label: t('context.copyLink'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Image actions
|
||||
if (hasImage) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.downloadURL(data!.srcURL!);
|
||||
},
|
||||
label: t('context.saveImage'),
|
||||
});
|
||||
template.push({
|
||||
click: () => {
|
||||
clipboard.writeText(data!.srcURL!);
|
||||
},
|
||||
label: t('context.copyImageAddress'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions
|
||||
template.push(
|
||||
{ label: t('edit.cut'), role: 'cut' },
|
||||
{ label: t('edit.copy'), role: 'copy' },
|
||||
{ label: t('edit.paste'), role: 'paste' },
|
||||
{ label: t('edit.selectAll'), role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private getChatContextMenuTemplate(data?: any): MenuItemConstructorOptions[] {
|
||||
console.log(data);
|
||||
private getChatContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
const hasLink = Boolean(data?.linkURL);
|
||||
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Look Up (macOS only) - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.showDefinitionForSelection();
|
||||
},
|
||||
label: t('edit.lookUp'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Link actions
|
||||
if (hasLink) {
|
||||
template.push({
|
||||
click: () => shell.openExternal(data!.linkURL!),
|
||||
label: t('context.openLink'),
|
||||
});
|
||||
template.push({
|
||||
click: () => clipboard.writeText(data!.linkURL!),
|
||||
label: t('context.copyLink'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Image actions
|
||||
if (hasImage) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.downloadURL(data!.srcURL!);
|
||||
},
|
||||
label: t('context.saveImage'),
|
||||
});
|
||||
template.push({
|
||||
click: () => {
|
||||
clipboard.writeText(data!.srcURL!);
|
||||
},
|
||||
label: t('context.copyImageAddress'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions for chat (copy/paste focused)
|
||||
template.push(
|
||||
{ accelerator: 'Command+C', label: t('edit.copy'), role: 'copy' },
|
||||
{ accelerator: 'Command+V', label: t('edit.paste'), role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.selectAll'), role: 'selectAll' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private getEditorContextMenuTemplate(_data?: any): MenuItemConstructorOptions[] {
|
||||
private getEditorContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Look Up (macOS only) - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.showDefinitionForSelection();
|
||||
},
|
||||
label: t('edit.lookUp'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions for editor (full edit capabilities)
|
||||
template.push(
|
||||
{ accelerator: 'Command+X', label: t('edit.cut'), role: 'cut' },
|
||||
{ accelerator: 'Command+C', label: t('edit.copy'), role: 'copy' },
|
||||
{ accelerator: 'Command+V', label: t('edit.paste'), role: 'paste' },
|
||||
|
|
@ -406,7 +582,21 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
{ accelerator: 'Command+A', label: t('edit.selectAll'), role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.delete'), role: 'delete' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private getTrayMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ describe('WindowsMenu', () => {
|
|||
});
|
||||
|
||||
it('should pass data to context menu', () => {
|
||||
const data = { text: 'selected text' };
|
||||
const data = { selectionText: 'selected text', x: 100, y: 200 };
|
||||
windowsMenu.buildContextMenu('editor', data);
|
||||
|
||||
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Menu, MenuItemConstructorOptions, app, shell } from 'electron';
|
||||
/* eslint-disable unicorn/no-array-push-push */
|
||||
import { Menu, MenuItemConstructorOptions, app, clipboard, shell } from 'electron';
|
||||
|
||||
import { isDev } from '@/const/env';
|
||||
|
||||
import type { IMenuPlatform, MenuOptions } from '../types';
|
||||
import type { ContextMenuData, IMenuPlatform, MenuOptions } from '../types';
|
||||
import { BaseMenuPlatform } from './BaseMenuPlatform';
|
||||
|
||||
export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
|
||||
|
|
@ -16,7 +17,7 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
return this.appMenu;
|
||||
}
|
||||
|
||||
buildContextMenu(type: string, data?: any): Menu {
|
||||
buildContextMenu(type: string, data?: ContextMenuData): Menu {
|
||||
let template: MenuItemConstructorOptions[];
|
||||
switch (type) {
|
||||
case 'chat': {
|
||||
|
|
@ -28,7 +29,7 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
template = this.getDefaultContextMenuTemplate();
|
||||
template = this.getDefaultContextMenuTemplate(data);
|
||||
}
|
||||
}
|
||||
return Menu.buildFromTemplate(template);
|
||||
|
|
@ -178,35 +179,175 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
return template;
|
||||
}
|
||||
|
||||
private getDefaultContextMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
private getDefaultContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
const hasLink = Boolean(data?.linkURL);
|
||||
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Link actions
|
||||
if (hasLink) {
|
||||
template.push({
|
||||
click: () => shell.openExternal(data!.linkURL!),
|
||||
label: t('context.openLink'),
|
||||
});
|
||||
template.push({
|
||||
click: () => clipboard.writeText(data!.linkURL!),
|
||||
label: t('context.copyLink'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Image actions
|
||||
if (hasImage) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.downloadURL(data!.srcURL!);
|
||||
},
|
||||
label: t('context.saveImage'),
|
||||
});
|
||||
template.push({
|
||||
click: () => {
|
||||
clipboard.writeText(data!.srcURL!);
|
||||
},
|
||||
label: t('context.copyImageAddress'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions
|
||||
template.push(
|
||||
{ label: t('edit.cut'), role: 'cut' },
|
||||
{ label: t('edit.copy'), role: 'copy' },
|
||||
{ label: t('edit.paste'), role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.selectAll'), role: 'selectAll' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private getChatContextMenuTemplate(data?: any): MenuItemConstructorOptions[] {
|
||||
console.log(data);
|
||||
private getChatContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
const hasLink = Boolean(data?.linkURL);
|
||||
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Link actions
|
||||
if (hasLink) {
|
||||
template.push({
|
||||
click: () => shell.openExternal(data!.linkURL!),
|
||||
label: t('context.openLink'),
|
||||
});
|
||||
template.push({
|
||||
click: () => clipboard.writeText(data!.linkURL!),
|
||||
label: t('context.copyLink'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Image actions
|
||||
if (hasImage) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.downloadURL(data!.srcURL!);
|
||||
},
|
||||
label: t('context.saveImage'),
|
||||
});
|
||||
template.push({
|
||||
click: () => {
|
||||
clipboard.writeText(data!.srcURL!);
|
||||
},
|
||||
label: t('context.copyImageAddress'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions for chat
|
||||
template.push(
|
||||
{ accelerator: 'Ctrl+C', label: t('edit.copy'), role: 'copy' },
|
||||
{ accelerator: 'Ctrl+V', label: t('edit.paste'), role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.selectAll'), role: 'selectAll' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private getEditorContextMenuTemplate(_data?: any): MenuItemConstructorOptions[] {
|
||||
private getEditorContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
||||
const t = this.app.i18n.ns('menu');
|
||||
const hasText = Boolean(data?.selectionText?.trim());
|
||||
|
||||
return [
|
||||
const template: MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Search with Google - only when text is selected
|
||||
if (hasText) {
|
||||
template.push({
|
||||
click: () => {
|
||||
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
||||
shell.openExternal(searchUrl);
|
||||
},
|
||||
label: t('context.searchWithGoogle'),
|
||||
});
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
// Standard edit actions for editor
|
||||
template.push(
|
||||
{ accelerator: 'Ctrl+X', label: t('edit.cut'), role: 'cut' },
|
||||
{ accelerator: 'Ctrl+C', label: t('edit.copy'), role: 'copy' },
|
||||
{ accelerator: 'Ctrl+V', label: t('edit.paste'), role: 'paste' },
|
||||
|
|
@ -214,7 +355,21 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|||
{ accelerator: 'Ctrl+A', label: t('edit.selectAll'), role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
{ label: t('edit.delete'), role: 'delete' },
|
||||
];
|
||||
);
|
||||
|
||||
// Inspect Element in dev mode
|
||||
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push({
|
||||
click: () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
||||
},
|
||||
label: t('context.inspectElement'),
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private getTrayMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,27 @@ export interface MenuOptions {
|
|||
// Other possible configuration items
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu data passed from renderer process
|
||||
* Based on Electron's ContextMenuParams
|
||||
*/
|
||||
export interface ContextMenuData {
|
||||
/** Whether the context is editable (input, textarea, contenteditable) */
|
||||
isEditable?: boolean;
|
||||
/** URL of the link if right-clicked on a link */
|
||||
linkURL?: string;
|
||||
/** Media type if right-clicked on media element */
|
||||
mediaType?: 'none' | 'image' | 'audio' | 'video' | 'canvas' | 'file' | 'plugin';
|
||||
/** Selected text */
|
||||
selectionText?: string;
|
||||
/** Source URL of media element (image/video/audio src) */
|
||||
srcURL?: string;
|
||||
/** X coordinate of the context menu */
|
||||
x?: number;
|
||||
/** Y coordinate of the context menu */
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export interface IMenuPlatform {
|
||||
/**
|
||||
* Build and set application menu
|
||||
|
|
@ -14,7 +35,7 @@ export interface IMenuPlatform {
|
|||
/**
|
||||
* Build context menu
|
||||
*/
|
||||
buildContextMenu(type: string, data?: any): Menu;
|
||||
buildContextMenu(type: string, data?: ContextMenuData): Menu;
|
||||
|
||||
/**
|
||||
* Build tray menu
|
||||
|
|
|
|||
|
|
@ -28,15 +28,18 @@ type AuthType =
|
|||
type PermissionType = 'authorized' | 'denied' | 'not determined' | 'restricted';
|
||||
|
||||
// Lazy-loaded module cache
|
||||
// @ts-ignore - node-mac-permissions is optional and only available on macOS
|
||||
let macPermissionsModule: typeof import('node-mac-permissions') | null = null;
|
||||
|
||||
// Test injection override (set via __setMacPermissionsModule for testing)
|
||||
// @ts-ignore - node-mac-permissions is optional and only available on macOS
|
||||
let testModuleOverride: typeof import('node-mac-permissions') | null = null;
|
||||
|
||||
/**
|
||||
* Lazily load the node-mac-permissions module (macOS only)
|
||||
* Returns null on non-macOS platforms
|
||||
*/
|
||||
// @ts-ignore - node-mac-permissions is optional and only available on macOS
|
||||
function getMacPermissionsModule(): typeof import('node-mac-permissions') | null {
|
||||
// Allow test injection to override the module
|
||||
if (testModuleOverride) {
|
||||
|
|
@ -70,6 +73,7 @@ export function __resetMacPermissionsModuleCache(): void {
|
|||
* @internal
|
||||
*/
|
||||
export function __setMacPermissionsModule(
|
||||
// @ts-ignore - node-mac-permissions is optional and only available on macOS
|
||||
module: typeof import('node-mac-permissions') | null,
|
||||
): void {
|
||||
testModuleOverride = module;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
},
|
||||
"include": [
|
||||
"src/main/**/*",
|
||||
"src/main/global.d.ts",
|
||||
"src/preload/**/*",
|
||||
"src/common/**/*",
|
||||
"electron-builder.js",
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@
|
|||
"@lobehub/icons": "^4.0.3",
|
||||
"@lobehub/market-sdk": "0.29.1",
|
||||
"@lobehub/tts": "^4.0.2",
|
||||
"@lobehub/ui": "^4.32.0",
|
||||
"@lobehub/ui": "^4.32.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.3",
|
||||
"@napi-rs/canvas": "^0.1.88",
|
||||
"@neondatabase/serverless": "^1.0.2",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ const lazyFileLoaders: Record<SupportedFileType, LazyLoaderFactory> = {
|
|||
globalThis.DOMPoint = canvas.DOMPoint;
|
||||
globalThis.DOMRect = canvas.DOMRect;
|
||||
globalThis.Path2D = canvas.Path2D;
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('Error importing @napi-rs/canvas:', e);
|
||||
// @napi-rs/canvas not available, pdfjs-dist may fail if DOMMatrix is needed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,7 +459,14 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
|
|||
if (!isDesktop) return;
|
||||
e.preventDefault();
|
||||
const { electronSystemService } = await import('@/services/electron/system');
|
||||
await electronSystemService.showContextMenu('edit');
|
||||
const input = e.target as HTMLInputElement;
|
||||
const selectionText = input.value.slice(
|
||||
input.selectionStart || 0,
|
||||
input.selectionEnd || 0,
|
||||
);
|
||||
await electronSystemService.showContextMenu('editor', {
|
||||
selectionText: selectionText || undefined,
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
|
|
|||
|
|
@ -165,12 +165,10 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|||
e.preventDefault();
|
||||
const { electronSystemService } = await import('@/services/electron/system');
|
||||
|
||||
const selectionValue = editor.getSelectionDocument('markdown') as unknown as string;
|
||||
const hasSelection = !!selectionValue;
|
||||
const selectionText = editor.getSelectionDocument('markdown') as unknown as string;
|
||||
|
||||
await electronSystemService.showContextMenu('editor', {
|
||||
hasSelection,
|
||||
value: selectionValue,
|
||||
selectionText: selectionText || undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -85,12 +85,17 @@ const MessageItem = memo<MessageItemProps>(
|
|||
if (isDesktop) {
|
||||
const { electronSystemService } = await import('@/services/electron/system');
|
||||
|
||||
// Get selected text for context menu features like Look Up and Search
|
||||
const selection = window.getSelection();
|
||||
const selectionText = selection?.toString() || '';
|
||||
|
||||
electronSystemService.showContextMenu('chat', {
|
||||
content: message.content,
|
||||
hasError: !!message.error,
|
||||
messageId: id,
|
||||
// For assistantGroup, we treat it as assistant for context menu purposes
|
||||
role: message.role === 'assistantGroup' ? 'assistant' : message.role,
|
||||
selectionText,
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -3,25 +3,19 @@
|
|||
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
||||
import { Button, Flexbox, Icon, type ModalInstance, createModal } from '@lobehub/ui';
|
||||
import { AlertCircle, LogIn } from 'lucide-react';
|
||||
import { type ReactNode, memo, useCallback, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
|
||||
interface ModalUpdateOptions {
|
||||
closable?: boolean;
|
||||
keyboard?: boolean;
|
||||
maskClosable?: boolean;
|
||||
title?: ReactNode;
|
||||
}
|
||||
|
||||
interface AuthRequiredModalContentProps {
|
||||
onClose: () => void;
|
||||
setModalProps: (props: ModalUpdateOptions) => void;
|
||||
onSigningInChange?: (isSigningIn: boolean) => void;
|
||||
}
|
||||
|
||||
const AuthRequiredModalContent = memo<AuthRequiredModalContentProps>(
|
||||
({ onClose, setModalProps }) => {
|
||||
({ onClose, onSigningInChange }) => {
|
||||
const { t } = useTranslation('auth');
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const isClosingRef = useRef(false);
|
||||
|
|
@ -34,18 +28,9 @@ const AuthRequiredModalContent = memo<AuthRequiredModalContentProps>(
|
|||
s.clearRemoteServerSyncError,
|
||||
]);
|
||||
|
||||
// Update modal props based on signing in state
|
||||
setModalProps({
|
||||
closable: !isSigningIn,
|
||||
keyboard: !isSigningIn,
|
||||
maskClosable: !isSigningIn,
|
||||
title: (
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Icon icon={AlertCircle} />
|
||||
{t('authModal.title')}
|
||||
</Flexbox>
|
||||
),
|
||||
});
|
||||
useEffect(() => {
|
||||
onSigningInChange?.(isSigningIn);
|
||||
}, [isSigningIn, onSigningInChange]);
|
||||
|
||||
// Listen for successful authorization to close the modal
|
||||
useWatchBroadcast('authorizationSuccessful', async () => {
|
||||
|
|
@ -104,30 +89,41 @@ AuthRequiredModalContent.displayName = 'AuthRequiredModalContent';
|
|||
* Hook to create and manage the auth required modal
|
||||
*/
|
||||
export const useAuthRequiredModal = () => {
|
||||
const { t } = useTranslation('auth');
|
||||
const instanceRef = useRef<ModalInstance | null>(null);
|
||||
|
||||
const open = useCallback(() => {
|
||||
// Don't open multiple modals
|
||||
if (instanceRef.current) return;
|
||||
|
||||
const setModalProps = (nextProps: ModalUpdateOptions) => {
|
||||
instanceRef.current?.update?.(nextProps);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
instanceRef.current?.close();
|
||||
instanceRef.current = null;
|
||||
};
|
||||
|
||||
const handleSigningInChange = (isSigningIn: boolean) => {
|
||||
instanceRef.current?.update?.({
|
||||
closable: !isSigningIn,
|
||||
keyboard: !isSigningIn,
|
||||
maskClosable: !isSigningIn,
|
||||
});
|
||||
};
|
||||
|
||||
instanceRef.current = createModal({
|
||||
children: <AuthRequiredModalContent onClose={handleClose} setModalProps={setModalProps} />,
|
||||
children: (
|
||||
<AuthRequiredModalContent onClose={handleClose} onSigningInChange={handleSigningInChange} />
|
||||
),
|
||||
closable: false,
|
||||
footer: null,
|
||||
keyboard: false,
|
||||
maskClosable: false,
|
||||
title: '',
|
||||
title: (
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Icon icon={AlertCircle} />
|
||||
{t('authModal.title')}
|
||||
</Flexbox>
|
||||
),
|
||||
});
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
return { open };
|
||||
};
|
||||
|
|
@ -138,8 +134,12 @@ export const useAuthRequiredModal = () => {
|
|||
const AuthRequiredModal = memo(() => {
|
||||
const { open } = useAuthRequiredModal();
|
||||
|
||||
// Listen for IPC event to open the modal
|
||||
const pathname = useLocation().pathname;
|
||||
useWatchBroadcast('authorizationRequired', () => {
|
||||
if (useElectronStore.getState().isConnectionDrawerOpen) return;
|
||||
|
||||
if (pathname.includes('/desktop-onboarding')) return;
|
||||
|
||||
open();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Center, Flexbox } from '@lobehub/ui';
|
||||
import { Drawer } from 'antd';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import LoginStep from '@/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep';
|
||||
import { BrandTextLoading } from '@/components/Loading';
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
import { isMacOS } from '@/utils/platform';
|
||||
|
||||
import RemoteStatus from './RemoteStatus';
|
||||
|
|
@ -23,17 +25,20 @@ const styles = createStaticStyles(({ css }) => {
|
|||
});
|
||||
|
||||
const Connection = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isOpen, setConnectionDrawerOpen] = useElectronStore((s) => [
|
||||
s.isConnectionDrawerOpen,
|
||||
s.setConnectionDrawerOpen,
|
||||
]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
setConnectionDrawerOpen(false);
|
||||
}, [setConnectionDrawerOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RemoteStatus
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
setConnectionDrawerOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Drawer
|
||||
|
|
@ -47,11 +52,19 @@ const Connection = () => {
|
|||
}}
|
||||
styles={{ body: { padding: 0 }, header: { padding: 0 } }}
|
||||
>
|
||||
<Center style={{ height: '100%', overflow: 'auto', padding: 24 }}>
|
||||
<Flexbox style={{ maxWidth: 560, width: '100%' }}>
|
||||
<LoginStep onBack={handleClose} onNext={handleClose} />
|
||||
</Flexbox>
|
||||
</Center>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Center style={{ height: '100%' }}>
|
||||
<BrandTextLoading debugId="Connection" />
|
||||
</Center>
|
||||
}
|
||||
>
|
||||
<Center style={{ height: '100%', overflow: 'auto', padding: 24 }}>
|
||||
<Flexbox style={{ maxWidth: 560, width: '100%' }}>
|
||||
<LoginStep onBack={handleClose} onNext={handleClose} />
|
||||
</Flexbox>
|
||||
</Center>
|
||||
</Suspense>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import { type FC } from 'react';
|
|||
import { ProductLogo } from '@/components/Branding/ProductLogo';
|
||||
import { electronStylish } from '@/styles/electron';
|
||||
|
||||
import { useWatchThemeUpdate } from '../system/useWatchThemeUpdate';
|
||||
|
||||
/**
|
||||
* A simple, minimal TitleBar for Electron windows.
|
||||
* Provides draggable area without business logic (navigation, updates, etc.)
|
||||
* Use this for secondary windows like onboarding, settings, etc.
|
||||
*/
|
||||
const SimpleTitleBar: FC = () => {
|
||||
useWatchThemeUpdate();
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type { ElectronStore } from '../store';
|
|||
// ======== Action Interface ======== //
|
||||
|
||||
export interface ElectronAppAction {
|
||||
setConnectionDrawerOpen: (isOpen: boolean) => void;
|
||||
updateElectronAppState: (state: ElectronAppState) => void;
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +33,10 @@ export const createElectronAppSlice: StateCreator<
|
|||
[],
|
||||
ElectronAppAction
|
||||
> = (set, get) => ({
|
||||
setConnectionDrawerOpen: (isOpen: boolean) => {
|
||||
set({ isConnectionDrawerOpen: isOpen }, false, 'setConnectionDrawerOpen');
|
||||
},
|
||||
|
||||
updateElectronAppState: (state: ElectronAppState) => {
|
||||
const prevState = get().appState;
|
||||
set({ appState: merge(prevState, state) });
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export const remoteSyncSlice: StateCreator<
|
|||
|
||||
set({ dataSyncConfig: data, isInitRemoteServerConfig: true });
|
||||
},
|
||||
suspense: false,
|
||||
},
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface ElectronState extends NavigationHistoryState {
|
|||
desktopHotkeys: Record<string, string>;
|
||||
isAppStateInit?: boolean;
|
||||
isConnectingServer?: boolean;
|
||||
isConnectionDrawerOpen?: boolean;
|
||||
isDesktopHotkeysInit: boolean;
|
||||
isInitRemoteServerConfig: boolean;
|
||||
isSyncActive?: boolean;
|
||||
|
|
@ -40,6 +41,7 @@ export const initialState: ElectronState = {
|
|||
desktopHotkeys: {},
|
||||
isAppStateInit: false,
|
||||
isConnectingServer: false,
|
||||
isConnectionDrawerOpen: false,
|
||||
isDesktopHotkeysInit: false,
|
||||
isInitRemoteServerConfig: false,
|
||||
isSyncActive: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue