claude 2
This commit is contained in:
98
src/services/backupService.js
Normal file
98
src/services/backupService.js
Normal 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}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/services/clipboardService.js
Normal file
70
src/services/clipboardService.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/services/secretService.js
Normal file
72
src/services/secretService.js
Normal 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
145
src/services/syncService.js
Normal 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}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/storage/clipboardStore.js
Normal file
56
src/storage/clipboardStore.js
Normal 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
70
src/storage/jsonStore.js
Normal 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
110
src/storage/secretsStore.js
Normal 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
78
src/storage/settings.js
Normal 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
195
src/ui/panelButton.js
Normal 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
170
src/ui/popupClipboard.js
Normal 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
153
src/ui/popupMain.js
Normal 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
330
src/ui/popupSecrets.js
Normal 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
354
src/ui/popupSettings.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user