From 0987058aa233cc68d8b68becb3d8e92dfb3e55fd Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Tue, 24 Mar 2026 08:55:39 +0530 Subject: [PATCH] fix: integrate PDF viewer into tab system for multitab support - Add tab type system ('markdown' and 'pdf') - Create PDF tabs with their own state (page, zoom, rotation) - Update closeTab to properly clean up PDF resources - Update updateUI to handle PDF tabs (hide toolbar, etc.) - Add visual indicators for PDF tabs in tab bar - Add CSS styles for PDF tab containers Fixes: PDF and markdown multitab function not working Amit Haridas --- src/renderer.js | 363 +++++++++++++++++++++++++++++++++++++----------- src/styles.css | 99 +++++++++++++ 2 files changed, 378 insertions(+), 84 deletions(-) diff --git a/src/renderer.js b/src/renderer.js index e28dd6d..2486456 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -95,7 +95,7 @@ class TabManager { this.autoSaveInterval = null; this.autoSaveDelay = 30000; // 30 seconds this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]'); - + // Initialize first tab this.tabs.set(1, { id: 1, @@ -105,9 +105,15 @@ class TabManager { isDirty: false, editorView: null, findMatches: [], - currentMatchIndex: -1 + currentMatchIndex: -1, + type: 'markdown', // 'markdown' or 'pdf' + // PDF-specific state + pdfDoc: null, + pdfCurrentPage: 1, + pdfZoomLevel: 1.0, + pdfRotation: 0 }); - + this.setupEventListeners(); this.updateUI(); } @@ -161,25 +167,218 @@ class TabManager { }); } - createNewTab() { + createNewTab(type = 'markdown') { const newTabId = this.nextTabId++; const tab = { id: newTabId, - title: 'Untitled', + title: type === 'pdf' ? 'PDF Document' : 'Untitled', content: '', filePath: null, isDirty: false, editorView: null, findMatches: [], - currentMatchIndex: -1 + currentMatchIndex: -1, + type: type, + // PDF-specific state + pdfDoc: null, + pdfCurrentPage: 1, + pdfZoomLevel: 1.0, + pdfRotation: 0 }; - + this.tabs.set(newTabId, tab); this.createTabElements(tab); this.switchToTab(newTabId); this.startAutoSave(); this.updateTabBar(); } + + createPdfTab(filePath) { + const newTabId = this.nextTabId++; + const fileName = require('path').basename(filePath); + const tab = { + id: newTabId, + title: fileName, + content: '', + filePath: filePath, + isDirty: false, + editorView: null, + findMatches: [], + currentMatchIndex: -1, + type: 'pdf', + // PDF-specific state + pdfDoc: null, + pdfCurrentPage: 1, + pdfZoomLevel: 1.0, + pdfRotation: 0 + }; + + this.tabs.set(newTabId, tab); + this.createPdfTabElements(tab); + this.switchToTab(newTabId); + this.loadPdfInTab(tab.id, filePath); + this.updateTabBar(); + return tab.id; + } + + createPdfTabElements(tab) { + // Create PDF tab content container + const tabContent = document.createElement('div'); + tabContent.className = 'tab-content'; + tabContent.id = `tab-content-${tab.id}`; + tabContent.dataset.tabId = tab.id; + tabContent.dataset.tabType = 'pdf'; + + tabContent.innerHTML = ` +
+
+ + + / 1 + + + 100% + + + + +
+
+ +
+
+ `; + + document.querySelector('.editor-container').appendChild(tabContent); + + // Add event listeners for PDF controls + this.setupPdfTabEvents(tab.id); + } + + setupPdfTabEvents(tabId) { + const container = document.getElementById(`pdf-container-${tabId}`); + if (!container) return; + + container.addEventListener('click', async (e) => { + const action = e.target.dataset.action; + if (!action) return; + + const tab = this.tabs.get(tabId); + if (!tab || !tab.pdfDoc) return; + + switch (action) { + case 'prev': + if (tab.pdfCurrentPage > 1) { + tab.pdfCurrentPage--; + await this.renderPdfPageInTab(tabId); + } + break; + case 'next': + if (tab.pdfCurrentPage < tab.pdfDoc.numPages) { + tab.pdfCurrentPage++; + await this.renderPdfPageInTab(tabId); + } + break; + case 'zoom-out': + if (tab.pdfZoomLevel > 0.25) { + tab.pdfZoomLevel -= 0.25; + await this.renderPdfPageInTab(tabId); + } + break; + case 'zoom-in': + if (tab.pdfZoomLevel < 4.0) { + tab.pdfZoomLevel += 0.25; + await this.renderPdfPageInTab(tabId); + } + break; + case 'fit-width': + const page = await tab.pdfDoc.getPage(tab.pdfCurrentPage); + const viewport = page.getViewport({ scale: 1, rotation: tab.pdfRotation }); + const containerWidth = container.querySelector('.pdf-canvas-container').clientWidth - 40; + tab.pdfZoomLevel = containerWidth / viewport.width; + await this.renderPdfPageInTab(tabId); + break; + case 'rotate-left': + tab.pdfRotation = (tab.pdfRotation - 90 + 360) % 360; + await this.renderPdfPageInTab(tabId); + break; + case 'rotate-right': + tab.pdfRotation = (tab.pdfRotation + 90) % 360; + await this.renderPdfPageInTab(tabId); + break; + } + }); + + // Page input handler + const pageInput = document.getElementById(`pdf-page-${tabId}`); + if (pageInput) { + pageInput.addEventListener('change', async (e) => { + const tab = this.tabs.get(tabId); + const pageNum = parseInt(e.target.value); + if (tab && tab.pdfDoc && pageNum >= 1 && pageNum <= tab.pdfDoc.numPages) { + tab.pdfCurrentPage = pageNum; + await this.renderPdfPageInTab(tabId); + } + }); + } + } + + async loadPdfInTab(tabId, filePath) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + try { + document.getElementById('status-text').textContent = 'Loading PDF...'; + + const loadingTask = getPdfjsLib().getDocument(filePath); + tab.pdfDoc = await loadingTask.promise; + tab.pdfCurrentPage = 1; + tab.pdfZoomLevel = 1.0; + tab.pdfRotation = 0; + + // Update UI + document.getElementById(`pdf-total-${tabId}`).textContent = tab.pdfDoc.numPages; + document.getElementById(`pdf-page-${tabId}`).value = 1; + document.getElementById(`pdf-page-${tabId}`).max = tab.pdfDoc.numPages; + document.getElementById(`pdf-zoom-${tabId}`).textContent = '100%'; + + await this.renderPdfPageInTab(tabId); + + document.getElementById('status-text').textContent = `PDF: ${tab.title} (${tab.pdfDoc.numPages} pages)`; + } catch (error) { + console.error('Error loading PDF:', error); + document.getElementById('status-text').textContent = 'Error loading PDF'; + alert('Error loading PDF: ' + error.message); + } + } + + async renderPdfPageInTab(tabId) { + const tab = this.tabs.get(tabId); + if (!tab || !tab.pdfDoc) return; + + try { + const page = await tab.pdfDoc.getPage(tab.pdfCurrentPage); + const canvas = document.getElementById(`pdf-canvas-${tabId}`); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const viewport = page.getViewport({ scale: tab.pdfZoomLevel, rotation: tab.pdfRotation }); + + canvas.width = viewport.width; + canvas.height = viewport.height; + + await page.render({ + canvasContext: ctx, + viewport: viewport + }).promise; + + // Update UI + document.getElementById(`pdf-page-${tabId}`).value = tab.pdfCurrentPage; + document.getElementById(`pdf-zoom-${tabId}`).textContent = Math.round(tab.pdfZoomLevel * 100) + '%'; + } catch (error) { + console.error('Error rendering PDF page:', error); + } + } createTabElements(tab) { // Create tab content container @@ -263,19 +462,30 @@ class TabManager { if (this.tabs.size === 1) return; // Don't close the last tab const tab = this.tabs.get(tabId); - if (tab.isDirty) { - // Show confirmation dialog for unsaved changes + if (!tab) return; + + if (tab.isDirty && tab.type === 'markdown') { + // Show confirmation dialog for unsaved changes (only for markdown) const result = confirm('You have unsaved changes. Do you want to close this tab without saving?'); if (!result) return; } - // Destroy CodeMirror view + // Destroy CodeMirror view (for markdown tabs) if (tab?.editorView) { tab.editorView.destroy(); } + // Destroy PDF document (for PDF tabs) + if (tab?.pdfDoc) { + try { + tab.pdfDoc.destroy(); + } catch (e) { + console.warn('Error destroying PDF:', e); + } + } + // Remove tab elements - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + const tabElement = document.querySelector(`.tab[data-tab-id="${tabId}"]`); const tabContent = document.getElementById(`tab-content-${tabId}`); if (tabElement?.classList.contains('tab')) { @@ -309,38 +519,57 @@ class TabManager { sortedTabs.forEach(tab => { const tabElement = document.createElement('div'); - tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`; + const typeClass = tab.type === 'pdf' ? 'pdf-tab' : 'markdown-tab'; + tabElement.className = `tab ${typeClass} ${tab.id === this.activeTabId ? 'active' : ''}`; tabElement.dataset.tabId = tab.id; - - const title = tab.filePath ? - tab.filePath.split('/').pop() : + tabElement.dataset.tabType = tab.type || 'markdown'; + + const title = tab.filePath ? + tab.filePath.split('/').pop() : tab.title; - + const dirtyIndicator = tab.isDirty ? ' •' : ''; - + const typeIndicator = tab.type === 'pdf' ? '📄 ' : ''; + tabElement.innerHTML = ` - ${title}${dirtyIndicator} + ${typeIndicator}${title}${dirtyIndicator} `; - + tabBar.insertBefore(tabElement, newTabBtn); }); } - + updateUI() { // Show/hide tab contents document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); - + const activeContent = document.getElementById(`tab-content-${this.activeTabId}`); if (activeContent) { activeContent.classList.add('active'); } - - // Update preview visibility - this.updatePreviewVisibility(); - this.updateLineNumbers(); + + // Get active tab to check type + const activeTab = this.tabs.get(this.activeTabId); + + // Show/hide toolbar based on tab type + const toolbar = document.querySelector('.toolbar'); + if (toolbar) { + if (activeTab?.type === 'pdf') { + toolbar.classList.add('hidden'); + } else { + toolbar.classList.remove('hidden'); + } + } + + // Update preview visibility (only for markdown tabs) + if (activeTab?.type !== 'pdf') { + this.updatePreviewVisibility(); + this.updateLineNumbers(); + } + this.updateTabBar(); } @@ -4093,12 +4322,13 @@ ipcRenderer.on('insert-content', (event, content) => { // PDF VIEWER FUNCTIONALITY // ============================================ +// Legacy PDF viewer globals - kept for PDF editor dialogs that still use them let pdfDoc = null; let pdfCurrentPage = 1; let pdfZoomLevel = 1.0; let pdfRotation = 0; let pdfFilePath = null; -let isPdfViewerActive = false; // Track if PDF viewer is currently shown +let isPdfViewerActive = false; // Initialize PDF.js // Lazy-load pdfjs-dist only when PDF viewer is needed @@ -4111,62 +4341,19 @@ function getPdfjsLib() { return _pdfjsLib; } -// Open PDF file +// Open PDF file - now creates a tab instead of replacing the entire view async function openPdfFile(filePath) { - // Prevent multiple simultaneous PDF loads - if (isPdfViewerActive && pdfFilePath === filePath) { - console.log('PDF already open:', filePath); - return; - } - - // Close any existing PDF first - if (pdfDoc) { - try { - await pdfDoc.destroy(); - } catch (e) { - console.warn('Error destroying previous PDF:', e); + // Check if this PDF is already open in a tab + for (const [tabId, tab] of tabManager.tabs) { + if (tab.type === 'pdf' && tab.filePath === filePath) { + // Just switch to the existing tab + tabManager.switchToTab(tabId); + return; } - pdfDoc = null; } - try { - // Show loading state - document.getElementById('status-text').textContent = 'Loading PDF...'; - - const loadingTask = getPdfjsLib().getDocument(filePath); - pdfDoc = await loadingTask.promise; - pdfFilePath = filePath; - pdfCurrentPage = 1; - pdfZoomLevel = 1.0; - pdfRotation = 0; - isPdfViewerActive = true; - - // Update UI - document.getElementById('pdf-total-pages').textContent = pdfDoc.numPages; - document.getElementById('pdf-page-input').value = 1; - document.getElementById('pdf-page-input').max = pdfDoc.numPages; - document.getElementById('pdf-filename').textContent = require('path').basename(filePath); - document.getElementById('pdf-zoom-level').textContent = '100%'; - - // Hide markdown toolbar, tabs, show PDF viewer - document.querySelector('.toolbar').classList.add('hidden'); - document.getElementById('tab-bar').classList.add('hidden'); - document.querySelectorAll('.tab-content').forEach(tc => tc.classList.add('hidden')); - document.getElementById('pdf-viewer-container').classList.remove('hidden'); - - // Render first page - await renderPdfPage(pdfCurrentPage); - - // Update status - document.getElementById('status-text').textContent = `PDF: ${require('path').basename(filePath)} (${pdfDoc.numPages} pages)`; - } catch (error) { - console.error('Error loading PDF:', error); - isPdfViewerActive = false; - pdfDoc = null; - pdfFilePath = null; - document.getElementById('status-text').textContent = 'Error loading PDF'; - alert('Error loading PDF: ' + error.message); - } + // Create a new PDF tab + tabManager.createPdfTab(filePath); } // Render PDF page @@ -4279,7 +4466,7 @@ document.getElementById('pdf-close')?.addEventListener('click', () => { }); async function closePdfViewer() { - // Destroy PDF document to free memory + // Legacy PDF viewer close - for backward compatibility with PDF editor dialogs if (pdfDoc) { try { await pdfDoc.destroy(); @@ -4292,19 +4479,27 @@ async function closePdfViewer() { pdfFilePath = null; isPdfViewerActive = false; - // Hide PDF viewer - document.getElementById('pdf-viewer-container').classList.add('hidden'); + // If we're using the new tab-based system, close the current PDF tab + if (tabManager) { + const activeTab = tabManager.tabs.get(tabManager.activeTabId); + if (activeTab && activeTab.type === 'pdf') { + tabManager.closeTab(tabManager.activeTabId); + return; + } + } + + // Legacy: Hide PDF viewer container + document.getElementById('pdf-viewer-container')?.classList.add('hidden'); // Show markdown tabs, tab bar, and toolbar - document.getElementById('tab-bar').classList.remove('hidden'); + document.getElementById('tab-bar')?.classList.remove('hidden'); document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('hidden')); - document.querySelector('.toolbar').classList.remove('hidden'); + document.querySelector('.toolbar')?.classList.remove('hidden'); // Activate the correct markdown tab if (tabManager) { const activeTab = document.querySelector(`.tab-content[data-tab-id="${tabManager.activeTabId}"]`); if (activeTab) activeTab.classList.add('active'); - // Refresh the active tab tabManager.updatePreview(tabManager.activeTabId); } diff --git a/src/styles.css b/src/styles.css index ba2b24a..ac3acf0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -107,6 +107,105 @@ body { outline-offset: 2px; } +/* PDF Tab Styles */ +.tab.pdf-tab { + background: #e3f2fd; + border-color: #90caf9; +} + +.tab.pdf-tab.active { + background: #fff; + border-color: #1976d2; +} + +.tab.pdf-tab .tab-title { + color: #1565c0; +} + +/* PDF Tab Container */ +.pdf-tab-container { + display: flex; + flex-direction: column; + height: 100%; + background: var(--bg-primary, #fff); +} + +.pdf-tab-container .pdf-controls { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: #f5f5f5; + border-bottom: 1px solid #ddd; +} + +.pdf-tab-container .pdf-nav-btn, +.pdf-tab-container .pdf-zoom-btn, +.pdf-tab-container .pdf-rotate-btn { + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 14px; + min-width: 28px; + text-align: center; +} + +.pdf-tab-container .pdf-nav-btn:hover, +.pdf-tab-container .pdf-zoom-btn:hover, +.pdf-tab-container .pdf-rotate-btn:hover { + background: #e0e0e0; +} + +.pdf-tab-container .pdf-page-input { + width: 50px; + text-align: center; + padding: 4px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.pdf-tab-container .pdf-page-info { + color: #666; + font-size: 13px; +} + +.pdf-tab-container .pdf-zoom-level { + min-width: 50px; + text-align: center; + font-size: 13px; + color: #666; +} + +.pdf-tab-container .pdf-canvas-container { + flex: 1; + overflow: auto; + display: flex; + justify-content: center; + padding: 20px; + background: #525659; +} + +.pdf-tab-container .pdf-canvas-container canvas { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); +} + +/* Dark theme PDF tab */ +body.dark .tab.pdf-tab { + background: #1a237e; + border-color: #3949ab; +} + +body.dark .tab.pdf-tab .tab-title { + color: #90caf9; +} + +body.dark .pdf-tab-container .pdf-controls { + background: #2d2d2d; + border-color: #444; +} + /* Tab Content */ .tab-content { display: flex;