Files
MaashkinoozheandClaude Opus 5.5 fe8956087e Reading activity grid in Settings: months, weeks, days, hours with drill-down
Active reader time is now kept per user in small UTC buckets (6 minutes for
120 days, then hours, dropped after three years), seeded once from the visits
the clock still remembers. Settings -> Reading activity shows them as a grid
whose cells glow with their total and carry a start-to-end strip; month/week
cells open their days, days open their hours. Finished chapters come from
progress and work without the clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:41:44 +01:00

75 lines
4.7 KiB
JavaScript
Raw Permalink 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.
'use strict';
// Two navigation levels; scraper tabs stay within their section, not another menu.
const SETTINGS_SECTIONS = [
{ id: 'user/reading', group: 'User', title: 'Reading', hint: 'Reader modes, page spacing and zoom.' },
{ id: 'user/reading-hours', group: 'User', title: 'Reading hours', hint: 'When this server allows chapter reading.' },
{ id: 'user/reading-time', group: 'User', title: 'Reading time & privacy', hint: 'Your clock, Reading Story and personal data.' },
{ id: 'user/activity', group: 'User', title: 'Reading activity', hint: 'When you read: by month, week, day or hour. Private to your account.' },
{ id: 'user/shortcuts', group: 'User', title: 'Keyboard shortcuts', hint: 'Every key the reader understands. Press ? in the reader for the same list.' },
{ id: 'user/notifications', group: 'User', title: 'Notifications', hint: 'Chapter alerts and your optional webhook.' },
{ id: 'user/account', group: 'User', title: 'Account', hint: 'Your account and password.' },
{ id: 'admin/scraper', group: 'Admin', title: 'Scraper', hint: 'Live activity, downloads, update checks and sources.' },
{ id: 'admin/peering', group: 'Admin', title: 'Peering', hint: 'Pair servers and manage their permissions.', href: 'peers.html#admin' },
{ id: 'admin/peer-catalog', group: 'Admin', title: 'Peer catalogs', hint: 'Approve titles to share and browse peer libraries.', href: 'peer-catalog.html' },
{ id: 'admin/library', group: 'Admin', title: 'Server & library', hint: 'Library location, scan status and maintenance.' },
{ id: 'admin/users', group: 'Admin', title: 'Users & access', hint: 'Manage accounts, passwords and administrators.' },
{ id: 'admin/collections', group: 'Admin', title: 'Collections & metadata', hint: 'Group manga and review their relationships.', href: 'collections.html' },
{ id: 'admin/health', group: 'Admin', title: 'Service health', hint: 'Service status, incidents and recovery.', href: 'health.html' },
];
const SCRAPER_TABS = ['activity', 'queue', 'updates', 'sources'];
const READING_DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
function readingDayText(schedule, index) {
const previous = (index + 6) % 7;
const carried = schedule.days[previous].filter(([start, end]) => end < start)
.map(([, end]) => `00:00–${end} (from ${READING_DAYS[previous]})`);
const starting = schedule.days[index].map(([start, end]) => `${start}–${end}${end < start ? ' (next day)' : ''}`);
return [...carried, ...starting].join(' · ') || 'Closed all day';
}
function settingsRoute(hash, admin) {
const parts = String(hash || '').replace(/^#/, '').split('/');
const candidate = parts.slice(0, 2).join('/'), tab = parts[2];
const section = SETTINGS_SECTIONS.find(item => item.id === candidate && (admin || item.group === 'User'));
return { id: section?.id || 'user/reading', tab: SCRAPER_TABS.includes(tab) ? tab : tab === 'hosters' ? 'sources' : 'activity' };
}
function settingsShell(me, active = 'user/reading') {
const main = $('main'); main.textContent = '';
const nav = h('nav', { class: 'settings-menu', 'aria-label': 'Settings sections' });
const links = new Map();
for (const group of me.admin ? ['User', 'Admin'] : ['User']) {
nav.append(h('div', { class: 'settings-menu-group' }, h('h2', {}, group),
SETTINGS_SECTIONS.filter(section => section.group === group).map(section => {
const link = h('a', { href: section.href || 'settings.html#' + section.id }, section.title);
links.set(section.id, link);
return link;
})));
}
const crumb = h('p', { class: 'settings-breadcrumb muted small' });
const title = h('h1', { tabindex: '-1' });
const titleRow = h('div', { class: 'settings-title-row' }, title);
const hint = h('p', { class: 'muted settings-hint' });
const content = h('div', { class: 'settings-content' });
main.append(h('div', { class: 'settings-layout' }, nav,
h('div', { class: 'settings-workspace' }, crumb, titleRow, hint, content)));
function select(id, focus = false) {
const section = SETTINGS_SECTIONS.find(item => item.id === id);
if (!section || (section.group === 'Admin' && !me.admin)) return;
for (const [key, link] of links) {
if (key === id) link.setAttribute('aria-current', 'page');
else link.removeAttribute('aria-current');
}
crumb.textContent = 'Settings / ' + section.group;
title.textContent = section.title;
hint.textContent = section.hint;
document.title = section.title + ' · Settings · MangaCircus';
if (focus) title.focus({ preventScroll: true });
}
select(active);
return { content, titleRow, select };
}
if (typeof module !== 'undefined') module.exports = { SETTINGS_SECTIONS, SCRAPER_TABS, settingsRoute, readingDayText };