feat(desktop): обёртка Tauri 2 вокруг веб-клиента
Первый инкремент Фазы 6 (docs/client-tauri.md): окно с тем же веб-клиентом (по умолчанию https://gl.mhspx.su, адрес настраивается), трей с меню и индикатором непрочитанных, нативные уведомления, глобальный push-to-talk и переключение микрофона/deafen, автозапуск, single-instance, сохранение геометрии окна, deep links glchat://…, автообновление по подписанному манифесту. Связь страницы с обёрткой — через платформенный адаптер (web/src/lib/platform.ts) и минимальный типизированный набор команд; в веб-клиенте появились системные уведомления об упоминаниях и личных сообщениях и переключатель в настройках. В web/src/stores/gateway.ts вместе с вызовом уведомления лежат правки по дефектам realtime (READY и сессии) из параллельной волны: файл коммитится целиком по договорённости с координатором.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// Страница настроек desktop-обёртки.
|
||||
//
|
||||
// Локальная страница (tauri://localhost) внутри главного окна: команды обёртки
|
||||
// вызываются через встроенный IPC Tauri. Ничего, кроме настроек платформы,
|
||||
// здесь нет — настройки клиента живут в самом клиенте.
|
||||
|
||||
/** Вызов команды обёртки: тонкая обёртка над IPC Tauri v2. */
|
||||
const invoke = (command, args) => window.__TAURI_INTERNALS__.invoke(command, args);
|
||||
|
||||
/** Варианты комбинаций: пустая строка — «выключено». */
|
||||
const OFF = { value: '', label: 'Выключено' };
|
||||
|
||||
const PTT_OPTIONS = [
|
||||
OFF,
|
||||
{ value: 'CommandOrControl+Shift+Space', label: 'Cmd/Ctrl + Shift + Пробел' },
|
||||
{ value: 'CommandOrControl+Shift+M', label: 'Cmd/Ctrl + Shift + M' },
|
||||
{ value: 'CommandOrControl+Shift+V', label: 'Cmd/Ctrl + Shift + V' },
|
||||
{ value: 'CommandOrControl+Shift+B', label: 'Cmd/Ctrl + Shift + B' },
|
||||
{ value: 'Control+Shift+Space', label: 'Ctrl + Shift + Пробел' },
|
||||
];
|
||||
|
||||
const MUTE_OPTIONS = [
|
||||
OFF,
|
||||
{ value: 'CommandOrControl+Shift+M', label: 'Cmd/Ctrl + Shift + M' },
|
||||
{ value: 'CommandOrControl+Shift+U', label: 'Cmd/Ctrl + Shift + U' },
|
||||
{ value: 'Control+Shift+M', label: 'Ctrl + Shift + M' },
|
||||
];
|
||||
|
||||
const DEAFEN_OPTIONS = [
|
||||
OFF,
|
||||
{ value: 'CommandOrControl+Shift+D', label: 'Cmd/Ctrl + Shift + D' },
|
||||
{ value: 'CommandOrControl+Shift+F', label: 'Cmd/Ctrl + Shift + F' },
|
||||
{ value: 'Control+Shift+D', label: 'Ctrl + Shift + D' },
|
||||
];
|
||||
|
||||
/** Поля формы и соответствующие настройки. */
|
||||
const fields = {
|
||||
instanceUrl: document.getElementById('instance-url'),
|
||||
autostart: document.getElementById('autostart'),
|
||||
closeToTray: document.getElementById('close-to-tray'),
|
||||
notifications: document.getElementById('notifications'),
|
||||
pttShortcut: document.getElementById('ptt-shortcut'),
|
||||
muteShortcut: document.getElementById('mute-shortcut'),
|
||||
deafenShortcut: document.getElementById('deafen-shortcut'),
|
||||
};
|
||||
|
||||
const status = document.getElementById('status');
|
||||
const version = document.getElementById('version');
|
||||
const updatesHint = document.getElementById('updates-hint');
|
||||
|
||||
/** Заполняет выпадающий список комбинаций. */
|
||||
function fillSelect(select, options, current) {
|
||||
select.replaceChildren();
|
||||
for (const option of options) {
|
||||
const node = document.createElement('option');
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
select.append(node);
|
||||
}
|
||||
// Комбинация могла быть задана вручную (например, прежней версией) — не теряем её.
|
||||
if (current && !options.some((option) => option.value === current)) {
|
||||
const node = document.createElement('option');
|
||||
node.value = current;
|
||||
node.textContent = current;
|
||||
select.append(node);
|
||||
}
|
||||
select.value = current ?? '';
|
||||
}
|
||||
|
||||
/** Показывает состояние сохранения. */
|
||||
function setStatus(text, isError = false) {
|
||||
status.textContent = text;
|
||||
status.classList.toggle('error', isError);
|
||||
}
|
||||
|
||||
/** Переносит настройки в форму. */
|
||||
function render(settings) {
|
||||
fields.instanceUrl.value = settings.instanceUrl ?? '';
|
||||
fields.autostart.checked = Boolean(settings.autostart);
|
||||
fields.closeToTray.checked = Boolean(settings.closeToTray);
|
||||
fields.notifications.checked = Boolean(settings.notifications);
|
||||
fillSelect(fields.pttShortcut, PTT_OPTIONS, settings.pttShortcut);
|
||||
fillSelect(fields.muteShortcut, MUTE_OPTIONS, settings.muteShortcut);
|
||||
fillSelect(fields.deafenShortcut, DEAFEN_OPTIONS, settings.deafenShortcut);
|
||||
}
|
||||
|
||||
/** Собирает настройки из формы. */
|
||||
function collect() {
|
||||
return {
|
||||
instanceUrl: fields.instanceUrl.value.trim(),
|
||||
autostart: fields.autostart.checked,
|
||||
closeToTray: fields.closeToTray.checked,
|
||||
notifications: fields.notifications.checked,
|
||||
pttShortcut: fields.pttShortcut.value,
|
||||
muteShortcut: fields.muteShortcut.value,
|
||||
deafenShortcut: fields.deafenShortcut.value,
|
||||
};
|
||||
}
|
||||
|
||||
/** Сохраняет настройки и, если нужно, открывает клиент. */
|
||||
async function save({ openClient = false } = {}) {
|
||||
setStatus('Сохраняем…');
|
||||
try {
|
||||
const saved = await invoke('desktop_save_settings', { settings: collect() });
|
||||
render(saved);
|
||||
setStatus('Сохранено');
|
||||
if (openClient) {
|
||||
await invoke('desktop_open_instance');
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(String(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('save').addEventListener('click', () => {
|
||||
void save();
|
||||
});
|
||||
|
||||
document.getElementById('open-client').addEventListener('click', () => {
|
||||
void save({ openClient: true });
|
||||
});
|
||||
|
||||
document.getElementById('reload-client').addEventListener('click', async () => {
|
||||
await save();
|
||||
try {
|
||||
await invoke('desktop_reload');
|
||||
} catch (error) {
|
||||
setStatus(String(error), true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('check-updates').addEventListener('click', async () => {
|
||||
updatesHint.textContent = 'Проверяем обновления…';
|
||||
try {
|
||||
await invoke('desktop_check_updates');
|
||||
updatesHint.textContent = 'Проверка завершена: результат показан в системном диалоге.';
|
||||
} catch (error) {
|
||||
updatesHint.textContent = String(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Автосохранение переключателей и списков: отдельная кнопка нужна только для адреса.
|
||||
for (const node of [
|
||||
fields.autostart,
|
||||
fields.closeToTray,
|
||||
fields.notifications,
|
||||
fields.pttShortcut,
|
||||
fields.muteShortcut,
|
||||
fields.deafenShortcut,
|
||||
]) {
|
||||
node.addEventListener('change', () => {
|
||||
void save();
|
||||
});
|
||||
}
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
render(await invoke('desktop_settings'));
|
||||
version.textContent = `Версия обёртки: ${await invoke('desktop_version')}`;
|
||||
} catch (error) {
|
||||
setStatus(`Не удалось прочитать настройки: ${error}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
void start();
|
||||
Reference in New Issue
Block a user