epub-reader / index.html
webdevsha's picture
nope, no popup when we hover on the footnote/endnote numbering. - Initial Deployment
7d5b725 verified
<!DOCTYPE html>
<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>