refactor: Remove duplicate feature files from reader root
Delete old versions of features that are now in /reader/features/: - offline-manager.ts (moved to features/) - reading-speed-tracker.ts (moved to features/) - panel-dock-system.ts (replaced by converted version in features/) - navigator-panel.ts (replaced by converted version in features/) All imports now use the /reader/features/ directory. This eliminates duplication and makes it clear which files are the active ones.
This commit is contained in:
1 parent
e04eb90d53
commit
779f34d4f5
5 files changed
+1
-574
No files matched your search
@@ -1,152 +0,0 @@
|
|||||||
// Navigator panel - shows full page with draggable viewport box
|
|
||||||
// Affinity/Photoshop-style mini-map for page navigation
|
|
||||||
// Procedural implementation (no OOP)
|
|
||||||
|
|
||||||
import { loadSettings } from "./settings-manager";
|
|
||||||
|
|
||||||
interface NavigatorState {
|
|
||||||
panelId: string;
|
|
||||||
container: HTMLElement;
|
|
||||||
viewport: HTMLElement;
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
scale: number;
|
|
||||||
contentImage: HTMLImageElement | null;
|
|
||||||
isDragging: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeNavigator(containerSelector: string): NavigatorState {
|
|
||||||
const container = document.querySelector(containerSelector) as HTMLElement;
|
|
||||||
if (!container) throw new Error("Navigator container not found");
|
|
||||||
|
|
||||||
const viewport = document.createElement("div");
|
|
||||||
viewport.className = "navigator-viewport-box";
|
|
||||||
viewport.style.cssText = `
|
|
||||||
position: absolute;
|
|
||||||
border: 2px solid var(--accent-color, #3b82f6);
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
cursor: move;
|
|
||||||
z-index: 10;
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(viewport);
|
|
||||||
|
|
||||||
const state: NavigatorState = {
|
|
||||||
panelId: "navigator",
|
|
||||||
container,
|
|
||||||
viewport,
|
|
||||||
currentPage: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
scale: 0.1,
|
|
||||||
contentImage: null,
|
|
||||||
isDragging: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
setupNavigatorDragHandler(state);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup draggable viewport box within navigator
|
|
||||||
function setupNavigatorDragHandler(state: NavigatorState): void {
|
|
||||||
state.viewport.addEventListener("mousedown", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
state.isDragging = true;
|
|
||||||
state.viewport.style.cursor = "grabbing";
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", (e) => {
|
|
||||||
if (!state.isDragging || !state.contentImage) return;
|
|
||||||
|
|
||||||
const containerRect = state.container.getBoundingClientRect();
|
|
||||||
const imgRect = state.contentImage.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Calculate position relative to scaled image
|
|
||||||
const relX = (e.clientX - imgRect.left) / imgRect.width;
|
|
||||||
const relY = (e.clientY - imgRect.top) / imgRect.height;
|
|
||||||
|
|
||||||
// Update main viewer's position (call external handler)
|
|
||||||
const mainViewer = document.getElementById("reader-content");
|
|
||||||
if (mainViewer) {
|
|
||||||
mainViewer.dataset.panX = relX.toString();
|
|
||||||
mainViewer.dataset.panY = relY.toString();
|
|
||||||
// Dispatch event for main viewer to handle
|
|
||||||
mainViewer.dispatchEvent(
|
|
||||||
new CustomEvent("navigator-pan", {
|
|
||||||
detail: { x: relX, y: relY },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mouseup", () => {
|
|
||||||
state.isDragging = false;
|
|
||||||
state.viewport.style.cursor = "move";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update navigator with current page image
|
|
||||||
async function updateNavigatorContent(
|
|
||||||
state: NavigatorState,
|
|
||||||
pageNumber: number,
|
|
||||||
): Promise<void> {
|
|
||||||
state.currentPage = pageNumber;
|
|
||||||
|
|
||||||
// Get current page image (from PDF viewer, comic reader, or manga reader)
|
|
||||||
const contentArea = document.getElementById("reader-content");
|
|
||||||
const img = contentArea?.querySelector("img, canvas") as
|
|
||||||
| HTMLImageElement
|
|
||||||
| HTMLCanvasElement
|
|
||||||
| null;
|
|
||||||
|
|
||||||
if (!img) return;
|
|
||||||
|
|
||||||
// Create thumbnail version for navigator
|
|
||||||
const thumb = document.createElement("img");
|
|
||||||
thumb.src = img.src || (img as HTMLCanvasElement).toDataURL();
|
|
||||||
thumb.style.cssText = `
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
pointer-events: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Clear and populate container
|
|
||||||
state.container.innerHTML = "";
|
|
||||||
state.container.appendChild(thumb);
|
|
||||||
state.contentImage = thumb;
|
|
||||||
|
|
||||||
// Recreate viewport box
|
|
||||||
const viewport = document.createElement("div");
|
|
||||||
viewport.className = "navigator-viewport-box";
|
|
||||||
viewport.style.cssText = `
|
|
||||||
position: absolute;
|
|
||||||
border: 2px solid var(--accent-color, #3b82f6);
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
cursor: move;
|
|
||||||
z-index: 10;
|
|
||||||
width: ${100 / state.scale}%;
|
|
||||||
height: ${100 / state.scale}%;
|
|
||||||
`;
|
|
||||||
state.container.appendChild(viewport);
|
|
||||||
state.viewport = viewport;
|
|
||||||
|
|
||||||
// Re-attach drag handler
|
|
||||||
setupNavigatorDragHandler(state);
|
|
||||||
|
|
||||||
// Calculate viewport size relative to container
|
|
||||||
const containerRect = state.container.getBoundingClientRect();
|
|
||||||
const viewportWidth = (containerRect.width / img.width) * 100;
|
|
||||||
const viewportHeight = (containerRect.height / img.height) * 100;
|
|
||||||
|
|
||||||
viewport.style.width = `${viewportWidth}%`;
|
|
||||||
viewport.style.height = `${viewportHeight}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
function handleNavigatorResize(state: NavigatorState): void {
|
|
||||||
if (state.contentImage) {
|
|
||||||
updateNavigatorContent(state, state.currentPage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { initializeNavigator, updateNavigatorContent, handleNavigatorResize };
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Offline manager for PWA functionality
|
|
||||||
// Feature Registration Pattern implementation
|
|
||||||
|
|
||||||
import type { ReaderContext } from "./core/reader-context";
|
|
||||||
|
|
||||||
export function init(context: ReaderContext): void {
|
|
||||||
registerServiceWorker();
|
|
||||||
|
|
||||||
window.addEventListener("online", () => {
|
|
||||||
const isOnline = checkOnlineStatus();
|
|
||||||
if (isOnline) {
|
|
||||||
context.events.emit("offline:online", {});
|
|
||||||
syncPendingChanges(context);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("offline", () => {
|
|
||||||
context.events.emit("offline:offline", {});
|
|
||||||
});
|
|
||||||
|
|
||||||
context.events.on("offline:check", () => {
|
|
||||||
const isOnline = checkOnlineStatus();
|
|
||||||
context.events.emit("offline:status", { isOnline });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerServiceWorker(): void {
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register("/static/service-worker.js")
|
|
||||||
.then((registration) => {
|
|
||||||
console.log("Service worker registered:", registration);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Service worker registration failed:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkOnlineStatus(): boolean {
|
|
||||||
if (typeof navigator !== "undefined" && navigator.onLine) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncPendingChanges(context: ReaderContext): void {
|
|
||||||
context.events.emit("offline:sync", {});
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
// Modular dockable panel system - handles drag, lock, snap-back, window-shade
|
|
||||||
// Procedural style: Functions, not classes
|
|
||||||
|
|
||||||
import {
|
|
||||||
saveSettings,
|
|
||||||
loadSettings,
|
|
||||||
getDefaultSettings,
|
|
||||||
} from "./settings-manager";
|
|
||||||
|
|
||||||
interface PanelDockState {
|
|
||||||
panels: Map<string, PanelState>;
|
|
||||||
dragState: DragState | null;
|
|
||||||
dockZones: DockZone[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DragState {
|
|
||||||
panelId: string;
|
|
||||||
startX: number;
|
|
||||||
startY: number;
|
|
||||||
currentX: number;
|
|
||||||
currentY: number;
|
|
||||||
isLocked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DockZone {
|
|
||||||
side: "left" | "right";
|
|
||||||
x: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state: PanelDockState = {
|
|
||||||
panels: new Map(),
|
|
||||||
dragState: null,
|
|
||||||
dockZones: [
|
|
||||||
{ side: "left", x: 0, width: 400, height: window.innerHeight },
|
|
||||||
{
|
|
||||||
side: "right",
|
|
||||||
x: window.innerWidth - 400,
|
|
||||||
width: 400,
|
|
||||||
height: window.innerHeight,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize all panels from settings
|
|
||||||
function initializePanelDockSystem(): void {
|
|
||||||
const settings = loadSettings();
|
|
||||||
|
|
||||||
for (const [panelId, panelState] of Object.entries(settings.panel_layout)) {
|
|
||||||
registerPanel(panelId, panelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDragHandlers();
|
|
||||||
setupWindowShadeHandlers();
|
|
||||||
setupLockHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register a panel with the dock system
|
|
||||||
function registerPanel(panelId: string, panelState: PanelState): void {
|
|
||||||
state.panels.set(panelId, panelState);
|
|
||||||
applyPanelState(panelId, panelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply panel state to DOM
|
|
||||||
function applyPanelState(panelId: string, panelState: PanelState): void {
|
|
||||||
const panel = document.querySelector(`[data-panel="${panelId}"]`);
|
|
||||||
if (!panel) return;
|
|
||||||
|
|
||||||
const container = panel.parentElement;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
// Apply side positioning
|
|
||||||
if (panelState.side === "left") {
|
|
||||||
container.style.left = "0";
|
|
||||||
container.style.right = "auto";
|
|
||||||
} else if (panelState.side === "right") {
|
|
||||||
container.style.right = "0";
|
|
||||||
container.style.left = "auto";
|
|
||||||
} else {
|
|
||||||
container.style.left = "-9999px";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply width
|
|
||||||
panel.style.width = `${panelState.width_px}px`;
|
|
||||||
|
|
||||||
// Apply collapsed (window-shade) state
|
|
||||||
if (panelState.collapsed) {
|
|
||||||
panel.classList.add("panel-collapsed");
|
|
||||||
panel.querySelector(".panel-content")?.classList.add("hidden");
|
|
||||||
} else {
|
|
||||||
panel.classList.remove("panel-collapsed");
|
|
||||||
panel.querySelector(".panel-content")?.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply lock state
|
|
||||||
const lockBtn = panel.querySelector(".panel-lock");
|
|
||||||
if (lockBtn) {
|
|
||||||
lockBtn.textContent = panelState.locked ? "🔒" : "🔓";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup mouse/touch drag handlers
|
|
||||||
function setupDragHandlers(): void {
|
|
||||||
document
|
|
||||||
.querySelectorAll(".dockable-panel .panel-header")
|
|
||||||
.forEach((header) => {
|
|
||||||
header.addEventListener("mousedown", handleDragStart);
|
|
||||||
header.addEventListener("touchstart", handleDragStart, {
|
|
||||||
passive: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleDragMove);
|
|
||||||
document.addEventListener("touchmove", handleDragMove, { passive: false });
|
|
||||||
document.addEventListener("mouseup", handleDragEnd);
|
|
||||||
document.addEventListener("touchend", handleDragEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragStart(e: MouseEvent | TouchEvent): void {
|
|
||||||
const header = e.target.closest(".panel-header") as HTMLElement;
|
|
||||||
const panel = header?.closest(".dockable-panel") as HTMLElement;
|
|
||||||
if (!panel) return;
|
|
||||||
|
|
||||||
const panelId = panel.dataset.panel;
|
|
||||||
const panelState = state.panels.get(panelId);
|
|
||||||
|
|
||||||
// Check if locked
|
|
||||||
if (panelState?.locked) return;
|
|
||||||
|
|
||||||
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
||||||
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
|
||||||
|
|
||||||
state.dragState = {
|
|
||||||
panelId: panelId!,
|
|
||||||
startX: clientX,
|
|
||||||
startY: clientY,
|
|
||||||
currentX: clientX,
|
|
||||||
currentY: clientY,
|
|
||||||
isLocked: panelState?.locked || false,
|
|
||||||
};
|
|
||||||
|
|
||||||
panel.classList.add("dragging");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragMove(e: MouseEvent | TouchEvent): void {
|
|
||||||
if (!state.dragState) return;
|
|
||||||
|
|
||||||
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
||||||
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
|
||||||
|
|
||||||
state.dragState.currentX = clientX;
|
|
||||||
state.dragState.currentY = clientY;
|
|
||||||
|
|
||||||
const panel = document.querySelector(
|
|
||||||
`[data-panel="${state.dragState.panelId}"]`,
|
|
||||||
);
|
|
||||||
const container = panel?.parentElement;
|
|
||||||
if (container) {
|
|
||||||
container.style.transform = `translateX(${clientX - state.dragState.startX}px)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragEnd(e: MouseEvent | TouchEvent): void {
|
|
||||||
if (!state.dragState) return;
|
|
||||||
|
|
||||||
const { panelId, currentX } = state.dragState;
|
|
||||||
const panel = document.querySelector(`[data-panel="${panelId}"]`);
|
|
||||||
const container = panel?.parentElement;
|
|
||||||
|
|
||||||
// Reset transform
|
|
||||||
container.style.transform = "";
|
|
||||||
panel?.classList.remove("dragging");
|
|
||||||
|
|
||||||
// Determine drop zone
|
|
||||||
const newSide = currentX < window.innerWidth / 2 ? "left" : "right";
|
|
||||||
const panelState = state.panels.get(panelId);
|
|
||||||
|
|
||||||
if (panelState) {
|
|
||||||
// Check if dropped in valid zone
|
|
||||||
const isValidDrop = newSide === "left" || newSide === "right";
|
|
||||||
|
|
||||||
if (isValidDrop) {
|
|
||||||
panelState.last_valid_side = panelState.side; // Save previous valid position
|
|
||||||
panelState.side = newSide;
|
|
||||||
} else {
|
|
||||||
// Snap back to last valid position
|
|
||||||
panelState.side = panelState.last_valid_side;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyPanelState(panelId, panelState);
|
|
||||||
savePanelState(panelId, panelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.dragState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup window-shade (collapse/expand) handlers
|
|
||||||
function setupWindowShadeHandlers(): void {
|
|
||||||
document.querySelectorAll(".window-shade-toggle").forEach((btn) => {
|
|
||||||
btn.addEventListener("click", (e) => {
|
|
||||||
const panel = (e.target as HTMLElement).closest(
|
|
||||||
".dockable-panel",
|
|
||||||
) as HTMLElement;
|
|
||||||
const panelId = panel.dataset.panel;
|
|
||||||
const panelState = state.panels.get(panelId);
|
|
||||||
|
|
||||||
if (panelState) {
|
|
||||||
panelState.collapsed = !panelState.collapsed;
|
|
||||||
applyPanelState(panelId, panelState);
|
|
||||||
savePanelState(panelId, panelState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup lock toggle handlers
|
|
||||||
function setupLockHandlers(): void {
|
|
||||||
document.querySelectorAll(".panel-lock").forEach((btn) => {
|
|
||||||
btn.addEventListener("click", (e) => {
|
|
||||||
const panel = (e.target as HTMLElement).closest(
|
|
||||||
".dockable-panel",
|
|
||||||
) as HTMLElement;
|
|
||||||
const panelId = panel.dataset.panel;
|
|
||||||
const panelState = state.panels.get(panelId);
|
|
||||||
|
|
||||||
if (panelState) {
|
|
||||||
panelState.locked = !panelState.locked;
|
|
||||||
applyPanelState(panelId, panelState);
|
|
||||||
savePanelState(panelId, panelState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist panel state to settings
|
|
||||||
async function savePanelState(
|
|
||||||
panelId: string,
|
|
||||||
panelState: PanelState,
|
|
||||||
): Promise<void> {
|
|
||||||
const settings = loadSettings();
|
|
||||||
settings.panel_layout[panelId as keyof typeof settings.panel_layout] =
|
|
||||||
panelState;
|
|
||||||
await saveSettings(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { initializePanelDockSystem, registerPanel, applyPanelState };
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// Track reading speed and update database
|
|
||||||
// Feature Registration Pattern implementation
|
|
||||||
|
|
||||||
import type { ReaderContext } from "./core/reader-context";
|
|
||||||
|
|
||||||
export function init(context: ReaderContext): void {
|
|
||||||
let state: ReadingSpeedTrackerState | null = null;
|
|
||||||
|
|
||||||
context.events.on("reader:loaded", (detail: { mediaItemId: string }) => {
|
|
||||||
state = createReadingSpeedTracker(detail.mediaItemId);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.events.on("reading-session:start", () => {
|
|
||||||
if (state) {
|
|
||||||
startReadingSession(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.events.on("page-changed", () => {
|
|
||||||
if (state) {
|
|
||||||
recordPageTurn(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.events.on("words-read", (detail: { wordCount: number }) => {
|
|
||||||
if (state) {
|
|
||||||
recordWordsRead(state, detail.wordCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.events.on("reading-session:end", async () => {
|
|
||||||
if (state) {
|
|
||||||
await syncReadingSpeed(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.events.on("reader:unload", async () => {
|
|
||||||
if (state) {
|
|
||||||
await syncReadingSpeed(state);
|
|
||||||
state = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReadingSpeedTrackerState {
|
|
||||||
startTime: number | null;
|
|
||||||
pagesRead: number;
|
|
||||||
wordsRead: number;
|
|
||||||
lastSync: number;
|
|
||||||
mediaItemId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createReadingSpeedTracker(
|
|
||||||
mediaItemId: string,
|
|
||||||
): ReadingSpeedTrackerState {
|
|
||||||
return {
|
|
||||||
startTime: null,
|
|
||||||
pagesRead: 0,
|
|
||||||
wordsRead: 0,
|
|
||||||
lastSync: Date.now(),
|
|
||||||
mediaItemId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function startReadingSession(
|
|
||||||
state: ReadingSpeedTrackerState,
|
|
||||||
): ReadingSpeedTrackerState {
|
|
||||||
state.startTime = Date.now();
|
|
||||||
state.pagesRead = 0;
|
|
||||||
state.wordsRead = 0;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordPageTurn(
|
|
||||||
state: ReadingSpeedTrackerState,
|
|
||||||
): ReadingSpeedTrackerState {
|
|
||||||
if (!state.startTime) return state;
|
|
||||||
|
|
||||||
state.pagesRead += 1;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (state.pagesRead % 5 === 0 || now - state.lastSync > 5 * 60 * 1000) {
|
|
||||||
syncReadingSpeed(state);
|
|
||||||
state.lastSync = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordWordsRead(
|
|
||||||
state: ReadingSpeedTrackerState,
|
|
||||||
wordCount: number,
|
|
||||||
): ReadingSpeedTrackerState {
|
|
||||||
state.wordsRead += wordCount;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncReadingSpeed(
|
|
||||||
state: ReadingSpeedTrackerState,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!state.startTime) return;
|
|
||||||
|
|
||||||
const minutesElapsed = (Date.now() - state.startTime) / (1000 * 60);
|
|
||||||
const pagesPerMinute = state.pagesRead / minutesElapsed;
|
|
||||||
const wordsPerMinute = state.wordsRead / minutesElapsed;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
await fetch(`/readers/${state.mediaItemId}/reading-speed`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
pages_per_minute: pagesPerMinute,
|
|
||||||
words_per_minute: wordsPerMinute,
|
|
||||||
pages_read: state.pagesRead,
|
|
||||||
total_reading_minutes: minutesElapsed,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync reading speed:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
|
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
|
||||||
/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}input:where(:not([type])),input:where([type=date]),input:where([type=datetime-local]),input:where([type=email]),input:where([type=month]),input:where([type=number]),input:where([type=password]),input:where([type=search]),input:where([type=tel]),input:where([type=text]),input:where([type=time]),input:where([type=url]),input:where([type=week]),select,select:where([multiple]),textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}input:where(:not([type])):focus,input:where([type=date]):focus,input:where([type=datetime-local]):focus,input:where([type=email]):focus,input:where([type=month]):focus,input:where([type=number]):focus,input:where([type=password]):focus,input:where([type=search]):focus,input:where([type=tel]):focus,input:where([type=text]):focus,input:where([type=time]):focus,input:where([type=url]):focus,input:where([type=week]):focus,select:focus,select:where([multiple]):focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3Line truncated
|
/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}input:where(:not([type])),input:where([type=date]),input:where([type=datetime-local]),input:where([type=email]),input:where([type=month]),input:where([type=number]),input:where([type=password]),input:where([type=search]),input:where([type=tel]),input:where([type=text]),input:where([type=time]),input:where([type=url]),input:where([type=week]),select,select:where([multiple]),textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}input:where(:not([type])):focus,input:where([type=date]):focus,input:where([type=datetime-local]):focus,input:where([type=email]):focus,input:where([type=month]):focus,input:where([type=number]):focus,input:where([type=password]):focus,input:where([type=search]):focus,input:where([type=tel]):focus,input:where([type=text]):focus,input:where([type=time]):focus,input:where([type=url]):focus,input:where([type=week]):focus,select:focus,select:where([multiple]):focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3Line truncated
|
||||||
Reference in new issue
Block a user