Initial commit - realms platform

This commit is contained in:
doomtube 2026-01-05 22:54:27 -05:00
parent c590ab6d18
commit c717c3751c
234 changed files with 74103 additions and 15231 deletions

View file

@ -0,0 +1,424 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { siteSettings } from '$lib/stores/siteSettings';
let readerContainer;
let ebook = null;
let loading = true;
let error = null;
let book = null;
let rendition = null;
let toc = [];
let currentLocation = null;
let showToc = false;
let progress = 0;
$: ebookId = $page.params.id;
async function loadEbook() {
if (!browser || !ebookId) return;
try {
// Fetch ebook metadata
const metaRes = await fetch(`/api/ebooks/${ebookId}`);
if (!metaRes.ok) {
if (metaRes.status === 404) {
error = 'Ebook not found';
} else {
error = 'Failed to load ebook';
}
loading = false;
return;
}
const data = await metaRes.json();
ebook = data.ebook;
// Increment read count
fetch(`/api/ebooks/${ebookId}/read`, { method: 'POST' }).catch(() => {});
// Dynamically import foliate-js to avoid SSR issues
const { EPUB } = await import('foliate-js/epub.js');
// Fetch the EPUB file
const fileRes = await fetch(ebook.filePath);
if (!fileRes.ok) {
error = 'Failed to load ebook file';
loading = false;
return;
}
const blob = await fileRes.blob();
// Initialize reader
book = await new EPUB({ blob }).init();
toc = book.toc || [];
if (readerContainer) {
// Create the view
rendition = book.render(readerContainer);
// Restore saved position
const savedPosition = localStorage.getItem(`ebook-pos-${ebookId}`);
if (savedPosition) {
try {
rendition.goTo(savedPosition);
} catch (e) {
console.warn('Could not restore position:', e);
}
}
// Listen for location changes
rendition.addEventListener('relocate', (e) => {
currentLocation = e.detail;
progress = e.detail.fraction ? Math.round(e.detail.fraction * 100) : 0;
// Save position
if (e.detail.cfi) {
localStorage.setItem(`ebook-pos-${ebookId}`, e.detail.cfi);
}
});
}
loading = false;
} catch (e) {
console.error('Failed to load ebook:', e);
error = 'Failed to load ebook: ' + e.message;
loading = false;
}
}
function goToTocItem(href) {
if (rendition && href) {
rendition.goTo(href);
showToc = false;
}
}
function prevPage() {
if (rendition) {
rendition.prev();
}
}
function nextPage() {
if (rendition) {
rendition.next();
}
}
function handleKeydown(event) {
if (event.key === 'ArrowLeft') {
prevPage();
} else if (event.key === 'ArrowRight') {
nextPage();
}
}
onMount(() => {
loadEbook();
if (browser) {
window.addEventListener('keydown', handleKeydown);
}
});
onDestroy(() => {
if (browser) {
window.removeEventListener('keydown', handleKeydown);
}
if (book) {
book.destroy?.();
}
});
</script>
<svelte:head>
<title>{ebook ? `${$siteSettings.site_title} - ${ebook.title}` : $siteSettings.site_title}</title>
</svelte:head>
<style>
.reader-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #1a1a1a;
display: flex;
flex-direction: column;
z-index: 1000;
}
.reader-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: #111;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 1rem;
}
.back-btn {
display: flex;
align-items: center;
gap: 0.5rem;
color: #3b82f6;
text-decoration: none;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition: background 0.2s;
}
.back-btn:hover {
background: rgba(59, 130, 246, 0.1);
}
.book-title {
font-size: 0.9rem;
color: var(--white);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 1rem;
}
.progress-indicator {
font-size: 0.85rem;
color: #3b82f6;
font-weight: 500;
}
.toc-btn {
padding: 0.5rem 0.75rem;
background: rgba(59, 130, 246, 0.2);
border: 1px solid rgba(59, 130, 246, 0.4);
border-radius: 6px;
color: #3b82f6;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.toc-btn:hover {
background: rgba(59, 130, 246, 0.3);
}
.reader-main {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.reader-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
overflow: auto;
}
.reader-container :global(*) {
max-width: 100%;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 100px;
background: rgba(0, 0, 0, 0.3);
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
}
.nav-btn:hover {
opacity: 1;
background: rgba(59, 130, 246, 0.5);
}
.reader-main:hover .nav-btn {
opacity: 0.5;
}
.nav-prev {
left: 0;
border-radius: 0 8px 8px 0;
}
.nav-next {
right: 0;
border-radius: 8px 0 0 8px;
}
.toc-sidebar {
position: absolute;
top: 0;
right: 0;
width: 300px;
height: 100%;
background: #111;
border-left: 1px solid #333;
overflow-y: auto;
z-index: 20;
transform: translateX(100%);
transition: transform 0.2s ease;
}
.toc-sidebar.open {
transform: translateX(0);
}
.toc-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #333;
}
.toc-header h3 {
margin: 0;
font-size: 1rem;
}
.toc-close {
background: none;
border: none;
color: var(--gray);
font-size: 1.5rem;
cursor: pointer;
}
.toc-list {
padding: 0.5rem 0;
}
.toc-item {
display: block;
padding: 0.75rem 1rem;
color: var(--white);
text-decoration: none;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
}
.toc-item:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.loading-state,
.error-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--gray);
}
.error-state {
color: #ef4444;
}
.error-state h2 {
margin-bottom: 0.5rem;
}
.keyboard-hint {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
color: #666;
font-size: 0.75rem;
padding: 0.5rem 1rem;
background: rgba(0, 0, 0, 0.5);
border-radius: 4px;
}
</style>
<div class="reader-page">
<div class="reader-toolbar">
<div class="toolbar-left">
<a href="/ebooks" class="back-btn">
<span>&#8592;</span> Back
</a>
{#if ebook}
<span class="book-title">{ebook.title}</span>
{/if}
</div>
<div class="toolbar-right">
<span class="progress-indicator">{progress}%</span>
{#if toc.length > 0}
<button class="toc-btn" on:click={() => showToc = !showToc}>
Contents
</button>
{/if}
</div>
</div>
<div class="reader-main">
{#if loading}
<div class="loading-state">
<p>Loading ebook...</p>
</div>
{:else if error}
<div class="error-state">
<h2>Error</h2>
<p>{error}</p>
<a href="/ebooks" class="back-btn" style="margin-top: 1rem;">
Return to library
</a>
</div>
{:else}
<button class="nav-btn nav-prev" on:click={prevPage} title="Previous page">&#8249;</button>
<div bind:this={readerContainer} class="reader-container"></div>
<button class="nav-btn nav-next" on:click={nextPage} title="Next page">&#8250;</button>
<div class="toc-sidebar" class:open={showToc}>
<div class="toc-header">
<h3>Table of Contents</h3>
<button class="toc-close" on:click={() => showToc = false}>×</button>
</div>
<div class="toc-list">
{#each toc as item}
<button class="toc-item" on:click={() => goToTocItem(item.href)}>
{item.label}
</button>
{/each}
</div>
</div>
<div class="keyboard-hint">
Use &#8592; &#8594; arrow keys to navigate
</div>
{/if}
</div>
</div>