@@ -696,6 +696,45 @@
|
||||
"max": 95,
|
||||
"step": 5
|
||||
},
|
||||
{
|
||||
"key": "webcamEnabled",
|
||||
"title": "Camera access",
|
||||
"group": "Camera",
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
"description": "Off by default. Press Allow now to start a temporary camera grant. Apply this switch if you want the feature remembered."
|
||||
},
|
||||
{
|
||||
"key": "webcamDevice",
|
||||
"title": "Camera device",
|
||||
"group": "Camera",
|
||||
"default": "",
|
||||
"type": "device",
|
||||
"mediaClass": "Video/Source",
|
||||
"description": "System default follows PipeWire. Refresh the list after connecting a camera."
|
||||
},
|
||||
{
|
||||
"key": "webcamGrantMinutes",
|
||||
"title": "Camera grant duration (minutes)",
|
||||
"group": "Camera",
|
||||
"default": 3,
|
||||
"type": "number",
|
||||
"description": "Camera access expires automatically. Changing this revokes an active grant.",
|
||||
"min": 1,
|
||||
"max": 15,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"key": "webcamMaxEdge",
|
||||
"title": "Camera still maximum edge (pixels)",
|
||||
"group": "Camera",
|
||||
"default": 720,
|
||||
"type": "number",
|
||||
"description": "Larger stills preserve detail but take more memory.",
|
||||
"min": 320,
|
||||
"max": 1280,
|
||||
"step": 80
|
||||
},
|
||||
{
|
||||
"key": "modelProfile",
|
||||
"title": "Chat model profile",
|
||||
|
||||
@@ -89,7 +89,7 @@ export class SettingsEditor {
|
||||
const scan = async () => {
|
||||
refresh.sensitive = false;
|
||||
try {
|
||||
const devices = await audioDevices(field.key === 'inputTarget' ? 'Audio/Source' : 'Audio/Sink');
|
||||
const devices = await audioDevices(field.mediaClass || (field.key === 'inputTarget' ? 'Audio/Source' : field.key === 'webcamDevice' ? 'Video/Source' : 'Audio/Sink'));
|
||||
options = [{ value: '', label: 'System default' }, ...devices];
|
||||
if (this.values[field.key] && !options.some(o => o.value === this.values[field.key])) options.push({ value: this.values[field.key], label: `${this.values[field.key]} (not connected)` });
|
||||
updating = true;
|
||||
|
||||
@@ -151,6 +151,56 @@ function grantRow() {
|
||||
return row;
|
||||
}
|
||||
|
||||
function cameraGrantRow() {
|
||||
const row = new Adw.ActionRow({
|
||||
title: 'Camera grant',
|
||||
subtitle: 'Press Allow now so Jarvis can capture a webcam still. This is not desktop ScreenCast.',
|
||||
});
|
||||
const allow = new Gtk.Button({ label: 'Allow now', valign: Gtk.Align.CENTER });
|
||||
allow.add_css_class('suggested-action');
|
||||
const revoke = new Gtk.Button({ label: 'Revoke', valign: Gtk.Align.CENTER });
|
||||
revoke.add_css_class('destructive-action');
|
||||
const refresh = () => {
|
||||
callDaemon('WebcamStatus', null, null, (error, reply) => {
|
||||
if (error) {
|
||||
row.subtitle = 'Jarvis daemon is unavailable. Start jarvisd, then try Allow now.';
|
||||
return;
|
||||
}
|
||||
let status = {};
|
||||
try {
|
||||
const unpacked = reply.deep_unpack?.() ?? reply.unpack?.();
|
||||
const raw = Array.isArray(unpacked) ? unpacked[0] : unpacked;
|
||||
status = JSON.parse(String(raw || '{}'));
|
||||
} catch {}
|
||||
if (status.active) {
|
||||
row.subtitle = 'Active. Jarvis can capture one webcam still per webcam tool call until this grant expires.';
|
||||
} else if (!status.enabled) {
|
||||
row.subtitle = 'Off. Press Allow now to start a camera grant, or enable Camera access and Apply to keep the feature on.';
|
||||
} else {
|
||||
row.subtitle = 'Enabled. Press Allow now so Jarvis can use the webcam for the configured grant duration.';
|
||||
}
|
||||
});
|
||||
};
|
||||
allow.connect('clicked', () => {
|
||||
callDaemon('WebcamGrant', null, null, (error) => {
|
||||
row.subtitle = error ? `Could not grant: ${error.message}` : 'Grant requested. Jarvis can use the camera until you revoke it or the timer ends.';
|
||||
refresh();
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 700, () => { refresh(); return GLib.SOURCE_REMOVE; });
|
||||
});
|
||||
});
|
||||
revoke.connect('clicked', () => {
|
||||
callDaemon('WebcamRevoke', null, null, (error) => {
|
||||
row.subtitle = error ? `Could not revoke: ${error.message}` : 'Camera grant revoked.';
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
row.add_suffix(allow);
|
||||
row.add_suffix(revoke);
|
||||
row.activatable_widget = allow;
|
||||
refresh();
|
||||
return row;
|
||||
}
|
||||
|
||||
export function fillSettingsWindow(window, settings, directory) {
|
||||
window.set_title(BRAND.product);
|
||||
window.set_default_size(840, 780);
|
||||
@@ -164,7 +214,7 @@ export function fillSettingsWindow(window, settings, directory) {
|
||||
const listening = editor.page(window, 'Listening', ['Listening', 'Wake and privacy', 'Detection tuning', 'Audio routing'], 'audio-input-microphone-symbolic');
|
||||
window.add(listening);
|
||||
|
||||
const desktop = editor.page(window, 'Desktop', ['Desktop access', 'Desktop images'], 'preferences-desktop-display-symbolic');
|
||||
const desktop = editor.page(window, 'Desktop', ['Desktop access', 'Desktop images', 'Camera'], 'preferences-desktop-display-symbolic');
|
||||
const desktopGroup = new Adw.PreferencesGroup({ title: 'Overlay and shortcuts', description: 'These appearance settings take effect immediately.' });
|
||||
desktopGroup.add(strvRow(settings, 'Hotkey', 'hotkey'));
|
||||
desktopGroup.add(accentRow(settings));
|
||||
@@ -172,6 +222,8 @@ export function fillSettingsWindow(window, settings, directory) {
|
||||
desktop.add(desktopGroup);
|
||||
const grantGroup = new Adw.PreferencesGroup({ title: 'Temporary desktop access' });
|
||||
grantGroup.add(grantRow()); desktop.add(grantGroup);
|
||||
const cameraGroup = new Adw.PreferencesGroup({ title: 'Temporary camera access' });
|
||||
cameraGroup.add(cameraGrantRow()); desktop.add(cameraGroup);
|
||||
window.add(desktop);
|
||||
const models = editor.page(window, 'Models', ['Chat model', 'Agent limits'], 'system-run-symbolic');
|
||||
const service = new Adw.PreferencesGroup({ title: 'Apply chat model changes' });
|
||||
|
||||
Reference in New Issue
Block a user