## Summary * **Goal**: Make browser tab titles, bookmarks, and history entries distinguishable across pages (including individual folders in the File Manager). * **Changes**: - Reordered `<title>` so the page name comes first, followed by `CrossPoint Reader`. Browser tabs usually truncate titles from the right, and bookmark/history lists are sorted alphabetically by title — putting the differentiating part first keeps it visible in both cases, instead of every entry starting with `CrossPoint Reader - …`. - On `FilesPage`, added a small JS hook that updates `document.title` with the current subfolder leaf when a `path` query parameter is present — previously all folders shared the same title. | URL | Title before | Title after | |-----|--------------|-------------| | `/` | `CrossPoint Reader` | `CrossPoint Reader` _(unchanged)_ | | `/settings` | `CrossPoint Reader - Settings` | `Settings - CrossPoint Reader` | | `/files` | `CrossPoint Reader - Files` | `Files - CrossPoint Reader` | | `/files?path=/Books` | `CrossPoint Reader - Files` | `Books - Files - CrossPoint Reader` | | `/files?path=/Books/Fantasy` | `CrossPoint Reader - Files` | `Fantasy - Files - CrossPoint Reader` | ## Additional Context * Pure client-side change: `<title>` tags plus ~3 lines of JavaScript on FilesPage. No network, behavior, or memory impact. * The static `<title>` tag remains as a fallback if JS fails. * `HomePage` title left as-is since the app name alone is the standard convention for a root/home page. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_
441 lines
11 KiB
HTML
441 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Settings - CrossPoint Reader</title>
|
|
<style>
|
|
:root {
|
|
--font-color: #333;
|
|
--bg: #f5f5f5;
|
|
--title-color: #2c3e50;
|
|
--card-bg: #FFF;
|
|
--label-color: #7f8c8d;
|
|
--border-color: #eee;
|
|
--accent-color: rgb(110, 154, 130);
|
|
--accent-hover-color: #5a8c73;
|
|
--toggle-bg: #ccc;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--font-color: #f5f5f5;
|
|
--bg: #333;
|
|
--title-color: #ecf0f1;
|
|
--card-bg: #444;
|
|
--label-color: #bdc3c7;
|
|
--border-color: #555;
|
|
--toggle-bg: #666;
|
|
color-scheme: dark;
|
|
}
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
Oxygen, Ubuntu, sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background-color: var(--bg);
|
|
color: var(--font-color);
|
|
}
|
|
h1 {
|
|
color: var(--title-color);
|
|
border-bottom: 2px solid var(--accent-color);
|
|
padding-bottom: 10px;
|
|
}
|
|
h2 {
|
|
color: var(--title-color);
|
|
margin-top: 0;
|
|
}
|
|
.card {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin: 15px 0;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.nav-links {
|
|
margin: 20px 0;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
.nav-links a {
|
|
padding: 10px 20px;
|
|
color: var(--font-color);
|
|
text-decoration: none;
|
|
border-radius: 4px;
|
|
}
|
|
.nav-links a.active {
|
|
background-color: var(--accent-color);
|
|
color: white;
|
|
}
|
|
.nav-links a:not(.active):hover {
|
|
background-color: var(--accent-hover-color);
|
|
color: white;
|
|
}
|
|
.setting-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.setting-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.setting-name {
|
|
font-weight: 500;
|
|
color: var(--label-color);
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding-right: 12px;
|
|
}
|
|
.setting-control {
|
|
flex-shrink: 0;
|
|
}
|
|
.setting-control select,
|
|
.setting-control input[type="number"],
|
|
.setting-control input[type="text"],
|
|
.setting-control input[type="password"] {
|
|
padding: 6px 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 0.95em;
|
|
background: var(--card-bg);
|
|
}
|
|
.setting-control select {
|
|
min-width: 160px;
|
|
}
|
|
.setting-control input[type="text"],
|
|
.setting-control input[type="password"] {
|
|
width: 220px;
|
|
}
|
|
.setting-control input[type="number"] {
|
|
width: 80px;
|
|
}
|
|
/* Toggle switch */
|
|
.toggle-switch {
|
|
display: inline-block;
|
|
position: relative;
|
|
width: 48px;
|
|
height: 26px;
|
|
}
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background-color: var(--toggle-bg);
|
|
border-radius: 26px;
|
|
transition: 0.3s;
|
|
}
|
|
.toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 20px;
|
|
width: 20px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
border-radius: 50%;
|
|
transition: 0.3s;
|
|
}
|
|
.toggle-switch input:checked + .toggle-slider {
|
|
background-color: var(--accent-color);
|
|
}
|
|
.toggle-switch input:checked + .toggle-slider:before {
|
|
transform: translateX(22px);
|
|
}
|
|
.save-container {
|
|
text-align: center;
|
|
margin: 20px 0;
|
|
}
|
|
.save-btn {
|
|
background-color: #27ae60;
|
|
color: white;
|
|
padding: 12px 40px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1.1em;
|
|
font-weight: 600;
|
|
}
|
|
.save-btn:hover {
|
|
background-color: #219a52;
|
|
}
|
|
.save-btn:disabled {
|
|
background-color: #95a5a6;
|
|
cursor: not-allowed;
|
|
}
|
|
.message {
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
margin: 15px 0;
|
|
text-align: center;
|
|
display: none;
|
|
}
|
|
.message.success {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
.message.error {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
.loader-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin: 20px 0;
|
|
}
|
|
.loader {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 5px solid #AAA;
|
|
border-bottom-color: transparent;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
box-sizing: border-box;
|
|
animation: rotation 1s linear infinite;
|
|
}
|
|
@keyframes rotation {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
@media (max-width: 600px) {
|
|
body {
|
|
padding: 10px;
|
|
font-size: 14px;
|
|
}
|
|
.card {
|
|
padding: 12px;
|
|
margin: 10px 0;
|
|
}
|
|
h1 {
|
|
font-size: 1.3em;
|
|
}
|
|
.nav-links a {
|
|
padding: 8px 12px;
|
|
font-size: 0.9em;
|
|
}
|
|
.setting-row {
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
.setting-control select,
|
|
.setting-control input[type="text"],
|
|
.setting-control input[type="password"] {
|
|
min-width: 0;
|
|
width: unset;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>⚙️ Settings</h1>
|
|
|
|
<div class="nav-links">
|
|
<a href="/">Home</a>
|
|
<a href="/files">File Manager</a>
|
|
<a href="/settings" class="active">Settings</a>
|
|
</div>
|
|
|
|
<div id="message" class="message"></div>
|
|
|
|
<div id="settings-container">
|
|
<div class="loader-container">
|
|
<span class="loader"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="save-container" id="save-container" style="display:none;">
|
|
<button class="save-btn" id="saveBtn" onclick="saveSettings()">Save Settings</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<p style="text-align: center; color: #95a5a6; margin: 0;">
|
|
CrossPoint E-Reader • Open Source
|
|
</p>
|
|
</div>
|
|
|
|
<script>
|
|
let allSettings = [];
|
|
let originalValues = {};
|
|
|
|
function escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function showMessage(text, isError) {
|
|
const msg = document.getElementById('message');
|
|
msg.textContent = text;
|
|
msg.className = 'message ' + (isError ? 'error' : 'success');
|
|
msg.style.display = 'block';
|
|
setTimeout(function() { msg.style.display = 'none'; }, 4000);
|
|
}
|
|
|
|
function renderControl(setting) {
|
|
const id = 'setting-' + setting.key;
|
|
|
|
if (setting.type === 'toggle') {
|
|
const checked = setting.value ? 'checked' : '';
|
|
return '<label class="toggle-switch">' +
|
|
'<input type="checkbox" id="' + id + '" ' + checked + ' onchange="markChanged()">' +
|
|
'<span class="toggle-slider"></span></label>';
|
|
}
|
|
|
|
if (setting.type === 'enum') {
|
|
let html = '<select id="' + id + '" onchange="markChanged()">';
|
|
setting.options.forEach(function(opt, idx) {
|
|
const selected = idx === setting.value ? ' selected' : '';
|
|
html += '<option value="' + idx + '"' + selected + '>' + escapeHtml(opt) + '</option>';
|
|
});
|
|
html += '</select>';
|
|
return html;
|
|
}
|
|
|
|
if (setting.type === 'value') {
|
|
return '<input type="number" id="' + id + '" value="' + setting.value + '"' +
|
|
' min="' + setting.min + '" max="' + setting.max + '" step="' + setting.step + '"' +
|
|
' onchange="markChanged()">';
|
|
}
|
|
|
|
if (setting.type === 'string') {
|
|
const inputType = setting.name.toLowerCase().includes('password') ? 'password' : 'text';
|
|
const val = setting.value || '';
|
|
return '<input type="' + inputType + '" id="' + id + '" value="' + escapeHtml(val) + '"' +
|
|
' oninput="markChanged()">';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function getValue(setting) {
|
|
const el = document.getElementById('setting-' + setting.key);
|
|
if (!el) return undefined;
|
|
|
|
if (setting.type === 'toggle') {
|
|
return el.checked ? 1 : 0;
|
|
}
|
|
if (setting.type === 'enum') {
|
|
return parseInt(el.value, 10);
|
|
}
|
|
if (setting.type === 'value') {
|
|
return parseInt(el.value, 10);
|
|
}
|
|
if (setting.type === 'string') {
|
|
return el.value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function markChanged() {
|
|
document.getElementById('saveBtn').disabled = false;
|
|
}
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
const response = await fetch('/api/settings');
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load settings: ' + response.status);
|
|
}
|
|
allSettings = await response.json();
|
|
|
|
// Store original values
|
|
originalValues = {};
|
|
allSettings.forEach(function(s) {
|
|
originalValues[s.key] = s.value;
|
|
});
|
|
|
|
// Group by category
|
|
const groups = {};
|
|
allSettings.forEach(function(s) {
|
|
if (!groups[s.category]) groups[s.category] = [];
|
|
groups[s.category].push(s);
|
|
});
|
|
|
|
const container = document.getElementById('settings-container');
|
|
let html = '';
|
|
|
|
for (const category in groups) {
|
|
html += '<div class="card"><h2>' + escapeHtml(category) + '</h2>';
|
|
groups[category].forEach(function(s) {
|
|
html += '<div class="setting-row">' +
|
|
'<span class="setting-name">' + escapeHtml(s.name) + '</span>' +
|
|
'<span class="setting-control">' + renderControl(s) + '</span>' +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
document.getElementById('save-container').style.display = '';
|
|
document.getElementById('saveBtn').disabled = true;
|
|
} catch (e) {
|
|
console.error(e);
|
|
document.getElementById('settings-container').innerHTML =
|
|
'<div class="card"><p style="text-align:center;color:#e74c3c;">Failed to load settings</p></div>';
|
|
}
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const btn = document.getElementById('saveBtn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Saving...';
|
|
|
|
// Collect only changed values
|
|
const changes = {};
|
|
allSettings.forEach(function(s) {
|
|
const current = getValue(s);
|
|
if (current !== undefined && current !== originalValues[s.key]) {
|
|
changes[s.key] = current;
|
|
}
|
|
});
|
|
|
|
if (Object.keys(changes).length === 0) {
|
|
showMessage('No changes to save.', false);
|
|
btn.textContent = 'Save Settings';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(changes)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(text || 'Save failed');
|
|
}
|
|
|
|
// Update original values to new values
|
|
for (const key in changes) {
|
|
originalValues[key] = changes[key];
|
|
}
|
|
|
|
showMessage('Settings saved successfully!', false);
|
|
} catch (e) {
|
|
console.error(e);
|
|
showMessage('Error: ' + e.message, true);
|
|
}
|
|
|
|
btn.textContent = 'Save Settings';
|
|
}
|
|
|
|
loadSettings();
|
|
</script>
|
|
</body>
|
|
</html>
|