Themes: share app theme colors between desktop and Android (app-themes.css), add theme picker and accent color to the Android app, and add Windows XP, Windows 11, macOS (Liquid Glass) and Cyberpunk 2077 themes with matching terminal schemes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+51
-10
@@ -193,10 +193,12 @@ async function reload() {
|
||||
S.lock = await api.call('lock:status');
|
||||
I18N.setLanguage(S.lock.meta.language || 'auto', S.info.locale);
|
||||
applyStatic();
|
||||
applyTheme(S.lock.meta);
|
||||
if (S.lock.locked) return showLock();
|
||||
S.vault = await api.call('vault:get');
|
||||
I18N.setLanguage(settings().language || 'auto', S.info.locale);
|
||||
applyStatic();
|
||||
applyTheme();
|
||||
S.sync = await api.call('sync:status').catch(() => null);
|
||||
$('#main').hidden = false;
|
||||
render();
|
||||
@@ -410,7 +412,33 @@ const KEYS = [
|
||||
];
|
||||
const mods = { ctrl: 0, alt: 0 }; // 0 aus, 1 einmal, 2 festgestellt
|
||||
|
||||
function termTheme() { return TERM_THEMES[settings().terminalTheme] || TERM_THEMES.mrterm; }
|
||||
function termTheme() {
|
||||
const id = settings().terminalTheme;
|
||||
if (!id || id === 'auto') return TERM_THEMES[(APP_THEMES[settings().appTheme] || APP_THEMES.midnight).term] || TERM_THEMES.mrterm;
|
||||
return TERM_THEMES[id] || TERM_THEMES.mrterm;
|
||||
}
|
||||
|
||||
// App-Design (gemeinsam mit dem Desktop: lib/app-themes.css + APP_THEMES)
|
||||
function applyTheme(st = settings()) {
|
||||
const root = document.documentElement;
|
||||
const t = APP_THEMES[st.appTheme] || APP_THEMES.midnight;
|
||||
if (st.appTheme && st.appTheme !== 'midnight' && APP_THEMES[st.appTheme]) root.dataset.theme = st.appTheme; else delete root.dataset.theme;
|
||||
if (st.accent) root.style.setProperty('--accent', st.accent); else root.style.removeProperty('--accent');
|
||||
root.classList.toggle('light', !!t.light);
|
||||
S.sessions?.forEach((x) => { x.term.options.theme = termTheme(); });
|
||||
document.documentElement.style.setProperty('--term-bg', termTheme().background);
|
||||
syncBars();
|
||||
}
|
||||
// Statusleiste passend zur sichtbaren Fläche (Terminal oder App)
|
||||
function syncBars() {
|
||||
const t = APP_THEMES[settings().appTheme] || APP_THEMES.midnight;
|
||||
const inTerm = !$('#termScreen').hidden;
|
||||
const bg = inTerm ? getComputedStyle(document.querySelector('.term-head')).backgroundColor : getComputedStyle(document.body).backgroundColor;
|
||||
const rgb = bg.match(/\d+/g)?.map(Number) || [20, 22, 29];
|
||||
const hex = '#' + rgb.slice(0, 3).map((n) => n.toString(16).padStart(2, '0')).join('');
|
||||
const light = inTerm ? (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 > 150 : !!t.light;
|
||||
Native.bars?.({ light, color: hex }).catch(() => {});
|
||||
}
|
||||
const fontSize = () => Number(settings().mobileFontSize) || 12;
|
||||
|
||||
class Session {
|
||||
@@ -537,6 +565,7 @@ function showSession(s) {
|
||||
$('#termScreen').hidden = false;
|
||||
$('#main').hidden = true;
|
||||
document.documentElement.style.setProperty('--term-bg', termTheme().background);
|
||||
syncBars();
|
||||
updateTermHead();
|
||||
drawKeybar();
|
||||
requestAnimationFrame(() => { s.refit(); if (s.alive) s.term.focus(); });
|
||||
@@ -548,6 +577,7 @@ function hideTerminal() {
|
||||
S.active?.term.blur();
|
||||
S.active = null;
|
||||
render();
|
||||
syncBars();
|
||||
}
|
||||
|
||||
function updateTermHead() {
|
||||
@@ -723,20 +753,31 @@ function viewSettings(v) {
|
||||
v.append(h(`<div class="top"><h1>${esc(T('Settings'))}</h1></div>`));
|
||||
const st = settings();
|
||||
|
||||
// Sprache & Terminal
|
||||
// Sprache, Design & Terminal
|
||||
const gen = h(`<div class="card"><h3>${esc(T('General'))}</h3>
|
||||
${field(T('Language'), 'language', st.language || 'auto', { type: 'select', options: [['auto', T('System language')], ...Object.entries(I18N.LANGUAGES)] })}
|
||||
${field(T('Terminal color scheme'), 'terminalTheme', st.terminalTheme || 'mrterm', { type: 'select', options: Object.entries(TERM_THEMES).map(([id, t]) => [id, t.name]) })}
|
||||
<div class="field"><label>Design</label><div class="theme-grid"></div></div>
|
||||
<div class="field"><label>${esc(T('Accent color'))}</label><div class="colors"></div></div>
|
||||
${field(T('Terminal color scheme'), 'terminalTheme', st.terminalTheme || 'auto', { type: 'select', options: [['auto', T('Match theme')], ...Object.entries(TERM_THEMES).map(([id, t]) => [id, t.name])] })}
|
||||
${field(T('Font size'), 'mobileFontSize', fontSize(), { type: 'select', options: [8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20].map((n) => [n, `${n} px`]) })}
|
||||
</div>`);
|
||||
$('[name=language]', gen).onchange = async (e) => { await call('vault:settings', { language: e.target.value }); await reload(); };
|
||||
$('[name=terminalTheme]', gen).onchange = async (e) => {
|
||||
await call('vault:settings', { terminalTheme: e.target.value }); S.vault.settings.terminalTheme = e.target.value;
|
||||
S.sessions.forEach((s) => { s.term.options.theme = termTheme(); });
|
||||
};
|
||||
const save = async (patch) => { await call('vault:settings', patch); Object.assign(S.vault.settings, patch); };
|
||||
$('[name=language]', gen).onchange = async (e) => { await save({ language: e.target.value }); await reload(); };
|
||||
for (const [id, t] of Object.entries(APP_THEMES)) {
|
||||
const p = h(`<button class="theme-prev ${(st.appTheme || 'midnight') === 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>${esc(T(t.name))}</button>`);
|
||||
p.onclick = async () => { await save({ appTheme: id }); applyTheme(); render(); };
|
||||
$('.theme-grid', gen).append(p);
|
||||
}
|
||||
['', ...COLORS.slice(0, 6), '#21c7a8', '#cba6f7', '#fcee0a'].forEach((col) => {
|
||||
const b = h(`<button title="${esc(col || T('Theme default'))}" class="${(st.accent || '') === col ? 'active' : ''}" style="background:${col || 'conic-gradient(#6e7bff,#21c7a8,#cba6f7,#ffb454,#6e7bff)'}"></button>`);
|
||||
b.onclick = async () => { await save({ accent: col }); applyTheme(); render(); };
|
||||
$('.colors', gen).append(b);
|
||||
});
|
||||
$('[name=terminalTheme]', gen).onchange = async (e) => { await save({ terminalTheme: e.target.value }); applyTheme(); };
|
||||
$('[name=mobileFontSize]', gen).onchange = async (e) => {
|
||||
const n = Number(e.target.value); await call('vault:settings', { mobileFontSize: n }); S.vault.settings.mobileFontSize = n;
|
||||
S.sessions.forEach((s) => { s.term.options.fontSize = n; });
|
||||
const n = Number(e.target.value); await save({ mobileFontSize: n });
|
||||
S.sessions.forEach((x) => { x.term.options.fontSize = n; });
|
||||
};
|
||||
v.append(gen);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>MrTerm</title>
|
||||
<link rel="stylesheet" href="lib/xterm.css" />
|
||||
<link rel="stylesheet" href="lib/app-themes.css" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+56
-24
@@ -1,26 +1,19 @@
|
||||
:root {
|
||||
--bg: #14161d;
|
||||
--surface: #1c1f29;
|
||||
--surface2: #252936;
|
||||
--line: #2c3040;
|
||||
--text: #e6e8ef;
|
||||
--muted: #9aa0b4;
|
||||
--faint: #5d6377;
|
||||
--accent: #6e7bff;
|
||||
--accent-soft: #6e7bff26;
|
||||
--green: #3ecf8e;
|
||||
--red: #ff5f6d;
|
||||
--orange: #ffb347;
|
||||
--radius: 14px;
|
||||
/* Capacitor setzt --safe-area-inset-* bei Edge-to-Edge; env() als Rückfall */
|
||||
/* Status-/Navigationsleiste und Tastatur rechnet MainActivity als Rand um den WebView heraus */
|
||||
/* Farben kommen aus lib/app-themes.css (gemeinsam mit dem Desktop, Auswahl über data-theme) */
|
||||
--surface: var(--card);
|
||||
--surface2: var(--card-hover);
|
||||
--line: var(--border);
|
||||
--accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
--r: calc(var(--r) + 4px);
|
||||
/* Status-/Navigationsleiste rechnet MainActivity als Rand um den WebView heraus */
|
||||
--top: 0px;
|
||||
--bottom: 0px;
|
||||
color-scheme: dark;
|
||||
}
|
||||
:root.light { color-scheme: light; }
|
||||
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); overflow: hidden; }
|
||||
body { font: 15px/1.4 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-user-select: none; user-select: none; overscroll-behavior: none; }
|
||||
body { font: 15px/1.4 var(--font); font-family: var(--font), system-ui, Roboto, sans-serif; -webkit-user-select: none; user-select: none; overscroll-behavior: none; }
|
||||
[hidden] { display: none !important; }
|
||||
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
||||
input, select, textarea { font: inherit; color: var(--text); -webkit-user-select: text; user-select: text; }
|
||||
@@ -55,7 +48,7 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.section .count { background: var(--surface2); border-radius: 8px; padding: 1px 7px; font-size: 11px; color: var(--faint); }
|
||||
.section svg { width: 16px; height: 16px; }
|
||||
|
||||
.list { background: var(--surface); border-radius: var(--radius); overflow: hidden; }
|
||||
.list { background: var(--surface); border-radius: var(--r); overflow: hidden; }
|
||||
.item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; min-height: 62px; position: relative; }
|
||||
.item + .item::before { content: ''; position: absolute; top: 0; left: 64px; right: 0; border-top: 1px solid var(--line); }
|
||||
.item:active { background: var(--surface2); }
|
||||
@@ -80,11 +73,11 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.empty .avatar svg { width: 28px; height: 28px; }
|
||||
.empty h3 { color: var(--text); margin: 0 0 6px; }
|
||||
|
||||
.fab { position: fixed; right: 18px; bottom: calc(76px + var(--bottom)); width: 58px; height: 58px; border-radius: 18px; background: var(--accent); color: #fff; display: grid; place-items: center; box-shadow: 0 8px 24px #0008; }
|
||||
.fab { position: fixed; right: 18px; bottom: calc(76px + var(--bottom)); width: 58px; height: 58px; border-radius: calc(var(--radius) + 8px); background: var(--accent); color: var(--on-accent); display: grid; place-items: center; box-shadow: 0 8px 24px #0008; }
|
||||
.fab svg { width: 26px; height: 26px; }
|
||||
|
||||
/* ---------- Karten / Einstellungen ---------- */
|
||||
.card { background: var(--surface); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 14px; }
|
||||
.card { background: var(--surface); border-radius: var(--r); padding: 14px 16px; margin-bottom: 14px; }
|
||||
.card h3 { margin: 0 0 4px; font-size: 16px; }
|
||||
.card p, .hint { color: var(--muted); font-size: 13.5px; margin: 4px 0 10px; }
|
||||
.row { display: flex; align-items: center; gap: 12px; padding: 10px 0; }
|
||||
@@ -93,9 +86,9 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.row .sub { color: var(--muted); font-size: 13px; }
|
||||
.btns { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
|
||||
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 16px; border-radius: 12px; background: var(--surface2); font-weight: 600; }
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 16px; border-radius: 12px; background: var(--surface2); border: 1px solid var(--line); font-weight: 600; }
|
||||
.btn svg { width: 18px; height: 18px; }
|
||||
.btn.primary { background: var(--accent); color: #fff; }
|
||||
.btn.primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||
.btn.danger { color: var(--red); }
|
||||
.btn.block { width: 100%; }
|
||||
.btn:disabled { opacity: .5; }
|
||||
@@ -117,7 +110,7 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.switch span { position: absolute; inset: 0; background: var(--surface2); border-radius: 20px; transition: .2s; }
|
||||
.switch span::after { content: ''; position: absolute; left: 3px; top: 3px; width: 24px; height: 24px; border-radius: 50%; background: var(--muted); transition: .2s; }
|
||||
.switch input:checked + span { background: var(--accent); }
|
||||
.switch input:checked + span::after { transform: translateX(20px); background: #fff; }
|
||||
.switch input:checked + span::after { transform: translateX(20px); background: var(--on-accent); }
|
||||
|
||||
.warn-box { border: 1px solid #ffb34755; background: #ffb34712; border-radius: 12px; padding: 10px 12px; color: var(--orange); font-size: 13px; margin: 8px 0; }
|
||||
|
||||
@@ -185,7 +178,46 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
#keybar::-webkit-scrollbar { display: none; }
|
||||
#keybar button { flex: none; min-width: 44px; height: 40px; padding: 0 10px; border-radius: 10px; background: var(--surface2); font: 600 14px ui-monospace, monospace; color: var(--text); display: grid; place-items: center; }
|
||||
#keybar button svg { width: 18px; height: 18px; }
|
||||
#keybar button.on { background: var(--accent); color: #fff; }
|
||||
#keybar button.lock { box-shadow: inset 0 0 0 2px #fff; }
|
||||
#keybar button.on { background: var(--accent); color: var(--on-accent); }
|
||||
#keybar button.lock { box-shadow: inset 0 0 0 2px var(--on-accent); }
|
||||
body.kbd-open #keybar { padding-bottom: 6px; }
|
||||
body.kbd-open #tabs, body.kbd-open .fab { display: none; }
|
||||
|
||||
/* Design-Extras */
|
||||
[data-theme='xp'] .page-head, [data-theme='xp'] .term-head { background: linear-gradient(#0a5fdc, #0846b8 55%, #0a3f9f); color: #fff; border-color: #0a3f9f; }
|
||||
[data-theme='xp'] .page-head .icon, [data-theme='xp'] .term-head .icon, [data-theme='xp'] .term-title svg { color: #fff; }
|
||||
[data-theme='xp'] #tabs { background: linear-gradient(#3168d5, #1d4fbf); border-color: #0a3f9f; }
|
||||
[data-theme='xp'] #tabs button { color: #c7d8ff; }
|
||||
[data-theme='xp'] #tabs button.active { color: #fff; }
|
||||
[data-theme='xp'] .btn.primary, [data-theme='xp'] .fab { background: linear-gradient(#3d86e8, #2159b8); }
|
||||
[data-theme='cyberpunk'] .btn.primary, [data-theme='cyberpunk'] .fab { clip-path: polygon(0 0, 100% 0, 100% 70%, calc(100% - 12px) 100%, 0 100%); }
|
||||
[data-theme='cyberpunk'] .btn.primary { text-transform: uppercase; letter-spacing: .6px; }
|
||||
[data-theme='cyberpunk'] .top h1 { text-transform: uppercase; letter-spacing: 1px; text-shadow: 2px 0 #ff003c88, -2px 0 #00f0ff88; }
|
||||
[data-theme='cyberpunk'] .list, [data-theme='cyberpunk'] .card { border-left: 2px solid var(--accent); }
|
||||
[data-theme='cyberpunk'] #tabs button.active { color: var(--accent); text-shadow: 0 0 8px #fcee0a88; }
|
||||
.theme-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 6px 0 14px; }
|
||||
.theme-prev { border-radius: 12px; padding: 8px; border: 2px solid var(--line); font-size: 12px; font-weight: 600; text-align: left; }
|
||||
.theme-prev.active { border-color: var(--accent); }
|
||||
.theme-prev .dp { display: flex; gap: 4px; height: 40px; margin-bottom: 6px; }
|
||||
.theme-prev .dp i { width: 12px; border-radius: 3px; }
|
||||
.theme-prev .dp div { flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
||||
.theme-prev .dp b { flex: 1; border-radius: 3px; position: relative; }
|
||||
.theme-prev .dp s { position: absolute; left: 4px; top: 4px; width: 14px; height: 6px; border-radius: 2px; }
|
||||
.colors { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.colors button { width: 34px; height: 34px; border-radius: 50%; border: 3px solid transparent; }
|
||||
.colors button.active { border-color: var(--text); }
|
||||
|
||||
/* macOS Liquid Glass */
|
||||
[data-theme='macos'] #main, [data-theme='macos'] #view { background: transparent; }
|
||||
[data-theme='macos'] :is(.list, .card, .search, #tabs, .page-head, .term-head, .sheet, .dialog, .toast, .chip, .btn, .fab, .field input, .field select, .field textarea, #keybar) {
|
||||
-webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
|
||||
}
|
||||
[data-theme='macos'] :is(.list, .card, .search, .sheet, .dialog, .toast, .chip) { border: 1px solid var(--border); box-shadow: var(--glass-edge); }
|
||||
[data-theme='macos'] .page, [data-theme='macos'] #lockScreen { background: var(--wallpaper); }
|
||||
[data-theme='macos'] .page-body { background: transparent; }
|
||||
[data-theme='macos'] #tabs, [data-theme='macos'] .page-head { background: rgba(255, 255, 255, .42); border-color: rgba(255, 255, 255, .7); }
|
||||
[data-theme='macos'] .btn { border-radius: 999px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9); }
|
||||
[data-theme='macos'] .btn.primary, [data-theme='macos'] .fab { background: linear-gradient(rgba(40, 145, 255, .92), rgba(0, 110, 240, .92)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55), 0 6px 18px rgba(0, 122, 255, .3); }
|
||||
[data-theme='macos'] .fab { border-radius: 50%; }
|
||||
[data-theme='macos'] .scrim { background: rgba(30, 40, 70, .18); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
|
||||
[data-theme='macos'] .sheet, [data-theme='macos'] .dialog { background: rgba(255, 255, 255, .62); }
|
||||
|
||||
Reference in New Issue
Block a user