Files
gnome-jarvis/apps/gnome-extension/[email protected]/access-settings.js
T
snxraven af9246151f
Rolling release / release (push) Failing after 1m50s
Updates
2026-09-14 11:31:41 -04:00

77 lines
4.9 KiB
JavaScript

import Adw from 'gi://Adw';
import Gtk from 'gi://Gtk';
import GLib from 'gi://GLib';
import { daemonCall } from './settings-editor.js';
function accessLabel(status, desktop = false) {
if (!status.active) return 'Off';
if (desktop && status.steps_used >= status.steps_max) return 'Action limit reached · Save and Allow to renew';
if (!status.backend || status.backend === 'none') return 'Waiting for permission or device…';
const seconds = Math.max(0, Math.ceil((status.grant_expires_at - Date.now()) / 1000));
return `${desktop ? (status.mode === 'observe' ? 'View only' : 'View and control') : 'On'} · ${Math.ceil(seconds / 60)} min left${desktop ? ` · ${Math.max(0, status.steps_max - status.steps_used)} actions left` : ''}`;
}
export function accessSettingsPage(editor, window) {
const intro = new Adw.PreferencesGroup({ title: 'Give the agent access', description: 'Choose desktop and camera access below, then Save and Allow. This saves your settings and starts temporary access in one step. File and vault permissions stay saved until you change them.' });
const row = new Adw.ActionRow({ title: 'Apply access choices', subtitle: 'Save only updates settings. Save and Allow also starts temporary access.', use_markup: false });
editor.statusRows.push(row);
const allow = new Gtk.Button({ label: 'Save & Allow', valign: Gtk.Align.CENTER }); allow.add_css_class('suggested-action');
const stop = new Gtk.Button({ label: 'Stop desktop & camera', valign: Gtk.Align.CENTER }); stop.add_css_class('destructive-action');
const save = new Gtk.Button({ label: 'Save only', valign: Gtk.Align.CENTER });
row.add_suffix(save); row.add_suffix(allow); intro.add(row);
const live = new Adw.ActionRow({ title: 'Current access', subtitle: 'Connecting…', use_markup: false }); live.add_suffix(stop); intro.add(live);
let refreshing = false;
let generation = 0;
const refresh = async () => {
if (refreshing) return;
refreshing = true;
try {
const results = await Promise.all([daemonCall('ComputerStatus'), daemonCall('WebcamStatus')]);
live.subtitle = `Desktop: ${accessLabel(JSON.parse(results[0][0]), true)}\nCamera: ${accessLabel(JSON.parse(results[1][0]))}`;
} catch (error) { live.subtitle = `Agent unavailable: ${error.message}`; }
finally { refreshing = false; }
};
save.connect('clicked', async () => {
save.sensitive = false; allow.sensitive = false;
try { await editor.apply(); row.subtitle = editor.message; }
finally { save.sensitive = true; allow.sensitive = true; await refresh(); }
});
allow.connect('clicked', async () => {
const current = ++generation;
const selected = { desktop: editor.values.computerMode !== 'off', camera: editor.values.webcamEnabled };
allow.sensitive = false; save.sensitive = false;
try {
row.subtitle = 'Saving and applying access choices…';
if (!await editor.apply()) throw new Error(editor.message || 'Settings could not be applied');
if (current !== generation) return;
const calls = [];
if (selected.desktop) calls.push(['Desktop', daemonCall('ComputerGrant', '(b)', [false])]);
else calls.push(['Desktop', daemonCall('ComputerRevoke')]);
if (selected.camera) calls.push(['Camera', daemonCall('WebcamGrant')]);
else calls.push(['Camera', daemonCall('WebcamRevoke')]);
const results = await Promise.allSettled(calls.map(([, call]) => call));
if (current !== generation) return;
const errors = results.flatMap((result, index) => result.status === 'rejected' ? [`${calls[index][0]}: ${result.reason.message}`] : []);
row.subtitle = errors.length ? errors.join(' · ') : !selected.desktop && !selected.camera ? 'Settings saved. Desktop and camera are off.' : 'Access requested. Complete any GNOME prompt; current access is shown below.';
} catch (error) { if (current === generation) row.subtitle = error.message; }
finally { allow.sensitive = true; save.sensitive = true; await refresh(); }
});
stop.connect('clicked', async () => {
generation++;
stop.sensitive = false;
try {
const results = await Promise.allSettled([daemonCall('ComputerRevoke'), daemonCall('WebcamRevoke')]);
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason.message);
row.subtitle = errors.length ? `Could not stop all access: ${errors.join(' · ')}` : 'Desktop and camera stopped. Saved file and vault permissions are unchanged.';
} finally { stop.sensitive = true; await refresh(); }
});
const page = editor.page(window, 'Access', ['Temporary access', 'Files and memory', 'Access limits'], 'security-high-symbolic', false, intro, { controls: false });
let timer = 0;
page.connect('map', () => {
refresh();
if (!timer) timer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, () => { refresh(); return GLib.SOURCE_CONTINUE; });
});
page.connect('unmap', () => { if (timer) GLib.Source.remove(timer); timer = 0; });
return page;
}