NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name AmneziaVPN Device Renamer
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Adds local device renaming to the AmneziaVPN control panel
// @match https://cp.amnezia.org/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'amnezia_device_names';
function getNames() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
} catch {
return {};
}
}
function saveName(uuid, name) {
const names = getNames();
if (name.trim()) {
names[uuid] = name.trim();
} else {
delete names[uuid];
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(names));
}
function extractUUID(tagText) {
const match = tagText.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
return match ? match[1] : null;
}
function exportNames() {
const names = getNames();
const json = JSON.stringify(names, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'amnezia_device_names.json';
a.click();
URL.revokeObjectURL(url);
}
function importNames() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (typeof data !== 'object' || Array.isArray(data)) throw new Error();
// Merge with existing names, imported values take precedence
const merged = { ...getNames(), ...data };
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
location.reload();
} catch {
alert('Invalid backup file.');
}
};
reader.readAsText(file);
});
input.click();
}
function injectToolbar() {
if (document.getElementById('amnezia-renamer-toolbar')) return;
const header = document.querySelector('[class*="expandHeader"]');
if (!header) return;
const toolbar = document.createElement('div');
toolbar.id = 'amnezia-renamer-toolbar';
const exportBtn = document.createElement('button');
exportBtn.className = 'amnezia-toolbar-btn';
exportBtn.textContent = '⬇️ Export names';
exportBtn.title = 'Download device names as a JSON backup file';
exportBtn.addEventListener('click', (e) => { e.stopPropagation(); exportNames(); });
const importBtn = document.createElement('button');
importBtn.className = 'amnezia-toolbar-btn';
importBtn.textContent = '⬆️ Import names';
importBtn.title = 'Restore device names from a JSON backup file';
importBtn.addEventListener('click', (e) => { e.stopPropagation(); importNames(); });
toolbar.appendChild(exportBtn);
toolbar.appendChild(importBtn);
// Insert between text section and close button
const expandBtn = header.querySelector('[class*="ExpandButton"]');
header.insertBefore(toolbar, expandBtn || null);
}
function injectStyles() {
if (document.getElementById('amnezia-renamer-styles')) return;
const style = document.createElement('style');
style.id = 'amnezia-renamer-styles';
style.textContent = `
.amnezia-custom-name {
font-weight: 600;
color: #a78bfa;
font-size: 13px;
margin-bottom: 2px;
}
.amnezia-rename-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px 6px;
font-size: 11px;
color: #9ca3af;
border-radius: 4px;
transition: color 0.15s, background 0.15s;
vertical-align: middle;
}
.amnezia-rename-btn:hover {
color: #a78bfa;
background: rgba(167,139,250,0.1);
}
.amnezia-rename-input {
font-size: 13px;
padding: 2px 6px;
border: 1px solid #a78bfa;
border-radius: 4px;
background: transparent;
color: inherit;
outline: none;
width: 160px;
}
.amnezia-rename-save {
background: #a78bfa;
border: none;
color: #fff;
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
margin-left: 4px;
}
.amnezia-rename-save:hover {
background: #7c3aed;
}
.amnezia-rename-cancel {
background: none;
border: none;
color: #9ca3af;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
margin-left: 2px;
}
#amnezia-renamer-toolbar {
display: flex;
gap: 8px;
align-items: center;
margin: 0 12px;
}
.amnezia-toolbar-btn {
background: transparent;
border: 1px solid #a78bfa;
color: #a78bfa;
font-size: 11px;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.amnezia-toolbar-btn:hover {
background: #a78bfa;
color: #fff;
}
`;
document.head.appendChild(style);
}
function processDevice(container) {
if (container.dataset.amneziaPatched) return;
container.dataset.amneziaPatched = '1';
const uuidEl = container.querySelector('[class*="uuidText"]');
if (!uuidEl) return;
const uuid = extractUUID(uuidEl.textContent);
if (!uuid) return;
const labelEl = container.querySelector('[class*="LabelText"]');
if (!labelEl) return;
const names = getNames();
const customName = names[uuid];
// Insert row with custom name and rename button
const nameRow = document.createElement('div');
nameRow.style.display = 'flex';
nameRow.style.alignItems = 'center';
nameRow.style.gap = '4px';
nameRow.style.marginBottom = '2px';
const customNameEl = document.createElement('span');
customNameEl.className = 'amnezia-custom-name';
customNameEl.textContent = customName || '';
customNameEl.style.display = customName ? 'inline' : 'none';
const renameBtn = document.createElement('button');
renameBtn.className = 'amnezia-rename-btn';
renameBtn.textContent = customName ? '✏️ rename' : '✏️ set name';
renameBtn.title = 'Rename this device (stored locally in your browser)';
nameRow.appendChild(customNameEl);
nameRow.appendChild(renameBtn);
// Insert before the platform label
labelEl.parentNode.insertBefore(nameRow, labelEl);
renameBtn.addEventListener('click', () => {
const currentName = getNames()[uuid] || '';
// Show inline edit form
const form = document.createElement('span');
form.style.display = 'inline-flex';
form.style.alignItems = 'center';
const input = document.createElement('input');
input.className = 'amnezia-rename-input';
input.value = currentName;
input.placeholder = 'Enter device name';
const saveBtn = document.createElement('button');
saveBtn.className = 'amnezia-rename-save';
saveBtn.textContent = 'Save';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'amnezia-rename-cancel';
cancelBtn.textContent = 'Cancel';
form.appendChild(input);
form.appendChild(saveBtn);
form.appendChild(cancelBtn);
nameRow.replaceWith(form);
input.focus();
input.select();
function applyRename() {
const newName = input.value.trim();
saveName(uuid, newName);
customNameEl.textContent = newName;
customNameEl.style.display = newName ? 'inline' : 'none';
renameBtn.textContent = newName ? '✏️ rename' : '✏️ set name';
nameRow.replaceChildren(customNameEl, renameBtn);
form.replaceWith(nameRow);
}
function cancelRename() {
form.replaceWith(nameRow);
}
saveBtn.addEventListener('click', applyRename);
cancelBtn.addEventListener('click', cancelRename);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') applyRename();
if (e.key === 'Escape') cancelRename();
});
});
}
function processAll() {
const containers = document.querySelectorAll('[class*="deviceContainer"]');
containers.forEach(processDevice);
}
function init() {
injectStyles();
injectToolbar();
processAll();
// Watch for DOM changes — device list may render asynchronously (React)
const observer = new MutationObserver(() => {
injectToolbar();
processAll();
});
observer.observe(document.body, { childList: true, subtree: true });
}
// Wait for the React app to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();