Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Online EPUB Reader</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/epubjs@0.3.93/dist/epub.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .footnote-popup { | |
| position: absolute; | |
| background: white; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 10px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| max-width: 300px; | |
| z-index: 100; | |
| display: none; | |
| } | |
| #viewer { | |
| height: calc(100vh - 160px); | |
| } | |
| .loading-spinner { | |
| border: 4px solid rgba(0, 0, 0, 0.1); | |
| border-radius: 50%; | |
| border-top: 4px solid #3498db; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background-color: #e5e7eb; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background-color: #3b82f6; | |
| transition: width 0.3s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="container mx-auto px-4 py-6"> | |
| <header class="mb-8 text-center"> | |
| <h1 class="text-3xl font-bold text-indigo-700">Online EPUB Reader</h1> | |
| <p class="text-gray-600 mt-2">Upload and read your EPUB books in the browser</p> | |
| </header> | |
| <div class="max-w-2xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6 mb-8"> | |
| <div class="text-center"> | |
| <div id="upload-section" class="space-y-4"> | |
| <div class="flex justify-center"> | |
| <div class="w-24 h-24 rounded-full bg-indigo-100 flex items-center justify-center"> | |
| <i class="fas fa-book-open text-4xl text-indigo-500"></i> | |
| </div> | |
| </div> | |
| <h2 class="text-xl font-semibold text-gray-800">Upload Your EPUB File</h2> | |
| <p class="text-gray-500">Supported formats: .epub</p> | |
| <div class="mt-6"> | |
| <label for="epub-upload" class="cursor-pointer"> | |
| <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-indigo-400 transition-colors"> | |
| <div class="flex flex-col items-center justify-center space-y-2"> | |
| <i class="fas fa-cloud-upload-alt text-3xl text-indigo-500"></i> | |
| <p class="text-gray-600">Drag & drop your EPUB file here or click to browse</p> | |
| <button id="upload-btn" class="mt-4 px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors"> | |
| Select EPUB File | |
| </button> | |
| </div> | |
| </div> | |
| <input id="epub-upload" type="file" accept=".epub" class="hidden"> | |
| </label> | |
| </div> | |
| </div> | |
| <div id="loading-section" class="hidden py-8"> | |
| <div class="flex flex-col items-center justify-center space-y-4"> | |
| <div class="loading-spinner"></div> | |
| <p class="text-gray-600">Loading your book...</p> | |
| <div class="w-full max-w-xs"> | |
| <div class="progress-bar"> | |
| <div id="progress-fill" class="progress-fill" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="reader-section" class="hidden bg-white rounded-xl shadow-md overflow-hidden"> | |
| <div class="flex items-center justify-between bg-gray-100 px-4 py-3 border-b"> | |
| <div id="book-title" class="font-semibold text-gray-700 truncate"></div> | |
| <div class="flex space-x-2"> | |
| <button id="prev-btn" class="p-2 text-gray-600 hover:text-indigo-600 hover:bg-gray-200 rounded-full"> | |
| <i class="fas fa-chevron-left"></i> | |
| </button> | |
| <button id="next-btn" class="p-2 text-gray-600 hover:text-indigo-600 hover:bg-gray-200 rounded-full"> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| <button id="settings-btn" class="p-2 text-gray-600 hover:text-indigo-600 hover:bg-gray-200 rounded-full"> | |
| <i class="fas fa-cog"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="viewer" class="w-full"></div> | |
| <div id="footnote-popup" class="footnote-popup"></div> | |
| <div class="bg-gray-100 px-4 py-3 border-t flex justify-between items-center"> | |
| <div id="page-info" class="text-sm text-gray-500">Page: - / -</div> | |
| <div class="flex space-x-4"> | |
| <button id="toc-btn" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium"> | |
| <i class="fas fa-list-ul mr-1"></i> Table of Contents | |
| </button> | |
| <button id="fullscreen-btn" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium"> | |
| <i class="fas fa-expand mr-1"></i> Fullscreen | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toc-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"> | |
| <div class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden"> | |
| <div class="border-b px-6 py-4 flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold text-gray-800">Table of Contents</h3> | |
| <button id="close-toc-btn" class="text-gray-400 hover:text-gray-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div id="toc-list" class="overflow-y-auto p-4" style="max-height: calc(80vh - 80px);"></div> | |
| </div> | |
| </div> | |
| <div id="settings-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"> | |
| <div class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden"> | |
| <div class="border-b px-6 py-4 flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold text-gray-800">Reading Settings</h3> | |
| <button id="close-settings-btn" class="text-gray-400 hover:text-gray-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-6 space-y-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Font Size</label> | |
| <div class="flex items-center space-x-4"> | |
| <button id="font-decrease" class="p-2 bg-gray-200 rounded-full hover:bg-gray-300"> | |
| <i class="fas fa-minus"></i> | |
| </button> | |
| <span id="font-size-display" class="text-lg">16px</span> | |
| <button id="font-increase" class="p-2 bg-gray-200 rounded-full hover:bg-gray-300"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Theme</label> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button data-theme="light" class="p-3 border rounded-lg hover:border-indigo-400 theme-btn"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-4 h-4 rounded-full bg-white border border-gray-300"></div> | |
| <span>Light</span> | |
| </div> | |
| </button> | |
| <button data-theme="sepia" class="p-3 border rounded-lg hover:border-indigo-400 theme-btn"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-4 h-4 rounded-full bg-amber-100"></div> | |
| <span>Sepia</span> | |
| </div> | |
| </button> | |
| <button data-theme="dark" class="p-3 border rounded-lg hover:border-indigo-400 theme-btn"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-4 h-4 rounded-full bg-gray-800"></div> | |
| <span>Dark</span> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Elements | |
| const uploadSection = document.getElementById('upload-section'); | |
| const loadingSection = document.getElementById('loading-section'); | |
| const readerSection = document.getElementById('reader-section'); | |
| const uploadBtn = document.getElementById('upload-btn'); | |
| const epubUpload = document.getElementById('epub-upload'); | |
| const progressFill = document.getElementById('progress-fill'); | |
| const bookTitle = document.getElementById('book-title'); | |
| const tocModal = document.getElementById('toc-modal'); | |
| const tocList = document.getElementById('toc-list'); | |
| const tocBtn = document.getElementById('toc-btn'); | |
| const closeTocBtn = document.getElementById('close-toc-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const closeSettingsBtn = document.getElementById('close-settings-btn'); | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const pageInfo = document.getElementById('page-info'); | |
| const fullscreenBtn = document.getElementById('fullscreen-btn'); | |
| const fontDecrease = document.getElementById('font-decrease'); | |
| const fontIncrease = document.getElementById('font-increase'); | |
| const fontSizeDisplay = document.getElementById('font-size-display'); | |
| const themeButtons = document.querySelectorAll('.theme-btn'); | |
| // EPUB.js variables | |
| let book; | |
| let rendition; | |
| let currentSectionIndex = 0; | |
| let spineItems = []; | |
| // Drag and drop functionality | |
| const dropArea = document.querySelector('.border-dashed'); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| dropArea.classList.add('border-indigo-400'); | |
| } | |
| function unhighlight() { | |
| dropArea.classList.remove('border-indigo-400'); | |
| } | |
| dropArea.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length > 0 && files[0].name.endsWith('.epub')) { | |
| handleFile(files[0]); | |
| } | |
| } | |
| // File upload handler | |
| uploadBtn.addEventListener('click', function() { | |
| epubUpload.click(); | |
| }); | |
| epubUpload.addEventListener('change', function(e) { | |
| if (e.target.files.length > 0) { | |
| handleFile(e.target.files[0]); | |
| } | |
| }); | |
| function handleFile(file) { | |
| uploadSection.classList.add('hidden'); | |
| loadingSection.classList.remove('hidden'); | |
| // Simulate progress for better UX | |
| let progress = 0; | |
| const progressInterval = setInterval(() => { | |
| progress += Math.random() * 10; | |
| if (progress >= 90) clearInterval(progressInterval); | |
| progressFill.style.width = `${Math.min(progress, 100)}%`; | |
| }, 200); | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const content = e.target.result; | |
| // Initialize EPUB.js | |
| book = ePub(content); | |
| book.loaded.metadata.then(function(meta) { | |
| bookTitle.textContent = meta.title || 'Untitled Book'; | |
| }); | |
| book.loaded.spine.then(function(spine) { | |
| spineItems = spine.spineItems; | |
| }); | |
| book.loaded.navigation.then(function(nav) { | |
| displayToc(nav.toc); | |
| }); | |
| // Initialize rendition | |
| rendition = book.renderTo("viewer", { | |
| width: "100%", | |
| height: "100%", | |
| spread: "none" | |
| }); | |
| rendition.display().then(function() { | |
| clearInterval(progressInterval); | |
| progressFill.style.width = '100%'; | |
| setTimeout(() => { | |
| loadingSection.classList.add('hidden'); | |
| readerSection.classList.remove('hidden'); | |
| updatePageInfo(); | |
| }, 500); | |
| }); | |
| // Navigation handlers | |
| rendition.on("relocated", function(location) { | |
| currentSectionIndex = location.start.index; | |
| updatePageInfo(); | |
| }); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| // Table of Contents | |
| function displayToc(toc) { | |
| tocList.innerHTML = ''; | |
| function createTocItems(items, level = 0) { | |
| items.forEach(item => { | |
| const itemElement = document.createElement('div'); | |
| itemElement.className = `py-2 pl-${level * 4} cursor-pointer hover:text-indigo-600`; | |
| itemElement.textContent = item.label; | |
| itemElement.addEventListener('click', function() { | |
| rendition.display(item.href); | |
| tocModal.classList.add('hidden'); | |
| }); | |
| tocList.appendChild(itemElement); | |
| if (item.subitems && item.subitems.length > 0) { | |
| createTocItems(item.subitems, level + 1); | |
| } | |
| }); | |
| } | |
| createTocItems(toc); | |
| } | |
| // Navigation controls | |
| prevBtn.addEventListener('click', function() { | |
| rendition.prev(); | |
| }); | |
| nextBtn.addEventListener('click', function() { | |
| rendition.next(); | |
| }); | |
| tocBtn.addEventListener('click', function() { | |
| tocModal.classList.remove('hidden'); | |
| }); | |
| closeTocBtn.addEventListener('click', function() { | |
| tocModal.classList.add('hidden'); | |
| }); | |
| settingsBtn.addEventListener('click', function() { | |
| settingsModal.classList.remove('hidden'); | |
| }); | |
| closeSettingsBtn.addEventListener('click', function() { | |
| settingsModal.classList.add('hidden'); | |
| }); | |
| // Update page info | |
| function updatePageInfo() { | |
| if (spineItems.length > 0) { | |
| pageInfo.textContent = `Page: ${currentSectionIndex + 1} / ${spineItems.length}`; | |
| } | |
| } | |
| // Fullscreen mode | |
| fullscreenBtn.addEventListener('click', function() { | |
| const viewer = document.getElementById('viewer'); | |
| if (!document.fullscreenElement) { | |
| viewer.requestFullscreen().catch(err => { | |
| console.error(`Error attempting to enable fullscreen: ${err.message}`); | |
| }); | |
| fullscreenBtn.innerHTML = '<i class="fas fa-compress mr-1"></i> Exit Fullscreen'; | |
| } else { | |
| document.exitFullscreen(); | |
| fullscreenBtn.innerHTML = '<i class="fas fa-expand mr-1"></i> Fullscreen'; | |
| } | |
| }); | |
| // Font size controls | |
| let fontSize = 16; | |
| fontDecrease.addEventListener('click', function() { | |
| if (fontSize > 12) { | |
| fontSize -= 2; | |
| updateFontSize(); | |
| } | |
| }); | |
| fontIncrease.addEventListener('click', function() { | |
| if (fontSize < 24) { | |
| fontSize += 2; | |
| updateFontSize(); | |
| } | |
| }); | |
| function updateFontSize() { | |
| if (rendition) { | |
| rendition.themes.fontSize(`${fontSize}px`); | |
| fontSizeDisplay.textContent = `${fontSize}px`; | |
| } | |
| } | |
| // Theme controls | |
| themeButtons.forEach(button => { | |
| button.addEventListener('click', function() { | |
| const theme = this.getAttribute('data-theme'); | |
| applyTheme(theme); | |
| }); | |
| }); | |
| // Footnote hover functionality | |
| document.addEventListener('mouseover', function(e) { | |
| const footnoteEl = e.target.closest('a[epub:type="noteref"], .footnote, .endnote, [role="doc-noteref"]'); | |
| if (footnoteEl) { | |
| const href = footnoteEl.getAttribute('href'); | |
| if (href) { | |
| const id = href.substring(1); // Remove # | |
| const footnoteContent = document.getElementById(id); | |
| if (footnoteContent) { | |
| const popup = document.getElementById('footnote-popup'); | |
| popup.innerHTML = footnoteContent.innerHTML; | |
| popup.style.display = 'block'; | |
| // Position near the hovered element | |
| const rect = footnoteEl.getBoundingClientRect(); | |
| popup.style.left = `${rect.left}px`; | |
| popup.style.top = `${rect.bottom + window.scrollY}px`; | |
| } | |
| } | |
| } | |
| }); | |
| document.addEventListener('mouseout', function(e) { | |
| const footnoteEl = e.target.closest('a[epub:type="noteref"], .footnote, .endnote, [role="doc-noteref"]'); | |
| if (footnoteEl) { | |
| const popup = document.getElementById('footnote-popup'); | |
| popup.style.display = 'none'; | |
| } | |
| }); | |
| function applyTheme(theme) { | |
| if (!rendition) return; | |
| switch (theme) { | |
| case 'light': | |
| rendition.themes.register('light', { | |
| 'body': { | |
| 'color': '#000000', | |
| 'background': '#ffffff' | |
| } | |
| }); | |
| rendition.themes.select('light'); | |
| break; | |
| case 'sepia': | |
| rendition.themes.register('sepia', { | |
| 'body': { | |
| 'color': '#5b4636', | |
| 'background': '#f4ecd8' | |
| } | |
| }); | |
| rendition.themes.select('sepia'); | |
| break; | |
| case 'dark': | |
| rendition.themes.register('dark', { | |
| 'body': { | |
| 'color': '#ffffff', | |
| 'background': '#1a1a1a' | |
| } | |
| }); | |
| rendition.themes.select('dark'); | |
| break; | |
| } | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=webdevsha/epub-reader" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |