mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50ffe8ed2 | ||
|
|
5376bddc59 | ||
|
|
d3b67b1388 | ||
|
|
aac496923e |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
||||||
|
|
||||||
**Current Version**: v1.2.1
|
**Current Version**: v1.5.0
|
||||||
**Author**: Amit Haridas (amit.wh@gmail.com)
|
**Author**: Amit Haridas (amit.wh@gmail.com)
|
||||||
**License**: MIT
|
**License**: MIT
|
||||||
**Repository**: https://github.com/amitwh/pan-converter
|
**Repository**: https://github.com/amitwh/pan-converter
|
||||||
@@ -13,24 +13,25 @@
|
|||||||
|
|
||||||
### Core Technologies
|
### Core Technologies
|
||||||
- **Electron** - Cross-platform desktop application framework
|
- **Electron** - Cross-platform desktop application framework
|
||||||
- **Pandoc** - Universal document converter (required dependency)
|
- **Pandoc** - Universal document converter (required system dependency)
|
||||||
- **marked** - Markdown parsing and rendering
|
- **marked** - Markdown parsing and rendering
|
||||||
- **highlight.js** - Syntax highlighting
|
- **highlight.js** - Syntax highlighting
|
||||||
- **XLSX** - Spreadsheet export functionality
|
- **KaTeX** - Mathematical expression rendering
|
||||||
- **DOMPurify** - HTML sanitization
|
- **DOMPurify** - HTML sanitization
|
||||||
|
|
||||||
### Application Structure
|
### Application Structure
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── main.js # Electron main process, menu system, IPC handlers
|
├── main.js # Electron main process, menu system, IPC handlers
|
||||||
├── renderer.js # UI logic, editor functionality, event handling
|
├── renderer.js # TabManager class, multi-file editing, event handling
|
||||||
├── index.html # Application layout and components
|
├── index.html # Application layout with tabbed interface
|
||||||
└── styles.css # Comprehensive styling with multi-theme support
|
└── styles.css # Comprehensive styling with multi-theme support
|
||||||
|
|
||||||
assets/
|
assets/
|
||||||
└── icon.png # Application icon
|
└── icon.png # Application icon
|
||||||
|
|
||||||
package.json # Dependencies and build configuration
|
package.json # Dependencies and build configuration
|
||||||
|
CLAUDE.md # Development documentation for AI assistants
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
@@ -108,6 +109,113 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
## Feature Implementation Guide
|
## Feature Implementation Guide
|
||||||
|
|
||||||
|
### v1.5.0 Enhanced Features & Open Source Compatibility (Latest)
|
||||||
|
|
||||||
|
#### 🔧 Export Function Fixes & Optional Advanced Options
|
||||||
|
**Export Dialog Enhancement** (`src/index.html:125-133`, `src/renderer.js:960-985`)
|
||||||
|
- Fixed export function issues after advanced export options integration
|
||||||
|
- Added optional advanced export options via checkbox toggle (unchecked by default)
|
||||||
|
- Basic export options always visible, advanced options hidden until enabled
|
||||||
|
- Clean UI separation between simple and advanced export workflows
|
||||||
|
|
||||||
|
#### 🏗️ Open Source Compatibility & Dependency Removal
|
||||||
|
**Removed Proprietary Dependencies** (`src/main.js:430-450`, `package.json:32-37`)
|
||||||
|
- Removed bundled Pandoc binaries - now requires system-installed Pandoc
|
||||||
|
- Replaced proprietary XLSX dependency with open-source CSV export
|
||||||
|
- Removed non-essential bundled binaries to ensure open-source compatibility
|
||||||
|
- Updated export functions to use system Pandoc installation
|
||||||
|
|
||||||
|
#### ✨ Advanced User Experience Features
|
||||||
|
**Auto-Save System** (`src/renderer.js:445-484`)
|
||||||
|
- Automatic saving every 30 seconds with visual indicators
|
||||||
|
- Smooth slide-in animation for auto-save notifications
|
||||||
|
- Prevents data loss during extended editing sessions
|
||||||
|
- Integrated with file opening and tab creation workflows
|
||||||
|
|
||||||
|
**Enhanced Document Statistics** (`src/renderer.js:385-444`)
|
||||||
|
- Comprehensive statistics: words, characters, lines, paragraphs, sentences
|
||||||
|
- Estimated reading time calculation (200 words per minute)
|
||||||
|
- Real-time updates as user types
|
||||||
|
- Professional presentation in status bar
|
||||||
|
|
||||||
|
**Recent Files Management** (`src/main.js:69-114`, `src/renderer.js:487-512`)
|
||||||
|
- Recent files menu with last 10 opened files
|
||||||
|
- Persistent storage via localStorage and user data directory
|
||||||
|
- Menu integration with File > Recent Files submenu
|
||||||
|
- Clear recent files functionality with menu rebuild
|
||||||
|
|
||||||
|
**Mathematical Expression Support** (`src/renderer.js:1099-1130`, `src/renderer.js:292-306`)
|
||||||
|
- KaTeX integration for rendering mathematical expressions
|
||||||
|
- Support for multiple delimiters: $$, $, \\[\\], \\(\\)
|
||||||
|
- Real-time math rendering in preview pane
|
||||||
|
- Fallback handling for missing KaTeX library
|
||||||
|
|
||||||
|
### v1.4.0 Advanced Export & Batch Processing
|
||||||
|
|
||||||
|
#### 🔧 Fixed File Association Support
|
||||||
|
**File Loading Fix** (`src/main.js:385-390`, `src/renderer.js:485-486`)
|
||||||
|
- Fixed timing issue with file association loading
|
||||||
|
- Added proper `renderer-ready` event to ensure TabManager is initialized
|
||||||
|
- Files now open correctly when double-clicked or opened via right-click menu
|
||||||
|
- Command-line file arguments are properly handled on startup
|
||||||
|
|
||||||
|
#### 🎛️ Advanced Export Options Dialog
|
||||||
|
**Template & Metadata Support** (`src/main.js:247-357`, `src/index.html:117-212`)
|
||||||
|
- Comprehensive export options dialog with professional UI
|
||||||
|
- Template selection (default or custom template files)
|
||||||
|
- Metadata fields (title, author, date, subject) with dynamic field addition
|
||||||
|
- Document options: Table of Contents, section numbering, citations
|
||||||
|
- PDF-specific options: Engine selection (XeLaTeX, PDFLaTeX, LuaLaTeX), custom margins
|
||||||
|
- Bibliography support: .bib, .yaml, .json files with CSL styling
|
||||||
|
- All export formats now use enhanced options dialog
|
||||||
|
|
||||||
|
#### 📁 Batch File Conversion System
|
||||||
|
**Multi-File Processing** (`src/main.js:179-186`, `src/main.js:559-690`)
|
||||||
|
- New "Batch" menu for converting entire folders
|
||||||
|
- Recursive folder processing with progress tracking
|
||||||
|
- Support for all export formats with advanced options
|
||||||
|
- Real-time progress bar and file-by-file status updates
|
||||||
|
- Maintains folder structure in output directory
|
||||||
|
- Error handling with completion statistics
|
||||||
|
|
||||||
|
#### 🎨 Enhanced UI Components
|
||||||
|
**Dialog System** (`src/styles.css:838-1361`)
|
||||||
|
- Professional modal dialogs with backdrop and animations
|
||||||
|
- Theme-aware styling for all new components
|
||||||
|
- Responsive layouts with proper accessibility
|
||||||
|
- Form validation and user feedback systems
|
||||||
|
- Progress indicators for long-running operations
|
||||||
|
|
||||||
|
### v1.3.x Tabbed Interface & Enhanced Features
|
||||||
|
|
||||||
|
#### 🗂️ Tabbed Multi-File Support (v1.3.0)
|
||||||
|
**TabManager Class** (`src/renderer.js`)
|
||||||
|
- Complete tab management system for multiple files
|
||||||
|
- Tab switching, creation, and closure
|
||||||
|
- State preservation per tab (content, cursor position, scroll)
|
||||||
|
- File path tracking for each tab
|
||||||
|
- Keyboard shortcuts: `Ctrl/Cmd+T` (new tab), `Ctrl/Cmd+W` (close tab)
|
||||||
|
|
||||||
|
#### 🎯 Enhanced PDF Export (v1.3.0)
|
||||||
|
**Multi-Engine Fallback System** (`src/main.js:239-280`)
|
||||||
|
- Primary: XeLaTeX with proper margins
|
||||||
|
- Fallback 1: PDFLaTeX
|
||||||
|
- Fallback 2: wkhtmltopdf
|
||||||
|
- Automatic engine detection and switching
|
||||||
|
|
||||||
|
#### 📁 File Association Support (v1.3.1)
|
||||||
|
**OS Integration** (`src/main.js:452-498`, `package.json:50-65`)
|
||||||
|
- Double-click .md files to open in PanConverter
|
||||||
|
- Command-line argument handling
|
||||||
|
- Pending file queue for startup loading
|
||||||
|
|
||||||
|
#### 🎨 Typography & Spacing (v1.3.2-1.3.3)
|
||||||
|
**Preview Enhancement** (`src/styles.css`)
|
||||||
|
- Restored ideal text spacing from v1.0
|
||||||
|
- Font sizes increased to 15px for better readability
|
||||||
|
- Comprehensive selector coverage for legacy and new containers
|
||||||
|
- Theme-aware typography for all content types
|
||||||
|
|
||||||
### v1.2.1 Comprehensive Editor Enhancements
|
### v1.2.1 Comprehensive Editor Enhancements
|
||||||
|
|
||||||
#### ✨ Advanced Editor Features
|
#### ✨ Advanced Editor Features
|
||||||
@@ -189,19 +297,24 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
## File Structure & Key Components
|
## File Structure & Key Components
|
||||||
|
|
||||||
### Main Process (`src/main.js`)
|
### Main Process (`src/main.js`)
|
||||||
- **Menu System**: Comprehensive menu with file operations, editing, conversion, view options
|
- **Menu System**: Comprehensive menu with file operations, editing, conversion, view options, batch processing
|
||||||
- **IPC Handlers**: Communication between main and renderer processes
|
- **IPC Handlers**: Communication between main and renderer processes
|
||||||
- **File Operations**: Open, save, import/export functionality
|
- **File Operations**: Open, save, import/export functionality with file association support
|
||||||
|
- **Export System**: Advanced export with templates, metadata, and Pandoc integration
|
||||||
|
- **Batch Processing**: Multi-file conversion with progress tracking and error handling
|
||||||
- **Theme Management**: Persistent theme storage and application
|
- **Theme Management**: Persistent theme storage and application
|
||||||
- **Spreadsheet Export**: Table extraction and XLSX generation
|
- **Spreadsheet Export**: Table extraction and XLSX generation
|
||||||
- **About Dialog**: Application information and feature list
|
- **About Dialog**: Application information and feature list
|
||||||
|
|
||||||
### Renderer Process (`src/renderer.js`)
|
### Renderer Process (`src/renderer.js`)
|
||||||
- **Editor Initialization**: CodeMirror-like functionality with custom implementation
|
- **Editor Initialization**: CodeMirror-like functionality with custom implementation
|
||||||
|
- **TabManager System**: Multi-file editing with state management
|
||||||
- **Find & Replace Engine**: Search algorithms with regex support
|
- **Find & Replace Engine**: Search algorithms with regex support
|
||||||
- **Undo/Redo Manager**: History stack management
|
- **Undo/Redo Manager**: History stack management
|
||||||
- **Auto-indentation Logic**: Smart list continuation
|
- **Auto-indentation Logic**: Smart list continuation
|
||||||
- **Live Preview**: Real-time markdown rendering with DOMPurify
|
- **Live Preview**: Real-time markdown rendering with DOMPurify
|
||||||
|
- **Export Dialog Management**: Advanced options collection and validation
|
||||||
|
- **Batch Conversion Interface**: Folder selection, progress tracking, and user feedback
|
||||||
- **Event Handling**: Keyboard shortcuts and UI interactions
|
- **Event Handling**: Keyboard shortcuts and UI interactions
|
||||||
- **Statistics Tracking**: Word/character counting
|
- **Statistics Tracking**: Word/character counting
|
||||||
|
|
||||||
@@ -214,8 +327,11 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
### HTML Structure (`src/index.html`)
|
### HTML Structure (`src/index.html`)
|
||||||
- **Toolbar**: Feature buttons with SVG icons
|
- **Toolbar**: Feature buttons with SVG icons
|
||||||
|
- **Tab Bar**: Multi-file navigation with close buttons
|
||||||
- **Find Dialog**: Search and replace interface
|
- **Find Dialog**: Search and replace interface
|
||||||
- **Editor Container**: Line numbers and text editor
|
- **Export Options Dialog**: Advanced export configuration with templates and metadata
|
||||||
|
- **Batch Conversion Dialog**: Folder selection and conversion progress
|
||||||
|
- **Editor Container**: Line numbers and text editor with tab content management
|
||||||
- **Preview Pane**: Rendered markdown display
|
- **Preview Pane**: Rendered markdown display
|
||||||
- **Status Bar**: Statistics and application status
|
- **Status Bar**: Statistics and application status
|
||||||
|
|
||||||
@@ -227,6 +343,9 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
- [ ] Line numbers sync correctly with content
|
- [ ] Line numbers sync correctly with content
|
||||||
- [ ] Undo/redo preserves cursor position
|
- [ ] Undo/redo preserves cursor position
|
||||||
- [ ] Auto-indentation works with various list types
|
- [ ] Auto-indentation works with various list types
|
||||||
|
- [ ] File association loading (double-click .md files)
|
||||||
|
- [ ] Export options dialog with templates and metadata
|
||||||
|
- [ ] Batch conversion with progress tracking
|
||||||
- [ ] All themes render correctly for new components
|
- [ ] All themes render correctly for new components
|
||||||
- [ ] Export functions work with various document formats
|
- [ ] Export functions work with various document formats
|
||||||
- [ ] Table creation and export functionality
|
- [ ] Table creation and export functionality
|
||||||
@@ -299,5 +418,5 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: September 1, 2025
|
**Last Updated**: September 15, 2025
|
||||||
**Claude Assistant**: Development completed for v1.2.1 comprehensive editor enhancements
|
**Claude Assistant**: Development completed for v1.5.0 with optional advanced export options, removed proprietary dependencies for open-source compatibility, enhanced auto-save functionality, comprehensive document statistics, recent files management, and mathematical expression support using KaTeX.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Amit Haridas
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -20,11 +20,14 @@ A cross-platform Markdown editor and converter powered by Pandoc.
|
|||||||
- 👁️ **Live Preview** - See your markdown rendered in real-time with synchronized scrolling
|
- 👁️ **Live Preview** - See your markdown rendered in real-time with synchronized scrolling
|
||||||
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes
|
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes
|
||||||
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
|
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
|
||||||
|
- 📊 **Enhanced Statistics** - Detailed document statistics including word count, character count, lines, paragraphs, sentences, and estimated reading time
|
||||||
|
- 🕒 **Recent Files** - Quick access to recently opened files via File menu
|
||||||
|
- 🧮 **Math Support** - Render mathematical expressions using KaTeX (supports $$, $, \\[\\], \\(\\) delimiters)
|
||||||
|
|
||||||
### 📤 Export & Conversion
|
### 📤 Export & Conversion
|
||||||
- 📄 **Enhanced PDF Export** - Robust PDF generation with multiple LaTeX engine fallbacks (XeLaTeX, PDFLaTeX, wkhtmltopdf)
|
- 📄 **Enhanced PDF Export** - Robust PDF generation with multiple LaTeX engine fallbacks (XeLaTeX, PDFLaTeX, wkhtmltopdf)
|
||||||
- 📄 **Document Export** - Convert to HTML, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP)
|
- 📄 **Document Export** - Convert to HTML, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP)
|
||||||
- 📊 **Spreadsheet Export** - Export markdown tables to Excel (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats
|
- 📊 **Spreadsheet Export** - Export markdown tables to CSV format for compatibility with any spreadsheet application
|
||||||
- 📥 **Document Import** - Import DOCX, ODT, RTF, HTML, PDF, and presentation files to markdown
|
- 📥 **Document Import** - Import DOCX, ODT, RTF, HTML, PDF, and presentation files to markdown
|
||||||
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation
|
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation
|
||||||
|
|
||||||
@@ -50,9 +53,9 @@ For optimal PDF export, install a LaTeX engine (recommended):
|
|||||||
Download the latest release for your platform from the [Releases](https://github.com/amitwh/pan-converter/releases) page.
|
Download the latest release for your platform from the [Releases](https://github.com/amitwh/pan-converter/releases) page.
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
- **AppImage**: `PanConverter-1.3.1.AppImage` (universal, may require `--no-sandbox` flag)
|
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
|
||||||
- **Debian Package**: `pan-converter_1.3.1_amd64.deb`
|
- **Debian Package**: `pan-converter_1.5.0_amd64.deb`
|
||||||
- **Snap Package**: `pan-converter_1.3.1_amd64.snap`
|
- **Snap Package**: `pan-converter_1.5.0_amd64.snap`
|
||||||
|
|
||||||
### Install from Source
|
### Install from Source
|
||||||
```bash
|
```bash
|
||||||
@@ -73,7 +76,9 @@ npm start
|
|||||||
### Export Options
|
### Export Options
|
||||||
- **Documents**: HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB
|
- **Documents**: HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB
|
||||||
- **Presentations**: PowerPoint (PPTX), OpenDocument Presentation (ODP)
|
- **Presentations**: PowerPoint (PPTX), OpenDocument Presentation (ODP)
|
||||||
- **Spreadsheets**: Excel (XLSX/XLS), OpenDocument Spreadsheet (ODS)
|
- **Spreadsheets**: CSV format for tables (compatible with Excel, LibreOffice, Google Sheets)
|
||||||
|
- **Advanced Export Options**: Optional template support, metadata insertion, table of contents, section numbering, and PDF-specific settings
|
||||||
|
- **Batch Conversion**: Convert entire folders of markdown files with progress tracking
|
||||||
|
|
||||||
### Import & Conversion
|
### Import & Conversion
|
||||||
- **Import Documents**: Convert DOCX, ODT, RTF, HTML, PDF, and presentation files to Markdown
|
- **Import Documents**: Convert DOCX, ODT, RTF, HTML, PDF, and presentation files to Markdown
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
+42
-6
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.3.3",
|
"version": "1.5.0",
|
||||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "electron-builder",
|
"build": "electron-builder",
|
||||||
"build:win": "electron-builder --win",
|
"build:win": "cross-env CSC_LINK=code-signing-cert.pfx CSC_KEY_PASSWORD=PanConverter2024! electron-builder --win",
|
||||||
|
"build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win",
|
||||||
"build:mac": "electron-builder --mac",
|
"build:mac": "electron-builder --mac",
|
||||||
"build:linux": "electron-builder --linux",
|
"build:linux": "electron-builder --linux",
|
||||||
"dist": "electron-builder --publish=never",
|
"dist": "electron-builder --publish=never",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"author": "Amit Haridas <amit.wh@gmail.com>",
|
"author": "Amit Haridas <amit.wh@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"cross-env": "^10.0.0",
|
||||||
"electron": "^37.4.0",
|
"electron": "^37.4.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"sharp": "^0.34.3"
|
"sharp": "^0.34.3"
|
||||||
@@ -32,8 +34,7 @@
|
|||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"marked": "^16.2.1",
|
"marked": "^16.2.1"
|
||||||
"xlsx": "^0.18.5"
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.panconverter.app",
|
"appId": "com.panconverter.app",
|
||||||
@@ -68,8 +69,43 @@
|
|||||||
"icon": "assets/icon.icns"
|
"icon": "assets/icon.icns"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": [
|
||||||
"icon": "assets/icon.ico"
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": ["x64"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "portable",
|
||||||
|
"arch": ["x64"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "zip",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": "assets/icon.png",
|
||||||
|
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||||
|
"requestedExecutionLevel": "asInvoker",
|
||||||
|
"signAndEditExecutable": true
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"perMachine": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"displayLanguageSelector": true,
|
||||||
|
"installerIcon": "assets/icon-proper.ico",
|
||||||
|
"uninstallerIcon": "assets/icon-proper.ico",
|
||||||
|
"installerHeaderIcon": "assets/icon-proper.ico",
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "PanConverter",
|
||||||
|
"runAfterFinish": true,
|
||||||
|
"menuCategory": "Productivity",
|
||||||
|
"license": "LICENSE",
|
||||||
|
"warningsAsErrors": false,
|
||||||
|
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||||
|
"deleteAppDataOnUninstall": false,
|
||||||
|
"differentialPackage": true
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
|
|||||||
+186
@@ -114,6 +114,192 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Options Dialog -->
|
||||||
|
<div id="export-dialog" class="export-dialog hidden">
|
||||||
|
<div class="export-dialog-content">
|
||||||
|
<div class="export-dialog-header">
|
||||||
|
<h3 id="export-dialog-title">Export Options</h3>
|
||||||
|
<button id="export-dialog-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-body">
|
||||||
|
<!-- Simple/Advanced Export Toggle -->
|
||||||
|
<div class="export-section export-mode-toggle">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="advanced-export-toggle">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Show Advanced Options
|
||||||
|
</label>
|
||||||
|
<small class="export-help">Enable for templates, metadata, bibliography, and advanced PDF settings</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Export Options (hidden by default) -->
|
||||||
|
<div id="advanced-export-options" class="advanced-options hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="export-template">Template:</label>
|
||||||
|
<select id="export-template">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="custom">Custom (Browse...)</option>
|
||||||
|
</select>
|
||||||
|
<input type="file" id="template-file-input" accept=".tex,.html,.docx,.odt" style="display: none;">
|
||||||
|
<input type="text" id="custom-template-path" placeholder="Template file path..." style="display: none;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Metadata:</label>
|
||||||
|
<div class="metadata-container">
|
||||||
|
<div class="metadata-field">
|
||||||
|
<input type="text" placeholder="title" class="metadata-key">
|
||||||
|
<input type="text" placeholder="Document title" class="metadata-value">
|
||||||
|
</div>
|
||||||
|
<div class="metadata-field">
|
||||||
|
<input type="text" placeholder="author" class="metadata-key">
|
||||||
|
<input type="text" placeholder="Author name" class="metadata-value">
|
||||||
|
</div>
|
||||||
|
<div class="metadata-field">
|
||||||
|
<input type="text" placeholder="date" class="metadata-key">
|
||||||
|
<input type="text" placeholder="Date" class="metadata-value">
|
||||||
|
</div>
|
||||||
|
<div class="metadata-field">
|
||||||
|
<input type="text" placeholder="subject" class="metadata-key">
|
||||||
|
<input type="text" placeholder="Subject" class="metadata-value">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="add-metadata-field" type="button">+ Add Field</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Document Options:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" id="export-toc"> Table of Contents</label>
|
||||||
|
<label><input type="checkbox" id="export-number-sections"> Number Sections</label>
|
||||||
|
<label><input type="checkbox" id="export-citeproc"> Process Citations</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="export-toc-depth">TOC Depth:</label>
|
||||||
|
<input type="number" id="export-toc-depth" min="1" max="6" value="3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section pdf-only">
|
||||||
|
<label>PDF Options:</label>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="pdf-engine">PDF Engine:</label>
|
||||||
|
<select id="pdf-engine">
|
||||||
|
<option value="xelatex">XeLaTeX (Recommended)</option>
|
||||||
|
<option value="pdflatex">PDFLaTeX</option>
|
||||||
|
<option value="lualatex">LuaLaTeX</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="pdf-geometry">Page Margins:</label>
|
||||||
|
<select id="pdf-geometry">
|
||||||
|
<option value="margin=1in">1 inch (Default)</option>
|
||||||
|
<option value="margin=0.75in">0.75 inch</option>
|
||||||
|
<option value="margin=1.25in">1.25 inch</option>
|
||||||
|
<option value="margin=2cm">2 cm</option>
|
||||||
|
<option value="margin=2.5cm">2.5 cm</option>
|
||||||
|
<option value="custom">Custom...</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="custom-geometry" placeholder="e.g., margin=1.5in" style="display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Bibliography:</label>
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="text" id="bibliography-file" placeholder="Bibliography file (.bib, .yaml, .json)">
|
||||||
|
<button id="browse-bibliography">Browse</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="text" id="csl-file" placeholder="Citation style file (.csl)">
|
||||||
|
<button id="browse-csl">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- End advanced-export-options -->
|
||||||
|
|
||||||
|
<!-- Basic Export Options (always visible) -->
|
||||||
|
<div class="export-section basic-options">
|
||||||
|
<label>Quick Export Settings:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" id="basic-toc"> Include Table of Contents</label>
|
||||||
|
<label><input type="checkbox" id="basic-number-sections"> Number Sections</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-footer">
|
||||||
|
<button id="export-cancel">Cancel</button>
|
||||||
|
<button id="export-confirm" class="primary">Export</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Conversion Dialog -->
|
||||||
|
<div id="batch-dialog" class="batch-dialog hidden">
|
||||||
|
<div class="batch-dialog-content">
|
||||||
|
<div class="batch-dialog-header">
|
||||||
|
<h3>Batch File Conversion</h3>
|
||||||
|
<button id="batch-dialog-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="batch-dialog-body">
|
||||||
|
<div class="batch-section">
|
||||||
|
<label>Input Folder:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="batch-input-folder" placeholder="Select folder containing markdown files..." readonly>
|
||||||
|
<button id="browse-input-folder">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-section">
|
||||||
|
<label>Output Folder:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="batch-output-folder" placeholder="Select output folder..." readonly>
|
||||||
|
<button id="browse-output-folder">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-section">
|
||||||
|
<label for="batch-format">Output Format:</label>
|
||||||
|
<select id="batch-format">
|
||||||
|
<option value="html">HTML</option>
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="docx">DOCX</option>
|
||||||
|
<option value="latex">LaTeX</option>
|
||||||
|
<option value="rtf">RTF</option>
|
||||||
|
<option value="odt">ODT</option>
|
||||||
|
<option value="epub">EPUB</option>
|
||||||
|
<option value="pptx">PowerPoint (PPTX)</option>
|
||||||
|
<option value="odp">OpenDocument Presentation (ODP)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-section">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="batch-include-subfolders" checked>
|
||||||
|
Include subfolders (recursive)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-section">
|
||||||
|
<button id="batch-show-options" type="button">Advanced Export Options...</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="batch-progress" class="batch-progress hidden">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="batch-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span id="batch-progress-text">Processing...</span>
|
||||||
|
<span id="batch-progress-count">0 / 0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="batch-dialog-footer">
|
||||||
|
<button id="batch-cancel">Cancel</button>
|
||||||
|
<button id="batch-start" class="primary" disabled>Start Conversion</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
||||||
<div id="editor-pane-1" class="pane">
|
<div id="editor-pane-1" class="pane">
|
||||||
|
|||||||
+394
-46
@@ -2,7 +2,21 @@ const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron')
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const XLSX = require('xlsx');
|
|
||||||
|
// Get the system Pandoc path
|
||||||
|
function getPandocPath() {
|
||||||
|
// Always use system pandoc - no bundled binaries
|
||||||
|
return 'pandoc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Pandoc is available
|
||||||
|
function checkPandocAvailable() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec('pandoc --version', (error) => {
|
||||||
|
resolve(!error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Simple storage implementation to replace electron-store
|
// Simple storage implementation to replace electron-store
|
||||||
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
||||||
@@ -49,12 +63,53 @@ function createWindow() {
|
|||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle pending file from file association
|
// Handle pending file from file association will be handled by renderer-ready event
|
||||||
if (app.pendingFile) {
|
}
|
||||||
mainWindow.webContents.once('dom-ready', () => {
|
|
||||||
openFileFromPath(app.pendingFile);
|
function buildRecentFilesMenu() {
|
||||||
app.pendingFile = null;
|
const recentFiles = getRecentFiles();
|
||||||
});
|
|
||||||
|
if (recentFiles.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'No recent files',
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentFileItems = recentFiles.map(filePath => ({
|
||||||
|
label: filePath.split(/[\\/]/).pop(), // Get filename only
|
||||||
|
click: () => {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
currentFile = filePath;
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
mainWindow.webContents.send('file-opened', { path: filePath, content });
|
||||||
|
} else {
|
||||||
|
dialog.showErrorBox('File Not Found', `The file "${filePath}" could not be found.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toolTip: filePath // Show full path in tooltip
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [
|
||||||
|
...recentFileItems,
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Clear Recent Files',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.webContents.send('clear-recent-files');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecentFiles() {
|
||||||
|
try {
|
||||||
|
const recentFiles = JSON.parse(fs.readFileSync(path.join(app.getPath('userData'), 'recent-files.json'), 'utf-8'));
|
||||||
|
return recentFiles.filter(file => fs.existsSync(file));
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +139,11 @@ function createMenu() {
|
|||||||
click: saveAsFile
|
click: saveAsFile
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Recent Files',
|
||||||
|
submenu: buildRecentFilesMenu()
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Import Document...',
|
label: 'Import Document...',
|
||||||
accelerator: 'CmdOrCtrl+I',
|
accelerator: 'CmdOrCtrl+I',
|
||||||
@@ -103,9 +163,7 @@ function createMenu() {
|
|||||||
{ label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') },
|
{ label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') },
|
||||||
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
|
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Excel (XLSX)', click: () => exportSpreadsheet('xlsx') },
|
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }
|
||||||
{ label: 'Excel Legacy (XLS)', click: () => exportSpreadsheet('xls') },
|
|
||||||
{ label: 'OpenDocument Spreadsheet (ODS)', click: () => exportSpreadsheet('ods') }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -153,6 +211,27 @@ function createMenu() {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Font Size',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Increase Font Size',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+Plus',
|
||||||
|
click: () => mainWindow.webContents.send('adjust-font-size', 'increase')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Decrease Font Size',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+-',
|
||||||
|
click: () => mainWindow.webContents.send('adjust-font-size', 'decrease')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reset Font Size',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+0',
|
||||||
|
click: () => mainWindow.webContents.send('adjust-font-size', 'reset')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
|
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
|
||||||
{ label: 'Toggle DevTools', accelerator: 'F12', role: 'toggleDevTools' },
|
{ label: 'Toggle DevTools', accelerator: 'F12', role: 'toggleDevTools' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -161,6 +240,15 @@ function createMenu() {
|
|||||||
{ label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }
|
{ label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Batch',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Convert Folder...',
|
||||||
|
click: () => showBatchConversionDialog()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -171,7 +259,7 @@ function createMenu() {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'About PanConverter',
|
title: 'About PanConverter',
|
||||||
message: 'PanConverter',
|
message: 'PanConverter',
|
||||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.3.3\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Enhanced PDF export with LaTeX engines\n• File association support for .md files\n• Improved preview typography and spacing\n• Enhanced font sizes for better readability\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality',
|
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.5.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Enhanced PDF export with LaTeX engines\n• Optional advanced export options\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to CSV format\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics',
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -225,6 +313,19 @@ function exportFile(format) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show export options dialog
|
||||||
|
showExportOptionsDialog(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showExportOptionsDialog(format) {
|
||||||
|
mainWindow.webContents.send('show-export-dialog', format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBatchConversionDialog() {
|
||||||
|
mainWindow.webContents.send('show-batch-dialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
function performExportWithOptions(format, options) {
|
||||||
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||||
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
||||||
filters: [
|
filters: [
|
||||||
@@ -233,37 +334,51 @@ function exportFile(format) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (outputFile) {
|
if (outputFile) {
|
||||||
let pandocCmd = `pandoc "${currentFile}" -o "${outputFile}"`;
|
let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`;
|
||||||
|
|
||||||
|
// Add template if specified
|
||||||
|
if (options.template && options.template !== 'default') {
|
||||||
|
pandocCmd += ` --template="${options.template}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
if (options.metadata) {
|
||||||
|
for (const [key, value] of Object.entries(options.metadata)) {
|
||||||
|
if (value.trim()) {
|
||||||
|
pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add variables
|
||||||
|
if (options.variables) {
|
||||||
|
for (const [key, value] of Object.entries(options.variables)) {
|
||||||
|
if (value.trim()) {
|
||||||
|
pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add other options
|
||||||
|
if (options.toc) pandocCmd += ' --toc';
|
||||||
|
if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`;
|
||||||
|
if (options.numberSections) pandocCmd += ' --number-sections';
|
||||||
|
if (options.citeproc) pandocCmd += ' --citeproc';
|
||||||
|
if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`;
|
||||||
|
if (options.csl) pandocCmd += ` --csl="${options.csl}"`;
|
||||||
|
|
||||||
// Add specific options for PDF export to ensure proper generation
|
// Add specific options for PDF export to ensure proper generation
|
||||||
if (format === 'pdf') {
|
if (format === 'pdf') {
|
||||||
pandocCmd = `pandoc "${currentFile}" --pdf-engine=xelatex -V geometry:margin=1in -o "${outputFile}"`;
|
const pdfEngine = options.pdfEngine || 'xelatex';
|
||||||
// Try with different PDF engines if xelatex fails
|
const geometry = options.geometry || 'margin=1in';
|
||||||
|
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
|
||||||
|
|
||||||
|
// Try with specified PDF engine
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// Fallback to pdflatex
|
// Try fallback engines if the specified one fails
|
||||||
const fallbackCmd = `pandoc "${currentFile}" --pdf-engine=pdflatex -V geometry:margin=1in -o "${outputFile}"`;
|
const fallbackEngines = ['pdflatex', 'lualatex'];
|
||||||
exec(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
|
tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error);
|
||||||
if (fallbackError) {
|
|
||||||
// Final fallback to wkhtmltopdf
|
|
||||||
const htmlToPdfCmd = `pandoc "${currentFile}" -t html5 | wkhtmltopdf - "${outputFile}"`;
|
|
||||||
exec(htmlToPdfCmd, (finalError) => {
|
|
||||||
if (finalError) {
|
|
||||||
dialog.showErrorBox('PDF Export Error',
|
|
||||||
`Failed to export PDF. Please ensure you have one of the following installed:\n` +
|
|
||||||
`• XeLaTeX (recommended): sudo apt-get install texlive-xetex\n` +
|
|
||||||
`• PDFLaTeX: sudo apt-get install texlive-latex-base\n` +
|
|
||||||
`• wkhtmltopdf: sudo apt-get install wkhtmltopdf\n\n` +
|
|
||||||
`Error: ${finalError.message}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showExportSuccess(outputFile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showExportSuccess(outputFile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
showExportSuccess(outputFile);
|
showExportSuccess(outputFile);
|
||||||
}
|
}
|
||||||
@@ -280,6 +395,44 @@ function exportFile(format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) {
|
||||||
|
if (index >= engines.length) {
|
||||||
|
dialog.showErrorBox('PDF Export Error',
|
||||||
|
`Failed to export PDF with all available engines. Please ensure you have one of the following installed:\n` +
|
||||||
|
`• XeLaTeX (recommended): sudo apt-get install texlive-xetex\n` +
|
||||||
|
`• PDFLaTeX: sudo apt-get install texlive-latex-base\n` +
|
||||||
|
`• LuaLaTeX: sudo apt-get install texlive-luatex\n\n` +
|
||||||
|
`Last error: ${lastError.message}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = engines[index];
|
||||||
|
const geometry = options.geometry || 'margin=1in';
|
||||||
|
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
|
||||||
|
|
||||||
|
// Add all other options
|
||||||
|
if (options.template && options.template !== 'default') {
|
||||||
|
pandocCmd += ` --template="${options.template}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.metadata) {
|
||||||
|
for (const [key, value] of Object.entries(options.metadata)) {
|
||||||
|
if (value.trim()) {
|
||||||
|
pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(pandocCmd, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
||||||
|
} else {
|
||||||
|
showExportSuccess(outputFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showExportSuccess(outputFile) {
|
function showExportSuccess(outputFile) {
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@@ -314,7 +467,7 @@ function importDocument() {
|
|||||||
const outputFile = inputFile.replace(/\.[^/.]+$/, '.md');
|
const outputFile = inputFile.replace(/\.[^/.]+$/, '.md');
|
||||||
|
|
||||||
// Convert to markdown using pandoc
|
// Convert to markdown using pandoc
|
||||||
const pandocCmd = `pandoc "${inputFile}" -t markdown -o "${outputFile}"`;
|
const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown -o "${outputFile}"`;
|
||||||
|
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -366,6 +519,35 @@ ipcMain.on('set-current-file', (event, filePath) => {
|
|||||||
currentFile = filePath;
|
currentFile = filePath;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle renderer ready for file association
|
||||||
|
ipcMain.on('renderer-ready', (event) => {
|
||||||
|
if (app.pendingFile) {
|
||||||
|
openFileFromPath(app.pendingFile);
|
||||||
|
app.pendingFile = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle export with options
|
||||||
|
ipcMain.on('export-with-options', (event, { format, options }) => {
|
||||||
|
performExportWithOptions(format, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle batch conversion
|
||||||
|
ipcMain.on('batch-convert', (event, { inputFolder, outputFolder, format, options }) => {
|
||||||
|
performBatchConversion(inputFolder, outputFolder, format, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle folder selection for batch conversion
|
||||||
|
ipcMain.on('select-folder', (event, type) => {
|
||||||
|
const folder = dialog.showOpenDialogSync(mainWindow, {
|
||||||
|
properties: ['openDirectory']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder && folder[0]) {
|
||||||
|
event.reply('folder-selected', { type, path: folder[0] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
||||||
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||||
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
||||||
@@ -384,21 +566,30 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create workbook
|
// Convert tables to CSV format
|
||||||
const wb = XLSX.utils.book_new();
|
let csvContent = '';
|
||||||
|
|
||||||
tables.forEach((table, index) => {
|
tables.forEach((table, index) => {
|
||||||
const ws = XLSX.utils.aoa_to_sheet(table);
|
if (index > 0) csvContent += '\n\n'; // Separate multiple tables
|
||||||
XLSX.utils.book_append_sheet(wb, ws, `Table ${index + 1}`);
|
if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`;
|
||||||
|
|
||||||
|
table.forEach(row => {
|
||||||
|
const csvRow = row.map(cell => {
|
||||||
|
// Escape quotes and wrap in quotes if necessary
|
||||||
|
const cleanCell = cell.replace(/"/g, '""');
|
||||||
|
return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n')
|
||||||
|
? `"${cleanCell}"` : cleanCell;
|
||||||
|
}).join(',');
|
||||||
|
csvContent += csvRow + '\n';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write file
|
// Write CSV file
|
||||||
XLSX.writeFile(wb, outputFile);
|
fs.writeFileSync(outputFile, csvContent, 'utf-8');
|
||||||
|
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Export Complete',
|
title: 'Export Complete',
|
||||||
message: `Spreadsheet exported successfully to ${outputFile}`,
|
message: `CSV exported successfully to ${outputFile}`,
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -449,6 +640,139 @@ function extractTablesFromMarkdown(markdown) {
|
|||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function performBatchConversion(inputFolder, outputFolder, format, options) {
|
||||||
|
if (!fs.existsSync(inputFolder)) {
|
||||||
|
dialog.showErrorBox('Error', 'Input folder does not exist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output folder if it doesn't exist
|
||||||
|
if (!fs.existsSync(outputFolder)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
dialog.showErrorBox('Error', `Failed to create output folder: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all markdown files in input folder
|
||||||
|
const markdownFiles = [];
|
||||||
|
|
||||||
|
function findMarkdownFiles(dir) {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(dir, file);
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
findMarkdownFiles(fullPath); // Recursive search
|
||||||
|
} else if (file.match(/\.(md|markdown)$/i)) {
|
||||||
|
markdownFiles.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findMarkdownFiles(inputFolder);
|
||||||
|
|
||||||
|
if (markdownFiles.length === 0) {
|
||||||
|
dialog.showErrorBox('No Files Found', 'No markdown files found in the selected folder');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show progress dialog
|
||||||
|
let completedCount = 0;
|
||||||
|
const totalCount = markdownFiles.length;
|
||||||
|
|
||||||
|
// Process each file
|
||||||
|
const processNextFile = (index) => {
|
||||||
|
if (index >= markdownFiles.length) {
|
||||||
|
// All files processed
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Batch Conversion Complete',
|
||||||
|
message: `Successfully converted ${completedCount} out of ${totalCount} files to ${format.toUpperCase()}.`,
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputFile = markdownFiles[index];
|
||||||
|
const relativePath = path.relative(inputFolder, inputFile);
|
||||||
|
const baseName = path.basename(relativePath, path.extname(relativePath));
|
||||||
|
const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${format}`));
|
||||||
|
|
||||||
|
// Create subdirectories in output folder if needed
|
||||||
|
const outputDir = path.dirname(outputFile);
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pandoc command
|
||||||
|
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
|
||||||
|
|
||||||
|
// Add template if specified
|
||||||
|
if (options.template && options.template !== 'default') {
|
||||||
|
pandocCmd += ` --template="${options.template}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
if (options.metadata) {
|
||||||
|
for (const [key, value] of Object.entries(options.metadata)) {
|
||||||
|
if (value.trim()) {
|
||||||
|
pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add variables
|
||||||
|
if (options.variables) {
|
||||||
|
for (const [key, value] of Object.entries(options.variables)) {
|
||||||
|
if (value.trim()) {
|
||||||
|
pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add other options
|
||||||
|
if (options.toc) pandocCmd += ' --toc';
|
||||||
|
if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`;
|
||||||
|
if (options.numberSections) pandocCmd += ' --number-sections';
|
||||||
|
if (options.citeproc) pandocCmd += ' --citeproc';
|
||||||
|
if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`;
|
||||||
|
if (options.csl) pandocCmd += ` --csl="${options.csl}"`;
|
||||||
|
|
||||||
|
// Add PDF-specific options
|
||||||
|
if (format === 'pdf') {
|
||||||
|
const pdfEngine = options.pdfEngine || 'xelatex';
|
||||||
|
const geometry = options.geometry || 'margin=1in';
|
||||||
|
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute conversion
|
||||||
|
exec(pandocCmd, (error, stdout, stderr) => {
|
||||||
|
if (!error) {
|
||||||
|
completedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress (you could send this to renderer for a progress bar)
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: !error
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file
|
||||||
|
processNextFile(index + 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start processing
|
||||||
|
processNextFile(0);
|
||||||
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
@@ -476,6 +800,30 @@ app.on('activate', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// IPC handlers for recent files
|
||||||
|
ipcMain.on('save-recent-files', (event, recentFiles) => {
|
||||||
|
try {
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const recentFilesPath = path.join(userDataPath, 'recent-files.json');
|
||||||
|
fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving recent files:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('clear-recent-files', (event) => {
|
||||||
|
try {
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const recentFilesPath = path.join(userDataPath, 'recent-files.json');
|
||||||
|
fs.writeFileSync(recentFilesPath, JSON.stringify([], null, 2));
|
||||||
|
// Rebuild menu to reflect changes
|
||||||
|
createMenu();
|
||||||
|
event.reply('recent-files-cleared');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing recent files:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle file opening on macOS
|
// Handle file opening on macOS
|
||||||
app.on('open-file', (event, filePath) => {
|
app.on('open-file', (event, filePath) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
+609
-3
@@ -25,6 +25,9 @@ class TabManager {
|
|||||||
this.nextTabId = 2;
|
this.nextTabId = 2;
|
||||||
this.isPreviewVisible = true;
|
this.isPreviewVisible = true;
|
||||||
this.showLineNumbers = false;
|
this.showLineNumbers = false;
|
||||||
|
this.autoSaveInterval = null;
|
||||||
|
this.autoSaveDelay = 30000; // 30 seconds
|
||||||
|
this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]');
|
||||||
|
|
||||||
// Initialize first tab
|
// Initialize first tab
|
||||||
this.tabs.set(1, {
|
this.tabs.set(1, {
|
||||||
@@ -112,6 +115,7 @@ class TabManager {
|
|||||||
this.tabs.set(newTabId, tab);
|
this.tabs.set(newTabId, tab);
|
||||||
this.createTabElements(tab);
|
this.createTabElements(tab);
|
||||||
this.switchToTab(newTabId);
|
this.switchToTab(newTabId);
|
||||||
|
this.startAutoSave();
|
||||||
this.updateTabBar();
|
this.updateTabBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +288,22 @@ class TabManager {
|
|||||||
const html = marked.parse(tab.content);
|
const html = marked.parse(tab.content);
|
||||||
const sanitizedHtml = DOMPurify.sanitize(html);
|
const sanitizedHtml = DOMPurify.sanitize(html);
|
||||||
preview.innerHTML = sanitizedHtml;
|
preview.innerHTML = sanitizedHtml;
|
||||||
|
|
||||||
|
// Render math expressions if KaTeX is available
|
||||||
|
if (window.katex && window.renderMathInElement) {
|
||||||
|
try {
|
||||||
|
window.renderMathInElement(preview, {
|
||||||
|
delimiters: [
|
||||||
|
{left: '$$', right: '$$', display: true},
|
||||||
|
{left: '$', right: '$', display: false},
|
||||||
|
{left: '\\[', right: '\\]', display: true},
|
||||||
|
{left: '\\(', right: '\\)', display: false}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (mathError) {
|
||||||
|
console.warn('Math rendering error:', mathError);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
preview.innerHTML = '<p>Error rendering preview</p>';
|
preview.innerHTML = '<p>Error rendering preview</p>';
|
||||||
}
|
}
|
||||||
@@ -325,9 +345,32 @@ class TabManager {
|
|||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const words = tab.content.trim().split(/\\s+/).filter(word => word.length > 0).length;
|
const content = tab.content;
|
||||||
const chars = tab.content.length;
|
const words = content.trim() ? content.trim().split(/\s+/).filter(word => word.length > 0).length : 0;
|
||||||
document.getElementById('word-count').textContent = `Words: ${words} | Characters: ${chars}`;
|
const chars = content.length;
|
||||||
|
const charsNoSpaces = content.replace(/\s/g, '').length;
|
||||||
|
|
||||||
|
// Enhanced statistics
|
||||||
|
const lines = content.split('\n').length;
|
||||||
|
const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim()).length;
|
||||||
|
const readingTime = Math.ceil(words / 200); // Average reading speed: 200 words/minute
|
||||||
|
const sentences = content.split(/[.!?]+/).filter(s => s.trim()).length;
|
||||||
|
|
||||||
|
// Update the word count display with enhanced stats
|
||||||
|
const basicStats = `Words: ${words} | Characters: ${chars} (${charsNoSpaces} no spaces)`;
|
||||||
|
const enhancedStats = `Lines: ${lines} | Paragraphs: ${paragraphs} | Sentences: ${sentences} | Reading time: ${readingTime} min`;
|
||||||
|
|
||||||
|
document.getElementById('word-count').textContent = basicStats;
|
||||||
|
|
||||||
|
// Add enhanced stats to a separate element
|
||||||
|
let enhancedEl = document.getElementById('enhanced-stats');
|
||||||
|
if (!enhancedEl) {
|
||||||
|
enhancedEl = document.createElement('div');
|
||||||
|
enhancedEl.id = 'enhanced-stats';
|
||||||
|
enhancedEl.className = 'enhanced-stats';
|
||||||
|
document.querySelector('.status-bar').appendChild(enhancedEl);
|
||||||
|
}
|
||||||
|
enhancedEl.textContent = enhancedStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEditorEvents() {
|
setupEditorEvents() {
|
||||||
@@ -409,6 +452,83 @@ class TabManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-save functionality
|
||||||
|
startAutoSave() {
|
||||||
|
if (this.autoSaveInterval) {
|
||||||
|
clearInterval(this.autoSaveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoSaveInterval = setInterval(() => {
|
||||||
|
this.performAutoSave();
|
||||||
|
}, this.autoSaveDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAutoSave() {
|
||||||
|
if (this.autoSaveInterval) {
|
||||||
|
clearInterval(this.autoSaveInterval);
|
||||||
|
this.autoSaveInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
performAutoSave() {
|
||||||
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
|
if (!tab || !tab.filePath || !tab.content) return;
|
||||||
|
|
||||||
|
// Only auto-save if content has changed since last save
|
||||||
|
if (tab.lastSavedContent !== tab.content) {
|
||||||
|
ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content });
|
||||||
|
tab.lastSavedContent = tab.content;
|
||||||
|
|
||||||
|
// Show brief auto-save indicator
|
||||||
|
this.showAutoSaveIndicator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAutoSaveIndicator() {
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.textContent = 'Auto-saved';
|
||||||
|
indicator.className = 'auto-save-indicator';
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
indicator.classList.add('fade-out');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (indicator.parentNode) {
|
||||||
|
indicator.parentNode.removeChild(indicator);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent files functionality
|
||||||
|
addToRecentFiles(filePath) {
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
// Remove if already exists
|
||||||
|
this.recentFiles = this.recentFiles.filter(f => f !== filePath);
|
||||||
|
|
||||||
|
// Add to beginning
|
||||||
|
this.recentFiles.unshift(filePath);
|
||||||
|
|
||||||
|
// Keep only last 10 files
|
||||||
|
this.recentFiles = this.recentFiles.slice(0, 10);
|
||||||
|
|
||||||
|
// Save to localStorage and sync with main process
|
||||||
|
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
|
||||||
|
window.electronAPI.send('save-recent-files', this.recentFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentFiles() {
|
||||||
|
return this.recentFiles.filter(file => {
|
||||||
|
// Check if file still exists (basic check by trying to access it)
|
||||||
|
try {
|
||||||
|
return file && file.length > 0;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setupToolbarEvents() {
|
setupToolbarEvents() {
|
||||||
// Existing toolbar setup...
|
// Existing toolbar setup...
|
||||||
document.getElementById('btn-preview-toggle').addEventListener('click', () => {
|
document.getElementById('btn-preview-toggle').addEventListener('click', () => {
|
||||||
@@ -459,6 +579,8 @@ class TabManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.restoreTabState(this.activeTabId);
|
this.restoreTabState(this.activeTabId);
|
||||||
|
this.startAutoSave();
|
||||||
|
this.addToRecentFiles(filePath);
|
||||||
this.updateTabBar();
|
this.updateTabBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,6 +604,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Request current theme
|
// Request current theme
|
||||||
ipcRenderer.send('get-theme');
|
ipcRenderer.send('get-theme');
|
||||||
|
|
||||||
|
// Signal that renderer is ready for file operations
|
||||||
|
ipcRenderer.send('renderer-ready');
|
||||||
|
|
||||||
// Set up auto-save interval
|
// Set up auto-save interval
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
// Auto-save logic for all tabs
|
// Auto-save logic for all tabs
|
||||||
@@ -538,3 +663,484 @@ ipcRenderer.on('toggle-find', () => {
|
|||||||
ipcRenderer.on('theme-changed', (event, theme) => {
|
ipcRenderer.on('theme-changed', (event, theme) => {
|
||||||
document.body.className = `theme-${theme}`;
|
document.body.className = `theme-${theme}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Font size adjustment
|
||||||
|
let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15;
|
||||||
|
|
||||||
|
function updateFontSizes(size) {
|
||||||
|
const editors = document.querySelectorAll('#editor, .editor-textarea');
|
||||||
|
const previews = document.querySelectorAll('#preview, .preview-content');
|
||||||
|
|
||||||
|
editors.forEach(editor => {
|
||||||
|
editor.style.fontSize = `${size}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
previews.forEach(preview => {
|
||||||
|
preview.style.fontSize = `${size}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('fontSize', size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply saved font size on load
|
||||||
|
updateFontSizes(currentFontSize);
|
||||||
|
|
||||||
|
ipcRenderer.on('adjust-font-size', (event, action) => {
|
||||||
|
if (action === 'increase' && currentFontSize < 24) {
|
||||||
|
currentFontSize++;
|
||||||
|
} else if (action === 'decrease' && currentFontSize > 10) {
|
||||||
|
currentFontSize--;
|
||||||
|
} else if (action === 'reset') {
|
||||||
|
currentFontSize = 15;
|
||||||
|
}
|
||||||
|
updateFontSizes(currentFontSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export Dialog functionality
|
||||||
|
let currentExportFormat = null;
|
||||||
|
|
||||||
|
ipcRenderer.on('show-export-dialog', (event, format) => {
|
||||||
|
currentExportFormat = format;
|
||||||
|
showExportDialog(format);
|
||||||
|
});
|
||||||
|
|
||||||
|
function showExportDialog(format) {
|
||||||
|
const dialog = document.getElementById('export-dialog');
|
||||||
|
const title = document.getElementById('export-dialog-title');
|
||||||
|
|
||||||
|
title.textContent = `Export as ${format.toUpperCase()}`;
|
||||||
|
dialog.setAttribute('data-format', format);
|
||||||
|
dialog.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Initialize form values
|
||||||
|
initializeExportForm(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideExportDialog() {
|
||||||
|
const dialog = document.getElementById('export-dialog');
|
||||||
|
dialog.classList.add('hidden');
|
||||||
|
currentExportFormat = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeExportForm(format) {
|
||||||
|
// Reset advanced export toggle to unchecked
|
||||||
|
const advancedToggle = document.getElementById('advanced-export-toggle');
|
||||||
|
const advancedOptions = document.getElementById('advanced-export-options');
|
||||||
|
|
||||||
|
advancedToggle.checked = false;
|
||||||
|
advancedOptions.classList.add('hidden');
|
||||||
|
|
||||||
|
// Reset form to defaults
|
||||||
|
document.getElementById('export-template').value = 'default';
|
||||||
|
document.getElementById('custom-template-path').style.display = 'none';
|
||||||
|
|
||||||
|
// Clear metadata fields
|
||||||
|
const metadataFields = document.querySelectorAll('.metadata-field');
|
||||||
|
metadataFields.forEach((field, index) => {
|
||||||
|
if (index < 4) { // Keep first 4 default fields
|
||||||
|
field.querySelector('.metadata-key').value = ['title', 'author', 'date', 'subject'][index] || '';
|
||||||
|
field.querySelector('.metadata-value').value = '';
|
||||||
|
} else {
|
||||||
|
field.remove(); // Remove additional fields
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset checkboxes and other fields
|
||||||
|
document.getElementById('export-toc').checked = false;
|
||||||
|
document.getElementById('export-number-sections').checked = false;
|
||||||
|
document.getElementById('export-citeproc').checked = false;
|
||||||
|
document.getElementById('export-toc-depth').value = 3;
|
||||||
|
|
||||||
|
// PDF-specific fields
|
||||||
|
if (format === 'pdf') {
|
||||||
|
document.getElementById('pdf-engine').value = 'xelatex';
|
||||||
|
document.getElementById('pdf-geometry').value = 'margin=1in';
|
||||||
|
document.getElementById('custom-geometry').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear bibliography fields
|
||||||
|
document.getElementById('bibliography-file').value = '';
|
||||||
|
document.getElementById('csl-file').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectExportOptions() {
|
||||||
|
const advancedMode = document.getElementById('advanced-export-toggle').checked;
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
if (advancedMode) {
|
||||||
|
// Collect advanced options
|
||||||
|
options.template = document.getElementById('export-template').value;
|
||||||
|
options.metadata = {};
|
||||||
|
options.variables = {};
|
||||||
|
options.toc = document.getElementById('export-toc').checked;
|
||||||
|
options.tocDepth = document.getElementById('export-toc-depth').value;
|
||||||
|
options.numberSections = document.getElementById('export-number-sections').checked;
|
||||||
|
options.citeproc = document.getElementById('export-citeproc').checked;
|
||||||
|
} else {
|
||||||
|
// Collect basic options only
|
||||||
|
options.template = 'default';
|
||||||
|
options.metadata = {};
|
||||||
|
options.variables = {};
|
||||||
|
options.toc = document.getElementById('basic-toc').checked;
|
||||||
|
options.tocDepth = 3;
|
||||||
|
options.numberSections = document.getElementById('basic-number-sections').checked;
|
||||||
|
options.citeproc = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advancedMode) {
|
||||||
|
// Collect custom template path
|
||||||
|
if (options.template === 'custom') {
|
||||||
|
options.template = document.getElementById('custom-template-path').value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect metadata
|
||||||
|
const metadataFields = document.querySelectorAll('.metadata-field');
|
||||||
|
metadataFields.forEach(field => {
|
||||||
|
const key = field.querySelector('.metadata-key').value.trim();
|
||||||
|
const value = field.querySelector('.metadata-value').value.trim();
|
||||||
|
if (key && value) {
|
||||||
|
options.metadata[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF-specific options
|
||||||
|
if (currentExportFormat === 'pdf') {
|
||||||
|
options.pdfEngine = document.getElementById('pdf-engine').value;
|
||||||
|
const geometrySelect = document.getElementById('pdf-geometry');
|
||||||
|
if (geometrySelect.value === 'custom') {
|
||||||
|
options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in';
|
||||||
|
} else {
|
||||||
|
options.geometry = geometrySelect.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bibliography
|
||||||
|
const bibFile = document.getElementById('bibliography-file').value.trim();
|
||||||
|
const cslFile = document.getElementById('csl-file').value.trim();
|
||||||
|
if (bibFile) options.bibliography = bibFile;
|
||||||
|
if (cslFile) options.csl = cslFile;
|
||||||
|
} else {
|
||||||
|
// Basic mode - set default PDF options if needed
|
||||||
|
if (currentExportFormat === 'pdf') {
|
||||||
|
options.pdfEngine = 'xelatex';
|
||||||
|
options.geometry = 'margin=1in';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners for export dialog
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Template selection
|
||||||
|
document.getElementById('export-template').addEventListener('change', (e) => {
|
||||||
|
const customPath = document.getElementById('custom-template-path');
|
||||||
|
const fileInput = document.getElementById('template-file-input');
|
||||||
|
|
||||||
|
if (e.target.value === 'custom') {
|
||||||
|
customPath.style.display = 'block';
|
||||||
|
fileInput.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
customPath.style.display = 'none';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
customPath.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advanced export toggle
|
||||||
|
document.getElementById('advanced-export-toggle').addEventListener('change', (e) => {
|
||||||
|
const advancedOptions = document.getElementById('advanced-export-options');
|
||||||
|
if (e.target.checked) {
|
||||||
|
advancedOptions.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
advancedOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Template file input
|
||||||
|
document.getElementById('template-file-input').addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
document.getElementById('custom-template-path').value = file.path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF geometry selection
|
||||||
|
document.getElementById('pdf-geometry').addEventListener('change', (e) => {
|
||||||
|
const customGeometry = document.getElementById('custom-geometry');
|
||||||
|
if (e.target.value === 'custom') {
|
||||||
|
customGeometry.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
customGeometry.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add metadata field
|
||||||
|
document.getElementById('add-metadata-field').addEventListener('click', () => {
|
||||||
|
const container = document.querySelector('.metadata-container');
|
||||||
|
const newField = document.createElement('div');
|
||||||
|
newField.className = 'metadata-field';
|
||||||
|
newField.innerHTML = `
|
||||||
|
<input type="text" placeholder="key" class="metadata-key">
|
||||||
|
<input type="text" placeholder="value" class="metadata-value">
|
||||||
|
`;
|
||||||
|
container.appendChild(newField);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browse bibliography
|
||||||
|
document.getElementById('browse-bibliography').addEventListener('click', () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.bib,.yaml,.yml,.json';
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
document.getElementById('bibliography-file').value = file.path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browse CSL
|
||||||
|
document.getElementById('browse-csl').addEventListener('click', () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.csl';
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
document.getElementById('csl-file').value = file.path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dialog close buttons
|
||||||
|
document.getElementById('export-dialog-close').addEventListener('click', hideExportDialog);
|
||||||
|
document.getElementById('export-cancel').addEventListener('click', hideExportDialog);
|
||||||
|
|
||||||
|
// Export confirm
|
||||||
|
document.getElementById('export-confirm').addEventListener('click', () => {
|
||||||
|
const options = collectExportOptions();
|
||||||
|
ipcRenderer.send('export-with-options', {
|
||||||
|
format: currentExportFormat,
|
||||||
|
options: options
|
||||||
|
});
|
||||||
|
hideExportDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
document.getElementById('export-dialog').addEventListener('click', (e) => {
|
||||||
|
if (e.target === document.getElementById('export-dialog')) {
|
||||||
|
hideExportDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on Escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && !document.getElementById('export-dialog').classList.contains('hidden')) {
|
||||||
|
hideExportDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Batch Conversion Dialog functionality
|
||||||
|
let currentBatchOptions = {};
|
||||||
|
|
||||||
|
ipcRenderer.on('show-batch-dialog', () => {
|
||||||
|
showBatchDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('batch-progress', (event, progress) => {
|
||||||
|
updateBatchProgress(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('folder-selected', (event, { type, path }) => {
|
||||||
|
if (type === 'input') {
|
||||||
|
document.getElementById('batch-input-folder').value = path;
|
||||||
|
} else if (type === 'output') {
|
||||||
|
document.getElementById('batch-output-folder').value = path;
|
||||||
|
}
|
||||||
|
validateBatchForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showBatchDialog() {
|
||||||
|
const dialog = document.getElementById('batch-dialog');
|
||||||
|
dialog.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
document.getElementById('batch-input-folder').value = '';
|
||||||
|
document.getElementById('batch-output-folder').value = '';
|
||||||
|
document.getElementById('batch-format').value = 'html';
|
||||||
|
document.getElementById('batch-include-subfolders').checked = true;
|
||||||
|
document.getElementById('batch-progress').classList.add('hidden');
|
||||||
|
document.getElementById('batch-start').disabled = true;
|
||||||
|
|
||||||
|
currentBatchOptions = {
|
||||||
|
template: 'default',
|
||||||
|
metadata: {},
|
||||||
|
variables: {},
|
||||||
|
toc: false,
|
||||||
|
tocDepth: 3,
|
||||||
|
numberSections: false,
|
||||||
|
citeproc: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideBatchDialog() {
|
||||||
|
const dialog = document.getElementById('batch-dialog');
|
||||||
|
dialog.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBatchProgress(progress) {
|
||||||
|
const progressSection = document.getElementById('batch-progress');
|
||||||
|
const progressFill = document.getElementById('batch-progress-fill');
|
||||||
|
const progressText = document.getElementById('batch-progress-text');
|
||||||
|
const progressCount = document.getElementById('batch-progress-count');
|
||||||
|
|
||||||
|
progressSection.classList.remove('hidden');
|
||||||
|
|
||||||
|
const percentage = Math.round((progress.completed / progress.total) * 100);
|
||||||
|
progressFill.style.width = `${percentage}%`;
|
||||||
|
|
||||||
|
if (progress.completed === progress.total) {
|
||||||
|
progressText.textContent = 'Conversion complete!';
|
||||||
|
} else {
|
||||||
|
progressText.textContent = `Processing: ${progress.currentFile}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCount.textContent = `${progress.completed} / ${progress.total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBatchForm() {
|
||||||
|
const inputFolder = document.getElementById('batch-input-folder').value.trim();
|
||||||
|
const outputFolder = document.getElementById('batch-output-folder').value.trim();
|
||||||
|
const startButton = document.getElementById('batch-start');
|
||||||
|
|
||||||
|
startButton.disabled = !inputFolder || !outputFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners for batch dialog
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Browse input folder
|
||||||
|
document.getElementById('browse-input-folder').addEventListener('click', () => {
|
||||||
|
ipcRenderer.send('select-folder', 'input');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browse output folder
|
||||||
|
document.getElementById('browse-output-folder').addEventListener('click', () => {
|
||||||
|
ipcRenderer.send('select-folder', 'output');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show advanced options
|
||||||
|
document.getElementById('batch-show-options').addEventListener('click', () => {
|
||||||
|
const format = document.getElementById('batch-format').value;
|
||||||
|
currentExportFormat = format;
|
||||||
|
showExportDialog(format);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dialog close buttons
|
||||||
|
document.getElementById('batch-dialog-close').addEventListener('click', hideBatchDialog);
|
||||||
|
document.getElementById('batch-cancel').addEventListener('click', hideBatchDialog);
|
||||||
|
|
||||||
|
// Start batch conversion
|
||||||
|
document.getElementById('batch-start').addEventListener('click', () => {
|
||||||
|
const inputFolder = document.getElementById('batch-input-folder').value.trim();
|
||||||
|
const outputFolder = document.getElementById('batch-output-folder').value.trim();
|
||||||
|
const format = document.getElementById('batch-format').value;
|
||||||
|
|
||||||
|
if (!inputFolder || !outputFolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use current export options from advanced dialog if they were set
|
||||||
|
const options = currentBatchOptions;
|
||||||
|
|
||||||
|
// Start batch conversion
|
||||||
|
ipcRenderer.send('batch-convert', {
|
||||||
|
inputFolder,
|
||||||
|
outputFolder,
|
||||||
|
format,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show progress
|
||||||
|
document.getElementById('batch-progress').classList.remove('hidden');
|
||||||
|
document.getElementById('batch-start').disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
document.getElementById('batch-dialog').addEventListener('click', (e) => {
|
||||||
|
if (e.target === document.getElementById('batch-dialog')) {
|
||||||
|
hideBatchDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on Escape key (modified to handle both dialogs)
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (!document.getElementById('export-dialog').classList.contains('hidden')) {
|
||||||
|
hideExportDialog();
|
||||||
|
} else if (!document.getElementById('batch-dialog').classList.contains('hidden')) {
|
||||||
|
hideBatchDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
document.getElementById('batch-input-folder').addEventListener('input', validateBatchForm);
|
||||||
|
document.getElementById('batch-output-folder').addEventListener('input', validateBatchForm);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override the export dialog confirm to also save batch options
|
||||||
|
const originalExportConfirm = document.getElementById('export-confirm');
|
||||||
|
if (originalExportConfirm) {
|
||||||
|
originalExportConfirm.addEventListener('click', () => {
|
||||||
|
// If batch dialog is open, save options for batch conversion
|
||||||
|
if (!document.getElementById('batch-dialog').classList.contains('hidden')) {
|
||||||
|
currentBatchOptions = collectExportOptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC event listeners for recent files functionality
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.electronAPI.on('recent-files-cleared', () => {
|
||||||
|
tabManager.recentFiles = [];
|
||||||
|
localStorage.setItem('recentFiles', JSON.stringify([]));
|
||||||
|
console.log('Recent files cleared');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add math rendering support using KaTeX for enhanced preview
|
||||||
|
function initMathSupport() {
|
||||||
|
// Add KaTeX CSS
|
||||||
|
const katexCSS = document.createElement('link');
|
||||||
|
katexCSS.rel = 'stylesheet';
|
||||||
|
katexCSS.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css';
|
||||||
|
katexCSS.crossOrigin = 'anonymous';
|
||||||
|
document.head.appendChild(katexCSS);
|
||||||
|
|
||||||
|
// Add KaTeX JS
|
||||||
|
const katexJS = document.createElement('script');
|
||||||
|
katexJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js';
|
||||||
|
katexJS.crossOrigin = 'anonymous';
|
||||||
|
katexJS.onload = () => {
|
||||||
|
// Add auto-render extension
|
||||||
|
const autoRenderJS = document.createElement('script');
|
||||||
|
autoRenderJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js';
|
||||||
|
autoRenderJS.crossOrigin = 'anonymous';
|
||||||
|
autoRenderJS.onload = () => {
|
||||||
|
console.log('Math support (KaTeX) initialized');
|
||||||
|
// Re-render current preview to include math
|
||||||
|
if (tabManager) {
|
||||||
|
tabManager.updatePreview();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.head.appendChild(autoRenderJS);
|
||||||
|
};
|
||||||
|
document.head.appendChild(katexJS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize math support on load
|
||||||
|
initMathSupport();
|
||||||
+809
@@ -469,6 +469,40 @@ body.theme-solarized {
|
|||||||
color: #657b83;
|
color: #657b83;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .tab-bar {
|
||||||
|
background: #eee8d5;
|
||||||
|
border-bottom-color: #93a1a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .tab {
|
||||||
|
background: #fdf6e3;
|
||||||
|
border-color: #93a1a1;
|
||||||
|
color: #657b83;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .tab.active {
|
||||||
|
background: #fdf6e3;
|
||||||
|
border-color: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .tab-close {
|
||||||
|
color: #657b83;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .tab-close:hover {
|
||||||
|
background: #93a1a1;
|
||||||
|
color: #dc322f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .new-tab-button {
|
||||||
|
border-color: #93a1a1;
|
||||||
|
color: #657b83;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .new-tab-button:hover {
|
||||||
|
background: #eee8d5;
|
||||||
|
}
|
||||||
|
|
||||||
body.theme-solarized .toolbar {
|
body.theme-solarized .toolbar {
|
||||||
background: #eee8d5;
|
background: #eee8d5;
|
||||||
border-bottom-color: #93a1a1;
|
border-bottom-color: #93a1a1;
|
||||||
@@ -510,6 +544,40 @@ body.theme-monokai {
|
|||||||
color: #f8f8f2;
|
color: #f8f8f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .tab-bar {
|
||||||
|
background: #3e3d32;
|
||||||
|
border-bottom-color: #75715e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .tab {
|
||||||
|
background: #49483e;
|
||||||
|
border-color: #75715e;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .tab.active {
|
||||||
|
background: #272822;
|
||||||
|
border-color: #a6e22e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .tab-close {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .tab-close:hover {
|
||||||
|
background: #75715e;
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .new-tab-button {
|
||||||
|
border-color: #75715e;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .new-tab-button:hover {
|
||||||
|
background: #49483e;
|
||||||
|
}
|
||||||
|
|
||||||
body.theme-monokai .toolbar {
|
body.theme-monokai .toolbar {
|
||||||
background: #3e3d32;
|
background: #3e3d32;
|
||||||
border-bottom-color: #75715e;
|
border-bottom-color: #75715e;
|
||||||
@@ -529,6 +597,11 @@ body.theme-monokai #editor {
|
|||||||
color: #f8f8f2;
|
color: #f8f8f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .editor-textarea {
|
||||||
|
background: #272822;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
body.theme-monokai #preview-pane {
|
body.theme-monokai #preview-pane {
|
||||||
background: #272822;
|
background: #272822;
|
||||||
}
|
}
|
||||||
@@ -571,6 +644,40 @@ body.theme-github {
|
|||||||
color: #24292e;
|
color: #24292e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.theme-github .tab-bar {
|
||||||
|
background: #fafbfc;
|
||||||
|
border-bottom-color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .tab {
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-color: #e1e4e8;
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .tab.active {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #0366d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .tab-close {
|
||||||
|
color: #586069;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .tab-close:hover {
|
||||||
|
background: #e1e4e8;
|
||||||
|
color: #cb2431;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .new-tab-button {
|
||||||
|
border-color: #e1e4e8;
|
||||||
|
color: #586069;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .new-tab-button:hover {
|
||||||
|
background: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
body.theme-github .toolbar {
|
body.theme-github .toolbar {
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
border-bottom-color: #e1e4e8;
|
border-bottom-color: #e1e4e8;
|
||||||
@@ -728,6 +835,531 @@ body.theme-github .status-bar {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Export Options Dialog */
|
||||||
|
.export-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-section label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-section select,
|
||||||
|
.export-section input[type="text"],
|
||||||
|
.export-section input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-section select:focus,
|
||||||
|
.export-section input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-container {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-field {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-field:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-key {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input,
|
||||||
|
.form-row select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row button:hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog[data-format="pdf"] .pdf-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-footer button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-footer button.primary {
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
border-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-footer button.primary:hover {
|
||||||
|
background: #005fa3;
|
||||||
|
border-color: #005fa3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-footer button:not(.primary) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-dialog-footer button:not(.primary):hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#export-dialog-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#export-dialog-close:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-metadata-field {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-metadata-field:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme support for export dialog */
|
||||||
|
body.theme-dark .export-dialog-content {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .export-dialog-header {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .export-dialog-footer {
|
||||||
|
background: #333;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .export-section label {
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .export-section select,
|
||||||
|
body.theme-dark .export-section input {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .metadata-container {
|
||||||
|
border-color: #444;
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .form-row button,
|
||||||
|
body.theme-dark #add-metadata-field {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .form-row button:hover,
|
||||||
|
body.theme-dark #add-metadata-field:hover {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #export-dialog-close:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Batch Conversion Dialog */
|
||||||
|
.batch-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-section label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-section select,
|
||||||
|
.batch-section input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-input-group input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-input-group button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-input-group button:hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-section label input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#batch-show-options {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#batch-show-options:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-progress {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #007acc;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer button.primary {
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
border-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer button.primary:hover:not(:disabled) {
|
||||||
|
background: #005fa3;
|
||||||
|
border-color: #005fa3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer button.primary:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
border-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer button:not(.primary) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-dialog-footer button:not(.primary):hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#batch-dialog-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#batch-dialog-close:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme support for batch dialog */
|
||||||
|
body.theme-dark .batch-dialog-content {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .batch-dialog-header {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .batch-dialog-footer {
|
||||||
|
background: #333;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .batch-section label {
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .batch-section select,
|
||||||
|
body.theme-dark .batch-section input {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .folder-input-group button,
|
||||||
|
body.theme-dark #batch-show-options {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .folder-input-group button:hover,
|
||||||
|
body.theme-dark #batch-show-options:hover {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .batch-progress {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .progress-text {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #batch-dialog-close:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
/* Theme support for find dialog */
|
/* Theme support for find dialog */
|
||||||
body.theme-dark .find-dialog {
|
body.theme-dark .find-dialog {
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
@@ -775,3 +1407,180 @@ body.theme-github .line-numbers {
|
|||||||
border-right-color: #e1e4e8;
|
border-right-color: #e1e4e8;
|
||||||
color: #586069;
|
color: #586069;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Advanced Export Toggle Styles */
|
||||||
|
.export-mode-toggle {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-help {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-options {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-options label {
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme support for advanced export toggle */
|
||||||
|
body.theme-dark .export-mode-toggle {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .export-help {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .basic-options {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .checkbox-label {
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .export-mode-toggle {
|
||||||
|
border-color: #eee8d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .export-help {
|
||||||
|
color: #586e75;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .basic-options {
|
||||||
|
background: #fdf6e3;
|
||||||
|
border-color: #eee8d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .export-mode-toggle {
|
||||||
|
border-color: #49483e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .export-help {
|
||||||
|
color: #75715e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .basic-options {
|
||||||
|
background: #2f2f2f;
|
||||||
|
border-color: #49483e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .export-mode-toggle {
|
||||||
|
border-color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .export-help {
|
||||||
|
color: #586069;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .basic-options {
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Statistics Styles */
|
||||||
|
.enhanced-stats {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
padding: 2px 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme support for enhanced stats */
|
||||||
|
body.theme-dark .enhanced-stats {
|
||||||
|
color: #999;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .enhanced-stats {
|
||||||
|
color: #586e75;
|
||||||
|
border-color: #eee8d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .enhanced-stats {
|
||||||
|
color: #75715e;
|
||||||
|
border-color: #49483e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .enhanced-stats {
|
||||||
|
color: #586069;
|
||||||
|
border-color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auto-save indicator */
|
||||||
|
.auto-save-indicator {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 10000;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
animation: slideInRight 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-save-indicator.fade-out {
|
||||||
|
animation: fadeOut 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user