RDM-Import, App-Designs, Auto-Update über Gitea, Logo; SFTP-Namensspalte repariert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+124
-15
@@ -140,14 +140,15 @@ async function reload() {
|
||||
const b = $('#vaultBadge');
|
||||
b.innerHTML = S.vault.encrypted ? `${ICONS.lock}<span>Vault verschlüsselt</span>` : `${ICONS.unlock}<span>Vault unverschlüsselt</span>`;
|
||||
b.title = S.vault.encrypted ? 'Daten werden mit dem System-Schlüsselbund verschlüsselt.' : 'Kein System-Schlüsselbund verfügbar (z. B. gnome-keyring/kwallet installieren).';
|
||||
applyAppTheme();
|
||||
S.activeForwards = new Set(await call('fw:active'));
|
||||
render();
|
||||
}
|
||||
|
||||
// ============================================================ Navigation
|
||||
$$('#sidebar .nav').forEach((n) => n.addEventListener('click', () => {
|
||||
$$('#sidebar .nav[data-view]').forEach((n) => n.addEventListener('click', () => {
|
||||
S.view = n.dataset.view;
|
||||
$$('#sidebar .nav').forEach((x) => x.classList.toggle('active', x === n));
|
||||
$$('#sidebar .nav[data-view]').forEach((x) => x.classList.toggle('active', x === n));
|
||||
closeDrawer();
|
||||
activateTab('home');
|
||||
render();
|
||||
@@ -456,7 +457,7 @@ function viewKeys(page) {
|
||||
for (const k of list) {
|
||||
const type = (k.publicKey || '').split(' ')[0] || 'key';
|
||||
const used = S.vault.hosts.filter((x) => x.keyId === k.id).length;
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:#2a2f45;color:var(--orange)">${ICONS.key}</div><div class="meta"><div class="title">${esc(k.label)}</div><div class="sub">${esc(type)} · ${used} Host${used === 1 ? '' : 's'}</div></div><div class="actions"><button data-a="copy" title="Public Key kopieren">${ICONS.copy}</button><button data-a="edit" title="Bearbeiten">${ICONS.edit}</button></div></div>`);
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:var(--icon-bg);color:var(--orange)">${ICONS.key}</div><div class="meta"><div class="title">${esc(k.label)}</div><div class="sub">${esc(type)} · ${used} Host${used === 1 ? '' : 's'}</div></div><div class="actions"><button data-a="copy" title="Public Key kopieren">${ICONS.copy}</button><button data-a="edit" title="Bearbeiten">${ICONS.edit}</button></div></div>`);
|
||||
card.onclick = (e) => {
|
||||
const a = e.target.closest('[data-a]')?.dataset.a;
|
||||
if (a === 'copy') { call('clipboard:write', k.publicKey || ''); toast('Public Key kopiert', 'ok'); }
|
||||
@@ -519,7 +520,7 @@ function viewSnippets(page) {
|
||||
if (!list.length) return c.append(emptyState('code', 'Keine Snippets', 'Speichere häufig genutzte Befehle und sende sie mit einem Klick an ein Terminal.', 'Neues Snippet', () => editSnippet()));
|
||||
const l = h('<div class="list"></div>');
|
||||
for (const s of list) {
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:#2a2f45;color:var(--green)">${ICONS.code}</div><div class="meta"><div class="title">${esc(s.label)}</div><div class="sub mono">${esc(s.command.split('\n')[0])}${s.command.includes('\n') ? ' …' : ''}</div></div><div class="actions"><button data-a="run" title="Im aktiven Terminal ausführen">${ICONS.play}</button><button data-a="copy" title="Kopieren">${ICONS.copy}</button><button data-a="del" title="Löschen">${ICONS.trash}</button></div></div>`);
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:var(--icon-bg);color:var(--green)">${ICONS.code}</div><div class="meta"><div class="title">${esc(s.label)}</div><div class="sub mono">${esc(s.command.split('\n')[0])}${s.command.includes('\n') ? ' …' : ''}</div></div><div class="actions"><button data-a="run" title="Im aktiven Terminal ausführen">${ICONS.play}</button><button data-a="copy" title="Kopieren">${ICONS.copy}</button><button data-a="del" title="Löschen">${ICONS.trash}</button></div></div>`);
|
||||
card.onclick = async (e) => {
|
||||
const a = e.target.closest('[data-a]')?.dataset.a;
|
||||
if (a === 'run') runSnippet(s);
|
||||
@@ -564,7 +565,7 @@ function viewForwards(page) {
|
||||
const desc = f.type === 'dynamic' ? `SOCKS5 ${f.bindAddress || '127.0.0.1'}:${f.bindPort}`
|
||||
: f.type === 'remote' ? `Remote ${f.bindPort} → ${f.destHost || '127.0.0.1'}:${f.destPort}`
|
||||
: `Lokal ${f.bindAddress || '127.0.0.1'}:${f.bindPort} → ${f.destHost}:${f.destPort}`;
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:#2a2f45;color:${on ? 'var(--green)' : 'var(--muted)'}">${ICONS.fwd}</div><div class="meta"><div class="title">${esc(f.label || desc)} <span class="status-pill ${on ? 'on' : ''}">${on ? 'aktiv' : 'inaktiv'}</span></div><div class="sub">${esc(desc)} · via ${esc(hst ? hst.label || hst.address : '??')}</div></div>
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:var(--icon-bg);color:${on ? 'var(--green)' : 'var(--muted)'}">${ICONS.fwd}</div><div class="meta"><div class="title">${esc(f.label || desc)} <span class="status-pill ${on ? 'on' : ''}">${on ? 'aktiv' : 'inaktiv'}</span></div><div class="sub">${esc(desc)} · via ${esc(hst ? hst.label || hst.address : '??')}</div></div>
|
||||
<button class="btn sm ${on ? '' : 'primary'}" data-a="toggle">${on ? ICONS.stop + 'Stoppen' : ICONS.play + 'Starten'}</button><div class="actions"><button data-a="del">${ICONS.trash}</button></div></div>`);
|
||||
card.onclick = async (e) => {
|
||||
const a = e.target.closest('[data-a]')?.dataset.a;
|
||||
@@ -608,7 +609,7 @@ function viewKnown(page) {
|
||||
if (!entries.length) return c.append(emptyState('shield', 'Keine bekannten Hosts', 'Host-Schlüssel werden bei der ersten Verbindung hier gespeichert.'));
|
||||
const l = h('<div class="list"></div>');
|
||||
for (const [id, v] of entries) {
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:#2a2f45;color:var(--green)">${ICONS.shield}</div><div class="meta"><div class="title">${esc(id)}</div><div class="sub mono">${esc(v.fingerprint)} · ${fmtDate(v.addedAt)}</div></div><div class="actions"><button title="Entfernen">${ICONS.trash}</button></div></div>`);
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:var(--icon-bg);color:var(--green)">${ICONS.shield}</div><div class="meta"><div class="title">${esc(id)}</div><div class="sub mono">${esc(v.fingerprint)} · ${fmtDate(v.addedAt)}</div></div><div class="actions"><button title="Entfernen">${ICONS.trash}</button></div></div>`);
|
||||
$('.actions button', card).onclick = async () => { await call('vault:forgetHost', id); reload(); };
|
||||
l.append(card);
|
||||
}
|
||||
@@ -636,7 +637,25 @@ async function viewSettings(page) {
|
||||
const c = h('<div class="content settings-grid"></div>'); page.append(c);
|
||||
const save = async (patch) => { await call('vault:settings', patch); S.vault.settings = { ...st, ...patch }; S.tabs.forEach((t) => t.applySettings?.()); };
|
||||
|
||||
// ---- App-Design
|
||||
const designCard = h('<div class="settings-card"><h3>Design</h3><div class="theme-list design-list"></div><div class="field" style="margin:16px 0 0"><label>Akzentfarbe</label><div class="colors accent-colors"></div></div></div>');
|
||||
for (const [id, t] of Object.entries(APP_THEMES)) {
|
||||
const p = h(`<div class="theme-prev design-prev ${st.appTheme === id ? 'active' : ''}" style="background:${t.bg};color:${t.text}">
|
||||
<div class="dp"><i style="background:${t.side}"></i><div><b style="background:${t.card}"><s style="background:${t.accent}"></s></b><b style="background:${t.card}"></b></div></div><div class="nm">${esc(t.name)}</div></div>`);
|
||||
p.onclick = async () => { await save({ appTheme: id }); applyAppTheme(); render(); };
|
||||
$('.design-list', designCard).append(p);
|
||||
}
|
||||
['', ...COLORS.slice(0, 7), '#21c7a8', '#cba6f7'].forEach((col) => {
|
||||
const b = h(`<button type="button" title="${col || 'Standard des Designs'}" class="${(st.accent || '') === col ? 'active' : ''}" style="background:${col || 'conic-gradient(#6e7bff,#21c7a8,#cba6f7,#ffb454,#6e7bff)'}"></button>`);
|
||||
b.onclick = async () => { await save({ accent: col }); applyAppTheme(); render(); };
|
||||
$('.accent-colors', designCard).append(b);
|
||||
});
|
||||
|
||||
const themeCard = h('<div class="settings-card"><h3>Terminal-Farbschema</h3><div class="theme-list"></div></div>');
|
||||
const autoT = TERM_THEMES[(APP_THEMES[st.appTheme] || APP_THEMES.midnight).term];
|
||||
const autoP = h(`<div class="theme-prev ${st.terminalTheme === 'auto' ? 'active' : ''}" style="background:${autoT.background};color:${autoT.foreground}"><div class="nm">Passend zum Design</div><span style="color:${autoT.green}">user@host</span>:<span style="color:${autoT.blue}">~</span>$ ls<br><span style="color:${autoT.blue}">folgt dem Design</span></div>`);
|
||||
autoP.onclick = async () => { await save({ terminalTheme: 'auto' }); render(); };
|
||||
$('.theme-list', themeCard).append(autoP);
|
||||
for (const [id, t] of Object.entries(TERM_THEMES)) {
|
||||
const p = h(`<div class="theme-prev ${st.terminalTheme === id ? 'active' : ''}" style="background:${t.background};color:${t.foreground}"><div class="nm">${esc(t.name)}</div><span style="color:${t.green}">user@host</span>:<span style="color:${t.blue}">~</span>$ ls<br><span style="color:${t.blue}">docs</span> <span style="color:${t.green}">run.sh</span> <span style="color:${t.red}">err</span></div>`);
|
||||
p.onclick = async () => { await save({ terminalTheme: id }); render(); };
|
||||
@@ -675,6 +694,31 @@ async function viewSettings(page) {
|
||||
];
|
||||
btns.forEach(([l, fn]) => { const b = h(`<button class="btn">${esc(l)}</button>`); b.onclick = fn; $('.row', dataCard).append(b); });
|
||||
|
||||
// ---- Import aus anderen Programmen
|
||||
const importCard = h(`<div class="settings-card"><h3>Import</h3><p style="color:var(--muted);margin-top:0">Ordner und Hosts (RDP & SSH) aus <b>Devolutions Remote Desktop Manager</b> übernehmen. In RDM: <i>Datei → Exportieren → Einträge exportieren</i> als <code>.rdm</code> (XML), JSON oder CSV. Passwörter werden von RDM nur unverschlüsselt exportiert, wenn du das beim Export aktivierst.</p></div>`);
|
||||
const rdmBtn = h('<button class="btn primary">Aus Remote Desktop Manager importieren…</button>');
|
||||
rdmBtn.onclick = importRdm;
|
||||
importCard.append(rdmBtn);
|
||||
|
||||
// ---- Updates
|
||||
const updCard = h(`<div class="settings-card"><h3>Updates</h3><p class="upd-info" style="color:var(--muted);margin-top:0">Installierte Version: …</p></div>`);
|
||||
api.call('app:version').then((v) => { $('.upd-info', updCard).textContent = `Installierte Version: ${v}`; });
|
||||
const updFields = h('<div></div>');
|
||||
updFields.append(
|
||||
field('Repository', 'updateUrl', st.updateUrl, { placeholder: 'https://git.mrblake.cc/MrBlake/MrTerm', hint: 'Gitea/Forgejo-Repository, dessen Releases die Installationspakete enthalten.' }),
|
||||
field('Zugriffstoken (nur für private Repos)', 'updateToken', st.updateToken, { type: 'password', placeholder: 'optional' }),
|
||||
check('Beim Start automatisch nach Updates suchen', 'updateAutoCheck', st.updateAutoCheck),
|
||||
check('Vorabversionen (Pre-Releases) einbeziehen', 'updatePrerelease', st.updatePrerelease),
|
||||
);
|
||||
updFields.addEventListener('change', () => save(formValues(updFields)));
|
||||
const chk = h(`<button class="btn">${ICONS.refresh}Jetzt nach Updates suchen</button>`);
|
||||
chk.onclick = async () => {
|
||||
chk.disabled = true;
|
||||
try { await save(formValues(updFields)); const r = await call('update:check'); if (r.available) showUpdate(r); else toast(`MrTerm ist aktuell (${r.current}).`, 'ok'); } catch {}
|
||||
chk.disabled = false;
|
||||
};
|
||||
updCard.append(updFields, chk);
|
||||
|
||||
const keysCard = h(`<div class="settings-card"><h3>Tastenkürzel</h3><div style="display:grid;grid-template-columns:auto 1fr;gap:8px 18px;color:var(--muted)">
|
||||
<span><kbd>Strg</kbd>+<kbd>K</kbd> / <kbd>Strg</kbd>+<kbd>T</kbd></span><span>Quick Connect / Befehlspalette</span>
|
||||
<span><kbd>Strg</kbd>+<kbd>W</kbd></span><span>Tab schließen</span>
|
||||
@@ -683,9 +727,73 @@ async function viewSettings(page) {
|
||||
<span><kbd>Strg</kbd>+<kbd>Shift</kbd>+<kbd>C/V</kbd></span><span>Kopieren / Einfügen im Terminal</span>
|
||||
<span><kbd>Strg</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd></span><span>Im Terminal suchen</span>
|
||||
<span><kbd>Strg</kbd>+<kbd>+/−/0</kbd></span><span>Schriftgröße</span></div></div>`);
|
||||
c.append(themeCard, termCard, rdpCard, dataCard, keysCard);
|
||||
c.append(designCard, themeCard, termCard, rdpCard, importCard, dataCard, updCard, keysCard);
|
||||
}
|
||||
|
||||
// ============================================================ Design
|
||||
function termTheme() {
|
||||
const id = settings().terminalTheme;
|
||||
if (id === 'auto') return TERM_THEMES[(APP_THEMES[settings().appTheme] || APP_THEMES.midnight).term] || TERM_THEMES.mrterm;
|
||||
return TERM_THEMES[id] || TERM_THEMES.mrterm;
|
||||
}
|
||||
function applyAppTheme() {
|
||||
const st = settings();
|
||||
const root = document.documentElement;
|
||||
if (st.appTheme && st.appTheme !== 'midnight') root.dataset.theme = st.appTheme; else delete root.dataset.theme;
|
||||
if (st.accent) root.style.setProperty('--accent', st.accent); else root.style.removeProperty('--accent');
|
||||
S.tabs.forEach((t) => t.applySettings?.());
|
||||
}
|
||||
|
||||
// ============================================================ RDM-Import
|
||||
async function importRdm() {
|
||||
const f = await call('import:pickFile', 'Remote Desktop Manager Export', ['rdm', 'xml', 'json', 'csv']);
|
||||
if (!f) return;
|
||||
let entries;
|
||||
try { entries = parseRdmExport(f.name, f.content); } catch (e) { return toast('Import fehlgeschlagen: ' + e.message, 'error'); }
|
||||
const hosts = entries.filter((e) => e.host);
|
||||
if (!hosts.length) return toast('Keine RDP- oder SSH-Einträge in der Datei gefunden.', 'error');
|
||||
const folders = new Set(entries.map((e) => e.folder.join(' / ')).filter(Boolean));
|
||||
const rdpN = hosts.filter((e) => e.host.protocol === 'rdp').length;
|
||||
const preview = hosts.slice(0, 8).map((e) => `<div class="card" style="padding:8px 10px;cursor:default">${avatar(e.host)}<div class="meta"><div class="title">${esc(e.host.label)}</div><div class="sub">${esc(e.folder.join(' / ') || 'Stammebene')} · ${esc(hostSub(e.host))}</div></div></div>`).join('');
|
||||
const ok = await modal({
|
||||
title: 'Import aus Remote Desktop Manager',
|
||||
text: `${f.name}: ${hosts.length} Hosts (${rdpN} RDP, ${hosts.length - rdpN} SSH) in ${folders.size} Ordnern gefunden.`,
|
||||
body: `<div class="list" style="max-height:300px;overflow:auto">${preview}</div>${hosts.length > 8 ? `<p style="margin:10px 0 0">… und ${hosts.length - 8} weitere</p>` : ''}`,
|
||||
buttons: [{ label: 'Abbrechen', value: false, cls: 'ghost' }, { label: 'Importieren', value: true, cls: 'primary' }],
|
||||
});
|
||||
if (!ok) return;
|
||||
const r = await call('vault:bulkImport', entries);
|
||||
toast(`${r.hosts} Hosts importiert${r.skipped ? `, ${r.skipped} bereits vorhanden` : ''}`, 'ok');
|
||||
await reload();
|
||||
$('[data-view=hosts]').click();
|
||||
}
|
||||
|
||||
// ============================================================ Updates
|
||||
let updateInfo = null;
|
||||
async function showUpdate(info) {
|
||||
updateInfo = info;
|
||||
const b = $('#updateBadge');
|
||||
b.style.display = '';
|
||||
b.querySelector('span').textContent = `Update ${info.version}`;
|
||||
const r = await modal({
|
||||
title: `MrTerm ${info.version} ist verfügbar`,
|
||||
text: `Installiert: ${info.current}${info.asset ? ` · Paket: ${info.asset.name} (${fmtSize(info.asset.size)})` : ''}`,
|
||||
body: `${info.notes ? `<div class="mono" style="white-space:pre-wrap;max-height:240px;overflow:auto;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:10px;margin-bottom:6px">${esc(info.notes)}</div>` : ''}
|
||||
${info.kind === 'dev' ? '<p>Entwicklungsmodus: Bitte per <code>git pull</code> aktualisieren.</p>' : !info.asset ? '<p>Im Release ist kein passendes Paket für dieses System.</p>' : ''}
|
||||
<div class="upd-prog" style="display:none"><div class="transfer" style="padding:6px 0"><span class="nm">Lade herunter …</span><span class="pct"></span><div class="bar"><i></i></div></div></div>`,
|
||||
buttons: [{ label: 'Später', value: false, cls: 'ghost' }, { label: 'Release-Seite', value: 'web', cls: '' }, ...(info.asset && info.kind !== 'dev' ? [{ label: 'Jetzt installieren', value: true, cls: 'primary' }] : [])],
|
||||
});
|
||||
if (r === 'web') return api.call('shell:open', info.url);
|
||||
if (r !== true) return;
|
||||
toast('Update wird heruntergeladen …');
|
||||
try { await call('update:download'); toast('Update wird installiert, MrTerm startet neu …', 'ok'); await call('update:install'); } catch {}
|
||||
}
|
||||
api.on('update:available', (info) => showUpdate(info));
|
||||
api.on('update:progress', (done, total) => {
|
||||
const b = $('#updateBadge span');
|
||||
if (b) b.textContent = total ? `Update ${Math.round((done / total) * 100)}%` : 'Update …';
|
||||
});
|
||||
|
||||
// ============================================================ Tabs
|
||||
function addTab(session) {
|
||||
S.tabs.push(session);
|
||||
@@ -758,7 +866,7 @@ class TerminalSession {
|
||||
const st = settings();
|
||||
this.term = new Terminal({
|
||||
fontFamily: st.fontFamily, fontSize: st.fontSize, cursorStyle: st.cursorStyle, cursorBlink: st.cursorBlink,
|
||||
scrollback: st.scrollback, theme: TERM_THEMES[st.terminalTheme] || TERM_THEMES.mrterm, allowProposedApi: true, macOptionIsMeta: true,
|
||||
scrollback: st.scrollback, theme: termTheme(), allowProposedApi: true, macOptionIsMeta: true,
|
||||
});
|
||||
this.fit = new FitAddon.FitAddon();
|
||||
this.search = new SearchAddon.SearchAddon();
|
||||
@@ -836,7 +944,7 @@ class TerminalSession {
|
||||
onShow() { this.doFit(); this.term.focus(); }
|
||||
applySettings() {
|
||||
const st = settings();
|
||||
Object.assign(this.term.options, { fontFamily: st.fontFamily, fontSize: st.fontSize, cursorStyle: st.cursorStyle, cursorBlink: st.cursorBlink, scrollback: st.scrollback, theme: TERM_THEMES[st.terminalTheme] || TERM_THEMES.mrterm });
|
||||
Object.assign(this.term.options, { fontFamily: st.fontFamily, fontSize: st.fontSize, cursorStyle: st.cursorStyle, cursorBlink: st.cursorBlink, scrollback: st.scrollback, theme: termTheme() });
|
||||
this.doFit();
|
||||
}
|
||||
|
||||
@@ -956,7 +1064,7 @@ class FilePane {
|
||||
const files = await this.list(dir);
|
||||
this.cwd = dir; this.files = files; this.sel.clear();
|
||||
$('.path', this.el).value = dir;
|
||||
const roots = $('.roots', this.el); if (roots) roots.value = dir.slice(0, 3).toUpperCase();
|
||||
const roots = $('.roots', this.el); if (roots) roots.value = dir.slice(0, 2).toUpperCase() + '\';
|
||||
this.draw();
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
}
|
||||
@@ -969,8 +1077,8 @@ class FilePane {
|
||||
const arrow = (k) => (this.sort.k === k ? (this.sort.dir > 0 ? ' ▲' : ' ▼') : '');
|
||||
const rows = this.view = this.sorted();
|
||||
box.innerHTML = `<table><thead><tr><th data-s="name">Name${arrow('name')}</th><th data-s="size" style="text-align:right">Größe${arrow('size')}</th><th data-s="mtime">Geändert${arrow('mtime')}</th></tr></thead><tbody>
|
||||
<tr data-up="1"><td class="name"><span class="ico-dir">${ICONS.folder}</span><span>..</span></td><td></td><td></td></tr>
|
||||
${rows.map((f, i) => `<tr data-i="${i}" draggable="true" class="${this.sel.has(f.name) ? 'sel' : ''}"><td class="name"><span class="${f.isDir ? 'ico-dir' : 'ico-file'}">${f.isDir ? ICONS.folder : ICONS.file}</span><span>${esc(f.name)}</span></td><td class="num">${f.isDir ? '' : fmtSize(f.size)}</td><td class="date">${fmtDate(f.mtime)}</td></tr>`).join('')}</tbody></table>`;
|
||||
<tr data-up="1"><td class="name"><div><span class="ico-dir">${ICONS.folder}</span><span>..</span></div></td><td></td><td></td></tr>
|
||||
${rows.map((f, i) => `<tr data-i="${i}" draggable="true" class="${this.sel.has(f.name) ? 'sel' : ''}"><td class="name"><div><span class="${f.isDir ? 'ico-dir' : 'ico-file'}">${f.isDir ? ICONS.folder : ICONS.file}</span><span>${esc(f.name)}</span></div></td><td class="num">${f.isDir ? '' : fmtSize(f.size)}</td><td class="date">${fmtDate(f.mtime)}</td></tr>`).join('')}</tbody></table>`;
|
||||
$$('th', box).forEach((th) => (th.onclick = () => { const k = th.dataset.s; this.sort = { k, dir: this.sort.k === k ? -this.sort.dir : 1 }; this.draw(); }));
|
||||
$('tr[data-up]', box).ondblclick = () => this.up();
|
||||
$$('tr[data-i]', box).forEach((tr) => {
|
||||
@@ -1113,11 +1221,11 @@ function openPalette() {
|
||||
hosts.sort((a, b) => ((recent.indexOf(a.id) + 1 || 999) - (recent.indexOf(b.id) + 1 || 999)));
|
||||
hosts.slice(0, 12).forEach((x) => {
|
||||
items.push({ grp: 'Hosts', icon: avatar(x), label: x.label || x.address, sub: hostSub(x), run: () => connectHost(x) });
|
||||
if (x.protocol !== 'rdp' && q) items.push({ grp: 'Hosts', icon: `<div class="avatar" style="background:#2a2f45;color:#6ea8ff">${ICONS.folder}</div>`, label: `SFTP: ${x.label || x.address}`, run: () => openSftp(x) });
|
||||
if (x.protocol !== 'rdp' && q) items.push({ grp: 'Hosts', icon: `<div class="avatar" style="background:var(--icon-bg);color:var(--folder)">${ICONS.folder}</div>`, label: `SFTP: ${x.label || x.address}`, run: () => openSftp(x) });
|
||||
});
|
||||
S.vault.snippets.filter((s) => q && s.label.toLowerCase().includes(q)).slice(0, 5).forEach((s) => items.push({ grp: 'Snippets', icon: `<div class="avatar" style="background:#2a2f45;color:var(--green)">${ICONS.code}</div>`, label: s.label, sub: s.command, run: () => runSnippet(s) }));
|
||||
S.vault.snippets.filter((s) => q && s.label.toLowerCase().includes(q)).slice(0, 5).forEach((s) => items.push({ grp: 'Snippets', icon: `<div class="avatar" style="background:var(--icon-bg);color:var(--green)">${ICONS.code}</div>`, label: s.label, sub: s.command, run: () => runSnippet(s) }));
|
||||
[['Neuer Host', () => { activateTab('home'); editHost({}); }], ['Schlüssel generieren', generateKey], ['Einstellungen', () => $('[data-view=settings]').click()]]
|
||||
.filter(([l]) => !q || l.toLowerCase().includes(q)).forEach(([l, run]) => items.push({ grp: 'Aktionen', icon: `<div class="avatar" style="background:#2a2f45;color:var(--accent-2)">${ICONS.plus}</div>`, label: l, run }));
|
||||
.filter(([l]) => !q || l.toLowerCase().includes(q)).forEach(([l, run]) => items.push({ grp: 'Aktionen', icon: `<div class="avatar" style="background:var(--icon-bg);color:var(--accent-2)">${ICONS.plus}</div>`, label: l, run }));
|
||||
idx = Math.min(idx, Math.max(0, items.length - 1));
|
||||
let last = '';
|
||||
res.innerHTML = '';
|
||||
@@ -1182,3 +1290,4 @@ api.on('toast', (m, t) => toast(m, t));
|
||||
api.on('win:state', (max) => { $('#winMax').innerHTML = max ? '<svg viewBox="0 0 12 12"><rect x="2" y="4" width="6" height="6"/><path d="M4 4V2h6v6H8"/></svg>' : '<svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8"/></svg>'; });
|
||||
|
||||
reload();
|
||||
$('#updateBadge').onclick = () => updateInfo && showUpdate(updateInfo);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<div id="app">
|
||||
<header id="titlebar">
|
||||
<div class="brand"><span class="logo">>_</span>MrTerm</div>
|
||||
<div class="brand"><img class="logo" src="logo.png" alt="" />Mr<span class="brand-term">Term</span></div>
|
||||
<div id="tabs">
|
||||
<button class="tab home active" data-tab="home" title="Vault">
|
||||
<svg viewBox="0 0 24 24"><path d="M4 5h16v14H4z M4 9h16" /></svg><span>Vault</span>
|
||||
@@ -35,6 +35,7 @@
|
||||
<button class="nav" data-view="known"><svg viewBox="0 0 24 24"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg><span>Known Hosts</span></button>
|
||||
<button class="nav" data-view="history"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span>Verlauf</span></button>
|
||||
<div class="spacer"></div>
|
||||
<button id="updateBadge" class="nav update-badge" style="display:none"><svg viewBox="0 0 24 24"><path d="M12 4v12M6 10l6 6 6-6M4 20h16"/></svg><span>Update</span></button>
|
||||
<div id="vaultBadge" class="vault-badge"></div>
|
||||
<button class="nav" data-view="settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg><span>Einstellungen</span></button>
|
||||
</nav>
|
||||
@@ -56,6 +57,7 @@
|
||||
<script src="../../node_modules/@xterm/addon-web-links/lib/addon-web-links.js"></script>
|
||||
<script src="../../node_modules/@xterm/addon-search/lib/addon-search.js"></script>
|
||||
<script src="themes.js"></script>
|
||||
<script src="rdm-import.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,142 @@
|
||||
// Parser für Exporte aus Devolutions Remote Desktop Manager.
|
||||
// Unterstützt: .rdm / .xml (Export "Remote Desktop Manager-Datei"), .json und .csv.
|
||||
// Liefert [{ folder: ['A','B'], host?: {...} }] für vault:bulkImport.
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
// Numerische ConnectionType-Werte aus RDM-JSON (die gängigsten)
|
||||
const NUMERIC_TYPES = { 1: 'RDPConfigured', 25: 'Group', 77: 'SSHShell', 24: 'Putty', 108: 'SSHShell' };
|
||||
|
||||
function classify(type) {
|
||||
const t = String(NUMERIC_TYPES[type] || type || '').toLowerCase();
|
||||
if (!t) return null;
|
||||
if (t === 'group' || t === 'folder' || t === 'repository') return 'folder';
|
||||
if (t.startsWith('rdp') || t === 'remotedesktop' || t === 'microsoftremotedesktop') return 'rdp';
|
||||
if (t.includes('ssh') || t === 'putty' || t === 'terminal' || t === 'sshshell') return 'ssh';
|
||||
return null; // VNC, Web, Credentials, Dokumente … werden übersprungen
|
||||
}
|
||||
|
||||
const splitPath = (p) => String(p || '').split(/[\\/]/).map((s) => s.trim()).filter(Boolean);
|
||||
|
||||
function splitHostPort(url, defPort) {
|
||||
let address = String(url || '').trim().replace(/^(rdp|ssh|https?):\/\//i, '').replace(/\/.*$/, '');
|
||||
let port;
|
||||
const m = address.match(/^\[(.+)\](?::(\d+))?$/) || address.match(/^([^:]+):(\d+)$/);
|
||||
if (m) { address = m[1]; port = m[2] ? Number(m[2]) : undefined; }
|
||||
return { address, port: port && port !== defPort ? port : undefined };
|
||||
}
|
||||
|
||||
function makeEntry(r) {
|
||||
const kind = classify(r.type);
|
||||
if (!kind) return null;
|
||||
let folder = splitPath(r.group);
|
||||
if (kind === 'folder') {
|
||||
// RDM speichert bei Ordnern meist den vollständigen Pfad inkl. eigenem Namen im Feld "Group"
|
||||
if (r.name && folder[folder.length - 1] !== r.name) folder.push(r.name);
|
||||
return folder.length ? { folder } : null;
|
||||
}
|
||||
const defPort = kind === 'rdp' ? 3389 : 22;
|
||||
const hp = splitHostPort(r.host || r.url, defPort);
|
||||
if (!hp.address) return null;
|
||||
const port = Number(r.port) && Number(r.port) !== defPort ? Number(r.port) : hp.port;
|
||||
const host = {
|
||||
protocol: kind,
|
||||
label: r.name || hp.address,
|
||||
address: hp.address,
|
||||
port,
|
||||
username: r.username || undefined,
|
||||
domain: kind === 'rdp' ? r.domain || undefined : undefined,
|
||||
tags: ['rdm'],
|
||||
};
|
||||
if (r.password) host.password = r.password;
|
||||
if (r.description) host.notes = r.description;
|
||||
return { folder, host };
|
||||
}
|
||||
|
||||
// ---------- XML ----------
|
||||
function child(el, path) {
|
||||
let cur = el;
|
||||
for (const name of path.split('/')) {
|
||||
cur = [...(cur?.children || [])].find((c) => c.localName.toLowerCase() === name.toLowerCase());
|
||||
if (!cur) return '';
|
||||
}
|
||||
return cur.textContent.trim();
|
||||
}
|
||||
const firstOf = (el, paths) => { for (const p of paths) { const v = child(el, p); if (v) return v; } return ''; };
|
||||
|
||||
function parseXml(text) {
|
||||
const doc = new DOMParser().parseFromString(text, 'application/xml');
|
||||
if (doc.querySelector('parsererror')) throw new Error('Datei ist kein gültiges XML.');
|
||||
const conns = [...doc.getElementsByTagName('Connection')].filter((c) => child(c, 'ConnectionType'));
|
||||
return conns.map((c) => makeEntry({
|
||||
type: child(c, 'ConnectionType'),
|
||||
name: child(c, 'Name'),
|
||||
group: firstOf(c, ['Group', 'GroupName', 'Folder']),
|
||||
url: firstOf(c, ['Url', 'Host', 'HostName']),
|
||||
host: firstOf(c, ['Terminal/Host', 'Putty/Host', 'SSHTunnel/Host', 'RDP/Host']),
|
||||
port: firstOf(c, ['Terminal/HostPort', 'Putty/Port', 'RDP/Port', 'Port', 'HostPort']),
|
||||
username: firstOf(c, ['RDP/UserName', 'Terminal/Username', 'Terminal/UserName', 'Putty/UserName', 'Putty/Username', 'UserName', 'Username']),
|
||||
domain: firstOf(c, ['RDP/Domain', 'Domain']),
|
||||
description: child(c, 'Description'),
|
||||
})).filter(Boolean);
|
||||
}
|
||||
|
||||
// ---------- JSON ----------
|
||||
function parseJson(text) {
|
||||
const data = JSON.parse(text);
|
||||
const list = Array.isArray(data) ? data : data.Connections || data.connections || data.Entries || [];
|
||||
const g = (o, ...keys) => { for (const k of keys) { const v = k.split('.').reduce((a, p) => a?.[p], o); if (v != null && v !== '') return v; } return ''; };
|
||||
return list.map((c) => makeEntry({
|
||||
type: g(c, 'ConnectionType', 'Type'),
|
||||
name: g(c, 'Name'),
|
||||
group: g(c, 'Group', 'GroupName', 'Folder'),
|
||||
url: g(c, 'Url', 'Host'),
|
||||
host: g(c, 'Terminal.Host', 'Putty.Host', 'RDP.Host'),
|
||||
port: g(c, 'Terminal.HostPort', 'Putty.Port', 'RDP.Port', 'Port'),
|
||||
username: g(c, 'RDP.UserName', 'Terminal.Username', 'Putty.UserName', 'UserName', 'Username'),
|
||||
domain: g(c, 'RDP.Domain', 'Domain'),
|
||||
description: g(c, 'Description'),
|
||||
})).filter(Boolean);
|
||||
}
|
||||
|
||||
// ---------- CSV ----------
|
||||
function csvRows(text) {
|
||||
const delim = (text.split('\n')[0].match(/;/g) || []).length > (text.split('\n')[0].match(/,/g) || []).length ? ';' : ',';
|
||||
const rows = []; let row = [], cur = '', q = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (q) {
|
||||
if (ch === '"' && text[i + 1] === '"') { cur += '"'; i++; }
|
||||
else if (ch === '"') q = false;
|
||||
else cur += ch;
|
||||
} else if (ch === '"') q = true;
|
||||
else if (ch === delim) { row.push(cur); cur = ''; }
|
||||
else if (ch === '\n' || ch === '\r') { if (ch === '\r' && text[i + 1] === '\n') i++; row.push(cur); rows.push(row); row = []; cur = ''; }
|
||||
else cur += ch;
|
||||
}
|
||||
if (cur || row.length) { row.push(cur); rows.push(row); }
|
||||
return rows.filter((r) => r.some((c) => c.trim()));
|
||||
}
|
||||
function parseCsv(text) {
|
||||
const [head, ...rows] = csvRows(text);
|
||||
const idx = (...names) => head.findIndex((h) => names.includes(h.trim().toLowerCase().replace(/[\s_]/g, '')));
|
||||
const col = {
|
||||
type: idx('connectiontype', 'type', 'protocol'), name: idx('name', 'displayname'), group: idx('group', 'folder', 'groupname'),
|
||||
host: idx('host', 'hostname', 'url', 'computername', 'address'), port: idx('port', 'hostport'),
|
||||
username: idx('username', 'user', 'login'), domain: idx('domain'), password: idx('password'), description: idx('description'),
|
||||
};
|
||||
if (col.host < 0 && col.type < 0) throw new Error('CSV-Spalten nicht erkannt (erwartet u. a. Name, Host, ConnectionType, Group).');
|
||||
return rows.map((r) => {
|
||||
const v = (k) => (col[k] >= 0 ? (r[col[k]] || '').trim() : '');
|
||||
return makeEntry({ type: v('type') || 'RDPConfigured', name: v('name'), group: v('group'), url: v('host'), port: v('port'), username: v('username'), domain: v('domain'), password: v('password'), description: v('description') });
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
window.parseRdmExport = function (name, content) {
|
||||
const t = content.trimStart();
|
||||
if (/\.json$/i.test(name) || t.startsWith('{') || t.startsWith('[')) return parseJson(content);
|
||||
if (/\.csv$/i.test(name)) return parseCsv(content);
|
||||
if (t.startsWith('<')) return parseXml(content);
|
||||
return parseCsv(content);
|
||||
};
|
||||
})();
|
||||
+86
-27
@@ -13,12 +13,59 @@
|
||||
--green: #3ecf8e;
|
||||
--red: #ff5f6d;
|
||||
--orange: #ffb454;
|
||||
--accent-3: #b36bff;
|
||||
--chrome: #0f1117;
|
||||
--chrome-line: #1f2330;
|
||||
--hover: #1a1d27;
|
||||
--nav-active: #1f2233;
|
||||
--icon-bg: #2a2f45;
|
||||
--folder: #6ea8ff;
|
||||
--row-line: #1c1f29;
|
||||
--overlay: #13151ce6;
|
||||
--tint: 255 255 255;
|
||||
--radius: 10px;
|
||||
--titlebar: 42px;
|
||||
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, 'Noto Sans', sans-serif;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
/* ---------- App-Designs ---------- */
|
||||
:root { --accent-2: color-mix(in srgb, var(--accent) 78%, white); }
|
||||
[data-theme='navy'] {
|
||||
--bg: #141729; --bg-2: #181c33; --panel: #1c2139; --card: #212742; --card-hover: #29304f; --border: #2e3657;
|
||||
--text: #e8eaf6; --muted: #8e95b8; --faint: #5f6690; --accent: #21c7a8; --accent-3: #3a8dff;
|
||||
--chrome: #0f1122; --chrome-line: #20264a; --hover: #1b2040; --nav-active: #232a52; --icon-bg: #2a3260; --row-line: #1c2140; --overlay: #141729e6;
|
||||
}
|
||||
[data-theme='nord'] {
|
||||
--bg: #2e3440; --bg-2: #323846; --panel: #3b4252; --card: #3b4252; --card-hover: #434c5e; --border: #4c566a;
|
||||
--text: #eceff4; --muted: #b5bdcc; --faint: #7b8598; --accent: #88c0d0; --accent-3: #81a1c1; --green: #a3be8c; --red: #bf616a; --orange: #d08770;
|
||||
--chrome: #272c36; --chrome-line: #3b4252; --hover: #3b4252; --nav-active: #434c5e; --icon-bg: #4c566a; --folder: #81a1c1; --row-line: #3b4252; --overlay: #2e3440e6;
|
||||
}
|
||||
[data-theme='dracula'] {
|
||||
--bg: #282a36; --bg-2: #2c2e3b; --panel: #313343; --card: #343746; --card-hover: #3e4153; --border: #44475a;
|
||||
--text: #f8f8f2; --muted: #b3b6c8; --faint: #6272a4; --accent: #bd93f9; --accent-3: #ff79c6; --green: #50fa7b; --red: #ff5555; --orange: #ffb86c;
|
||||
--chrome: #21222c; --chrome-line: #343746; --hover: #2f3140; --nav-active: #383a4c; --icon-bg: #44475a; --folder: #8be9fd; --row-line: #313343; --overlay: #282a36e6;
|
||||
}
|
||||
[data-theme='mocha'] {
|
||||
--bg: #1e1e2e; --bg-2: #232334; --panel: #28283b; --card: #2a2a3d; --card-hover: #313244; --border: #3b3b52;
|
||||
--text: #cdd6f4; --muted: #a6adc8; --faint: #6c7086; --accent: #cba6f7; --accent-3: #f5c2e7; --green: #a6e3a1; --red: #f38ba8; --orange: #fab387;
|
||||
--chrome: #181825; --chrome-line: #292939; --hover: #252536; --nav-active: #313244; --icon-bg: #3a3a55; --folder: #89b4fa; --row-line: #28283b; --overlay: #1e1e2ee6;
|
||||
}
|
||||
[data-theme='forest'] {
|
||||
--bg: #141a17; --bg-2: #18201c; --panel: #1c2621; --card: #1f2a24; --card-hover: #26342c; --border: #2d3d34;
|
||||
--text: #e3ece6; --muted: #92a69a; --faint: #5e7266; --accent: #4cc38a; --accent-3: #b5e655; --folder: #7cc6a5;
|
||||
--chrome: #0f1411; --chrome-line: #1f2a24; --hover: #1a231e; --nav-active: #213029; --icon-bg: #294034; --row-line: #1c2621; --overlay: #141a17e6;
|
||||
}
|
||||
[data-theme='light'] {
|
||||
--bg: #f6f7fb; --bg-2: #eef0f6; --panel: #ffffff; --card: #ffffff; --card-hover: #f0f2fa; --border: #dde1ec;
|
||||
--text: #1f2330; --muted: #5d6477; --faint: #9097aa; --accent: #5b67f1; --accent-3: #9b59f5; --green: #1f9d63; --red: #d63a4a; --orange: #c97a00;
|
||||
--chrome: #e9ecf4; --chrome-line: #d8dce8; --hover: #dfe3ee; --nav-active: #d9ddf5; --icon-bg: #e6e9f7; --folder: #3d7de0; --row-line: #eceef5; --overlay: #f6f7fbe6;
|
||||
--tint: 0 0 0;
|
||||
--accent-2: color-mix(in srgb, var(--accent) 85%, black);
|
||||
}
|
||||
[data-theme='light'] .card { border-color: var(--border); }
|
||||
[data-theme='light'] .avatar .proto { background: var(--panel); }
|
||||
[data-theme='light'] ::-webkit-scrollbar-thumb { background: #c5cad8; background-clip: padding-box; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: var(--bg); overflow: hidden; }
|
||||
button { font: inherit; color: inherit; cursor: pointer; }
|
||||
@@ -31,15 +78,17 @@ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width:
|
||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||
|
||||
/* ---------- Titlebar & Tabs ---------- */
|
||||
#titlebar { height: var(--titlebar); display: flex; align-items: center; background: #0f1117; -webkit-app-region: drag; border-bottom: 1px solid #1f2330; flex: none; }
|
||||
#titlebar { height: var(--titlebar); display: flex; align-items: center; background: var(--chrome); -webkit-app-region: drag; border-bottom: 1px solid var(--chrome-line); flex: none; }
|
||||
.brand { width: 212px; padding-left: 16px; font-weight: 700; letter-spacing: .3px; display: flex; align-items: center; gap: 10px; flex: none; }
|
||||
.logo { background: linear-gradient(135deg, var(--accent), #b36bff); color: white; font-family: monospace; font-size: 11px; padding: 3px 6px; border-radius: 6px; }
|
||||
.logo { width: 24px; height: 24px; border-radius: 6px; }
|
||||
.brand { gap: 8px !important; }
|
||||
.brand-term { color: #5ef0c4; margin-left: -8px; }
|
||||
#tabs { display: flex; gap: 4px; height: 100%; align-items: flex-end; overflow-x: auto; min-width: 0; -webkit-app-region: no-drag; }
|
||||
#tabs::-webkit-scrollbar { height: 0; }
|
||||
.tab { -webkit-app-region: no-drag; display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px 0 12px; border: none; background: transparent; border-radius: 8px 8px 0 0; color: var(--muted); max-width: 220px; min-width: 110px; white-space: nowrap; position: relative; }
|
||||
.tab.home { min-width: 0; }
|
||||
.tab span { overflow: hidden; text-overflow: ellipsis; flex: 1; text-align: left; }
|
||||
.tab:hover { background: #1a1d27; color: var(--text); }
|
||||
.tab:hover { background: var(--hover); color: var(--text); }
|
||||
.tab.active { background: var(--bg); color: var(--text); }
|
||||
.tab .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--faint); flex: none; }
|
||||
.tab .dot.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
||||
@@ -47,25 +96,25 @@ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width:
|
||||
.tab .dot.wait { background: var(--orange); animation: pulse 1s infinite; }
|
||||
.tab .x { opacity: 0; border: none; background: none; padding: 2px; border-radius: 4px; color: var(--muted); display: flex; }
|
||||
.tab:hover .x, .tab.active .x { opacity: 1; }
|
||||
.tab .x:hover { background: #ffffff18; color: var(--text); }
|
||||
.tab .x:hover { background: rgb(var(--tint) / 0.094); color: var(--text); }
|
||||
.tab .x svg { width: 12px; height: 12px; }
|
||||
@keyframes pulse { 50% { opacity: .35; } }
|
||||
.icon-btn { -webkit-app-region: no-drag; border: none; background: transparent; color: var(--muted); width: 30px; height: 30px; border-radius: 8px; font-size: 18px; margin-left: 4px; flex: none; }
|
||||
.icon-btn:hover { background: #1f2330; color: var(--text); }
|
||||
.icon-btn:hover { background: var(--chrome-line); color: var(--text); }
|
||||
.drag-fill { flex: 1; min-width: 40px; height: 100%; }
|
||||
.win-ctrls { display: flex; height: 100%; -webkit-app-region: no-drag; }
|
||||
.win-ctrls button { width: 46px; border: none; background: transparent; color: var(--muted); display: flex; align-items: center; justify-content: center; }
|
||||
.win-ctrls button svg { width: 12px; height: 12px; stroke-width: 1.2; }
|
||||
.win-ctrls button:hover { background: #ffffff12; color: var(--text); }
|
||||
.win-ctrls button:hover { background: rgb(var(--tint) / 0.071); color: var(--text); }
|
||||
.win-ctrls button.close:hover { background: #e81123; color: white; }
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
#body { flex: 1; display: flex; min-height: 0; position: relative; }
|
||||
#sidebar { width: 212px; background: #0f1117; padding: 10px 10px 12px; display: flex; flex-direction: column; gap: 2px; border-right: 1px solid #1f2330; flex: none; }
|
||||
#sidebar { width: 212px; background: var(--chrome); padding: 10px 10px 12px; display: flex; flex-direction: column; gap: 2px; border-right: 1px solid var(--chrome-line); flex: none; }
|
||||
#sidebar .spacer { flex: 1; }
|
||||
.nav { display: flex; align-items: center; gap: 12px; border: none; background: none; padding: 9px 12px; border-radius: 8px; color: var(--muted); text-align: left; font-weight: 500; }
|
||||
.nav:hover { background: #1a1d27; color: var(--text); }
|
||||
.nav.active { background: #1f2233; color: var(--text); }
|
||||
.nav:hover { background: var(--hover); color: var(--text); }
|
||||
.nav.active { background: var(--nav-active); color: var(--text); }
|
||||
.nav.active svg { color: var(--accent-2); }
|
||||
.vault-badge { font-size: 11px; color: var(--faint); padding: 8px 12px; display: flex; gap: 6px; align-items: center; }
|
||||
.vault-badge svg { width: 13px; height: 13px; }
|
||||
@@ -87,7 +136,7 @@ body.in-session #sidebar { display: none; }
|
||||
.btn.primary { background: var(--accent); border-color: var(--accent); color: white; }
|
||||
.btn.primary:hover { background: var(--accent-2); }
|
||||
.btn.danger { color: var(--red); }
|
||||
.btn.danger:hover { background: #ff5f6d1a; }
|
||||
.btn.danger:hover { background: color-mix(in srgb, var(--red) 10%, transparent); }
|
||||
.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); }
|
||||
.btn.ghost:hover { background: var(--card); color: var(--text); }
|
||||
.btn.sm { height: 28px; padding: 0 10px; font-size: 12px; }
|
||||
@@ -97,7 +146,7 @@ body.in-session #sidebar { display: none; }
|
||||
.section-title .count { background: var(--card); padding: 1px 7px; border-radius: 10px; font-size: 11px; }
|
||||
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.chip { border: 1px solid var(--border); background: transparent; color: var(--muted); padding: 4px 10px; border-radius: 20px; font-size: 12px; }
|
||||
.chip.active, .chip:hover { background: #6e7bff22; border-color: var(--accent); color: var(--text); }
|
||||
.chip.active, .chip:hover { background: color-mix(in srgb, var(--accent) 13%, transparent); border-color: var(--accent); color: var(--text); }
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
|
||||
@@ -110,14 +159,14 @@ body.in-session #sidebar { display: none; }
|
||||
.card .actions { display: flex; gap: 2px; opacity: 0; transition: opacity .12s; }
|
||||
.card:hover .actions { opacity: 1; }
|
||||
.card .actions button { border: none; background: none; color: var(--muted); padding: 6px; border-radius: 6px; display: flex; }
|
||||
.card .actions button:hover { background: #ffffff14; color: var(--text); }
|
||||
.card .actions button:hover { background: rgb(var(--tint) / 0.078); color: var(--text); }
|
||||
.card .actions svg { width: 16px; height: 16px; }
|
||||
.avatar { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 15px; color: white; flex: none; position: relative; }
|
||||
.avatar svg { width: 20px; height: 20px; }
|
||||
.avatar .proto { position: absolute; right: -4px; bottom: -4px; font-size: 8px; background: #0f1117; color: var(--muted); padding: 1px 3px; border-radius: 4px; border: 1px solid var(--border); font-weight: 700; }
|
||||
.group-card .avatar { background: #2a2f45; color: var(--accent-2); }
|
||||
.avatar .proto { position: absolute; right: -4px; bottom: -4px; font-size: 8px; background: var(--chrome); color: var(--muted); padding: 1px 3px; border-radius: 4px; border: 1px solid var(--border); font-weight: 700; }
|
||||
.group-card .avatar { background: var(--icon-bg); color: var(--accent-2); }
|
||||
.tags { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
|
||||
.tag { font-size: 10px; background: #ffffff0f; color: var(--muted); padding: 1px 6px; border-radius: 4px; }
|
||||
.tag { font-size: 10px; background: rgb(var(--tint) / 0.059); color: var(--muted); padding: 1px 6px; border-radius: 4px; }
|
||||
.empty { text-align: center; color: var(--muted); padding: 70px 20px; }
|
||||
.empty .big { width: 64px; height: 64px; margin: 0 auto 16px; border-radius: 18px; background: var(--card); display: flex; align-items: center; justify-content: center; color: var(--accent-2); }
|
||||
.empty .big svg { width: 30px; height: 30px; }
|
||||
@@ -128,8 +177,8 @@ body.in-session #sidebar { display: none; }
|
||||
.list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.list .card { padding: 11px 14px; }
|
||||
.mono { font-family: 'Cascadia Code', 'JetBrains Mono', Consolas, monospace; font-size: 12px; }
|
||||
.status-pill { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: #ffffff10; color: var(--muted); }
|
||||
.status-pill.on { background: #3ecf8e22; color: var(--green); }
|
||||
.status-pill { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: rgb(var(--tint) / 0.063); color: var(--muted); }
|
||||
.status-pill.on { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }
|
||||
|
||||
/* ---------- Drawer (Editor) ---------- */
|
||||
.drawer { position: absolute; top: 0; right: 0; bottom: 0; width: 400px; background: var(--bg-2); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform .18s ease; z-index: 20; display: flex; flex-direction: column; box-shadow: -20px 0 40px #0006; }
|
||||
@@ -167,7 +216,7 @@ body.in-session #sidebar { display: none; }
|
||||
.session .term-wrap { flex: 1; position: relative; min-height: 0; display: flex; }
|
||||
.session .term { flex: 1; padding: 8px 0 4px 10px; min-width: 0; }
|
||||
.session .term .xterm { height: 100%; }
|
||||
.session .overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; background: #13151ce6; z-index: 3; }
|
||||
.session .overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; background: var(--overlay); z-index: 3; }
|
||||
.session .overlay .spinner { width: 36px; height: 36px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.9s linear infinite; }
|
||||
.session .overlay .msg { color: var(--muted); max-width: 520px; text-align: center; }
|
||||
.session .overlay .msg.err { color: var(--red); }
|
||||
@@ -191,17 +240,18 @@ body.in-session #sidebar { display: none; }
|
||||
.pane .phead input { flex: 1; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px; outline: none; min-width: 60px; font-family: 'Cascadia Code', Consolas, monospace; font-size: 12px; }
|
||||
.pane .phead select { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 4px; }
|
||||
.pane .files { flex: 1; overflow: auto; }
|
||||
.pane.dragover .files { outline: 2px dashed var(--accent); outline-offset: -6px; background: #6e7bff0d; }
|
||||
.pane.dragover .files { outline: 2px dashed var(--accent); outline-offset: -6px; background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
.pane table { width: 100%; border-collapse: collapse; }
|
||||
.pane th { position: sticky; top: 0; background: var(--bg); text-align: left; font-size: 11px; color: var(--faint); font-weight: 600; padding: 7px 10px; text-transform: uppercase; letter-spacing: .4px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.pane td { padding: 6px 10px; white-space: nowrap; border-bottom: 1px solid #1c1f29; }
|
||||
.pane td.name { display: flex; align-items: center; gap: 8px; max-width: 0; width: 100%; }
|
||||
.pane td.name span { overflow: hidden; text-overflow: ellipsis; }
|
||||
.pane td { padding: 6px 10px; white-space: nowrap; border-bottom: 1px solid var(--row-line); }
|
||||
.pane td.name { max-width: 0; width: 100%; }
|
||||
.pane td.name > div { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.pane td.name span:last-child { overflow: hidden; text-overflow: ellipsis; }
|
||||
.pane td.num { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.pane td.date { color: var(--muted); }
|
||||
.pane tr:hover td { background: #ffffff06; }
|
||||
.pane tr.sel td { background: #6e7bff26; }
|
||||
.pane .ico-dir { color: #6ea8ff; }
|
||||
.pane tr:hover td { background: rgb(var(--tint) / 0.024); }
|
||||
.pane tr.sel td { background: color-mix(in srgb, var(--accent) 15%, transparent); }
|
||||
.pane .ico-dir { color: var(--folder); }
|
||||
.pane .ico-file { color: var(--faint); }
|
||||
.pane-empty { flex: 1; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 14px; color: var(--muted); }
|
||||
.host-picker { width: min(520px, 90%); max-height: 60%; overflow: auto; display: flex; flex-direction: column; gap: 6px; }
|
||||
@@ -228,12 +278,12 @@ body.in-session #sidebar { display: none; }
|
||||
.palette .res { max-height: 50vh; overflow: auto; padding: 8px; }
|
||||
.palette .item { display: flex; gap: 12px; align-items: center; padding: 8px 10px; border-radius: 8px; cursor: pointer; }
|
||||
.palette .item .avatar { width: 32px; height: 32px; font-size: 13px; border-radius: 8px; }
|
||||
.palette .item.active { background: #6e7bff26; }
|
||||
.palette .item.active { background: color-mix(in srgb, var(--accent) 15%, transparent); }
|
||||
.palette .item .k { margin-left: auto; color: var(--faint); font-size: 11px; }
|
||||
.palette .grp { font-size: 11px; color: var(--faint); padding: 8px 10px 4px; text-transform: uppercase; letter-spacing: .5px; }
|
||||
.ctx { position: fixed; z-index: 200; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 5px; min-width: 190px; box-shadow: 0 14px 40px #000a; }
|
||||
.ctx button { display: flex; align-items: center; gap: 10px; width: 100%; border: none; background: none; padding: 7px 10px; border-radius: 6px; text-align: left; }
|
||||
.ctx button:hover { background: #6e7bff33; }
|
||||
.ctx button:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
|
||||
.ctx button.danger { color: var(--red); }
|
||||
.ctx hr { border: none; border-top: 1px solid var(--border); margin: 4px 0; }
|
||||
.ctx svg { width: 15px; height: 15px; color: var(--muted); }
|
||||
@@ -250,3 +300,12 @@ body.in-session #sidebar { display: none; }
|
||||
.theme-prev.active { border-color: var(--accent); }
|
||||
.theme-prev .nm { font-family: 'Segoe UI', sans-serif; font-weight: 600; margin-bottom: 4px; }
|
||||
kbd { background: var(--card); border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 4px; padding: 0 5px; font-size: 11px; font-family: inherit; }
|
||||
.update-badge { color: var(--accent-2) !important; background: color-mix(in srgb, var(--accent) 14%, transparent) !important; }
|
||||
.design-prev { padding: 8px; }
|
||||
.design-prev .dp { display: flex; gap: 6px; height: 62px; margin-bottom: 8px; }
|
||||
.design-prev .dp i { width: 22%; border-radius: 4px; }
|
||||
.design-prev .dp > div { flex: 1; display: flex; flex-direction: column; gap: 6px; }
|
||||
.design-prev .dp b { flex: 1; border-radius: 4px; position: relative; }
|
||||
.design-prev .dp s { position: absolute; left: 6px; top: 50%; transform: translateY(-50%); width: 12px; height: 12px; border-radius: 3px; }
|
||||
.design-prev .nm { margin: 0; }
|
||||
[data-theme='light'] .brand-term { color: #14a37a; }
|
||||
|
||||
@@ -22,3 +22,20 @@ window.TERM_THEMES = {
|
||||
black: '#2b2f3a', red: '#d63a4a', green: '#1f9d63', yellow: '#b68600', blue: '#3d63dd', magenta: '#9d45c9', cyan: '#138a9a', white: '#c9ccd6',
|
||||
brightBlack: '#6b7080', brightRed: '#e8505f', brightGreen: '#27b474', brightYellow: '#cc9a00', brightBlue: '#5a7cf0', brightMagenta: '#b25bdd', brightCyan: '#1ca3b5', brightWhite: '#ffffff' },
|
||||
};
|
||||
|
||||
TERM_THEMES.mocha = { name: 'Catppuccin Mocha', background: '#1e1e2e', foreground: '#cdd6f4', cursor: '#f5e0dc', selectionBackground: '#585b7066',
|
||||
black: '#45475a', red: '#f38ba8', green: '#a6e3a1', yellow: '#f9e2af', blue: '#89b4fa', magenta: '#f5c2e7', cyan: '#94e2d5', white: '#bac2de',
|
||||
brightBlack: '#585b70', brightRed: '#f38ba8', brightGreen: '#a6e3a1', brightYellow: '#f9e2af', brightBlue: '#89b4fa', brightMagenta: '#f5c2e7', brightCyan: '#94e2d5', brightWhite: '#a6adc8' };
|
||||
TERM_THEMES.navy = { ...TERM_THEMES.mrterm, name: 'Navy', background: '#141729', cursor: '#21c7a8', cursorAccent: '#141729', selectionBackground: '#21c7a844', black: '#212742' };
|
||||
TERM_THEMES.forest = { ...TERM_THEMES.mrterm, name: 'Forest', background: '#141a17', foreground: '#e3ece6', cursor: '#4cc38a', cursorAccent: '#141a17', selectionBackground: '#4cc38a44', black: '#1f2a24' };
|
||||
|
||||
// App-Designs: Vorschaufarben + passendes Terminal-Schema (für "Passend zum Design")
|
||||
window.APP_THEMES = {
|
||||
midnight: { name: 'Midnight', bg: '#13151c', side: '#0f1117', card: '#212533', accent: '#6e7bff', text: '#e6e8ef', term: 'mrterm' },
|
||||
navy: { name: 'Navy', bg: '#141729', side: '#0f1122', card: '#212742', accent: '#21c7a8', text: '#e8eaf6', term: 'navy' },
|
||||
nord: { name: 'Nord', bg: '#2e3440', side: '#272c36', card: '#3b4252', accent: '#88c0d0', text: '#eceff4', term: 'nord' },
|
||||
dracula: { name: 'Dracula', bg: '#282a36', side: '#21222c', card: '#343746', accent: '#bd93f9', text: '#f8f8f2', term: 'dracula' },
|
||||
mocha: { name: 'Catppuccin', bg: '#1e1e2e', side: '#181825', card: '#2a2a3d', accent: '#cba6f7', text: '#cdd6f4', term: 'mocha' },
|
||||
forest: { name: 'Forest', bg: '#141a17', side: '#0f1411', card: '#1f2a24', accent: '#4cc38a', text: '#e3ece6', term: 'forest' },
|
||||
light: { name: 'Hell', bg: '#f6f7fb', side: '#e9ecf4', card: '#ffffff', accent: '#5b67f1', text: '#1f2330', term: 'light' },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user