Files
offlineu/templates/settings.html
T

525 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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; }
html, body { height: 100%; }
body {
font-family: var(--font-family);
font-size: var(--font-size-base);
background: var(--bg-primary);
color: var(--text-primary);
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.page-content {
flex: 1;
padding: 20px;
}
.app-header {
background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));
padding: 18px 0;
border-bottom: 3px solid var(--accent);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
}
.app-header .header-inner {
max-width: 800px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 12px;
}
.app-header .brand-icon {
font-size: 1.6em;
}
.app-header a.brand-link {
color: var(--accent);
text-decoration: none;
font-size: 1.3em;
font-weight: 600;
letter-spacing: 0.5px;
}
.app-footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: 18px 0;
}
.app-footer .footer-inner {
max-width: 800px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
flex-wrap: wrap;
}
.footer-brand {
color: var(--text-muted);
font-size: 0.9em;
display: flex;
align-items: center;
gap: 8px;
}
.footer-links {
display: flex;
gap: 20px;
}
.footer-links a {
color: var(--accent);
text-decoration: none;
font-size: 0.9em;
}
.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="app-header">
<div class="header-inner">
<span class="brand-icon">📚</span>
<a href="/reset_course" class="brand-link">OfflineU</a>
</div>
</div>
<div class="page-content">
<div class="container">
<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 %}>{{ theme_display_names[choice] }}</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 %}>{{ 'Monaspace' if choice == 'monaspace' else 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="card">
<h2>Library</h2>
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
<label for="library_path">Default courses directory
<span class="setting-desc">Where the Library browser starts. Leave blank to use the server default ({{ default_library_path }}).</span>
</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="library_path" class="hex-input" style="flex: 1; width: auto; font-family: var(--font-family);"
placeholder="{{ default_library_path }}" value="{{ settings.library_path }}">
<button class="btn" onclick="saveLibraryPath()">Save</button>
</div>
<span id="library-path-status" style="font-size: 0.85em; min-height: 1.2em;"></span>
</div>
</div>
<div class="card">
<h2>Load a Course Manually</h2>
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
<label for="manual-course-path">Course directory path
<span class="setting-desc">For a course outside your library folder, or if it isn't showing up in the browser for some reason.</span>
</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="manual-course-path" class="hex-input" style="flex: 1; width: auto; font-family: var(--font-family);"
placeholder="e.g., /app/courses/My Course">
<button class="btn" onclick="loadCourseFromPath()">Load Course</button>
</div>
<span id="manual-course-status" style="font-size: 0.85em; min-height: 1.2em;"></span>
</div>
</div>
<div class="card">
<h2>Video Player</h2>
<div class="setting-row">
<label>Player size
<span class="setting-desc">
{% if settings.video_width and settings.video_height %}
Remembered at {{ settings.video_width }}×{{ settings.video_height }}px from your last resize
{% else %}
Using default responsive full-width sizing
{% endif %}
</span>
</label>
<button class="btn btn-secondary" onclick="resetVideoSize()">Reset size</button>
</div>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
<span id="save-status">✓ Saved</span>
</div>
</div>
</div>
<footer class="app-footer">
<div class="footer-inner">
<div class="footer-brand">
📚 <a href="/reset_course" style="color: inherit; text-decoration: none;">OfflineU</a>
</div>
<div class="footer-links">
<a href="/help">❓ Help</a>
<a href="/">← Back to app</a>
</div>
</div>
</footer>
<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 saveLibraryPath() {
const input = document.getElementById('library_path');
const status = document.getElementById('library-path-status');
fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ library_path: input.value.trim() })
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.style.color = '#28a745';
status.textContent = '✓ Saved — reload the main page to see it take effect';
showSaved();
} else {
status.style.color = '#ff6b6b';
status.textContent = data.error || 'Could not save';
}
})
.catch(err => {
status.style.color = '#ff6b6b';
status.textContent = 'Could not reach the server';
});
}
function loadCourseFromPath() {
const input = document.getElementById('manual-course-path');
const status = document.getElementById('manual-course-status');
const coursePath = input.value.trim();
if (!coursePath) {
status.style.color = '#ff6b6b';
status.textContent = 'Please enter a course path.';
return;
}
status.style.color = 'var(--text-muted)';
status.textContent = 'Loading course...';
fetch('/load_course', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ course_path: coursePath })
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.style.color = '#28a745';
status.textContent = `✓ Loaded "${data.course_name}" — redirecting...`;
setTimeout(() => { window.location.href = '/'; }, 1000);
} else {
status.style.color = '#ff6b6b';
status.textContent = data.error || 'Could not load course';
}
})
.catch(() => {
status.style.color = '#ff6b6b';
status.textContent = 'Could not reach the server';
});
}
document.getElementById('manual-course-path').addEventListener('keypress', (e) => {
if (e.key === 'Enter') loadCourseFromPath();
});
function resetVideoSize() {
fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_width: '', video_height: '' })
})
.then(r => r.json())
.then(data => {
if (data.success) location.reload();
})
.catch(err => console.error('Failed to reset video size:', err));
}
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>