320 lines
12 KiB
HTML
320 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Settings - OfflineU</title>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #1a1a1a;
|
|
--bg-secondary: #2d2d2d;
|
|
--bg-tertiary: #3d3d3d;
|
|
--bg-tertiary-hover: #404040;
|
|
--text-primary: #e0e0e0;
|
|
--text-muted: #999;
|
|
--border-color: #555;
|
|
--accent: #007acc;
|
|
--accent-hover: #005a9e;
|
|
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
--font-size-base: 16px;
|
|
--container-max-width: 1600px;
|
|
--radius: 8px;
|
|
}
|
|
[data-theme="light"] {
|
|
--bg-primary: #f2f2f2;
|
|
--bg-secondary: #ffffff;
|
|
--bg-tertiary: #eeeeee;
|
|
--bg-tertiary-hover: #e2e2e2;
|
|
--text-primary: #222222;
|
|
--text-muted: #666666;
|
|
--border-color: #cccccc;
|
|
}
|
|
[data-card-style="elevated"] .card {
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
}
|
|
[data-card-style="bordered"] .card {
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: var(--font-family);
|
|
font-size: var(--font-size-base);
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
margin: 20px;
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
color: var(--accent);
|
|
margin-bottom: 5px;
|
|
}
|
|
.subtitle {
|
|
color: var(--text-muted);
|
|
margin-bottom: 25px;
|
|
}
|
|
.card {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius);
|
|
padding: 20px 25px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.card h2 {
|
|
font-size: 1.1em;
|
|
margin-bottom: 15px;
|
|
color: var(--text-primary);
|
|
}
|
|
.setting-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid var(--bg-tertiary);
|
|
gap: 20px;
|
|
}
|
|
.setting-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.setting-row label {
|
|
flex: 1;
|
|
}
|
|
.setting-row .setting-desc {
|
|
display: block;
|
|
font-size: 0.85em;
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
select {
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
padding: 8px 12px;
|
|
border-radius: var(--radius);
|
|
font-size: 14px;
|
|
min-width: 160px;
|
|
}
|
|
input[type="color"] {
|
|
width: 44px;
|
|
height: 34px;
|
|
padding: 0;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--radius);
|
|
background: none;
|
|
cursor: pointer;
|
|
}
|
|
.color-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
input[type="text"].hex-input {
|
|
width: 90px;
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
padding: 8px 10px;
|
|
border-radius: var(--radius);
|
|
font-family: monospace;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 10px;
|
|
}
|
|
.btn {
|
|
background: var(--accent);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
font-size: 14px;
|
|
transition: background 0.3s;
|
|
}
|
|
.btn:hover { background: var(--accent-hover); }
|
|
.btn-secondary {
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
}
|
|
.btn-secondary:hover { background: var(--bg-tertiary-hover); }
|
|
#save-status {
|
|
font-size: 0.9em;
|
|
color: #28a745;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
#save-status.visible { opacity: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<a href="/" style="color: var(--text-muted); text-decoration: none; font-size: 0.9em;">← Back</a>
|
|
<h1 style="margin-top: 10px;">Settings</h1>
|
|
<p class="subtitle">Changes save and apply immediately across the app.</p>
|
|
|
|
<div class="card">
|
|
<h2>Appearance</h2>
|
|
<div class="setting-row">
|
|
<label for="theme">Theme
|
|
<span class="setting-desc">Overall light/dark palette</span>
|
|
</label>
|
|
<select id="theme" data-setting="theme">
|
|
{% for choice in theme_choices %}
|
|
<option value="{{ choice }}" {% if settings.theme == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label for="accent-hex">Accent color
|
|
<span class="setting-desc">Used for links, buttons, and highlights</span>
|
|
</label>
|
|
<div class="color-row">
|
|
<input type="color" id="accent-picker" value="{{ settings.accent_color }}">
|
|
<input type="text" id="accent-hex" class="hex-input" value="{{ settings.accent_color }}" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label for="corner_radius">Corner style
|
|
<span class="setting-desc">Sharp, rounded, or pill-shaped elements</span>
|
|
</label>
|
|
<select id="corner_radius" data-setting="corner_radius">
|
|
{% for choice in corner_radius_choices %}
|
|
<option value="{{ choice }}" {% if settings.corner_radius == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label for="card_style">Card style
|
|
<span class="setting-desc">Flat, elevated with shadow, or bordered</span>
|
|
</label>
|
|
<select id="card_style" data-setting="card_style">
|
|
{% for choice in card_style_choices %}
|
|
<option value="{{ choice }}" {% if settings.card_style == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Typography</h2>
|
|
<div class="setting-row">
|
|
<label for="font_family">Font
|
|
<span class="setting-desc">Typeface used across the app</span>
|
|
</label>
|
|
<select id="font_family" data-setting="font_family">
|
|
{% for choice in font_family_choices %}
|
|
<option value="{{ choice }}" {% if settings.font_family == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label for="font_size">Text size
|
|
<span class="setting-desc">Base font size</span>
|
|
</label>
|
|
<select id="font_size" data-setting="font_size">
|
|
{% for choice in font_size_choices %}
|
|
<option value="{{ choice }}" {% if settings.font_size == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Layout</h2>
|
|
<div class="setting-row">
|
|
<label for="layout_width">Page width
|
|
<span class="setting-desc">How much of the browser window content uses</span>
|
|
</label>
|
|
<select id="layout_width" data-setting="layout_width">
|
|
{% for choice in layout_width_choices %}
|
|
<option value="{{ choice }}" {% if settings.layout_width == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label for="density">Density
|
|
<span class="setting-desc">Spacing and padding throughout the app</span>
|
|
</label>
|
|
<select id="density" data-setting="density">
|
|
{% for choice in density_choices %}
|
|
<option value="{{ choice }}" {% if settings.density == choice %}selected{% endif %}>{{ choice|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
|
|
<span id="save-status">✓ Saved</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/theme.js"></script>
|
|
<script>
|
|
let saveTimeout = null;
|
|
|
|
function showSaved() {
|
|
const status = document.getElementById('save-status');
|
|
status.classList.add('visible');
|
|
clearTimeout(saveTimeout);
|
|
saveTimeout = setTimeout(() => status.classList.remove('visible'), 1500);
|
|
}
|
|
|
|
function saveSetting(key, value) {
|
|
fetch('/api/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ [key]: value })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success && window.__offlineuApplyTheme) {
|
|
window.__offlineuApplyTheme(data);
|
|
showSaved();
|
|
}
|
|
})
|
|
.catch(err => console.error('Failed to save setting:', err));
|
|
}
|
|
|
|
document.querySelectorAll('select[data-setting]').forEach(el => {
|
|
el.addEventListener('change', () => saveSetting(el.dataset.setting, el.value));
|
|
});
|
|
|
|
const accentPicker = document.getElementById('accent-picker');
|
|
const accentHex = document.getElementById('accent-hex');
|
|
|
|
accentPicker.addEventListener('input', () => {
|
|
accentHex.value = accentPicker.value;
|
|
saveSetting('accent_color', accentPicker.value);
|
|
});
|
|
|
|
accentHex.addEventListener('change', () => {
|
|
let value = accentHex.value.trim();
|
|
if (!value.startsWith('#')) value = '#' + value;
|
|
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
|
|
accentPicker.value = value;
|
|
saveSetting('accent_color', value);
|
|
} else {
|
|
accentHex.value = accentPicker.value; // revert invalid input
|
|
}
|
|
});
|
|
|
|
function resetSettings() {
|
|
fetch('/api/settings/reset', { method: 'POST' })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
}
|
|
})
|
|
.catch(err => console.error('Failed to reset settings:', err));
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|