This commit is contained in:
2026-01-17 13:10:10 +01:00
parent 17fad55e9f
commit 3a6d443b3f
25 changed files with 2698 additions and 1 deletions

View File

@@ -0,0 +1,98 @@
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import * as JsonStore from '../storage/jsonStore.js';
export class BackupService {
constructor(extension, settings, secretsStore, clipboardStore) {
this._extension = extension;
this._settings = settings;
this._secretsStore = secretsStore;
this._clipboardStore = clipboardStore;
}
isConfigured() {
const path = this._settings.backupPath;
if (!path || path.trim() === '') {
return false;
}
const file = Gio.File.new_for_path(path);
return file.query_exists(null);
}
getBackupPath() {
return this._settings.backupPath;
}
_formatTimestamp() {
const now = GLib.DateTime.new_now_local();
return now.format('%Y-%m-%d_%H-%M-%S');
}
_ensureBackupDir(backupDir) {
const file = Gio.File.new_for_path(backupDir);
if (!file.query_exists(null)) {
file.make_directory_with_parents(null);
}
GLib.chmod(backupDir, 0o700);
return true;
}
backup() {
const basePath = this._settings.backupPath;
if (!basePath || basePath.trim() === '') {
return { success: false, message: 'Chemin de backup non configuré' };
}
const baseFile = Gio.File.new_for_path(basePath);
if (!baseFile.query_exists(null)) {
return { success: false, message: 'Chemin de backup invalide' };
}
try {
const timestamp = this._formatTimestamp();
const backupDir = GLib.build_filenamev([basePath, timestamp]);
this._ensureBackupDir(backupDir);
// Export settings
const settingsData = {
history_size: this._settings.historySize,
show_passwords: this._settings.showPasswords,
click_mode: this._settings.clickMode,
backup_path: this._settings.backupPath,
};
const settingsPath = GLib.build_filenamev([backupDir, 'settings.json']);
JsonStore.writeJson(settingsPath, settingsData);
JsonStore.setPermissions(settingsPath, 0o600);
// Export clipboard (favorites + history)
const clipboardData = this._clipboardStore.load();
const clipboardPath = GLib.build_filenamev([backupDir, 'clipboard.json']);
JsonStore.writeJson(clipboardPath, clipboardData);
JsonStore.setPermissions(clipboardPath, 0o600);
// Export secrets (already obfuscated, never in clear)
const secretsData = this._secretsStore.loadSecrets();
const secretsPath = GLib.build_filenamev([backupDir, 'secrets.json']);
JsonStore.writeJson(secretsPath, secretsData);
JsonStore.setPermissions(secretsPath, 0o600);
// Create manifest
const manifestData = {
uuid: this._extension.metadata.uuid,
version: this._extension.metadata.version,
timestamp: timestamp,
created_at: GLib.DateTime.new_now_utc().format('%Y-%m-%dT%H:%M:%SZ'),
};
const manifestPath = GLib.build_filenamev([backupDir, 'manifest.json']);
JsonStore.writeJson(manifestPath, manifestData);
JsonStore.setPermissions(manifestPath, 0o600);
return { success: true, message: 'Backup OK', path: backupDir };
} catch (error) {
console.log(`[ClipCoffre] Backup failed: ${error}`);
return { success: false, message: `Backup échoué: ${error.message}` };
}
}
}

View File

@@ -0,0 +1,70 @@
import GLib from 'gi://GLib';
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
const POLL_INTERVAL_MS = 1000; // Vérifie toutes les secondes
export class ClipboardService {
constructor(store) {
this._store = store;
this._clipboard = St.Clipboard.get_default();
this._lastContent = null;
this._pollId = null;
this._startPolling();
}
_startPolling() {
// Récupérer le contenu initial
this._clipboard.get_text(St.ClipboardType.CLIPBOARD, (clipboard, text) => {
this._lastContent = text;
});
// Polling régulier
this._pollId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, POLL_INTERVAL_MS, () => {
this._checkClipboard();
return GLib.SOURCE_CONTINUE;
});
}
_checkClipboard() {
this._clipboard.get_text(St.ClipboardType.CLIPBOARD, (clipboard, text) => {
if (text && text !== this._lastContent) {
this._lastContent = text;
this._store.addItem(text);
}
});
}
state() {
return this._store.load();
}
add(entry) {
if (!entry) return this.state();
const sanitized = entry.trim();
if (!sanitized) return this.state();
this._lastContent = sanitized; // Éviter de l'ajouter 2 fois
const payload = this._store.addItem(sanitized);
return payload;
}
toggleFavorite(id) {
const payload = this._store.toggleFavorite(id);
return payload;
}
copy(content) {
if (!content) return;
this._lastContent = content; // Éviter de l'ajouter 2 fois via polling
this._clipboard.set_text(St.ClipboardType.CLIPBOARD, content);
Main.notify('ClipCoffre', 'Copié dans le presse-papiers');
}
destroy() {
if (this._pollId) {
GLib.source_remove(this._pollId);
this._pollId = null;
}
}
}

View File

@@ -0,0 +1,72 @@
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
export class SecretService {
constructor(store, settings) {
this._store = store;
this._settings = settings;
this._clipboard = St.Clipboard.get_default();
}
get entries() {
const payload = this._store.loadSecrets();
return payload.map(entry => {
const raw = this._store.decodePassword(entry.password);
const display = raw
? this._settings.showPasswords
? raw
: '••••••••'
: '';
return {
id: entry.id,
user: entry.user,
label: entry.label,
rawPassword: raw,
displayPassword: display,
};
});
}
addEntry(user, password, label = '') {
if (!user || !password) return false;
const list = this._store.loadSecrets();
const encoded = this._store.encodePassword(password);
list.unshift({
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
user,
password: encoded,
label,
});
this._store.saveSecrets(list);
return true;
}
deleteEntry(id) {
if (!id) return false;
const list = this._store.loadSecrets();
const filtered = list.filter(entry => entry.id !== id);
this._store.saveSecrets(filtered);
return true;
}
updateEntry(id, user, password, label) {
if (!id) return false;
const list = this._store.loadSecrets();
const index = list.findIndex(entry => entry.id === id);
if (index === -1) return false;
if (user !== undefined) list[index].user = user;
if (label !== undefined) list[index].label = label;
if (password !== undefined) {
list[index].password = this._store.encodePassword(password);
}
this._store.saveSecrets(list);
return true;
}
copyToClipboard(value) {
if (!value) return;
this._clipboard.set_text(St.ClipboardType.CLIPBOARD, value);
Main.notify('ClipCoffre', 'Copié dans le presse-papiers');
}
}

145
src/services/syncService.js Normal file
View File

@@ -0,0 +1,145 @@
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import * as JsonStore from '../storage/jsonStore.js';
const SYNC_SUBFOLDER = 'clipcoffre-secrets';
const SECRETS_FILE = 'secrets.json';
const KEY_FILE = 'key.json';
export class SyncService {
constructor(extension, settings, secretsStore) {
this._extension = extension;
this._settings = settings;
this._secretsStore = secretsStore;
this._dataDir = JsonStore.getDataDir(extension);
}
isConfigured() {
const path = this._settings.syncPath;
if (!path || path.trim() === '') {
return false;
}
const file = Gio.File.new_for_path(path);
return file.query_exists(null);
}
getSyncPath() {
return this._settings.syncPath;
}
_getSyncFolder() {
const basePath = this._settings.syncPath;
return GLib.build_filenamev([basePath, SYNC_SUBFOLDER]);
}
_ensureSyncFolder() {
const syncFolder = this._getSyncFolder();
const file = Gio.File.new_for_path(syncFolder);
if (!file.query_exists(null)) {
file.make_directory_with_parents(null);
}
GLib.chmod(syncFolder, 0o700);
return syncFolder;
}
/**
* Exporte les secrets vers le dossier de sync (dépôt Git)
*/
exportSecrets() {
if (!this.isConfigured()) {
return { success: false, message: 'Chemin de sync non configuré' };
}
try {
const syncFolder = this._ensureSyncFolder();
// Copier secrets.json
const secretsSrc = GLib.build_filenamev([this._dataDir, SECRETS_FILE]);
const secretsDst = GLib.build_filenamev([syncFolder, SECRETS_FILE]);
const srcFile = Gio.File.new_for_path(secretsSrc);
if (srcFile.query_exists(null)) {
const dstFile = Gio.File.new_for_path(secretsDst);
srcFile.copy(dstFile, Gio.FileCopyFlags.OVERWRITE, null, null);
GLib.chmod(secretsDst, 0o600);
}
// Copier key.json
const keySrc = GLib.build_filenamev([this._dataDir, KEY_FILE]);
const keyDst = GLib.build_filenamev([syncFolder, KEY_FILE]);
const keySrcFile = Gio.File.new_for_path(keySrc);
if (keySrcFile.query_exists(null)) {
const keyDstFile = Gio.File.new_for_path(keyDst);
keySrcFile.copy(keyDstFile, Gio.FileCopyFlags.OVERWRITE, null, null);
GLib.chmod(keyDst, 0o600);
}
return {
success: true,
message: 'Secrets exportés',
path: syncFolder
};
} catch (error) {
console.log(`[ClipCoffre] Sync export failed: ${error}`);
return { success: false, message: `Export échoué: ${error.message}` };
}
}
/**
* Importe les secrets depuis le dossier de sync (dépôt Git)
*/
importSecrets() {
if (!this.isConfigured()) {
return { success: false, message: 'Chemin de sync non configuré' };
}
const syncFolder = this._getSyncFolder();
const syncFolderFile = Gio.File.new_for_path(syncFolder);
if (!syncFolderFile.query_exists(null)) {
return { success: false, message: 'Dossier clipcoffre-secrets introuvable' };
}
try {
// Vérifier que les fichiers existent dans le dossier sync
const secretsSrc = GLib.build_filenamev([syncFolder, SECRETS_FILE]);
const keySrc = GLib.build_filenamev([syncFolder, KEY_FILE]);
const secretsSrcFile = Gio.File.new_for_path(secretsSrc);
const keySrcFile = Gio.File.new_for_path(keySrc);
if (!secretsSrcFile.query_exists(null)) {
return { success: false, message: 'secrets.json introuvable dans le dépôt' };
}
if (!keySrcFile.query_exists(null)) {
return { success: false, message: 'key.json introuvable dans le dépôt' };
}
// S'assurer que le dossier data existe
JsonStore.ensureDataDir(this._extension);
// Copier key.json d'abord (nécessaire pour déchiffrer)
const keyDst = GLib.build_filenamev([this._dataDir, KEY_FILE]);
const keyDstFile = Gio.File.new_for_path(keyDst);
keySrcFile.copy(keyDstFile, Gio.FileCopyFlags.OVERWRITE, null, null);
GLib.chmod(keyDst, 0o600);
// Copier secrets.json
const secretsDst = GLib.build_filenamev([this._dataDir, SECRETS_FILE]);
const secretsDstFile = Gio.File.new_for_path(secretsDst);
secretsSrcFile.copy(secretsDstFile, Gio.FileCopyFlags.OVERWRITE, null, null);
GLib.chmod(secretsDst, 0o600);
return {
success: true,
message: 'Secrets importés - Redémarrez l\'extension',
needsReload: true
};
} catch (error) {
console.log(`[ClipCoffre] Sync import failed: ${error}`);
return { success: false, message: `Import échoué: ${error.message}` };
}
}
}

View File

@@ -0,0 +1,56 @@
import GLib from 'gi://GLib';
import * as JsonStore from './jsonStore.js';
function _buildPath(directory, filename) {
return GLib.build_filenamev([directory, filename]);
}
export class ClipboardStore {
constructor(extension, settings) {
this._extension = extension;
this._settings = settings;
this._dataDir = JsonStore.ensureDataDir(extension);
this._filePath = _buildPath(this._dataDir, 'clipboard.json');
}
load() {
const payload = JsonStore.readJson(this._filePath, {
favorites: [],
history: [],
});
return payload;
}
save(payload) {
JsonStore.writeJson(this._filePath, payload);
}
addItem(content) {
const payload = this.load();
const cleaned = payload.history.filter(entry => entry.content !== content);
const item = {
id: `${Date.now()}-${Math.random()}`,
content,
favorite: Boolean(payload.favorites.find(favorite => favorite.content === content)),
};
cleaned.unshift(item);
const limit = Math.max(10, this._settings.historySize);
payload.history = cleaned.slice(0, limit);
payload.favorites = payload.history.filter(entry => entry.favorite);
this.save(payload);
return payload;
}
toggleFavorite(itemId) {
const payload = this.load();
payload.history = payload.history.map(entry => {
if (entry.id === itemId) {
entry.favorite = !entry.favorite;
}
return entry;
});
payload.favorites = payload.history.filter(entry => entry.favorite);
this.save(payload);
return payload;
}
}

70
src/storage/jsonStore.js Normal file
View File

@@ -0,0 +1,70 @@
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
export function getDataDir(extension) {
return GLib.build_filenamev([
GLib.get_user_data_dir(),
'gnome-shell',
'extensions',
extension.metadata.uuid,
'data',
]);
}
export function ensureDataDir(extension) {
const dir = getDataDir(extension);
const file = Gio.File.new_for_path(dir);
if (!file.query_exists(null)) {
file.make_directory_with_parents(null);
}
// Set permissions 0700
GLib.chmod(dir, 0o700);
return dir;
}
export function readJson(filePath, fallback = {}) {
try {
const file = Gio.File.new_for_path(filePath);
if (!file.query_exists(null)) {
return fallback;
}
const [success, contents] = file.load_contents(null);
if (!success) return fallback;
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(contents);
if (!text) return fallback;
return JSON.parse(text);
} catch (error) {
console.log(`[ClipCoffre] Failed to read ${filePath}: ${error}`);
return fallback;
}
}
export function writeJson(filePath, value) {
try {
const text = JSON.stringify(value, null, 2);
const file = Gio.File.new_for_path(filePath);
const parentDir = file.get_parent();
if (parentDir && !parentDir.query_exists(null)) {
parentDir.make_directory_with_parents(null);
}
file.replace_contents(
new TextEncoder().encode(text),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null
);
} catch (error) {
console.log(`[ClipCoffre] Failed to write ${filePath}: ${error}`);
}
}
export function setPermissions(filePath, mode) {
const file = Gio.File.new_for_path(filePath);
if (file.query_exists(null)) {
GLib.chmod(filePath, mode);
}
}

110
src/storage/secretsStore.js Normal file
View File

@@ -0,0 +1,110 @@
import GLib from 'gi://GLib';
import * as JsonStore from './jsonStore.js';
const SECRETS_FILENAME = 'secrets.json';
const KEY_FILENAME = 'key.json';
function _buildPath(directory, filename) {
return GLib.build_filenamev([directory, filename]);
}
function _generateId() {
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
export class SecretsStore {
constructor(extension) {
this._extension = extension;
this._dataDir = JsonStore.ensureDataDir(extension);
this._secretsPath = _buildPath(this._dataDir, SECRETS_FILENAME);
this._keyPath = _buildPath(this._dataDir, KEY_FILENAME);
this._key = this._loadKey();
JsonStore.setPermissions(this._secretsPath, 0o600);
}
_normalize(entry) {
return {
id: entry.id || _generateId(),
user: entry.user || '',
password: entry.password || '',
label: entry.label || '',
};
}
_normalizeCollection(secrets) {
return secrets.map(entry => this._normalize(entry));
}
_loadKey() {
const payload = JsonStore.readJson(this._keyPath, null);
if (payload && payload.key) {
return this._base64ToBytes(payload.key);
}
// Generate new key
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
bytes[i] = Math.floor(Math.random() * 256);
}
const serialized = this._bytesToBase64(bytes);
JsonStore.writeJson(this._keyPath, { key: serialized });
JsonStore.setPermissions(this._keyPath, 0o600);
return bytes;
}
_bytesToBase64(bytes) {
return GLib.base64_encode(bytes);
}
_base64ToBytes(base64) {
const decoded = GLib.base64_decode(base64);
return new Uint8Array(decoded);
}
_xored(bytes) {
const result = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
result[i] = bytes[i] ^ this._key[i % this._key.length];
}
return result;
}
_encode(value) {
const encoder = new TextEncoder();
const buffer = encoder.encode(value || '');
const xored = this._xored(buffer);
return this._bytesToBase64(xored);
}
_decode(value) {
if (!value) return '';
const bytes = this._base64ToBytes(value);
const xored = this._xored(bytes);
const decoder = new TextDecoder();
return decoder.decode(xored);
}
loadSecrets() {
const payload = JsonStore.readJson(this._secretsPath, []);
return this._normalizeCollection(payload);
}
saveSecrets(secrets) {
const normalized = this._normalizeCollection(secrets);
JsonStore.writeJson(this._secretsPath, normalized);
JsonStore.setPermissions(this._secretsPath, 0o600);
}
encodePassword(value) {
return this._encode(value);
}
decodePassword(value) {
try {
return this._decode(value);
} catch (error) {
console.log(`[ClipCoffre] Unable to decode secret: ${error}`);
return '';
}
}
}

78
src/storage/settings.js Normal file
View File

@@ -0,0 +1,78 @@
export class SettingsWrapper {
constructor(settings) {
this._settings = settings;
this._connections = [];
}
get historySize() {
return this._settings.get_int('history-size');
}
set historySize(value) {
this._settings.set_int('history-size', value);
}
get showPasswords() {
return this._settings.get_boolean('show-passwords');
}
set showPasswords(value) {
this._settings.set_boolean('show-passwords', value);
}
get clickMode() {
return this._settings.get_string('click-mode');
}
set clickMode(value) {
this._settings.set_string('click-mode', value);
}
get backupPath() {
return this._settings.get_string('backup-path');
}
set backupPath(value) {
this._settings.set_string('backup-path', value);
}
get syncPath() {
return this._settings.get_string('sync-path');
}
set syncPath(value) {
this._settings.set_string('sync-path', value);
}
get gitUrl() {
return this._settings.get_string('git-url');
}
set gitUrl(value) {
this._settings.set_string('git-url', value);
}
get gitUser() {
return this._settings.get_string('git-user');
}
set gitUser(value) {
this._settings.set_string('git-user', value);
}
connect(signal, callback) {
const id = this._settings.connect(signal, callback);
this._connections.push(id);
return id;
}
disconnect(signalId) {
this._settings.disconnect(signalId);
this._connections = this._connections.filter(id => id !== signalId);
}
destroy() {
this._connections.forEach(id => this._settings.disconnect(id));
this._connections = [];
}
}

195
src/ui/panelButton.js Normal file
View File

@@ -0,0 +1,195 @@
import Clutter from 'gi://Clutter';
import St from 'gi://St';
import GObject from 'gi://GObject';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import { PopupMain } from './popupMain.js';
import { PopupSecrets } from './popupSecrets.js';
import { PopupClipboard } from './popupClipboard.js';
import { PopupSettings } from './popupSettings.js';
import { BackupService } from '../services/backupService.js';
const ICON_NAME = 'dialog-password-symbolic';
const MODE_UNIFIE = 'unifie';
const MODE_SEPARE = 'separe';
export const PanelButton = GObject.registerClass(
class PanelButton extends PanelMenu.Button {
_init(settings, services) {
super._init(0.0, 'ClipCoffre');
this._settings = settings;
this._services = services;
this._currentMode = null;
this._settingsChangedId = null;
this._icon = new St.Icon({
icon_name: ICON_NAME,
style_class: 'system-status-icon clipcoffre-icon',
});
this.add_child(this._icon);
this._buildMenu();
this._settingsChangedId = this._settings.connect(
'changed::click-mode',
this._onModeChanged.bind(this)
);
}
_buildMenu() {
this.menu.removeAll();
this._currentMode = this._settings.clickMode;
if (this._currentMode === MODE_UNIFIE) {
this._buildUnifiedMode();
} else {
this._buildSeparateMode();
}
}
_buildUnifiedMode() {
this._mainPopup = new PopupMain(this._settings, this._services, {
onSettingsChanged: () => this._onSettingsChanged(),
onBackupClick: () => this._doBackup(),
});
this.menu.addMenuItem(this._mainPopup.widget);
}
_buildSeparateMode() {
this._showSecretsView = true;
this._secretsPopup = new PopupSecrets(this._settings, this._services, {
onSettingsClick: () => this._showSettings(),
onBackupClick: () => this._doBackup(),
});
this._clipboardPopup = new PopupClipboard(this._settings, this._services, {
onSettingsClick: () => this._showSettings(),
onBackupClick: () => this._doBackup(),
});
this._settingsPopup = new PopupSettings(this._settings, {
onSave: () => {
this._onSettingsChanged();
this._showSecrets();
},
onCancel: () => this._showSecrets(),
});
this._rebuildSeparateContent();
}
_rebuildSeparateContent() {
this.menu.removeAll();
if (this._showSettingsView) {
const headerItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const headerBox = new St.BoxLayout({
style_class: 'clipcoffre-tabs-header',
x_expand: true,
});
const backBtn = new St.Button({
style_class: 'clipcoffre-back-btn',
child: new St.Icon({
icon_name: 'go-previous-symbolic',
icon_size: 16,
}),
});
backBtn.connect('clicked', () => this._showSecrets());
headerBox.add_child(backBtn);
const title = new St.Label({
text: 'Paramètres',
style_class: 'clipcoffre-title',
x_expand: true,
});
headerBox.add_child(title);
headerItem.add_child(headerBox);
this.menu.addMenuItem(headerItem);
this.menu.addMenuItem(this._settingsPopup.widget);
} else if (this._showSecretsView) {
this._secretsPopup.refresh();
this.menu.addMenuItem(this._secretsPopup.widget);
} else {
this._clipboardPopup.refresh();
this.menu.addMenuItem(this._clipboardPopup.widget);
}
}
_showSecrets() {
this._showSecretsView = true;
this._showSettingsView = false;
this._rebuildSeparateContent();
}
_showClipboard() {
this._showSecretsView = false;
this._showSettingsView = false;
this._rebuildSeparateContent();
}
_showSettings() {
this._showSettingsView = true;
this._settingsPopup.refresh();
this._rebuildSeparateContent();
}
_onModeChanged() {
this._buildMenu();
}
_onSettingsChanged() {
if (this._currentMode !== this._settings.clickMode) {
this._buildMenu();
}
}
_doBackup() {
const backupService = new BackupService(
this._services.secretService._store._extension,
this._settings,
this._services.secretService._store,
this._services.clipboardService._store
);
if (!backupService.isConfigured()) {
Main.notify('ClipCoffre', 'Configurer le chemin de backup dans les paramètres');
return;
}
const result = backupService.backup();
Main.notify('ClipCoffre', result.message);
}
vfunc_event(event) {
if (event.type() === Clutter.EventType.BUTTON_PRESS) {
const button = event.get_button();
if (this._currentMode === MODE_SEPARE) {
if (button === Clutter.BUTTON_PRIMARY) {
this._showSecretsView = true;
this._showSettingsView = false;
this._rebuildSeparateContent();
} else if (button === Clutter.BUTTON_SECONDARY) {
this._showSecretsView = false;
this._showSettingsView = false;
this._rebuildSeparateContent();
}
}
}
return super.vfunc_event(event);
}
destroy() {
if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = null;
}
super.destroy();
}
});

170
src/ui/popupClipboard.js Normal file
View File

@@ -0,0 +1,170 @@
import St from 'gi://St';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
const MAX_DISPLAY_LENGTH = 50;
export class PopupClipboard {
constructor(settings, services, callbacks = {}) {
this._settings = settings;
this._clipboardService = services.clipboardService;
this._callbacks = callbacks;
this._section = new PopupMenu.PopupMenuSection();
this._build();
}
_truncate(text) {
if (!text) return '';
const singleLine = text.replace(/\n/g, ' ').trim();
if (singleLine.length <= MAX_DISPLAY_LENGTH) return singleLine;
return singleLine.slice(0, MAX_DISPLAY_LENGTH - 3) + '...';
}
_build() {
this._section.removeAll();
this._buildHeader();
const state = this._clipboardService.state();
this._buildFavorites(state.favorites || []);
this._buildHistory(state.history || []);
}
_buildHeader() {
const headerBox = new St.BoxLayout({
style_class: 'clipcoffre-header',
x_expand: true,
});
const title = new St.Label({
text: 'Clipboard',
style_class: 'clipcoffre-title',
x_expand: true,
});
headerBox.add_child(title);
if (this._callbacks.onBackupClick) {
const backupButton = new St.Button({
style_class: 'clipcoffre-header-button clipcoffre-backup-btn',
child: new St.Icon({
icon_name: 'document-save-symbolic',
icon_size: 16,
}),
});
backupButton.connect('clicked', () => {
this._callbacks.onBackupClick();
});
headerBox.add_child(backupButton);
}
if (this._callbacks.onSettingsClick) {
const settingsButton = new St.Button({
style_class: 'clipcoffre-header-button',
child: new St.Icon({
icon_name: 'emblem-system-symbolic',
icon_size: 16,
}),
});
settingsButton.connect('clicked', () => {
this._callbacks.onSettingsClick();
});
headerBox.add_child(settingsButton);
}
const headerItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
headerItem.add_child(headerBox);
this._section.addMenuItem(headerItem);
}
_buildFavorites(favorites) {
if (favorites.length === 0) return;
const labelItem = new PopupMenu.PopupMenuItem('Favoris', {
reactive: false,
});
labelItem.add_style_class_name('clipcoffre-section-label');
this._section.addMenuItem(labelItem);
for (const item of favorites) {
this._buildClipboardRow(item, true);
}
this._section.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
}
_buildHistory(history) {
const nonFavorites = history.filter(item => !item.favorite);
if (nonFavorites.length === 0 && history.length === 0) {
const emptyItem = new PopupMenu.PopupMenuItem('Historique vide', {
reactive: false,
});
emptyItem.add_style_class_name('clipcoffre-empty');
this._section.addMenuItem(emptyItem);
return;
}
if (nonFavorites.length === 0) return;
const labelItem = new PopupMenu.PopupMenuItem('Historique', {
reactive: false,
});
labelItem.add_style_class_name('clipcoffre-section-label');
this._section.addMenuItem(labelItem);
for (const item of nonFavorites) {
this._buildClipboardRow(item, false);
}
}
_buildClipboardRow(item, isFavorite) {
const menuItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-clipboard-row',
x_expand: true,
});
const starIcon = isFavorite ? 'starred-symbolic' : 'non-starred-symbolic';
const starBtn = new St.Button({
style_class: 'clipcoffre-star-btn',
child: new St.Icon({
icon_name: starIcon,
icon_size: 16,
}),
});
starBtn.connect('clicked', () => {
this._clipboardService.toggleFavorite(item.id);
this._build();
});
rowBox.add_child(starBtn);
const contentLabel = new St.Label({
text: this._truncate(item.content),
style_class: 'clipcoffre-clipboard-text',
x_expand: true,
});
rowBox.add_child(contentLabel);
const copyBtn = new St.Button({
style_class: 'clipcoffre-copy-btn',
child: new St.Icon({
icon_name: 'edit-copy-symbolic',
icon_size: 14,
}),
});
copyBtn.connect('clicked', () => {
this._clipboardService.copy(item.content);
});
rowBox.add_child(copyBtn);
menuItem.add_child(rowBox);
this._section.addMenuItem(menuItem);
}
refresh() {
this._build();
}
get widget() {
return this._section;
}
}

153
src/ui/popupMain.js Normal file
View File

@@ -0,0 +1,153 @@
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import { PopupSecrets } from './popupSecrets.js';
import { PopupClipboard } from './popupClipboard.js';
import { PopupSettings } from './popupSettings.js';
const TAB_SECRETS = 'secrets';
const TAB_CLIPBOARD = 'clipboard';
const TAB_SETTINGS = 'settings';
export class PopupMain {
constructor(settings, services, callbacks = {}) {
this._settings = settings;
this._services = services;
this._callbacks = callbacks;
this._section = new PopupMenu.PopupMenuSection();
this._activeTab = TAB_SECRETS;
this._build();
}
_createSecretsPopup() {
return new PopupSecrets(this._settings, this._services, {
onSettingsClick: () => this._switchTab(TAB_SETTINGS),
onBackupClick: () => this._doBackup(),
});
}
_createClipboardPopup() {
return new PopupClipboard(this._settings, this._services, {
onSettingsClick: () => this._switchTab(TAB_SETTINGS),
onBackupClick: () => this._doBackup(),
});
}
_createSettingsPopup() {
return new PopupSettings(this._settings, {
onSave: () => {
this._switchTab(TAB_SECRETS);
if (this._callbacks.onSettingsChanged) {
this._callbacks.onSettingsChanged();
}
},
onCancel: () => this._switchTab(TAB_SECRETS),
});
}
_build() {
this._section.removeAll();
this._buildTabs();
this._buildContent();
}
_buildTabs() {
if (this._activeTab === TAB_SETTINGS) {
const headerItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const headerBox = new St.BoxLayout({
style_class: 'clipcoffre-tabs-header',
x_expand: true,
});
const backBtn = new St.Button({
style_class: 'clipcoffre-back-btn',
child: new St.Icon({
icon_name: 'go-previous-symbolic',
icon_size: 16,
}),
});
backBtn.connect('clicked', () => this._switchTab(TAB_SECRETS));
headerBox.add_child(backBtn);
const title = new St.Label({
text: 'Paramètres',
style_class: 'clipcoffre-title',
x_expand: true,
});
headerBox.add_child(title);
headerItem.add_child(headerBox);
this._section.addMenuItem(headerItem);
return;
}
const tabsItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const tabsBox = new St.BoxLayout({
style_class: 'clipcoffre-tabs',
x_expand: true,
});
const secretsTab = new St.Button({
style_class:
this._activeTab === TAB_SECRETS
? 'clipcoffre-tab clipcoffre-tab-active'
: 'clipcoffre-tab',
label: 'Secrets',
x_expand: true,
});
secretsTab.connect('clicked', () => this._switchTab(TAB_SECRETS));
const clipboardTab = new St.Button({
style_class:
this._activeTab === TAB_CLIPBOARD
? 'clipcoffre-tab clipcoffre-tab-active'
: 'clipcoffre-tab',
label: 'Clipboard',
x_expand: true,
});
clipboardTab.connect('clicked', () => this._switchTab(TAB_CLIPBOARD));
tabsBox.add_child(secretsTab);
tabsBox.add_child(clipboardTab);
tabsItem.add_child(tabsBox);
this._section.addMenuItem(tabsItem);
}
_buildContent() {
// Créer un nouveau popup à chaque fois pour éviter les problèmes de parent
switch (this._activeTab) {
case TAB_SECRETS:
this._section.addMenuItem(this._createSecretsPopup().widget);
break;
case TAB_CLIPBOARD:
this._section.addMenuItem(this._createClipboardPopup().widget);
break;
case TAB_SETTINGS:
this._section.addMenuItem(this._createSettingsPopup().widget);
break;
}
}
_switchTab(tab) {
this._activeTab = tab;
this._build();
}
_doBackup() {
if (this._callbacks.onBackupClick) {
this._callbacks.onBackupClick();
}
}
refresh() {
this._build();
}
get widget() {
return this._section;
}
}

330
src/ui/popupSecrets.js Normal file
View File

@@ -0,0 +1,330 @@
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import { SyncService } from '../services/syncService.js';
export class PopupSecrets {
constructor(settings, services, callbacks = {}) {
this._settings = settings;
this._secretService = services.secretService;
this._callbacks = callbacks;
this._section = new PopupMenu.PopupMenuSection();
this._addMode = false;
this._build();
}
_build() {
this._section.removeAll();
this._buildHeader();
this._buildList();
if (this._addMode) {
this._buildAddForm();
}
}
_buildHeader() {
const headerBox = new St.BoxLayout({
style_class: 'clipcoffre-header',
x_expand: true,
});
const title = new St.Label({
text: 'Secrets',
style_class: 'clipcoffre-title',
x_expand: true,
});
headerBox.add_child(title);
const addButton = new St.Button({
style_class: 'clipcoffre-header-button',
child: new St.Icon({
icon_name: 'list-add-symbolic',
icon_size: 16,
}),
});
addButton.connect('clicked', () => {
this._addMode = !this._addMode;
this._build();
});
headerBox.add_child(addButton);
// Bouton sync export (envoyer vers le dépôt)
const syncExportBtn = new St.Button({
style_class: 'clipcoffre-header-button clipcoffre-sync-btn',
child: new St.Icon({
icon_name: 'send-to-symbolic',
icon_size: 16,
}),
});
syncExportBtn.connect('clicked', () => {
this._doSyncExport();
});
headerBox.add_child(syncExportBtn);
// Bouton sync import (récupérer depuis le dépôt)
const syncImportBtn = new St.Button({
style_class: 'clipcoffre-header-button clipcoffre-sync-btn',
child: new St.Icon({
icon_name: 'document-open-symbolic',
icon_size: 16,
}),
});
syncImportBtn.connect('clicked', () => {
this._doSyncImport();
});
headerBox.add_child(syncImportBtn);
if (this._callbacks.onBackupClick) {
const backupButton = new St.Button({
style_class: 'clipcoffre-header-button clipcoffre-backup-btn',
child: new St.Icon({
icon_name: 'document-save-symbolic',
icon_size: 16,
}),
});
backupButton.connect('clicked', () => {
this._callbacks.onBackupClick();
});
headerBox.add_child(backupButton);
}
if (this._callbacks.onSettingsClick) {
const settingsButton = new St.Button({
style_class: 'clipcoffre-header-button',
child: new St.Icon({
icon_name: 'emblem-system-symbolic',
icon_size: 16,
}),
});
settingsButton.connect('clicked', () => {
this._callbacks.onSettingsClick();
});
headerBox.add_child(settingsButton);
}
const headerItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
headerItem.add_child(headerBox);
this._section.addMenuItem(headerItem);
}
_buildList() {
const entries = this._secretService.entries;
if (entries.length === 0) {
const emptyItem = new PopupMenu.PopupMenuItem('Aucun secret enregistré', {
reactive: false,
});
emptyItem.add_style_class_name('clipcoffre-empty');
this._section.addMenuItem(emptyItem);
return;
}
for (const entry of entries) {
this._buildEntryRow(entry);
}
}
_buildEntryRow(entry) {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-secret-row',
x_expand: true,
});
// Colonne 1 : Label (app/service)
const labelBox = new St.BoxLayout({
style_class: 'clipcoffre-col clipcoffre-col-label',
x_expand: true,
});
const labelText = new St.Label({
text: entry.label || '—',
style_class: 'clipcoffre-text',
x_expand: true,
});
labelBox.add_child(labelText);
rowBox.add_child(labelBox);
// Colonne 2 : User
const userBox = new St.BoxLayout({
style_class: 'clipcoffre-col clipcoffre-col-user',
x_expand: true,
});
const userText = new St.Label({
text: entry.user,
style_class: 'clipcoffre-text',
x_expand: true,
});
userBox.add_child(userText);
const copyUserBtn = new St.Button({
style_class: 'clipcoffre-copy-btn',
child: new St.Icon({
icon_name: 'edit-copy-symbolic',
icon_size: 14,
}),
});
copyUserBtn.connect('clicked', () => {
this._secretService.copyToClipboard(entry.user);
});
userBox.add_child(copyUserBtn);
rowBox.add_child(userBox);
// Colonne 3 : Password
const pwdBox = new St.BoxLayout({
style_class: 'clipcoffre-col clipcoffre-col-pwd',
x_expand: true,
});
const pwdText = new St.Label({
text: entry.displayPassword,
style_class: 'clipcoffre-text clipcoffre-password',
x_expand: true,
});
pwdBox.add_child(pwdText);
const copyPwdBtn = new St.Button({
style_class: 'clipcoffre-copy-btn',
child: new St.Icon({
icon_name: 'edit-copy-symbolic',
icon_size: 14,
}),
});
copyPwdBtn.connect('clicked', () => {
this._secretService.copyToClipboard(entry.rawPassword);
});
pwdBox.add_child(copyPwdBtn);
const deleteBtn = new St.Button({
style_class: 'clipcoffre-delete-btn',
child: new St.Icon({
icon_name: 'edit-delete-symbolic',
icon_size: 14,
}),
});
deleteBtn.connect('clicked', () => {
this._secretService.deleteEntry(entry.id);
this._build();
});
pwdBox.add_child(deleteBtn);
rowBox.add_child(pwdBox);
item.add_child(rowBox);
this._section.addMenuItem(item);
}
_buildAddForm() {
const formItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const formBox = new St.BoxLayout({
style_class: 'clipcoffre-add-form',
vertical: true,
x_expand: true,
});
const labelEntry = new St.Entry({
style_class: 'clipcoffre-input',
hint_text: 'App / Service',
can_focus: true,
});
const userEntry = new St.Entry({
style_class: 'clipcoffre-input',
hint_text: 'Utilisateur',
can_focus: true,
});
const pwdEntry = new St.Entry({
style_class: 'clipcoffre-input',
hint_text: 'Mot de passe',
can_focus: true,
});
pwdEntry.clutter_text.set_password_char('•');
const buttonsBox = new St.BoxLayout({
style_class: 'clipcoffre-form-buttons',
x_expand: true,
});
const saveBtn = new St.Button({
style_class: 'clipcoffre-btn clipcoffre-btn-save',
label: 'Enregistrer',
});
saveBtn.connect('clicked', () => {
const label = labelEntry.get_text().trim();
const user = userEntry.get_text().trim();
const pwd = pwdEntry.get_text();
if (user && pwd) {
this._secretService.addEntry(user, pwd, label);
this._addMode = false;
this._build();
}
});
const cancelBtn = new St.Button({
style_class: 'clipcoffre-btn clipcoffre-btn-cancel',
label: 'Annuler',
});
cancelBtn.connect('clicked', () => {
this._addMode = false;
this._build();
});
buttonsBox.add_child(saveBtn);
buttonsBox.add_child(cancelBtn);
formBox.add_child(labelEntry);
formBox.add_child(userEntry);
formBox.add_child(pwdEntry);
formBox.add_child(buttonsBox);
formItem.add_child(formBox);
this._section.addMenuItem(formItem);
}
refresh() {
this._build();
}
_doSyncExport() {
const syncService = new SyncService(
this._secretService._store._extension,
this._settings,
this._secretService._store
);
if (!syncService.isConfigured()) {
Main.notify('ClipCoffre', 'Configurer le chemin sync Git dans les paramètres');
return;
}
const result = syncService.exportSecrets();
Main.notify('ClipCoffre', result.message);
}
_doSyncImport() {
const syncService = new SyncService(
this._secretService._store._extension,
this._settings,
this._secretService._store
);
if (!syncService.isConfigured()) {
Main.notify('ClipCoffre', 'Configurer le chemin sync Git dans les paramètres');
return;
}
const result = syncService.importSecrets();
Main.notify('ClipCoffre', result.message);
if (result.success) {
// Rafraîchir l'affichage après import
this._build();
}
}
get widget() {
return this._section;
}
}

354
src/ui/popupSettings.js Normal file
View File

@@ -0,0 +1,354 @@
import St from 'gi://St';
import Clutter from 'gi://Clutter';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
export class PopupSettings {
constructor(settings, callbacks = {}) {
this._settings = settings;
this._callbacks = callbacks;
this._section = new PopupMenu.PopupMenuSection();
this._tempHistorySize = settings.historySize;
this._tempShowPasswords = settings.showPasswords;
this._tempClickMode = settings.clickMode;
this._tempBackupPath = settings.backupPath;
this._tempSyncPath = settings.syncPath;
this._build();
}
_build() {
this._section.removeAll();
this._buildHeader();
this._buildHistorySizeSetting();
this._buildShowPasswordsSetting();
this._buildClickModeSetting();
this._buildBackupPathSetting();
this._buildSyncPathSetting();
this._buildButtons();
}
_buildHeader() {
const headerBox = new St.BoxLayout({
style_class: 'clipcoffre-header',
x_expand: true,
});
const title = new St.Label({
text: 'Paramètres',
style_class: 'clipcoffre-title',
x_expand: true,
});
headerBox.add_child(title);
const headerItem = new PopupMenu.PopupBaseMenuItem({ reactive: false });
headerItem.add_child(headerBox);
this._section.addMenuItem(headerItem);
}
_buildHistorySizeSetting() {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-settings-row',
x_expand: true,
});
const label = new St.Label({
text: 'Taille historique',
style_class: 'clipcoffre-settings-label',
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
rowBox.add_child(label);
const spinBox = new St.BoxLayout({
style_class: 'clipcoffre-spin-box',
});
const minusBtn = new St.Button({
style_class: 'clipcoffre-spin-btn',
label: '-',
});
minusBtn.connect('clicked', () => {
if (this._tempHistorySize > 10) {
this._tempHistorySize -= 10;
this._updateSpinLabel();
}
});
this._historySizeLabel = new St.Label({
text: String(this._tempHistorySize),
style_class: 'clipcoffre-spin-value',
x_align: Clutter.ActorAlign.CENTER,
});
const plusBtn = new St.Button({
style_class: 'clipcoffre-spin-btn',
label: '+',
});
plusBtn.connect('clicked', () => {
if (this._tempHistorySize < 200) {
this._tempHistorySize += 10;
this._updateSpinLabel();
}
});
spinBox.add_child(minusBtn);
spinBox.add_child(this._historySizeLabel);
spinBox.add_child(plusBtn);
rowBox.add_child(spinBox);
item.add_child(rowBox);
this._section.addMenuItem(item);
}
_updateSpinLabel() {
this._historySizeLabel.set_text(String(this._tempHistorySize));
}
_buildShowPasswordsSetting() {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-settings-row',
x_expand: true,
});
const label = new St.Label({
text: 'Afficher mots de passe',
style_class: 'clipcoffre-settings-label',
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
rowBox.add_child(label);
this._switchBtn = new St.Button({
style_class: this._tempShowPasswords
? 'clipcoffre-switch clipcoffre-switch-on'
: 'clipcoffre-switch clipcoffre-switch-off',
label: this._tempShowPasswords ? 'ON' : 'OFF',
});
this._switchBtn.connect('clicked', () => {
this._tempShowPasswords = !this._tempShowPasswords;
this._updateSwitchStyle();
});
rowBox.add_child(this._switchBtn);
item.add_child(rowBox);
this._section.addMenuItem(item);
}
_updateSwitchStyle() {
if (this._tempShowPasswords) {
this._switchBtn.remove_style_class_name('clipcoffre-switch-off');
this._switchBtn.add_style_class_name('clipcoffre-switch-on');
this._switchBtn.set_label('ON');
} else {
this._switchBtn.remove_style_class_name('clipcoffre-switch-on');
this._switchBtn.add_style_class_name('clipcoffre-switch-off');
this._switchBtn.set_label('OFF');
}
}
_buildClickModeSetting() {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-settings-row',
x_expand: true,
});
const label = new St.Label({
text: 'Mode clic',
style_class: 'clipcoffre-settings-label',
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
rowBox.add_child(label);
const comboBox = new St.BoxLayout({
style_class: 'clipcoffre-combo-box',
});
this._unifieBtn = new St.Button({
style_class:
this._tempClickMode === 'unifie'
? 'clipcoffre-combo-btn clipcoffre-combo-btn-active'
: 'clipcoffre-combo-btn',
label: 'Unifié',
});
this._unifieBtn.connect('clicked', () => {
this._tempClickMode = 'unifie';
this._updateComboStyle();
});
this._separeBtn = new St.Button({
style_class:
this._tempClickMode === 'separe'
? 'clipcoffre-combo-btn clipcoffre-combo-btn-active'
: 'clipcoffre-combo-btn',
label: 'Séparé',
});
this._separeBtn.connect('clicked', () => {
this._tempClickMode = 'separe';
this._updateComboStyle();
});
comboBox.add_child(this._unifieBtn);
comboBox.add_child(this._separeBtn);
rowBox.add_child(comboBox);
item.add_child(rowBox);
this._section.addMenuItem(item);
}
_updateComboStyle() {
if (this._tempClickMode === 'unifie') {
this._unifieBtn.add_style_class_name('clipcoffre-combo-btn-active');
this._separeBtn.remove_style_class_name('clipcoffre-combo-btn-active');
} else {
this._unifieBtn.remove_style_class_name('clipcoffre-combo-btn-active');
this._separeBtn.add_style_class_name('clipcoffre-combo-btn-active');
}
}
_buildBackupPathSetting() {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-settings-row',
vertical: true,
x_expand: true,
});
const labelRow = new St.BoxLayout({
x_expand: true,
});
const label = new St.Label({
text: 'Chemin de backup',
style_class: 'clipcoffre-settings-label',
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
labelRow.add_child(label);
rowBox.add_child(labelRow);
this._backupPathEntry = new St.Entry({
style_class: 'clipcoffre-input clipcoffre-backup-path-input',
hint_text: '/chemin/vers/backup',
text: this._tempBackupPath || '',
can_focus: true,
x_expand: true,
});
this._backupPathEntry.clutter_text.connect('text-changed', () => {
this._tempBackupPath = this._backupPathEntry.get_text();
});
rowBox.add_child(this._backupPathEntry);
item.add_child(rowBox);
this._section.addMenuItem(item);
}
_buildSyncPathSetting() {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const rowBox = new St.BoxLayout({
style_class: 'clipcoffre-settings-row',
vertical: true,
x_expand: true,
});
const labelRow = new St.BoxLayout({
x_expand: true,
});
const label = new St.Label({
text: 'Chemin sync Git (secrets)',
style_class: 'clipcoffre-settings-label',
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
labelRow.add_child(label);
rowBox.add_child(labelRow);
this._syncPathEntry = new St.Entry({
style_class: 'clipcoffre-input clipcoffre-backup-path-input',
hint_text: '/chemin/vers/depot/git',
text: this._tempSyncPath || '',
can_focus: true,
x_expand: true,
});
this._syncPathEntry.clutter_text.connect('text-changed', () => {
this._tempSyncPath = this._syncPathEntry.get_text();
});
rowBox.add_child(this._syncPathEntry);
item.add_child(rowBox);
this._section.addMenuItem(item);
}
_buildButtons() {
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const buttonsBox = new St.BoxLayout({
style_class: 'clipcoffre-settings-buttons',
x_expand: true,
});
const okBtn = new St.Button({
style_class: 'clipcoffre-btn clipcoffre-btn-save',
label: 'OK',
x_expand: true,
});
okBtn.connect('clicked', () => {
this._settings.historySize = this._tempHistorySize;
this._settings.showPasswords = this._tempShowPasswords;
this._settings.clickMode = this._tempClickMode;
this._settings.backupPath = this._tempBackupPath;
this._settings.syncPath = this._tempSyncPath;
if (this._callbacks.onSave) {
this._callbacks.onSave();
}
});
const cancelBtn = new St.Button({
style_class: 'clipcoffre-btn clipcoffre-btn-cancel',
label: 'Annuler',
x_expand: true,
});
cancelBtn.connect('clicked', () => {
this._tempHistorySize = this._settings.historySize;
this._tempShowPasswords = this._settings.showPasswords;
this._tempClickMode = this._settings.clickMode;
this._tempBackupPath = this._settings.backupPath;
this._tempSyncPath = this._settings.syncPath;
this._build();
if (this._callbacks.onCancel) {
this._callbacks.onCancel();
}
});
buttonsBox.add_child(okBtn);
buttonsBox.add_child(cancelBtn);
item.add_child(buttonsBox);
this._section.addMenuItem(item);
}
refresh() {
this._tempHistorySize = this._settings.historySize;
this._tempShowPasswords = this._settings.showPasswords;
this._tempClickMode = this._settings.clickMode;
this._tempBackupPath = this._settings.backupPath;
this._tempSyncPath = this._settings.syncPath;
this._build();
}
get widget() {
return this._section;
}
}