GTVT / index.html
Translsis's picture
Update index.html
2c37878 verified
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Viewer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #2c3e50;
height: 100vh;
overflow: hidden;
}
.container {
height: 100vh;
display: flex;
flex-direction: column;
background: white;
}
/* Menu Bar */
.menubar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 8px 15px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
flex-shrink: 0;
}
.menubar h1 {
font-size: 1.1em;
color: white;
margin: 0;
font-weight: 600;
}
.menubar-spacer {
flex: 1;
}
.btn {
padding: 6px 14px;
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 5px;
cursor: pointer;
font-weight: 500;
font-size: 0.85em;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn:hover:not(:disabled) {
background: rgba(255,255,255,0.3);
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.btn-primary {
background: white;
color: #667eea;
border-color: white;
font-weight: 600;
}
.btn-primary:hover {
background: #f8f9fa;
}
#fileInput {
display: none;
}
.file-count {
padding: 6px 12px;
background: rgba(255,255,255,0.9);
color: #667eea;
border-radius: 15px;
font-weight: 600;
font-size: 0.85em;
}
.page-info {
color: white;
font-size: 0.9em;
font-weight: 500;
}
.zoom-controls {
display: flex;
gap: 8px;
align-items: center;
}
.zoom-level {
color: white;
font-size: 0.85em;
min-width: 50px;
text-align: center;
}
/* Tabs */
.tabs {
display: flex;
background: #f8f9fa;
padding: 0 10px;
overflow-x: auto;
border-bottom: 1px solid #dee2e6;
gap: 2px;
flex-shrink: 0;
}
.tabs::-webkit-scrollbar {
height: 5px;
}
.tabs::-webkit-scrollbar-track {
background: #f1f1f1;
}
.tabs::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 3px;
}
.tab {
padding: 8px 12px;
cursor: pointer;
border: none;
background: transparent;
color: #6c757d;
font-weight: 500;
font-size: 0.8em;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
border-radius: 6px 6px 0 0;
margin-top: 2px;
display: flex;
align-items: center;
gap: 5px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
.tab:hover {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
}
.tab.active {
background: white;
color: #667eea;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #667eea;
}
.close-tab {
margin-left: auto;
color: #dc3545;
font-weight: bold;
font-size: 1em;
opacity: 0.6;
padding: 0 4px;
}
.close-tab:hover {
opacity: 1;
}
/* Content Area */
.content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
background: #525252;
}
.tab-content {
display: none;
flex: 1;
overflow: hidden;
}
.tab-content.active {
display: flex;
flex-direction: column;
}
.pdf-viewer-container {
flex: 1;
overflow: auto;
background: #525252;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.pdf-viewer-container::-webkit-scrollbar {
width: 10px;
}
.pdf-viewer-container::-webkit-scrollbar-track {
background: #3a3a3a;
}
.pdf-viewer-container::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 5px;
}
.pdf-viewer-container::-webkit-scrollbar-thumb:hover {
background: #5a6fd8;
}
.pdf-page {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
background: white;
transition: opacity 0.3s ease;
}
.pdf-page.loading {
opacity: 0.5;
}
.page-placeholder {
background: #ddd;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 1.2em;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #aaa;
padding: 50px;
pointer-events: none;
}
.content:not(:has(.tab-content.active)) .empty-state {
display: flex;
}
.content:has(.tab-content.active) .empty-state {
display: none;
}
.empty-state svg {
width: 100px;
height: 100px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h2 {
font-size: 1.5em;
margin-bottom: 10px;
}
.loading {
text-align: center;
padding: 50px;
color: white;
font-size: 1.2em;
font-weight: bold;
}
/* Drag and Drop Overlay */
.drop-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.95);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
}
.drop-overlay.active {
display: flex;
}
.drop-message {
color: white;
font-size: 2em;
font-weight: bold;
text-align: center;
}
</style>
</head>
<body>
<div class="drop-overlay" id="dropOverlay">
<div class="drop-message">📄 Thả file PDF vào đây</div>
</div>
<div class="container">
<!-- Menu Bar -->
<div class="menubar">
<h1>📄 PDF Viewer</h1>
<label for="fileInput" class="btn btn-primary">
📁 Mở file
</label>
<input type="file" id="fileInput" accept=".pdf" multiple>
<div class="file-count" id="fileCount" style="display: none;">0 file</div>
<div class="menubar-spacer"></div>
<div class="zoom-controls" id="zoomControls" style="display: none;">
<button class="btn" id="zoomOut">-</button>
<span class="zoom-level" id="zoomLevel">100%</span>
<button class="btn" id="zoomIn">+</button>
</div>
<div class="page-info" id="pageInfo" style="display: none;">
Trang <span id="currentPage">1</span> / <span id="totalPages">1</span>
</div>
<button class="btn" id="prevPage" style="display: none;"></button>
<button class="btn" id="nextPage" style="display: none;"></button>
</div>
<!-- Tabs -->
<div class="tabs" id="tabs"></div>
<!-- Content -->
<div class="content" id="content">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
</svg>
<h2>Chưa có PDF nào</h2>
<p>Nhấn "Mở file" hoặc kéo thả file PDF vào đây</p>
</div>
</div>
</div>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const fileInput = document.getElementById('fileInput');
const tabsContainer = document.getElementById('tabs');
const contentContainer = document.getElementById('content');
const fileCountEl = document.getElementById('fileCount');
const pageInfo = document.getElementById('pageInfo');
const currentPageEl = document.getElementById('currentPage');
const totalPagesEl = document.getElementById('totalPages');
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
const dropOverlay = document.getElementById('dropOverlay');
const zoomControls = document.getElementById('zoomControls');
const zoomLevelEl = document.getElementById('zoomLevel');
const zoomInBtn = document.getElementById('zoomIn');
const zoomOutBtn = document.getElementById('zoomOut');
let pdfFiles = [];
let pdfDocs = [];
let currentPages = [];
let zoomLevels = [];
let scrollPositions = [];
let activeIndex = -1;
let renderQueue = [];
let isRendering = false;
// Load saved state from localStorage
function loadSavedState() {
try {
const savedState = localStorage.getItem('pdfViewerState');
if (savedState) {
const state = JSON.parse(savedState);
currentPages = state.currentPages || [];
zoomLevels = state.zoomLevels || [];
scrollPositions = state.scrollPositions || [];
activeIndex = state.activeIndex || -1;
}
} catch (e) {
console.log('No saved state found');
}
}
// Save state to localStorage
function saveState() {
try {
const state = {
currentPages,
zoomLevels,
scrollPositions,
activeIndex,
fileNames: pdfFiles.map(f => f.name)
};
localStorage.setItem('pdfViewerState', JSON.stringify(state));
} catch (e) {
console.error('Failed to save state:', e);
}
}
// Auto-save state periodically
setInterval(saveState, 2000);
// Save state before page unload
window.addEventListener('beforeunload', saveState);
// Load saved state on page load
loadSavedState();
// File input
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
// Global drag and drop
let dragCounter = 0;
document.body.addEventListener('dragenter', (e) => {
e.preventDefault();
dragCounter++;
dropOverlay.classList.add('active');
});
document.body.addEventListener('dragleave', (e) => {
e.preventDefault();
dragCounter--;
if (dragCounter === 0) {
dropOverlay.classList.remove('active');
}
});
document.body.addEventListener('dragover', (e) => {
e.preventDefault();
});
document.body.addEventListener('drop', (e) => {
e.preventDefault();
dragCounter = 0;
dropOverlay.classList.remove('active');
const files = Array.from(e.dataTransfer.files).filter(f => f.type === 'application/pdf');
if (files.length > 0) {
handleFiles(files);
}
});
// Page navigation
prevPageBtn.addEventListener('click', () => scrollToPrevPage());
nextPageBtn.addEventListener('click', () => scrollToNextPage());
// Zoom controls
zoomInBtn.addEventListener('click', () => changeZoom(0.25));
zoomOutBtn.addEventListener('click', () => changeZoom(-0.25));
function handleFiles(files) {
if (files.length === 0) return;
const newFiles = Array.from(files);
const startIndex = pdfFiles.length;
pdfFiles = [...pdfFiles, ...newFiles];
fileCountEl.style.display = 'inline-block';
fileCountEl.textContent = `${pdfFiles.length} file`;
newFiles.forEach((file, idx) => {
const fileIndex = startIndex + idx;
// Use saved zoom or default
if (!zoomLevels[fileIndex]) {
zoomLevels[fileIndex] = 1.5;
}
if (!currentPages[fileIndex]) {
currentPages[fileIndex] = 1;
}
if (!scrollPositions[fileIndex]) {
scrollPositions[fileIndex] = 0;
}
addTab(file, fileIndex);
});
if (activeIndex === -1) {
switchTab(startIndex);
}
saveState();
}
function addTab(file, index) {
const tab = document.createElement('button');
tab.className = 'tab';
tab.innerHTML = `
<span>📄</span>
<span>${file.name}</span>
<span class="close-tab">×</span>
`;
tab.querySelector('.close-tab').addEventListener('click', (e) => {
e.stopPropagation();
closeTab(index);
});
tab.addEventListener('click', () => switchTab(index));
tab.dataset.index = index;
tab.title = file.name;
tabsContainer.appendChild(tab);
const tabContent = document.createElement('div');
tabContent.className = 'tab-content';
tabContent.dataset.index = index;
tabContent.innerHTML = '<div class="loading">⏳ Đang tải PDF...</div>';
contentContainer.appendChild(tabContent);
loadPDF(file, index, tabContent);
}
async function loadPDF(file, index, container) {
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise;
pdfDocs[index] = pdf;
currentPages[index] = 1;
const viewerContainer = document.createElement('div');
viewerContainer.className = 'pdf-viewer-container';
viewerContainer.dataset.index = index;
// Create placeholders for all pages
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({scale: zoomLevels[index]});
const canvas = document.createElement('canvas');
canvas.className = 'pdf-page';
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.dataset.page = pageNum;
canvas.dataset.rendered = 'false';
viewerContainer.appendChild(canvas);
}
container.innerHTML = '';
container.appendChild(viewerContainer);
// Hide empty state when first PDF loads
const emptyState = contentContainer.querySelector('.empty-state');
if (emptyState) {
emptyState.style.display = 'none';
}
// Setup scroll tracking with throttle
let scrollTimeout;
viewerContainer.addEventListener('scroll', () => {
// Save scroll position
scrollPositions[index] = viewerContainer.scrollTop;
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
updateCurrentPage(index, viewerContainer);
renderVisiblePages(index, viewerContainer);
saveState();
}, 100);
});
// Restore scroll position
if (scrollPositions[index]) {
setTimeout(() => {
viewerContainer.scrollTop = scrollPositions[index];
}, 100);
}
// Initial render of visible pages
renderVisiblePages(index, viewerContainer);
} catch (error) {
container.innerHTML = `
<div style="text-align: center; padding: 50px; color: #dc3545;">
<h2>❌ Lỗi khi tải PDF</h2>
<p>${error.message}</p>
</div>
`;
}
}
async function renderVisiblePages(index, container) {
const pages = container.querySelectorAll('.pdf-page');
const containerRect = container.getBoundingClientRect();
pages.forEach((canvas, idx) => {
const pageNum = parseInt(canvas.dataset.page);
const rect = canvas.getBoundingClientRect();
// Check if page is visible or near viewport
const isVisible = rect.top < containerRect.bottom + 500 &&
rect.bottom > containerRect.top - 500;
if (isVisible && canvas.dataset.rendered === 'false') {
renderQueue.push({index, pageNum, canvas});
}
});
processRenderQueue();
}
async function processRenderQueue() {
if (isRendering || renderQueue.length === 0) return;
isRendering = true;
while (renderQueue.length > 0) {
const {index, pageNum, canvas} = renderQueue.shift();
try {
const pdf = pdfDocs[index];
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({scale: zoomLevels[index]});
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
canvas.dataset.rendered = 'true';
} catch (error) {
console.error('Render error:', error);
}
}
isRendering = false;
}
function updateCurrentPage(index, container) {
if (index !== activeIndex) return;
const pages = container.querySelectorAll('.pdf-page');
const containerRect = container.getBoundingClientRect();
const containerCenter = containerRect.top + containerRect.height / 2;
let closestPage = 1;
let closestDistance = Infinity;
pages.forEach((page, idx) => {
const pageRect = page.getBoundingClientRect();
const pageCenter = pageRect.top + pageRect.height / 2;
const distance = Math.abs(pageCenter - containerCenter);
if (distance < closestDistance) {
closestDistance = distance;
closestPage = idx + 1;
}
});
currentPages[index] = closestPage;
currentPageEl.textContent = closestPage;
saveState();
}
function scrollToPrevPage() {
const container = contentContainer.querySelector(`.tab-content.active .pdf-viewer-container`);
if (!container) return;
const targetPage = Math.max(1, currentPages[activeIndex] - 1);
scrollToPage(container, targetPage);
}
function scrollToNextPage() {
const container = contentContainer.querySelector(`.tab-content.active .pdf-viewer-container`);
if (!container) return;
const pdf = pdfDocs[activeIndex];
const targetPage = Math.min(pdf.numPages, currentPages[activeIndex] + 1);
scrollToPage(container, targetPage);
}
function scrollToPage(container, pageNum) {
const page = container.querySelector(`[data-page="${pageNum}"]`);
if (page) {
page.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function changeZoom(delta) {
if (activeIndex === -1) return;
zoomLevels[activeIndex] = Math.max(0.5, Math.min(3, zoomLevels[activeIndex] + delta));
zoomLevelEl.textContent = Math.round(zoomLevels[activeIndex] * 100) + '%';
saveState();
// Re-render current PDF with new zoom
const tabContent = contentContainer.querySelector(`.tab-content[data-index="${activeIndex}"]`);
const file = pdfFiles[activeIndex];
if (tabContent && file) {
const currentScroll = tabContent.querySelector('.pdf-viewer-container')?.scrollTop || 0;
loadPDF(file, activeIndex, tabContent).then(() => {
const container = tabContent.querySelector('.pdf-viewer-container');
if (container) {
container.scrollTop = currentScroll * (zoomLevels[activeIndex] / (zoomLevels[activeIndex] - delta));
scrollPositions[activeIndex] = container.scrollTop;
}
});
}
}
function switchTab(index) {
activeIndex = index;
const tabs = tabsContainer.querySelectorAll('.tab');
tabs.forEach((tab) => {
tab.classList.toggle('active', parseInt(tab.dataset.index) === index);
});
const contents = contentContainer.querySelectorAll('.tab-content');
contents.forEach((content) => {
content.classList.toggle('active', parseInt(content.dataset.index) === index);
});
// Update controls
if (pdfDocs[index]) {
pageInfo.style.display = 'block';
prevPageBtn.style.display = 'inline-block';
nextPageBtn.style.display = 'inline-block';
zoomControls.style.display = 'flex';
totalPagesEl.textContent = pdfDocs[index].numPages;
currentPageEl.textContent = currentPages[index] || 1;
zoomLevelEl.textContent = Math.round(zoomLevels[index] * 100) + '%';
// Render visible pages when switching tabs
const container = contentContainer.querySelector(`.tab-content.active .pdf-viewer-container`);
if (container) {
setTimeout(() => renderVisiblePages(index, container), 100);
}
}
saveState();
}
function closeTab(index) {
pdfFiles.splice(index, 1);
pdfDocs.splice(index, 1);
currentPages.splice(index, 1);
zoomLevels.splice(index, 1);
scrollPositions.splice(index, 1);
const tab = tabsContainer.querySelector(`[data-index="${index}"]`);
if (tab) tab.remove();
const content = contentContainer.querySelector(`.tab-content[data-index="${index}"]`);
if (content) content.remove();
// Update indices
tabsContainer.querySelectorAll('.tab').forEach((tab) => {
const idx = parseInt(tab.dataset.index);
if (idx > index) {
tab.dataset.index = idx - 1;
}
});
contentContainer.querySelectorAll('.tab-content').forEach((content) => {
const idx = parseInt(content.dataset.index);
if (idx > index) {
content.dataset.index = idx - 1;
}
});
if (pdfFiles.length === 0) {
fileCountEl.style.display = 'none';
pageInfo.style.display = 'none';
prevPageBtn.style.display = 'none';
nextPageBtn.style.display = 'none';
zoomControls.style.display = 'none';
activeIndex = -1;
// Remove all tabs and content
tabsContainer.innerHTML = '';
contentContainer.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
</svg>
<h2>Chưa có PDF nào</h2>
<p>Nhấn "Mở file" hoặc kéo thả file PDF vào đây</p>
</div>
`;
localStorage.removeItem('pdfViewerState');
} else {
fileCountEl.textContent = `${pdfFiles.length} file`;
if (activeIndex >= pdfFiles.length || activeIndex === index) {
switchTab(Math.max(0, Math.min(index, pdfFiles.length - 1)));
}
}
saveState();
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (activeIndex === -1) return;
if (e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault();
scrollToPrevPage();
} else if (e.key === 'ArrowDown' || e.key === 'PageDown') {
e.preventDefault();
scrollToNextPage();
} else if (e.key === '+' || e.key === '=') {
e.preventDefault();
changeZoom(0.25);
} else if (e.key === '-') {
e.preventDefault();
changeZoom(-0.25);
}
});
</script>
</body>
</html>