mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
Release v3.0.0: PDF Viewer & Editor Integration
Major Features: - Integrated PDF viewer with zoom, page navigation, and fit modes - PDF editor toolbar with direct access to all PDF operations - New app branding with docico1.png icon - ConcreteInfo theme (light & dark variants) - Comprehensive PDF editing: merge, split, compress, rotate, delete, reorder - Watermark, encrypt, decrypt, and permissions management - Overwrite original file option for all PDF operations UI Improvements: - PDF viewer takes full screen when active (hides tabs and toolbar) - Compact modals with reduced font sizes - Dark theme fixes for icons and dialogs - Save/Save As options for PDF operations - Improved PDF toolbar theming Technical: - Added isPdfViewerActive state tracking - Proper PDF document lifecycle management - Fixed hang issues when switching between PDF and markdown - All dependencies are open-source (MIT, Apache 2.0, BSD) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,601 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ASCII Art Generator - MarkdownConverter</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ci-dark-gray: #464646;
|
||||
--ci-medium-gray: #9a9696;
|
||||
--ci-accent: #e5461f;
|
||||
--ci-light-gray: #e3e3e3;
|
||||
--ci-black: #0d0b09;
|
||||
--ci-white: #ffffff;
|
||||
--ci-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--ci-bg);
|
||||
color: var(--ci-dark-gray);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--ci-white);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--ci-white);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ci-dark-gray);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
padding: 10px 20px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
background: var(--ci-white);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mode-tab:hover {
|
||||
border-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.mode-tab.active {
|
||||
background: var(--ci-accent);
|
||||
border-color: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
background: var(--ci-black);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: #00ff00;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.template-btn {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
background: var(--ci-white);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-btn:hover {
|
||||
border-color: var(--ci-accent);
|
||||
background: rgba(229, 70, 31, 0.05);
|
||||
}
|
||||
|
||||
.template-btn.active {
|
||||
border-color: var(--ci-accent);
|
||||
background: rgba(229, 70, 31, 0.1);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: var(--ci-white);
|
||||
border-top: 1px solid var(--ci-light-gray);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #c93a18;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ci-light-gray);
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.mode-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mode-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-category {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.template-category-title {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ci-medium-gray);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>ASCII Art Generator</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<div class="mode-tabs">
|
||||
<button class="mode-tab active" data-mode="text">Text Banner</button>
|
||||
<button class="mode-tab" data-mode="box">Box/Frame</button>
|
||||
<button class="mode-tab" data-mode="templates">Templates</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Banner Mode -->
|
||||
<div id="text-mode" class="mode-section active">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Text to Convert</label>
|
||||
<input type="text" id="text-input" class="form-input" placeholder="Enter your text..." maxlength="30">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Style</label>
|
||||
<select id="font-style" class="form-select">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="banner">Banner</option>
|
||||
<option value="block">Block</option>
|
||||
<option value="bubble">Bubble</option>
|
||||
<option value="digital">Digital</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Box/Frame Mode -->
|
||||
<div id="box-mode" class="mode-section">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Text Content</label>
|
||||
<textarea id="box-text" class="form-textarea" placeholder="Enter text for the box..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Box Style</label>
|
||||
<select id="box-style" class="form-select">
|
||||
<option value="single">Single Line</option>
|
||||
<option value="double">Double Line</option>
|
||||
<option value="rounded">Rounded</option>
|
||||
<option value="bold">Bold</option>
|
||||
<option value="ascii">ASCII (+|-)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Padding</label>
|
||||
<input type="number" id="box-padding" class="form-input" min="0" max="10" value="2" style="width: 100px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Mode -->
|
||||
<div id="templates-mode" class="mode-section">
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Arrows & Flow</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="arrow-right">Arrow Right</button>
|
||||
<button class="template-btn" data-template="arrow-down">Arrow Down</button>
|
||||
<button class="template-btn" data-template="decision">Decision</button>
|
||||
<button class="template-btn" data-template="process">Process Flow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Diagrams</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="flowchart">Flowchart</button>
|
||||
<button class="template-btn" data-template="sequence">Sequence</button>
|
||||
<button class="template-btn" data-template="network">Network</button>
|
||||
<button class="template-btn" data-template="hierarchy">Hierarchy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Boxes & Containers</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="header">Header</button>
|
||||
<button class="template-btn" data-template="note">Note Box</button>
|
||||
<button class="template-btn" data-template="warning">Warning</button>
|
||||
<button class="template-btn" data-template="info">Info Box</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Decorative</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="divider">Divider</button>
|
||||
<button class="template-btn" data-template="separator">Separator</button>
|
||||
<button class="template-btn" data-template="banner">Banner</button>
|
||||
<button class="template-btn" data-template="checklist">Checklist</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Preview</div>
|
||||
<div class="preview-container">
|
||||
<div id="preview" class="preview-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<button class="btn btn-secondary" id="btn-generate">Generate Preview</button>
|
||||
<button class="btn btn-primary" id="btn-insert">Insert to Editor</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ASCII Art Generator Logic
|
||||
const FONTS = {
|
||||
standard: {
|
||||
height: 5,
|
||||
chars: {
|
||||
'A': [' /\\ ', ' / \\ ', '/----\\', '| |', '| |'],
|
||||
'B': ['|----\\', '| |', '|----/', '| \\', '|----/'],
|
||||
'C': ['/----\\', '| ', '| ', '| ', '\\----/'],
|
||||
'D': ['|----\\', '| |', '| |', '| |', '|----/'],
|
||||
'E': ['|----', '| ', '|--- ', '| ', '|----'],
|
||||
'F': ['|----', '| ', '|--- ', '| ', '| '],
|
||||
'G': ['/----\\', '| ', '| |--\\', '| |', '\\----/'],
|
||||
'H': ['| |', '| |', '|----/', '| |', '| |'],
|
||||
'I': ['|---|', ' | ', ' | ', ' | ', '|---|'],
|
||||
'J': [' |', ' |', ' |', '| |', '\\---/'],
|
||||
'K': ['| /', '| / ', '|-- ', '| \\ ', '| \\'],
|
||||
'L': ['| ', '| ', '| ', '| ', '|----'],
|
||||
'M': ['|\\ /|', '| \\/ |', '| |', '| |', '| |'],
|
||||
'N': ['|\\ |', '| \\ |', '| \\ |', '| \\|', '| |'],
|
||||
'O': ['/----\\', '| |', '| |', '| |', '\\----/'],
|
||||
'P': ['|----\\', '| |', '|----/', '| ', '| '],
|
||||
'Q': ['/----\\', '| |', '| \\ |', '| \\|', '\\----\\'],
|
||||
'R': ['|----\\', '| |', '|----/', '| \\ ', '| \\ '],
|
||||
'S': ['/----\\', '| ', '\\----\\', ' |', '\\----/'],
|
||||
'T': ['-----', ' | ', ' | ', ' | ', ' | '],
|
||||
'U': ['| |', '| |', '| |', '| |', '\\----/'],
|
||||
'V': ['| |', '| |', ' \\ / ', ' \\/ ', ' '],
|
||||
'W': ['| |', '| |', '| |', '| /\\ |', '|/ \\|'],
|
||||
'X': ['\\ /', ' \\ / ', ' \\/ ', ' /\\ ', ' / \\ '],
|
||||
'Y': ['\\ /', ' \\ / ', ' | ', ' | ', ' | '],
|
||||
'Z': ['-----', ' / ', ' / ', ' / ', '-----'],
|
||||
' ': [' ', ' ', ' ', ' ', ' '],
|
||||
'0': ['/---\\', '| |', '| / |', '|/ |', '\\---/'],
|
||||
'1': [' /| ', ' / | ', ' | ', ' | ', ' ----'],
|
||||
'2': ['/---\\', ' |', ' ---/', '/ ', '-----'],
|
||||
'3': ['----\\', ' |', ' ---/', ' |', '----/'],
|
||||
'4': ['| |', '| |', '-----', ' |', ' |'],
|
||||
'5': ['-----', '| ', '----\\', ' |', '----/'],
|
||||
'6': ['/----', '| ', '|---\\', '| |', '\\---/'],
|
||||
'7': ['-----', ' / ', ' / ', ' / ', '/ '],
|
||||
'8': ['/---\\', '| |', ' --- ', '| |', '\\---/'],
|
||||
'9': ['/---\\', '| |', '\\----', ' |', '----/']
|
||||
}
|
||||
},
|
||||
banner: {
|
||||
height: 7,
|
||||
chars: {
|
||||
'A': [' ##### ', ' ## ##', '## ##', '#########', '## ##', '## ##', '## ##'],
|
||||
'B': ['######## ', '## ##', '## ##', '######## ', '## ##', '## ##', '######## '],
|
||||
'C': [' ###### ', '## ##', '## ', '## ', '## ', '## ##', ' ###### '],
|
||||
'D': ['######## ', '## ##', '## ##', '## ##', '## ##', '## ##', '######## '],
|
||||
'E': ['########', '## ', '## ', '###### ', '## ', '## ', '########'],
|
||||
'F': ['########', '## ', '## ', '###### ', '## ', '## ', '## '],
|
||||
'G': [' ###### ', '## ##', '## ', '## ####', '## ##', '## ##', ' ###### '],
|
||||
'H': ['## ##', '## ##', '## ##', '#########', '## ##', '## ##', '## ##'],
|
||||
'I': ['####', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', '####'],
|
||||
'J': [' ##', ' ##', ' ##', ' ##', '## ##', '## ##', ' ###### '],
|
||||
'K': ['## ##', '## ## ', '## ## ', '##### ', '## ## ', '## ## ', '## ##'],
|
||||
'L': ['## ', '## ', '## ', '## ', '## ', '## ', '########'],
|
||||
'M': ['## ##', '### ###', '#### ####', '## ### ##', '## ##', '## ##', '## ##'],
|
||||
'N': ['## ##', '### ##', '#### ##', '## ## ##', '## ####', '## ###', '## ##'],
|
||||
'O': [' ####### ', '## ##', '## ##', '## ##', '## ##', '## ##', ' ####### '],
|
||||
'P': ['######## ', '## ##', '## ##', '######## ', '## ', '## ', '## '],
|
||||
'Q': [' ####### ', '## ##', '## ##', '## ##', '## ## ##', '## ## ', ' ##### ##'],
|
||||
'R': ['######## ', '## ##', '## ##', '######## ', '## ## ', '## ## ', '## ##'],
|
||||
'S': [' ###### ', '## ##', '## ', ' ###### ', ' ##', '## ##', ' ###### '],
|
||||
'T': ['########', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '],
|
||||
'U': ['## ##', '## ##', '## ##', '## ##', '## ##', '## ##', ' ####### '],
|
||||
'V': ['## ##', '## ##', '## ##', '## ##', ' ## ## ', ' ## ## ', ' ### '],
|
||||
'W': ['## ##', '## ## ##', '## ## ##', '## ## ##', '## ## ##', '## ## ##', ' ### ### '],
|
||||
'X': ['## ##', ' ## ## ', ' ## ## ', ' ### ', ' ## ## ', ' ## ## ', '## ##'],
|
||||
'Y': ['## ##', ' ## ## ', ' #### ', ' ## ', ' ## ', ' ## ', ' ## '],
|
||||
'Z': ['########', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', '########'],
|
||||
' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
'0': [' ###### ', '## ##', '## ##', '## ##', '## ##', '## ##', ' ###### '],
|
||||
'1': [' ## ', ' #### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ###### '],
|
||||
'2': [' ###### ', '## ##', ' ## ', ' ## ', ' ## ', ' ## ', '########'],
|
||||
'3': [' ###### ', '## ##', ' ## ', ' #### ', ' ## ', '## ##', ' ###### '],
|
||||
'4': [' ## ', ' ### ', ' # ## ', ' # ## ', '########', ' ## ', ' ## '],
|
||||
'5': ['########', '## ', '####### ', ' ##', ' ##', '## ##', ' ###### '],
|
||||
'6': [' ###### ', '## ', '####### ', '## ##', '## ##', '## ##', ' ###### '],
|
||||
'7': ['########', '## ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '],
|
||||
'8': [' ###### ', '## ##', '## ##', ' ###### ', '## ##', '## ##', ' ###### '],
|
||||
'9': [' ###### ', '## ##', '## ##', ' #######', ' ##', '## ##', ' ###### ']
|
||||
}
|
||||
},
|
||||
block: {
|
||||
height: 6,
|
||||
chars: {
|
||||
'A': ['█████╗ ', '██╔══██╗', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
'B': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '],
|
||||
'C': ['█████╗ ', '██╔══██╗', '██║ ', '██║ ', '╚█████╔╝', ' ╚════╝ '],
|
||||
'D': ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '],
|
||||
'E': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
|
||||
'F': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '██║ ', '╚═╝ '],
|
||||
'G': ['█████╗ ', '██╔══██╗', '██║ ███', '██║ ██', '╚█████╔╝', ' ╚════╝ '],
|
||||
'H': ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
'I': ['██╗', '██║', '██║', '██║', '██║', '╚═╝'],
|
||||
'J': [' ██╗', ' ██║', ' ██║', '██ ██║', '╚████╔╝', ' ╚═══╝ '],
|
||||
'K': ['██╗ ██╗', '██║ ██╔╝', '█████╔╝ ', '██╔═██╗ ', '██║ ██╗', '╚═╝ ╚═╝'],
|
||||
'L': ['██╗ ', '██║ ', '██║ ', '██║ ', '███████╗', '╚══════╝'],
|
||||
'M': ['███╗ ███╗', '████╗ ████║', '██╔████╔██║', '██║╚██╔╝██║', '██║ ╚═╝ ██║', '╚═╝ ╚═╝'],
|
||||
'N': ['███╗ ██╗', '████╗ ██║', '██╔██╗ ██║', '██║╚██╗██║', '██║ ╚████║', '╚═╝ ╚═══╝'],
|
||||
'O': ['█████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '╚█████╔╝', ' ╚════╝ '],
|
||||
'P': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔═══╝ ', '██║ ', '╚═╝ '],
|
||||
'Q': ['█████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '╚██████╗', ' ╚═══██╝'],
|
||||
'R': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
'S': ['█████╗ ', '██╔══╝ ', '█████╗ ', '╚══██║ ', '█████║ ', '╚════╝ '],
|
||||
'T': ['████████╗', '╚══██╔══╝', ' ██║ ', ' ██║ ', ' ██║ ', ' ╚═╝ '],
|
||||
'U': ['██╗ ██╗', '██║ ██║', '██║ ██║', '██║ ██║', '╚█████╔╝', ' ╚════╝ '],
|
||||
'V': ['██╗ ██╗', '██║ ██║', '██║ ██║', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚═══╝ '],
|
||||
'W': ['██╗ ██╗', '██║ ██║', '██║ █╗ ██║', '██║███╗██║', '╚███╔███╔╝', ' ╚══╝╚══╝ '],
|
||||
'X': ['██╗ ██╗', '╚██╗██╔╝', ' ╚███╔╝ ', ' ██╔██╗ ', '██╔╝ ██╗', '╚═╝ ╚═╝'],
|
||||
'Y': ['██╗ ██╗', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚██╔╝ ', ' ██║ ', ' ╚═╝ '],
|
||||
'Z': ['███████╗', '╚════██║', ' ███╔═╝', ' ██╔══╝ ', '███████╗', '╚══════╝'],
|
||||
' ': [' ', ' ', ' ', ' ', ' ', ' ']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TEMPLATES = {
|
||||
'arrow-right': ' ┌─────────────────────┐\n──▶│ Process or Action │──▶\n └─────────────────────┘',
|
||||
'arrow-down': ' │\n ▼\n┌───────────────┐\n│ Process │\n└───────────────┘\n │\n ▼',
|
||||
'decision': ' ╱╲\n ╱ ╲\n ╱ ? ╲\n ╱ ╲\n ╱────────╲\n ╱ ╲\n YES NO\n │ │\n ▼ ▼',
|
||||
'process': '┌─────┐ ┌─────┐ ┌─────┐\n│ 1 │──▶│ 2 │──▶│ 3 │\n└─────┘ └─────┘ └─────┘',
|
||||
'flowchart': '┌─────────────┐\n│ START │\n└──────┬──────┘\n │\n ▼\n┌─────────────┐\n│ Process A │\n└──────┬──────┘\n │\n ▼\n ╱────────╲\n ╱ Decision ╲\n ╲ ? ╱\n ╲────────╱\n │ │\n YES NO\n │ │\n ▼ ▼\n┌──────┐ ┌──────┐\n│ B │ │ C │\n└──────┘ └──────┘',
|
||||
'sequence': ' User System Database\n │ │ │\n │ Request │ │\n ├──────────►│ │\n │ │ Query │\n │ ├──────────►│\n │ │ │\n │ │ Result │\n │ │◄──────────┤\n │ Response │ │\n │◄──────────┤ │\n │ │ │',
|
||||
'network': ' ┌─────────┐\n │ Server │\n └────┬────┘\n │\n ┌─────────┼─────────┐\n │ │ │\n┌────┴────┐ ┌──┴──┐ ┌────┴────┐\n│ Client1 │ │ DB │ │ Client2 │\n└─────────┘ └─────┘ └─────────┘',
|
||||
'hierarchy': ' ┌─────────┐\n │ CEO │\n └────┬────┘\n ┌─────────┼─────────┐\n │ │ │\n ┌───┴───┐ ┌───┴───┐ ┌───┴───┐\n │ VP1 │ │ VP2 │ │ VP3 │\n └───┬───┘ └───┬───┘ └───┬───┘\n │ │ │\n ┌───┴───┐ ┌───┴───┐ ┌───┴───┐\n │ Team1 │ │ Team2 │ │ Team3 │\n └───────┘ └───────┘ └───────┘',
|
||||
'header': '╔════════════════════════════════════╗\n║ SECTION TITLE ║\n╚════════════════════════════════════╝',
|
||||
'note': '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n┃ NOTE: ┃\n┃ This is an important note ┃\n┃ that requires attention! ┃\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
|
||||
'warning': '╔════════════════════════════════════╗\n║ ⚠️ WARNING ║\n║ ║\n║ Critical information here! ║\n╚════════════════════════════════════╝',
|
||||
'info': '╭────────────────────────────────────╮\n│ ℹ️ INFO │\n│ │\n│ Helpful information here. │\n╰────────────────────────────────────╯',
|
||||
'divider': '════════════════════════════════════════',
|
||||
'separator': '╭──────────────────────────────────────╮\n│ │\n╰──────────────────────────────────────╯',
|
||||
'banner': '★══════════════════════════════════════★\n║ YOUR TITLE HERE ║\n★══════════════════════════════════════★',
|
||||
'checklist': '☐ Task 1 - Not completed\n☑ Task 2 - Completed \n☐ Task 3 - Not completed\n☐ Task 4 - Not completed'
|
||||
};
|
||||
|
||||
const BOX_STYLES = {
|
||||
single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
|
||||
double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
|
||||
rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
|
||||
bold: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' },
|
||||
ascii: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' }
|
||||
};
|
||||
|
||||
let currentMode = 'text';
|
||||
let currentTemplate = null;
|
||||
|
||||
// Mode switching
|
||||
document.querySelectorAll('.mode-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
currentMode = tab.dataset.mode;
|
||||
|
||||
document.querySelectorAll('.mode-section').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById(currentMode + '-mode').classList.add('active');
|
||||
|
||||
generatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Template selection
|
||||
document.querySelectorAll('.template-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.template-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTemplate = btn.dataset.template;
|
||||
generatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Generate text banner
|
||||
function generateTextBanner(text, style) {
|
||||
const font = FONTS[style] || FONTS.standard;
|
||||
const lines = Array(font.height).fill('');
|
||||
|
||||
for (const char of text.toUpperCase()) {
|
||||
const charArt = font.chars[char] || font.chars[' '];
|
||||
if (charArt) {
|
||||
for (let i = 0; i < font.height; i++) {
|
||||
lines[i] += charArt[i] + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Generate box
|
||||
function generateBox(text, style, padding) {
|
||||
const box = BOX_STYLES[style] || BOX_STYLES.single;
|
||||
const lines = text.split('\n');
|
||||
const maxLen = Math.max(...lines.map(l => l.length)) + padding * 2;
|
||||
|
||||
let result = box.tl + box.h.repeat(maxLen + 2) + box.tr + '\n';
|
||||
|
||||
// Add padding lines at top
|
||||
for (let i = 0; i < Math.floor(padding / 2); i++) {
|
||||
result += box.v + ' '.repeat(maxLen + 2) + box.v + '\n';
|
||||
}
|
||||
|
||||
// Add text lines
|
||||
for (const line of lines) {
|
||||
const paddedLine = ' '.repeat(padding) + line.padEnd(maxLen - padding) + ' ';
|
||||
result += box.v + ' ' + paddedLine + box.v + '\n';
|
||||
}
|
||||
|
||||
// Add padding lines at bottom
|
||||
for (let i = 0; i < Math.floor(padding / 2); i++) {
|
||||
result += box.v + ' '.repeat(maxLen + 2) + box.v + '\n';
|
||||
}
|
||||
|
||||
result += box.bl + box.h.repeat(maxLen + 2) + box.br;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Generate preview
|
||||
function generatePreview() {
|
||||
let result = '';
|
||||
|
||||
if (currentMode === 'text') {
|
||||
const text = document.getElementById('text-input').value || 'HELLO';
|
||||
const style = document.getElementById('font-style').value;
|
||||
result = generateTextBanner(text, style);
|
||||
} else if (currentMode === 'box') {
|
||||
const text = document.getElementById('box-text').value || 'Your text here';
|
||||
const style = document.getElementById('box-style').value;
|
||||
const padding = parseInt(document.getElementById('box-padding').value) || 2;
|
||||
result = generateBox(text, style, padding);
|
||||
} else if (currentMode === 'templates') {
|
||||
result = currentTemplate ? TEMPLATES[currentTemplate] : 'Select a template...';
|
||||
}
|
||||
|
||||
document.getElementById('preview').textContent = result;
|
||||
}
|
||||
|
||||
// Event listeners for live preview
|
||||
document.getElementById('text-input').addEventListener('input', generatePreview);
|
||||
document.getElementById('font-style').addEventListener('change', generatePreview);
|
||||
document.getElementById('box-text').addEventListener('input', generatePreview);
|
||||
document.getElementById('box-style').addEventListener('change', generatePreview);
|
||||
document.getElementById('box-padding').addEventListener('input', generatePreview);
|
||||
|
||||
document.getElementById('btn-generate').addEventListener('click', generatePreview);
|
||||
|
||||
document.getElementById('btn-insert').addEventListener('click', () => {
|
||||
const content = document.getElementById('preview').textContent;
|
||||
if (content && window.electronAPI) {
|
||||
// Wrap in code block for markdown
|
||||
const wrapped = '```\n' + content + '\n```';
|
||||
window.electronAPI.send('insert-generated-content', wrapped);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial preview
|
||||
generatePreview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+155
-28
@@ -3,12 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PanConverter - Markdown Editor</title>
|
||||
<title>MarkdownConverter</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="styles-modern.css">
|
||||
<link rel="stylesheet" href="styles-concreteinfo.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||
<!-- Mermaid.js for diagram rendering -->
|
||||
@@ -16,6 +17,16 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- App Header with ConcreteInfo Logo -->
|
||||
<div class="app-header" id="app-header">
|
||||
<div class="app-header-left">
|
||||
<img src="../assets/docico1.png" alt="MarkdownConverter" class="app-logo" id="app-logo">
|
||||
<span class="app-title">MarkdownConverter</span>
|
||||
</div>
|
||||
<div class="app-header-right">
|
||||
<span class="app-version">v3.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-bar" id="tab-bar">
|
||||
<div class="tab active" data-tab-id="1">
|
||||
<span class="tab-title">Untitled</span>
|
||||
@@ -870,10 +881,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="compress-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="compress-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="compress-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-compress-output">Browse</button>
|
||||
<input type="text" id="compress-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-compress-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -901,10 +915,13 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="rotate-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="rotate-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="rotate-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-rotate-output">Browse</button>
|
||||
<input type="text" id="rotate-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-rotate-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -924,10 +941,13 @@
|
||||
<small>Separate page numbers/ranges with commas</small>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="delete-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="delete-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="delete-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-delete-output">Browse</button>
|
||||
<input type="text" id="delete-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-delete-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -953,10 +973,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="reorder-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="reorder-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="reorder-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-reorder-output">Browse</button>
|
||||
<input type="text" id="reorder-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-reorder-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1009,10 +1032,13 @@
|
||||
<input type="text" id="watermark-custom-pages" placeholder="e.g., 1-5, 7, 9-12" class="hidden">
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="watermark-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="watermark-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="watermark-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-watermark-output">Browse</button>
|
||||
<input type="text" id="watermark-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-watermark-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1058,10 +1084,13 @@
|
||||
<small>These permissions apply when owner password is set</small>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="encrypt-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="encrypt-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="encrypt-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-encrypt-output">Browse</button>
|
||||
<input type="text" id="encrypt-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-encrypt-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1081,10 +1110,13 @@
|
||||
<small>Enter the password to unlock and remove protection</small>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="decrypt-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="decrypt-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="decrypt-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-decrypt-output">Browse</button>
|
||||
<input type="text" id="decrypt-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-decrypt-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1121,10 +1153,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label>Output File:</label>
|
||||
<label class="checkbox-inline"><input type="checkbox" id="permissions-overwrite"> Overwrite original file</label>
|
||||
</div>
|
||||
<div class="export-section" id="permissions-saveas-section">
|
||||
<label>Save As:</label>
|
||||
<div class="folder-input-group">
|
||||
<input type="text" id="permissions-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-permissions-output">Browse</button>
|
||||
<input type="text" id="permissions-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-permissions-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1271,12 +1306,104 @@
|
||||
</div>
|
||||
<div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div>
|
||||
<div id="preview-pane-1" class="pane">
|
||||
<div class="preview-header">
|
||||
<button class="preview-popout-btn" id="preview-popout-1" title="Pop out preview in new window">⬜</button>
|
||||
</div>
|
||||
<div id="preview-1" class="preview-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF Viewer Container (hidden by default) -->
|
||||
<div id="pdf-viewer-container" class="pdf-viewer-container hidden">
|
||||
<div class="pdf-viewer-toolbar pdf-toolbar-main">
|
||||
<!-- Navigation -->
|
||||
<button id="pdf-prev-page" title="Previous Page">◀</button>
|
||||
<span id="pdf-page-info">Page <input type="number" id="pdf-page-input" min="1" value="1"> of <span id="pdf-total-pages">0</span></span>
|
||||
<button id="pdf-next-page" title="Next Page">▶</button>
|
||||
<div class="pdf-toolbar-separator"></div>
|
||||
<!-- Zoom -->
|
||||
<button id="pdf-zoom-out" title="Zoom Out">−</button>
|
||||
<span id="pdf-zoom-level">100%</span>
|
||||
<button id="pdf-zoom-in" title="Zoom In">+</button>
|
||||
<button id="pdf-fit-width" title="Fit Width">↔</button>
|
||||
<button id="pdf-fit-page" title="Fit Page">⬜</button>
|
||||
<div class="pdf-toolbar-separator"></div>
|
||||
<!-- Rotation -->
|
||||
<button id="pdf-rotate-left" title="Rotate Left">↺</button>
|
||||
<button id="pdf-rotate-right" title="Rotate Right">↻</button>
|
||||
<div class="pdf-toolbar-separator"></div>
|
||||
<!-- PDF Editor Functions -->
|
||||
<button id="pdf-tb-merge" class="pdf-editor-btn" title="Merge PDFs">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="9" rx="1"></rect>
|
||||
<rect x="14" y="3" width="7" height="9" rx="1"></rect>
|
||||
<path d="M12 15v6m-3-3h6"></path>
|
||||
</svg>
|
||||
<span>Merge</span>
|
||||
</button>
|
||||
<button id="pdf-tb-split" class="pdf-editor-btn" title="Split PDF">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="9" rx="1"></rect>
|
||||
<path d="M12 12v9M9 18l3 3 3-3"></path>
|
||||
</svg>
|
||||
<span>Split</span>
|
||||
</button>
|
||||
<button id="pdf-tb-compress" class="pdf-editor-btn" title="Compress PDF">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 14h16M4 10h16M7 6l5 4 5-4M7 18l5-4 5 4"></path>
|
||||
</svg>
|
||||
<span>Compress</span>
|
||||
</button>
|
||||
<button id="pdf-tb-rotate" class="pdf-editor-btn" title="Rotate Pages">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 11-6.219-8.56"></path>
|
||||
<polyline points="21 3 21 9 15 9"></polyline>
|
||||
</svg>
|
||||
<span>Rotate</span>
|
||||
</button>
|
||||
<button id="pdf-tb-delete" class="pdf-editor-btn" title="Delete Pages">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14z"></path>
|
||||
</svg>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
<button id="pdf-tb-reorder" class="pdf-editor-btn" title="Reorder Pages">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
<polyline points="7 9 4 12 7 15"></polyline>
|
||||
<polyline points="17 9 20 12 17 15"></polyline>
|
||||
</svg>
|
||||
<span>Reorder</span>
|
||||
</button>
|
||||
<button id="pdf-tb-watermark" class="pdf-editor-btn" title="Add Watermark">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
|
||||
<path d="M2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
||||
</svg>
|
||||
<span>Watermark</span>
|
||||
</button>
|
||||
<button id="pdf-tb-encrypt" class="pdf-editor-btn" title="Encrypt PDF">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0110 0v4"></path>
|
||||
</svg>
|
||||
<span>Encrypt</span>
|
||||
</button>
|
||||
<button id="pdf-tb-decrypt" class="pdf-editor-btn" title="Decrypt PDF">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 019.9-1"></path>
|
||||
</svg>
|
||||
<span>Decrypt</span>
|
||||
</button>
|
||||
<div class="pdf-toolbar-separator"></div>
|
||||
<!-- File Info and Close -->
|
||||
<span id="pdf-filename" class="pdf-filename"></span>
|
||||
<button id="pdf-close" title="Close PDF">✕</button>
|
||||
</div>
|
||||
<div id="pdf-viewer" class="pdf-viewer">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
|
||||
+556
-93
@@ -1,7 +1,7 @@
|
||||
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { exec } = require('child_process');
|
||||
const { exec, execFile, spawn } = require('child_process');
|
||||
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
||||
const WordTemplateExporter = require('./wordTemplateExporter');
|
||||
|
||||
@@ -25,12 +25,95 @@ function getPandocPath() {
|
||||
function checkPandocAvailable() {
|
||||
return new Promise((resolve) => {
|
||||
const pandocPath = getPandocPath();
|
||||
exec(`${pandocPath} --version`, (error) => {
|
||||
execFile(pandocPath, ['--version'], (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe command execution using execFile (no shell injection risk)
|
||||
* @param {string} command - The command to execute
|
||||
* @param {string[]} args - Array of arguments
|
||||
* @param {object} options - Options for execFile
|
||||
* @returns {Promise<{stdout: string, stderr: string}>}
|
||||
*/
|
||||
function safeExecFile(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject({ error, stdout, stderr });
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Pandoc command safely with execFile
|
||||
* @param {string[]} args - Pandoc arguments array
|
||||
* @param {Function} callback - Callback function (error, stdout, stderr)
|
||||
*/
|
||||
function runPandoc(args, callback) {
|
||||
const pandocPath = getPandocPath();
|
||||
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a Pandoc command string safely using execFile
|
||||
* Parses the command string and uses execFile to prevent shell injection
|
||||
* @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"')
|
||||
* @param {Function} callback - Callback function (error, stdout, stderr)
|
||||
*/
|
||||
function runPandocCmd(cmdString, callback) {
|
||||
const parsed = parseCommand(cmdString);
|
||||
// Skip 'pandoc' if it's the first element (command itself)
|
||||
const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
|
||||
const pandocPath = getPandocPath();
|
||||
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a command string into command and arguments array
|
||||
* This helps transition from exec() to execFile() safely
|
||||
* @param {string} cmdString - Full command string
|
||||
* @returns {{command: string, args: string[]}}
|
||||
*/
|
||||
function parseCommand(cmdString) {
|
||||
// Handle quoted strings properly
|
||||
const parts = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
let quoteChar = '';
|
||||
|
||||
for (let i = 0; i < cmdString.length; i++) {
|
||||
const char = cmdString[i];
|
||||
if ((char === '"' || char === "'") && !inQuotes) {
|
||||
inQuotes = true;
|
||||
quoteChar = char;
|
||||
} else if (char === quoteChar && inQuotes) {
|
||||
inQuotes = false;
|
||||
quoteChar = '';
|
||||
} else if (char === ' ' && !inQuotes) {
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
|
||||
return {
|
||||
command: parts[0],
|
||||
args: parts.slice(1)
|
||||
};
|
||||
}
|
||||
|
||||
// Simple storage implementation to replace electron-store
|
||||
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
||||
const store = {
|
||||
@@ -180,15 +263,15 @@ if (!gotTheLock) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if pandoc is available
|
||||
// Check if pandoc is available (using execFile for consistency)
|
||||
function checkPandocAvailability() {
|
||||
return new Promise((resolve) => {
|
||||
if (pandocAvailable !== null) {
|
||||
resolve(pandocAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
exec('pandoc --version', (error, stdout, stderr) => {
|
||||
|
||||
execFile('pandoc', ['--version'], (error, stdout, stderr) => {
|
||||
pandocAvailable = !error;
|
||||
resolve(pandocAvailable);
|
||||
});
|
||||
@@ -284,6 +367,11 @@ function createMenu() {
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: openFile
|
||||
},
|
||||
{
|
||||
label: 'Open PDF',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: openPdfFile
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
accelerator: 'CmdOrCtrl+S',
|
||||
@@ -401,29 +489,33 @@ function createMenu() {
|
||||
{
|
||||
label: 'Theme',
|
||||
submenu: [
|
||||
// Light Themes (grouped first)
|
||||
{ label: 'Atom One Light (Default)', click: () => setTheme('atomonelight') },
|
||||
{ label: 'GitHub Light', click: () => setTheme('github') },
|
||||
{ label: 'Light', click: () => setTheme('light') },
|
||||
{ label: 'Dark', click: () => setTheme('dark') },
|
||||
{ label: 'Solarized', click: () => setTheme('solarized') },
|
||||
{ label: 'Monokai', click: () => setTheme('monokai') },
|
||||
{ label: 'GitHub', click: () => setTheme('github') },
|
||||
{ label: 'Solarized Light', click: () => setTheme('solarized') },
|
||||
{ label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
|
||||
{ label: 'Ayu Light', click: () => setTheme('ayu-light') },
|
||||
{ label: 'Sepia', click: () => setTheme('sepia') },
|
||||
{ label: 'Paper', click: () => setTheme('paper') },
|
||||
{ label: 'Rose Pine Dawn', click: () => setTheme('rosepine-dawn') },
|
||||
{ label: 'Concrete Light', click: () => setTheme('concrete-light') },
|
||||
{ type: 'separator' },
|
||||
// Dark Themes
|
||||
{ label: 'Dark', click: () => setTheme('dark') },
|
||||
{ label: 'One Dark', click: () => setTheme('onedark') },
|
||||
{ label: 'Dracula', click: () => setTheme('dracula') },
|
||||
{ label: 'Nord', click: () => setTheme('nord') },
|
||||
{ label: 'One Dark', click: () => setTheme('onedark') },
|
||||
{ label: 'Atom One Light', click: () => setTheme('atomonelight') },
|
||||
{ label: 'Monokai', click: () => setTheme('monokai') },
|
||||
{ label: 'Material', click: () => setTheme('material') },
|
||||
{ label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') },
|
||||
{ label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
|
||||
{ label: 'Tokyo Night', click: () => setTheme('tokyonight') },
|
||||
{ label: 'Palenight', click: () => setTheme('palenight') },
|
||||
{ label: 'Ayu Dark', click: () => setTheme('ayu-dark') },
|
||||
{ label: 'Ayu Light', click: () => setTheme('ayu-light') },
|
||||
{ label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') },
|
||||
{ label: 'Oceanic Next', click: () => setTheme('oceanic-next') },
|
||||
{ label: 'Cobalt2', click: () => setTheme('cobalt2') },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Concrete Dark', click: () => setTheme('concrete-dark') },
|
||||
{ label: 'Concrete Light', click: () => setTheme('concrete-light') },
|
||||
{ label: 'Concrete Warm', click: () => setTheme('concrete-warm') }
|
||||
]
|
||||
},
|
||||
@@ -461,8 +553,25 @@ function createMenu() {
|
||||
label: 'Batch',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Convert Folder...',
|
||||
label: 'Convert Markdown Folder...',
|
||||
click: () => showBatchConversionDialog()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Batch Image Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'image')
|
||||
},
|
||||
{
|
||||
label: 'Batch Audio Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'audio')
|
||||
},
|
||||
{
|
||||
label: 'Batch Video Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'video')
|
||||
},
|
||||
{
|
||||
label: 'Batch PDF Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'pdf')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -473,27 +582,18 @@ function createMenu() {
|
||||
label: 'Universal File Converter...',
|
||||
accelerator: 'CmdOrCtrl+Shift+C',
|
||||
click: () => showUniversalConverterDialog()
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'About Converter',
|
||||
click: () => {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'About Universal Converter',
|
||||
message: 'Open-Source File Converter',
|
||||
detail: 'PanConverter includes a powerful open-source file conversion system.\n\nSupported Converters:\n• LibreOffice - Document conversions (DOCX, PDF, ODT, etc.)\n• ImageMagick - Image format conversions\n• FFmpeg - Media file conversions\n• Pandoc - Document markup conversions\n\nFeatures:\n• 100% open-source and free\n• No API keys required\n• Runs completely offline\n• Supports 100+ file formats\n• High-quality professional conversions',
|
||||
buttons: ['OK']
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'PDF Editor',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open PDF File...',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: () => openPDFFile()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Merge PDFs...',
|
||||
click: () => showPDFEditorDialog('merge')
|
||||
@@ -570,12 +670,15 @@ function createMenu() {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Table Generator',
|
||||
click: () => mainWindow.webContents.send('show-table-generator')
|
||||
accelerator: 'CmdOrCtrl+Shift+T',
|
||||
click: () => openTableGenerator()
|
||||
},
|
||||
{
|
||||
label: 'ASCII Art Generator',
|
||||
click: () => mainWindow.webContents.send('show-ascii-generator')
|
||||
accelerator: 'CmdOrCtrl+Shift+A',
|
||||
click: () => openAsciiGenerator()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Document Compare',
|
||||
click: () => mainWindow.webContents.send('show-document-compare')
|
||||
@@ -586,20 +689,26 @@ function createMenu() {
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About',
|
||||
click: () => {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'About PanConverter',
|
||||
message: 'PanConverter',
|
||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 2.1.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\n✨ New in v2.1.0:\n• Subtle & Small Preview Popout Button - Cleaner, minimalist design\n• Simplified Table Headers - Light gray background with better readability\n• Comprehensive Format-to-Markdown Conversion - Support for 30+ formats (RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, JSON, and more)\n• Exhaustive ASCII Art Generator - 5 text banner styles (Standard, Banner, Block, Bubble, Digital) and 19 professional templates\n• Enhanced Templates - Flowcharts, sequence diagrams, network diagrams, hierarchy trees, timelines, note boxes, warning boxes, and decorative elements\n\n✨ Features from v2.0.0:\n• Export Profiles - Save and load export configurations\n• Mermaid.js diagram support - Render flowcharts, sequence diagrams, and more\n• Command Palette (Ctrl+Shift+P) - Quick access to all commands\n• GitHub Light/Dark preview themes - Beautiful code preview styling\n• Table Generator - Interactive table creation tool\n• Resizable Preview Pane - Drag divider to adjust editor/preview sizes\n• Pop-out Preview Window - Open preview in separate window with live sync\n\nCore Features:\n• Configurable page sizes (A3, A4, A5, B4, B5, Letter, Legal, Tabloid, Custom) with orientation support\n• Page size support for all export formats (PDF, DOCX, ODT, PowerPoint) and batch conversion\n• Preserved ASCII art, charts, tables, flowcharts in exports with full-width background\n• Custom Headers & Footers for PDF, DOCX, ODT, and PowerPoint exports\n• Dynamic field support: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$\n• Logo/image embedding in headers and footers\n• Modern glassmorphism UI with gradient backgrounds\n• Enhanced PDF export via Word template with configurable start page\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• Enhanced Word export with template support (single file & batch)\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Undo/redo functionality\n• Live word count and statistics',
|
||||
buttons: ['OK']
|
||||
});
|
||||
}
|
||||
label: 'About MarkdownConverter',
|
||||
click: () => showAboutDialog()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Dependencies & Requirements',
|
||||
click: () => showDependenciesDialog()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Documentation',
|
||||
click: () => shell.openExternal('https://github.com/amitwh/pan-converter')
|
||||
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter')
|
||||
},
|
||||
{
|
||||
label: 'Report Issue',
|
||||
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues')
|
||||
},
|
||||
{
|
||||
label: 'Check for Updates',
|
||||
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases')
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -609,6 +718,237 @@ function createMenu() {
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
// Show About Dialog with logo
|
||||
function showAboutDialog() {
|
||||
const aboutWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 600,
|
||||
parent: mainWindow,
|
||||
modal: true,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
},
|
||||
icon: path.join(__dirname, '../assets/icon.png')
|
||||
});
|
||||
|
||||
// Convert images to base64 for data URL compatibility
|
||||
let logoBase64 = '';
|
||||
let iconBase64 = '';
|
||||
try {
|
||||
const logoPath = path.join(__dirname, '../assets/logo.png');
|
||||
const iconPath = path.join(__dirname, '../assets/icon.png');
|
||||
if (fs.existsSync(logoPath)) {
|
||||
logoBase64 = 'data:image/png;base64,' + fs.readFileSync(logoPath).toString('base64');
|
||||
}
|
||||
if (fs.existsSync(iconPath)) {
|
||||
iconBase64 = 'data:image/png;base64,' + fs.readFileSync(iconPath).toString('base64');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading about dialog images:', e);
|
||||
}
|
||||
|
||||
const aboutHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>About MarkdownConverter</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #464646 0%, #0d0b09 100%);
|
||||
color: #e3e3e3;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.logo { width: 120px; margin-bottom: 20px; border-radius: 20px; }
|
||||
h1 { font-size: 24px; margin-bottom: 5px; color: #fff; }
|
||||
.version { color: #e5461f; font-size: 14px; margin-bottom: 20px; }
|
||||
.company { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 20px; }
|
||||
.company-logo { height: 24px; }
|
||||
.section { background: rgba(255,255,255,0.1); border-radius: 10px; padding: 15px; margin: 15px 0; text-align: left; }
|
||||
.section h3 { color: #e5461f; font-size: 14px; margin-bottom: 10px; }
|
||||
.section p, .section li { font-size: 12px; line-height: 1.6; }
|
||||
ul { padding-left: 20px; }
|
||||
.footer { margin-top: 20px; font-size: 11px; color: #9a9696; }
|
||||
a { color: #e5461f; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src="${iconBase64}" class="logo" alt="MarkdownConverter">
|
||||
<h1>MarkdownConverter</h1>
|
||||
<div class="version">Version 3.0.0</div>
|
||||
|
||||
<div class="company">
|
||||
<span>by</span>
|
||||
<img src="${logoBase64}" class="company-logo" alt="ConcreteInfo">
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li>Professional Markdown editing with syntax highlighting</li>
|
||||
<li>Universal file converter (Image, Audio, Video, PDF)</li>
|
||||
<li>Batch conversion for all media types</li>
|
||||
<li>Advanced PDF Editor (merge, split, watermark, encrypt)</li>
|
||||
<li>Export to PDF, DOCX, HTML, LaTeX, EPUB & more</li>
|
||||
<li>23+ beautiful themes including ConcreteInfo theme</li>
|
||||
<li>ASCII Art & Table generators</li>
|
||||
<li>Code file syntax highlighting</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Contact</h3>
|
||||
<p>Email: <a href="mailto:amit.wh@gmail.com">amit.wh@gmail.com</a></p>
|
||||
<p>Website: <a href="https://github.com/amitwh/markdown-converter">GitHub Repository</a></p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>License: MIT</p>
|
||||
<p>© 2024-2025 ConcreteInfo. All rights reserved.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
aboutWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(aboutHTML));
|
||||
aboutWindow.setMenuBarVisibility(false);
|
||||
}
|
||||
|
||||
// Show Dependencies Dialog
|
||||
function showDependenciesDialog() {
|
||||
const depsWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 700,
|
||||
parent: mainWindow,
|
||||
modal: true,
|
||||
resizable: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
},
|
||||
icon: path.join(__dirname, '../assets/icon.png')
|
||||
});
|
||||
|
||||
const depsHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dependencies & Requirements</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #464646;
|
||||
padding: 30px;
|
||||
}
|
||||
h1 { color: #e5461f; font-size: 20px; margin-bottom: 20px; border-bottom: 2px solid #e5461f; padding-bottom: 10px; }
|
||||
h2 { color: #464646; font-size: 16px; margin: 20px 0 10px; }
|
||||
.dep-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.dep-name { font-weight: bold; color: #0d0b09; }
|
||||
.dep-desc { font-size: 13px; color: #666; margin: 5px 0; }
|
||||
.dep-link { color: #e5461f; font-size: 12px; text-decoration: none; }
|
||||
.dep-link:hover { text-decoration: underline; }
|
||||
.required { background: #fff3cd; border-left: 4px solid #ffc107; }
|
||||
.optional { background: #d4edda; border-left: 4px solid #28a745; }
|
||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; margin-left: 10px; }
|
||||
.tag-required { background: #ffc107; color: #000; }
|
||||
.tag-optional { background: #28a745; color: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Dependencies & Requirements</h1>
|
||||
|
||||
<h2>Required Dependencies</h2>
|
||||
|
||||
<div class="dep-card required">
|
||||
<div class="dep-name">Pandoc <span class="tag tag-required">Required</span></div>
|
||||
<div class="dep-desc">Universal document converter. Required for most export formats (PDF, DOCX, LaTeX, EPUB).</div>
|
||||
<a class="dep-link" href="https://pandoc.org/installing.html" target="_blank">https://pandoc.org/installing.html</a>
|
||||
</div>
|
||||
|
||||
<h2>Optional Dependencies (for extended features)</h2>
|
||||
|
||||
<div class="dep-card optional">
|
||||
<div class="dep-name">FFmpeg <span class="tag tag-optional">Optional</span></div>
|
||||
<div class="dep-desc">Required for audio/video conversion and processing.</div>
|
||||
<a class="dep-link" href="https://ffmpeg.org/download.html" target="_blank">https://ffmpeg.org/download.html</a>
|
||||
</div>
|
||||
|
||||
<div class="dep-card optional">
|
||||
<div class="dep-name">ImageMagick <span class="tag tag-optional">Optional</span></div>
|
||||
<div class="dep-desc">Required for image format conversion and processing.</div>
|
||||
<a class="dep-link" href="https://imagemagick.org/script/download.php" target="_blank">https://imagemagick.org/script/download.php</a>
|
||||
</div>
|
||||
|
||||
<div class="dep-card optional">
|
||||
<div class="dep-name">LibreOffice <span class="tag tag-optional">Optional</span></div>
|
||||
<div class="dep-desc">Required for enhanced document conversion (Office formats).</div>
|
||||
<a class="dep-link" href="https://www.libreoffice.org/download/download/" target="_blank">https://www.libreoffice.org/download/download/</a>
|
||||
</div>
|
||||
|
||||
<div class="dep-card optional">
|
||||
<div class="dep-name">MiKTeX / TeX Live <span class="tag tag-optional">Optional</span></div>
|
||||
<div class="dep-desc">Required for PDF export via LaTeX (higher quality PDFs).</div>
|
||||
<a class="dep-link" href="https://miktex.org/download" target="_blank">https://miktex.org/download</a>
|
||||
</div>
|
||||
|
||||
<h2>Bundled Libraries</h2>
|
||||
|
||||
<div class="dep-card">
|
||||
<div class="dep-name">pdf-lib</div>
|
||||
<div class="dep-desc">PDF manipulation library for merge, split, watermark, and encryption features.</div>
|
||||
</div>
|
||||
|
||||
<div class="dep-card">
|
||||
<div class="dep-name">marked</div>
|
||||
<div class="dep-desc">Markdown parser for preview rendering.</div>
|
||||
</div>
|
||||
|
||||
<div class="dep-card">
|
||||
<div class="dep-name">highlight.js</div>
|
||||
<div class="dep-desc">Syntax highlighting for code blocks.</div>
|
||||
</div>
|
||||
|
||||
<div class="dep-card">
|
||||
<div class="dep-name">DOMPurify</div>
|
||||
<div class="dep-desc">HTML sanitization for security.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
depsWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(depsHTML));
|
||||
depsWindow.setMenuBarVisibility(false);
|
||||
}
|
||||
|
||||
// Open PDF File for viewing/editing
|
||||
function openPDFFile() {
|
||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'PDF Files', extensions: ['pdf'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (files && files[0]) {
|
||||
mainWindow.webContents.send('open-pdf-viewer', files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function openFile() {
|
||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
@@ -625,6 +965,20 @@ function openFile() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPdfFile() {
|
||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'PDF Files', extensions: ['pdf'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (files && files[0]) {
|
||||
mainWindow.webContents.send('open-pdf-file', files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAsFile() {
|
||||
const file = dialog.showSaveDialogSync(mainWindow, {
|
||||
defaultExt: '.md',
|
||||
@@ -1133,15 +1487,15 @@ async function exportPDFViaWordTemplate() {
|
||||
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||
await exporter.convert(content, tempDocxPath);
|
||||
|
||||
// Step 2: Convert DOCX to PDF using LibreOffice
|
||||
// Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety)
|
||||
const soffice = process.platform === 'win32'
|
||||
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||
? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
|
||||
: 'soffice';
|
||||
|
||||
const outputDir = path.dirname(result.filePath);
|
||||
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
|
||||
const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath];
|
||||
|
||||
exec(convertCmd, (error, stdout, stderr) => {
|
||||
execFile(soffice, sofficeArgs, (error, stdout, stderr) => {
|
||||
// Clean up temporary DOCX file
|
||||
try {
|
||||
fs.unlinkSync(tempDocxPath);
|
||||
@@ -1190,26 +1544,34 @@ function showPDFEditorDialog(operation) {
|
||||
mainWindow.webContents.send('show-pdf-editor-dialog', operation);
|
||||
}
|
||||
|
||||
// Check if conversion tool is available
|
||||
// Handle PDF editor from toolbar (with optional file path)
|
||||
ipcMain.on('show-pdf-editor-from-toolbar', (event, { operation, filePath }) => {
|
||||
mainWindow.webContents.send('show-pdf-editor-dialog', operation, filePath);
|
||||
});
|
||||
|
||||
// Check if conversion tool is available (using execFile for safety)
|
||||
function checkConverterAvailable(tool) {
|
||||
return new Promise((resolve) => {
|
||||
let command;
|
||||
const isWin = process.platform === 'win32';
|
||||
const locateCmd = isWin ? 'where' : 'which';
|
||||
let toolName;
|
||||
|
||||
switch (tool) {
|
||||
case 'libreoffice':
|
||||
command = process.platform === 'win32' ? 'where soffice' : 'which soffice';
|
||||
toolName = 'soffice';
|
||||
break;
|
||||
case 'imagemagick':
|
||||
command = process.platform === 'win32' ? 'where magick' : 'which convert';
|
||||
toolName = isWin ? 'magick' : 'convert';
|
||||
break;
|
||||
case 'ffmpeg':
|
||||
command = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg';
|
||||
toolName = 'ffmpeg';
|
||||
break;
|
||||
default:
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
exec(command, (error) => {
|
||||
execFile(locateCmd, [toolName], (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
@@ -1230,26 +1592,27 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
|
||||
mainWindow.webContents.send('conversion-status', 'Converting file...');
|
||||
|
||||
const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`);
|
||||
let conversionCmd;
|
||||
let conversionInfo;
|
||||
|
||||
switch (tool) {
|
||||
case 'libreoffice':
|
||||
conversionCmd = convertWithLibreOffice(filePath, toFormat, outputPath);
|
||||
conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath);
|
||||
break;
|
||||
case 'imagemagick':
|
||||
conversionCmd = convertWithImageMagick(filePath, outputPath);
|
||||
conversionInfo = convertWithImageMagick(filePath, outputPath);
|
||||
break;
|
||||
case 'ffmpeg':
|
||||
conversionCmd = convertWithFFmpeg(filePath, outputPath);
|
||||
conversionInfo = convertWithFFmpeg(filePath, outputPath);
|
||||
break;
|
||||
case 'pandoc':
|
||||
conversionCmd = `pandoc "${filePath}" -o "${outputPath}"`;
|
||||
conversionInfo = convertWithPandoc(filePath, outputPath);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown conversion tool: ${tool}`);
|
||||
}
|
||||
|
||||
exec(conversionCmd, (error, stdout, stderr) => {
|
||||
// Use execFile for safety (prevents command injection)
|
||||
execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
mainWindow.webContents.send('conversion-complete', {
|
||||
success: false,
|
||||
@@ -1294,11 +1657,11 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
|
||||
}
|
||||
});
|
||||
|
||||
// LibreOffice conversion command builder
|
||||
// LibreOffice conversion - returns {command, args} for execFile (safer than exec)
|
||||
function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
|
||||
const outputDir = path.dirname(outputPath);
|
||||
const soffice = process.platform === 'win32'
|
||||
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||
? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
|
||||
: 'soffice';
|
||||
|
||||
// LibreOffice conversion format mapping
|
||||
@@ -1321,19 +1684,36 @@ function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
|
||||
|
||||
const format = formatMap[outputFormat] || outputFormat;
|
||||
|
||||
// Use headless mode for conversion
|
||||
return `${soffice} --headless --convert-to ${format} --outdir "${outputDir}" "${inputFile}"`;
|
||||
// Return command and args for execFile
|
||||
return {
|
||||
command: soffice,
|
||||
args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile]
|
||||
};
|
||||
}
|
||||
|
||||
// ImageMagick conversion command builder
|
||||
// ImageMagick conversion - returns {command, args} for execFile
|
||||
function convertWithImageMagick(inputFile, outputPath) {
|
||||
const magick = process.platform === 'win32' ? 'magick' : 'convert';
|
||||
return `${magick} "${inputFile}" "${outputPath}"`;
|
||||
return {
|
||||
command: magick,
|
||||
args: [inputFile, outputPath]
|
||||
};
|
||||
}
|
||||
|
||||
// FFmpeg conversion command builder
|
||||
// FFmpeg conversion - returns {command, args} for execFile
|
||||
function convertWithFFmpeg(inputFile, outputPath) {
|
||||
return `ffmpeg -i "${inputFile}" "${outputPath}" -y`;
|
||||
return {
|
||||
command: 'ffmpeg',
|
||||
args: ['-i', inputFile, outputPath, '-y']
|
||||
};
|
||||
}
|
||||
|
||||
// Pandoc conversion - returns {command, args} for execFile
|
||||
function convertWithPandoc(inputFile, outputPath) {
|
||||
return {
|
||||
command: 'pandoc',
|
||||
args: [inputFile, '-o', outputPath]
|
||||
};
|
||||
}
|
||||
|
||||
function performExportWithOptions(format, options) {
|
||||
@@ -1449,8 +1829,8 @@ function performExportWithOptions(format, options) {
|
||||
pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"';
|
||||
}
|
||||
|
||||
// Try with specified PDF engine
|
||||
exec(pandocCmd, (error) => {
|
||||
// Try with specified PDF engine (using runPandocCmd for safety)
|
||||
runPandocCmd(pandocCmd, (error) => {
|
||||
if (error) {
|
||||
// Try fallback engines if the specified one fails
|
||||
const fallbackEngines = ['pdflatex', 'lualatex'];
|
||||
@@ -1544,7 +1924,8 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
|
||||
}
|
||||
}
|
||||
|
||||
exec(pandocCmd, (error) => {
|
||||
// Use runPandocCmd for safety (prevents command injection)
|
||||
runPandocCmd(pandocCmd, (error) => {
|
||||
if (error) {
|
||||
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
||||
} else {
|
||||
@@ -1562,11 +1943,11 @@ function showExportSuccess(outputFile) {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to export with pandoc (general)
|
||||
// Helper function to export with pandoc (general) - uses runPandocCmd for safety
|
||||
function exportWithPandoc(pandocCmd, outputFile, format) {
|
||||
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
||||
|
||||
exec(pandocCmd, async (error, stdout, stderr) => {
|
||||
runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Pandoc error for ${format}:`, error);
|
||||
console.error(`Pandoc stderr:`, stderr);
|
||||
@@ -1642,14 +2023,14 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to export PDF with pandoc (with fallbacks)
|
||||
// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety
|
||||
function exportWithPandocPDF(pandocCmd, outputFile) {
|
||||
exec(pandocCmd, (error, stdout, stderr) => {
|
||||
runPandocCmd(pandocCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log('XeLaTeX failed, trying PDFLaTeX...');
|
||||
// Fallback to pdflatex
|
||||
const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex');
|
||||
exec(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
|
||||
runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
|
||||
if (fallbackError) {
|
||||
console.log('PDFLaTeX failed, trying Electron PDF...');
|
||||
// Final fallback to Electron PDF
|
||||
@@ -1970,10 +2351,10 @@ function importDocument() {
|
||||
additionalOptions = '--from=json -t markdown';
|
||||
}
|
||||
|
||||
// Convert to markdown using pandoc
|
||||
// Convert to markdown using pandoc (using runPandocCmd for safety)
|
||||
const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`;
|
||||
|
||||
exec(pandocCmd, (error, stdout, stderr) => {
|
||||
runPandocCmd(pandocCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
dialog.showErrorBox('Import Error', `Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`);
|
||||
} else {
|
||||
@@ -2014,7 +2395,7 @@ ipcMain.on('save-current-file', (event, content) => {
|
||||
});
|
||||
|
||||
ipcMain.on('get-theme', (event) => {
|
||||
const theme = store.get('theme', 'github');
|
||||
const theme = store.get('theme', 'atomonelight');
|
||||
event.reply('theme-changed', theme);
|
||||
});
|
||||
|
||||
@@ -2288,15 +2669,15 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
||||
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||
await exporter.convert(content, tempDocxPath);
|
||||
|
||||
// Step 2: Convert DOCX to PDF using LibreOffice
|
||||
// Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety)
|
||||
const soffice = process.platform === 'win32'
|
||||
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||
? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
|
||||
: 'soffice';
|
||||
|
||||
const outputDir = path.dirname(outputFile);
|
||||
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
|
||||
const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath];
|
||||
|
||||
exec(convertCmd, (error, stdout, stderr) => {
|
||||
execFile(soffice, sofficeArgs, (error, stdout, stderr) => {
|
||||
// Clean up temporary DOCX file
|
||||
try {
|
||||
fs.unlinkSync(tempDocxPath);
|
||||
@@ -2447,8 +2828,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
||||
pandocCmd += ` --variable footer="${footerText}"`;
|
||||
}
|
||||
|
||||
// Execute conversion
|
||||
exec(pandocCmd, async (error, stdout, stderr) => {
|
||||
// Execute conversion (using runPandocCmd for safety)
|
||||
runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
|
||||
if (!error) {
|
||||
// Add headers/footers to DOCX if enabled
|
||||
if (format === 'docx' && headerFooterSettings.enabled) {
|
||||
@@ -2563,24 +2944,25 @@ function performCLIConversion(inputPath, format) {
|
||||
|
||||
console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`);
|
||||
|
||||
// Use existing export functions but with CLI output
|
||||
// Use existing export functions but with CLI output (using runPandocCmd for safety)
|
||||
const pandocCommand = buildPandocCommand(content, format, outputPath);
|
||||
|
||||
exec(pandocCommand, (error, stdout, stderr) => {
|
||||
|
||||
runPandocCmd(pandocCommand, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Conversion failed: ${error.message}`);
|
||||
if (stderr) console.error(`Details: ${stderr}`);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Successfully converted to: ${outputPath}`);
|
||||
|
||||
// Show Windows notification
|
||||
|
||||
// Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command)
|
||||
if (process.platform === 'win32') {
|
||||
exec(`powershell -Command "New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${path.join(__dirname, '../assets/icon.png')}'"`, () => {});
|
||||
const iconPath = path.join(__dirname, '../assets/icon.png');
|
||||
execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {});
|
||||
}
|
||||
|
||||
|
||||
app.quit();
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -3329,4 +3711,85 @@ ipcMain.on('select-pdf-folder', (event, inputId) => {
|
||||
if (folder && folder[0]) {
|
||||
event.reply('pdf-folder-selected', { inputId, path: folder[0] });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ASCII Art Generator Window
|
||||
// ============================================
|
||||
let asciiGeneratorWindow = null;
|
||||
|
||||
function openAsciiGenerator() {
|
||||
if (asciiGeneratorWindow) {
|
||||
asciiGeneratorWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
asciiGeneratorWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 700,
|
||||
parent: mainWindow,
|
||||
modal: false,
|
||||
title: 'ASCII Art Generator',
|
||||
icon: path.join(__dirname, '../assets/icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
asciiGeneratorWindow.loadFile(path.join(__dirname, 'ascii-generator.html'));
|
||||
asciiGeneratorWindow.setMenuBarVisibility(false);
|
||||
|
||||
asciiGeneratorWindow.on('closed', () => {
|
||||
asciiGeneratorWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('open-ascii-generator', () => {
|
||||
openAsciiGenerator();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Table Generator Window
|
||||
// ============================================
|
||||
let tableGeneratorWindow = null;
|
||||
|
||||
function openTableGenerator() {
|
||||
if (tableGeneratorWindow) {
|
||||
tableGeneratorWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
tableGeneratorWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 700,
|
||||
parent: mainWindow,
|
||||
modal: false,
|
||||
title: 'Table Generator',
|
||||
icon: path.join(__dirname, '../assets/icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
tableGeneratorWindow.loadFile(path.join(__dirname, 'table-generator.html'));
|
||||
tableGeneratorWindow.setMenuBarVisibility(false);
|
||||
|
||||
tableGeneratorWindow.on('closed', () => {
|
||||
tableGeneratorWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('open-table-generator', () => {
|
||||
openTableGenerator();
|
||||
});
|
||||
|
||||
// IPC Handler to receive generated content from generator windows
|
||||
ipcMain.on('insert-generated-content', (event, content) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('insert-content', content);
|
||||
}
|
||||
});
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Preload Script for PanConverter
|
||||
*
|
||||
* This script creates a secure bridge between the main process and renderer process.
|
||||
* It exposes only specific IPC channels, preventing direct Node.js access in the renderer.
|
||||
*
|
||||
* Security Benefits:
|
||||
* - No direct access to Node.js APIs (fs, path, child_process, etc.)
|
||||
* - All IPC channels are explicitly whitelisted
|
||||
* - Prevents XSS from escalating to full system access
|
||||
*
|
||||
* @version 2.2.0
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Define allowed IPC channels for security
|
||||
const ALLOWED_SEND_CHANNELS = [
|
||||
// File operations
|
||||
'save-file',
|
||||
'save-current-file',
|
||||
'set-current-file',
|
||||
'save-recent-files',
|
||||
'clear-recent-files',
|
||||
'renderer-ready',
|
||||
|
||||
// Theme
|
||||
'get-theme',
|
||||
|
||||
// Print
|
||||
'do-print',
|
||||
|
||||
// Export
|
||||
'export-with-options',
|
||||
'export-spreadsheet',
|
||||
|
||||
// Batch conversion
|
||||
'batch-convert',
|
||||
'select-folder',
|
||||
|
||||
// Universal converter
|
||||
'universal-convert',
|
||||
'universal-convert-batch',
|
||||
|
||||
// Image converter
|
||||
'image-convert',
|
||||
'image-batch-convert',
|
||||
'image-resize',
|
||||
'image-compress',
|
||||
'image-rotate',
|
||||
|
||||
// Audio converter
|
||||
'audio-convert',
|
||||
'audio-batch-convert',
|
||||
'audio-extract',
|
||||
'audio-trim',
|
||||
'audio-merge',
|
||||
|
||||
// Video converter
|
||||
'video-convert',
|
||||
'video-batch-convert',
|
||||
'video-compress',
|
||||
'video-trim',
|
||||
'video-frames',
|
||||
'video-gif',
|
||||
|
||||
// Header/Footer
|
||||
'get-header-footer-settings',
|
||||
'save-header-footer-settings',
|
||||
'browse-header-footer-logo',
|
||||
'save-header-footer-logo',
|
||||
'clear-header-footer-logo',
|
||||
|
||||
// Page settings
|
||||
'get-page-settings',
|
||||
'update-page-settings',
|
||||
|
||||
// Template settings
|
||||
'set-custom-start-page',
|
||||
|
||||
// PDF operations
|
||||
'process-pdf-operation',
|
||||
'get-pdf-page-count',
|
||||
'select-pdf-folder',
|
||||
|
||||
// ASCII generator (separate window)
|
||||
'open-ascii-generator',
|
||||
|
||||
// Table generator (separate window)
|
||||
'open-table-generator',
|
||||
|
||||
// Insert generated content
|
||||
'insert-generated-content'
|
||||
];
|
||||
|
||||
const ALLOWED_RECEIVE_CHANNELS = [
|
||||
// File operations
|
||||
'file-new',
|
||||
'file-opened',
|
||||
'file-save',
|
||||
'get-content-for-save',
|
||||
'get-content-for-spreadsheet',
|
||||
'recent-files-cleared',
|
||||
|
||||
// UI toggles
|
||||
'toggle-preview',
|
||||
'toggle-find',
|
||||
|
||||
// Theme
|
||||
'theme-changed',
|
||||
'theme-data',
|
||||
|
||||
// Edit operations
|
||||
'undo',
|
||||
'redo',
|
||||
|
||||
// Font
|
||||
'adjust-font-size',
|
||||
|
||||
// Print
|
||||
'print-preview',
|
||||
'print-preview-styled',
|
||||
|
||||
// Export dialogs
|
||||
'show-export-dialog',
|
||||
'show-batch-dialog',
|
||||
'show-universal-converter-dialog',
|
||||
'show-table-generator',
|
||||
'show-pdf-editor-dialog',
|
||||
|
||||
// Converter dialogs
|
||||
'show-image-converter',
|
||||
'show-audio-converter',
|
||||
'show-video-converter',
|
||||
|
||||
// PDF viewer
|
||||
'open-pdf-viewer',
|
||||
|
||||
// Conversion status
|
||||
'conversion-status',
|
||||
'conversion-complete',
|
||||
'batch-progress',
|
||||
'image-conversion-complete',
|
||||
'audio-conversion-complete',
|
||||
'video-conversion-complete',
|
||||
|
||||
// Folder selection
|
||||
'folder-selected',
|
||||
'pdf-folder-selected',
|
||||
|
||||
// Header/Footer
|
||||
'header-footer-settings-data',
|
||||
'header-footer-logo-selected',
|
||||
'header-footer-logo-saved',
|
||||
|
||||
// Page settings
|
||||
'page-settings-data',
|
||||
|
||||
// PDF operations
|
||||
'pdf-page-count',
|
||||
'pdf-operation-complete',
|
||||
'pdf-operation-error',
|
||||
|
||||
// ASCII Art Generator
|
||||
'show-ascii-generator-window',
|
||||
'show-ascii-generator',
|
||||
|
||||
// Table Generator
|
||||
'show-table-generator-window',
|
||||
|
||||
// Header/Footer dialog
|
||||
'open-header-footer-dialog',
|
||||
'header-footer-logo-cleared',
|
||||
|
||||
// PDF operation progress
|
||||
'pdf-operation-progress',
|
||||
|
||||
// Insert content from generator windows
|
||||
'insert-content'
|
||||
];
|
||||
|
||||
/**
|
||||
* Secure API exposed to renderer process
|
||||
* Access via window.electronAPI in renderer
|
||||
*/
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// ============================================
|
||||
// SEND METHODS (Renderer -> Main)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Send a message to the main process
|
||||
* @param {string} channel - IPC channel name
|
||||
* @param {any} data - Data to send
|
||||
*/
|
||||
send: (channel, data) => {
|
||||
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
|
||||
ipcRenderer.send(channel, data);
|
||||
} else {
|
||||
console.warn(`[Preload] Blocked send to unauthorized channel: ${channel}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke a main process handler and get a response
|
||||
* @param {string} channel - IPC channel name
|
||||
* @param {any} data - Data to send
|
||||
* @returns {Promise<any>} Response from main process
|
||||
*/
|
||||
invoke: async (channel, data) => {
|
||||
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
|
||||
return await ipcRenderer.invoke(channel, data);
|
||||
} else {
|
||||
console.warn(`[Preload] Blocked invoke to unauthorized channel: ${channel}`);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// RECEIVE METHODS (Main -> Renderer)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Register a listener for messages from main process
|
||||
* @param {string} channel - IPC channel name
|
||||
* @param {Function} callback - Function to call with received data
|
||||
* @returns {Function} Cleanup function to remove listener
|
||||
*/
|
||||
on: (channel, callback) => {
|
||||
if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
|
||||
const subscription = (event, ...args) => callback(...args);
|
||||
ipcRenderer.on(channel, subscription);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
} else {
|
||||
console.warn(`[Preload] Blocked listener for unauthorized channel: ${channel}`);
|
||||
return () => {}; // No-op cleanup
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a one-time listener for messages from main process
|
||||
* @param {string} channel - IPC channel name
|
||||
* @param {Function} callback - Function to call with received data
|
||||
*/
|
||||
once: (channel, callback) => {
|
||||
if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
|
||||
ipcRenderer.once(channel, (event, ...args) => callback(...args));
|
||||
} else {
|
||||
console.warn(`[Preload] Blocked once listener for unauthorized channel: ${channel}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all listeners for a channel
|
||||
* @param {string} channel - IPC channel name
|
||||
*/
|
||||
removeAllListeners: (channel) => {
|
||||
if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// CONVENIENCE METHODS
|
||||
// ============================================
|
||||
|
||||
// File Operations
|
||||
file: {
|
||||
save: (filePath, content) => ipcRenderer.send('save-file', { path: filePath, content }),
|
||||
saveCurrent: (content) => ipcRenderer.send('save-current-file', content),
|
||||
setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath),
|
||||
saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles),
|
||||
clearRecent: () => ipcRenderer.send('clear-recent-files'),
|
||||
rendererReady: () => ipcRenderer.send('renderer-ready')
|
||||
},
|
||||
|
||||
// Theme Operations
|
||||
theme: {
|
||||
get: () => ipcRenderer.send('get-theme')
|
||||
},
|
||||
|
||||
// Print Operations
|
||||
print: {
|
||||
doPrint: (options) => ipcRenderer.send('do-print', options)
|
||||
},
|
||||
|
||||
// Export Operations
|
||||
export: {
|
||||
withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }),
|
||||
spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format })
|
||||
},
|
||||
|
||||
// Batch Conversion
|
||||
batch: {
|
||||
convert: (inputFolder, outputFolder, format, options) => {
|
||||
ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options });
|
||||
},
|
||||
selectFolder: (type) => ipcRenderer.send('select-folder', type)
|
||||
},
|
||||
|
||||
// Universal Converter
|
||||
converter: {
|
||||
convert: (tool, fromFormat, toFormat, filePath) => {
|
||||
ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath });
|
||||
},
|
||||
convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => {
|
||||
ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder });
|
||||
}
|
||||
},
|
||||
|
||||
// Header/Footer Operations
|
||||
headerFooter: {
|
||||
getSettings: () => ipcRenderer.send('get-header-footer-settings'),
|
||||
saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings),
|
||||
browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position),
|
||||
saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }),
|
||||
clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position)
|
||||
},
|
||||
|
||||
// Page Settings
|
||||
page: {
|
||||
getSettings: () => ipcRenderer.send('get-page-settings'),
|
||||
updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings),
|
||||
setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber)
|
||||
},
|
||||
|
||||
// PDF Operations
|
||||
pdf: {
|
||||
processOperation: (data) => ipcRenderer.send('process-pdf-operation', data),
|
||||
getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath),
|
||||
selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId)
|
||||
},
|
||||
|
||||
// Image Converter Operations
|
||||
image: {
|
||||
convert: (data) => ipcRenderer.send('image-convert', data),
|
||||
batchConvert: (data) => ipcRenderer.send('image-batch-convert', data),
|
||||
resize: (data) => ipcRenderer.send('image-resize', data),
|
||||
compress: (data) => ipcRenderer.send('image-compress', data),
|
||||
rotate: (data) => ipcRenderer.send('image-rotate', data)
|
||||
},
|
||||
|
||||
// Audio Converter Operations
|
||||
audio: {
|
||||
convert: (data) => ipcRenderer.send('audio-convert', data),
|
||||
batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data),
|
||||
extract: (data) => ipcRenderer.send('audio-extract', data),
|
||||
trim: (data) => ipcRenderer.send('audio-trim', data),
|
||||
merge: (data) => ipcRenderer.send('audio-merge', data)
|
||||
},
|
||||
|
||||
// Video Converter Operations
|
||||
video: {
|
||||
convert: (data) => ipcRenderer.send('video-convert', data),
|
||||
batchConvert: (data) => ipcRenderer.send('video-batch-convert', data),
|
||||
compress: (data) => ipcRenderer.send('video-compress', data),
|
||||
trim: (data) => ipcRenderer.send('video-trim', data),
|
||||
extractFrames: (data) => ipcRenderer.send('video-frames', data),
|
||||
toGif: (data) => ipcRenderer.send('video-gif', data)
|
||||
},
|
||||
|
||||
// Generator Windows
|
||||
generators: {
|
||||
openAscii: () => ipcRenderer.send('open-ascii-generator'),
|
||||
openTable: () => ipcRenderer.send('open-table-generator')
|
||||
}
|
||||
});
|
||||
|
||||
// Log successful preload initialization
|
||||
console.log('[Preload] Secure IPC bridge initialized');
|
||||
console.log('[Preload] Allowed send channels:', ALLOWED_SEND_CHANNELS.length);
|
||||
console.log('[Preload] Allowed receive channels:', ALLOWED_RECEIVE_CHANNELS.length);
|
||||
+471
-16
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* MarkdownConverter Renderer Process
|
||||
* @version 3.0.0
|
||||
*/
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const marked = require('marked');
|
||||
const DOMPurify = require('dompurify');
|
||||
@@ -10,7 +15,6 @@ marked.setOptions({
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (err) {
|
||||
// Fallback to auto highlighting if language-specific highlighting fails
|
||||
console.warn('Syntax highlighting failed for language:', lang, err.message);
|
||||
}
|
||||
}
|
||||
@@ -308,6 +312,11 @@ class TabManager {
|
||||
if (!tab || !preview) return;
|
||||
|
||||
try {
|
||||
// Check if libraries are available
|
||||
if (!marked || !DOMPurify) {
|
||||
preview.innerHTML = '<p style="color: red; padding: 20px;">Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.</p>';
|
||||
return;
|
||||
}
|
||||
const html = marked.parse(tab.content);
|
||||
const sanitizedHtml = DOMPurify.sanitize(html);
|
||||
preview.innerHTML = sanitizedHtml;
|
||||
@@ -1043,7 +1052,6 @@ class TabManager {
|
||||
this.updateTabBar();
|
||||
|
||||
// Notify main process about current file for exports
|
||||
const { ipcRenderer } = require('electron');
|
||||
ipcRenderer.send('set-current-file', filePath);
|
||||
|
||||
console.log('File opened successfully');
|
||||
@@ -2308,12 +2316,12 @@ ipcRenderer.on('show-table-generator', () => {
|
||||
});
|
||||
|
||||
// Show PDF Editor Dialog
|
||||
ipcRenderer.on('show-pdf-editor-dialog', (event, operation) => {
|
||||
ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => {
|
||||
currentPDFOperation = operation;
|
||||
showPDFEditorDialog(operation);
|
||||
showPDFEditorDialog(operation, openedFilePath);
|
||||
});
|
||||
|
||||
function showPDFEditorDialog(operation) {
|
||||
function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
const dialog = document.getElementById('pdf-editor-dialog');
|
||||
const title = document.getElementById('pdf-editor-title');
|
||||
|
||||
@@ -2329,43 +2337,82 @@ function showPDFEditorDialog(operation) {
|
||||
sectionId = 'pdf-merge-section';
|
||||
titleText = 'Merge PDFs';
|
||||
mergeFilePaths = [];
|
||||
// If we have an opened file, add it as the first file to merge
|
||||
if (openedFilePath) {
|
||||
mergeFilePaths.push(openedFilePath);
|
||||
}
|
||||
updateMergeFilesList();
|
||||
break;
|
||||
case 'split':
|
||||
sectionId = 'pdf-split-section';
|
||||
titleText = 'Split PDF';
|
||||
// Pre-fill input path if we have an opened file
|
||||
if (openedFilePath) {
|
||||
document.getElementById('split-input-path').value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'compress':
|
||||
sectionId = 'pdf-compress-section';
|
||||
titleText = 'Compress PDF';
|
||||
if (openedFilePath) {
|
||||
document.getElementById('compress-input-path').value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'rotate':
|
||||
sectionId = 'pdf-rotate-section';
|
||||
titleText = 'Rotate Pages';
|
||||
if (openedFilePath) {
|
||||
const rotateInput = document.getElementById('rotate-input-path');
|
||||
if (rotateInput) rotateInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
sectionId = 'pdf-delete-section';
|
||||
titleText = 'Delete Pages';
|
||||
if (openedFilePath) {
|
||||
const deleteInput = document.getElementById('delete-input-path');
|
||||
if (deleteInput) deleteInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'reorder':
|
||||
sectionId = 'pdf-reorder-section';
|
||||
titleText = 'Reorder Pages';
|
||||
if (openedFilePath) {
|
||||
const reorderInput = document.getElementById('reorder-input-path');
|
||||
if (reorderInput) reorderInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'watermark':
|
||||
sectionId = 'pdf-watermark-section';
|
||||
titleText = 'Add Watermark';
|
||||
if (openedFilePath) {
|
||||
const watermarkInput = document.getElementById('watermark-input-path');
|
||||
if (watermarkInput) watermarkInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'encrypt':
|
||||
sectionId = 'pdf-encrypt-section';
|
||||
titleText = 'Password Protection';
|
||||
if (openedFilePath) {
|
||||
const encryptInput = document.getElementById('encrypt-input-path');
|
||||
if (encryptInput) encryptInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'decrypt':
|
||||
sectionId = 'pdf-decrypt-section';
|
||||
titleText = 'Remove Password';
|
||||
if (openedFilePath) {
|
||||
const decryptInput = document.getElementById('decrypt-input-path');
|
||||
if (decryptInput) decryptInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'permissions':
|
||||
sectionId = 'pdf-permissions-section';
|
||||
titleText = 'Set Permissions';
|
||||
if (openedFilePath) {
|
||||
const permInput = document.getElementById('permissions-input-path');
|
||||
if (permInput) permInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2545,6 +2592,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Overwrite checkbox handlers - toggle Save As section visibility
|
||||
const overwriteCheckboxes = [
|
||||
{ checkbox: 'compress-overwrite', section: 'compress-saveas-section' },
|
||||
{ checkbox: 'rotate-overwrite', section: 'rotate-saveas-section' },
|
||||
{ checkbox: 'delete-overwrite', section: 'delete-saveas-section' },
|
||||
{ checkbox: 'reorder-overwrite', section: 'reorder-saveas-section' },
|
||||
{ checkbox: 'watermark-overwrite', section: 'watermark-saveas-section' },
|
||||
{ checkbox: 'encrypt-overwrite', section: 'encrypt-saveas-section' },
|
||||
{ checkbox: 'decrypt-overwrite', section: 'decrypt-saveas-section' },
|
||||
{ checkbox: 'permissions-overwrite', section: 'permissions-saveas-section' }
|
||||
];
|
||||
|
||||
overwriteCheckboxes.forEach(item => {
|
||||
const checkbox = document.getElementById(item.checkbox);
|
||||
const section = document.getElementById(item.section);
|
||||
if (checkbox && section) {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
section.classList.add('hidden');
|
||||
} else {
|
||||
section.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load current page order button
|
||||
const loadCurrentOrder = document.getElementById('load-current-order');
|
||||
if (loadCurrentOrder) {
|
||||
@@ -2619,33 +2692,36 @@ function processPDFOperation() {
|
||||
|
||||
case 'compress':
|
||||
operationData.inputPath = document.getElementById('compress-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('compress-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('compress-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('compress-output-path').value.trim();
|
||||
operationData.compressionLevel = document.getElementById('compress-level').value;
|
||||
operationData.compressImages = document.getElementById('compress-images').checked;
|
||||
operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked;
|
||||
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath) {
|
||||
alert('Please select input and output file paths');
|
||||
alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths'));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rotate':
|
||||
operationData.inputPath = document.getElementById('rotate-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('rotate-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('rotate-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('rotate-output-path').value.trim();
|
||||
operationData.pages = document.getElementById('rotate-pages').value.trim();
|
||||
operationData.angle = parseInt(document.getElementById('rotate-angle').value);
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath) {
|
||||
alert('Please select input and output file paths');
|
||||
alert('Please select input file' + (operationData.overwrite ? '' : ' and output file'));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
operationData.inputPath = document.getElementById('delete-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('delete-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('delete-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('delete-output-path').value.trim();
|
||||
operationData.pages = document.getElementById('delete-pages').value.trim();
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) {
|
||||
@@ -2656,7 +2732,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'reorder':
|
||||
operationData.inputPath = document.getElementById('reorder-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('reorder-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('reorder-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('reorder-output-path').value.trim();
|
||||
operationData.newOrder = document.getElementById('reorder-pages').value.trim();
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) {
|
||||
@@ -2667,7 +2744,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'watermark':
|
||||
operationData.inputPath = document.getElementById('watermark-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('watermark-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('watermark-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('watermark-output-path').value.trim();
|
||||
operationData.text = document.getElementById('watermark-text').value.trim();
|
||||
operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value);
|
||||
operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100;
|
||||
@@ -2687,7 +2765,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'encrypt':
|
||||
operationData.inputPath = document.getElementById('encrypt-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('encrypt-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('encrypt-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('encrypt-output-path').value.trim();
|
||||
operationData.userPassword = document.getElementById('encrypt-user-password').value;
|
||||
operationData.ownerPassword = document.getElementById('encrypt-owner-password').value;
|
||||
operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value);
|
||||
@@ -2704,14 +2783,15 @@ function processPDFOperation() {
|
||||
};
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) {
|
||||
alert('Please select files and enter a user password');
|
||||
alert('Please select file and enter a user password');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'decrypt':
|
||||
operationData.inputPath = document.getElementById('decrypt-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('decrypt-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('decrypt-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('decrypt-output-path').value.trim();
|
||||
operationData.password = document.getElementById('decrypt-password').value;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.password) {
|
||||
@@ -2722,7 +2802,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'permissions':
|
||||
operationData.inputPath = document.getElementById('permissions-input-path').value.trim();
|
||||
operationData.outputPath = document.getElementById('permissions-output-path').value.trim();
|
||||
operationData.overwrite = document.getElementById('permissions-overwrite').checked;
|
||||
operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('permissions-output-path').value.trim();
|
||||
operationData.currentPassword = document.getElementById('permissions-current-password').value;
|
||||
operationData.ownerPassword = document.getElementById('permissions-owner-password').value;
|
||||
|
||||
@@ -4057,3 +4138,377 @@ document.addEventListener('click', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Insert Content from Generator Windows
|
||||
// ============================================
|
||||
ipcRenderer.on('insert-content', (event, content) => {
|
||||
if (tabManager) {
|
||||
const activeTabId = tabManager.activeTabId;
|
||||
const editor = document.getElementById(`editor-${activeTabId}`);
|
||||
if (editor) {
|
||||
const cursorPos = editor.selectionStart;
|
||||
const textBefore = editor.value.substring(0, cursorPos);
|
||||
const textAfter = editor.value.substring(cursorPos);
|
||||
|
||||
editor.value = textBefore + content + textAfter;
|
||||
editor.focus();
|
||||
|
||||
const newPos = cursorPos + content.length;
|
||||
editor.setSelectionRange(newPos, newPos);
|
||||
|
||||
// Update state and preview
|
||||
tabManager.tabs.get(activeTabId).content = editor.value;
|
||||
tabManager.tabs.get(activeTabId).modified = true;
|
||||
tabManager.updatePreview(activeTabId);
|
||||
tabManager.updateTabTitle(activeTabId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// PDF VIEWER FUNCTIONALITY
|
||||
// ============================================
|
||||
|
||||
let pdfDoc = null;
|
||||
let pdfCurrentPage = 1;
|
||||
let pdfZoomLevel = 1.0;
|
||||
let pdfRotation = 0;
|
||||
let pdfFilePath = null;
|
||||
let isPdfViewerActive = false; // Track if PDF viewer is currently shown
|
||||
|
||||
// Initialize PDF.js
|
||||
const pdfjsLib = require('pdfjs-dist');
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve('pdfjs-dist/build/pdf.worker.js');
|
||||
|
||||
// Open PDF file
|
||||
async function openPdfFile(filePath) {
|
||||
// Prevent multiple simultaneous PDF loads
|
||||
if (isPdfViewerActive && pdfFilePath === filePath) {
|
||||
console.log('PDF already open:', filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any existing PDF first
|
||||
if (pdfDoc) {
|
||||
try {
|
||||
await pdfDoc.destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying previous PDF:', e);
|
||||
}
|
||||
pdfDoc = null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
document.getElementById('status-text').textContent = 'Loading PDF...';
|
||||
|
||||
const loadingTask = pdfjsLib.getDocument(filePath);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
pdfFilePath = filePath;
|
||||
pdfCurrentPage = 1;
|
||||
pdfZoomLevel = 1.0;
|
||||
pdfRotation = 0;
|
||||
isPdfViewerActive = true;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('pdf-total-pages').textContent = pdfDoc.numPages;
|
||||
document.getElementById('pdf-page-input').value = 1;
|
||||
document.getElementById('pdf-page-input').max = pdfDoc.numPages;
|
||||
document.getElementById('pdf-filename').textContent = require('path').basename(filePath);
|
||||
document.getElementById('pdf-zoom-level').textContent = '100%';
|
||||
|
||||
// Hide markdown toolbar, tabs, show PDF viewer
|
||||
document.querySelector('.toolbar').classList.add('hidden');
|
||||
document.getElementById('tab-bar').classList.add('hidden');
|
||||
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.add('hidden'));
|
||||
document.getElementById('pdf-viewer-container').classList.remove('hidden');
|
||||
|
||||
// Render first page
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
|
||||
// Update status
|
||||
document.getElementById('status-text').textContent = `PDF: ${require('path').basename(filePath)} (${pdfDoc.numPages} pages)`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
isPdfViewerActive = false;
|
||||
pdfDoc = null;
|
||||
pdfFilePath = null;
|
||||
document.getElementById('status-text').textContent = 'Error loading PDF';
|
||||
alert('Error loading PDF: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Render PDF page
|
||||
async function renderPdfPage(pageNum) {
|
||||
if (!pdfDoc) return;
|
||||
|
||||
try {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const canvas = document.getElementById('pdf-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Calculate viewport with zoom and rotation
|
||||
const viewport = page.getViewport({ scale: pdfZoomLevel, rotation: pdfRotation });
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: ctx,
|
||||
viewport: viewport
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
// Update page input
|
||||
document.getElementById('pdf-page-input').value = pageNum;
|
||||
} catch (error) {
|
||||
console.error('Error rendering PDF page:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// PDF navigation handlers
|
||||
document.getElementById('pdf-prev-page')?.addEventListener('click', async () => {
|
||||
if (pdfCurrentPage > 1) {
|
||||
pdfCurrentPage--;
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pdf-next-page')?.addEventListener('click', async () => {
|
||||
if (pdfDoc && pdfCurrentPage < pdfDoc.numPages) {
|
||||
pdfCurrentPage++;
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pdf-page-input')?.addEventListener('change', async (e) => {
|
||||
const pageNum = parseInt(e.target.value);
|
||||
if (pdfDoc && pageNum >= 1 && pageNum <= pdfDoc.numPages) {
|
||||
pdfCurrentPage = pageNum;
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
}
|
||||
});
|
||||
|
||||
// PDF zoom handlers
|
||||
document.getElementById('pdf-zoom-out')?.addEventListener('click', async () => {
|
||||
if (pdfZoomLevel > 0.25) {
|
||||
pdfZoomLevel -= 0.25;
|
||||
document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pdf-zoom-in')?.addEventListener('click', async () => {
|
||||
if (pdfZoomLevel < 4.0) {
|
||||
pdfZoomLevel += 0.25;
|
||||
document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pdf-fit-width')?.addEventListener('click', async () => {
|
||||
if (!pdfDoc) return;
|
||||
const page = await pdfDoc.getPage(pdfCurrentPage);
|
||||
const viewport = page.getViewport({ scale: 1, rotation: pdfRotation });
|
||||
const containerWidth = document.getElementById('pdf-viewer').clientWidth - 40;
|
||||
pdfZoomLevel = containerWidth / viewport.width;
|
||||
document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
});
|
||||
|
||||
document.getElementById('pdf-fit-page')?.addEventListener('click', async () => {
|
||||
if (!pdfDoc) return;
|
||||
const page = await pdfDoc.getPage(pdfCurrentPage);
|
||||
const viewport = page.getViewport({ scale: 1, rotation: pdfRotation });
|
||||
const container = document.getElementById('pdf-viewer');
|
||||
const containerWidth = container.clientWidth - 40;
|
||||
const containerHeight = container.clientHeight - 40;
|
||||
const scaleX = containerWidth / viewport.width;
|
||||
const scaleY = containerHeight / viewport.height;
|
||||
pdfZoomLevel = Math.min(scaleX, scaleY);
|
||||
document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
});
|
||||
|
||||
// PDF rotation handlers
|
||||
document.getElementById('pdf-rotate-left')?.addEventListener('click', async () => {
|
||||
pdfRotation = (pdfRotation - 90 + 360) % 360;
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
});
|
||||
|
||||
document.getElementById('pdf-rotate-right')?.addEventListener('click', async () => {
|
||||
pdfRotation = (pdfRotation + 90) % 360;
|
||||
await renderPdfPage(pdfCurrentPage);
|
||||
});
|
||||
|
||||
// Close PDF viewer
|
||||
document.getElementById('pdf-close')?.addEventListener('click', () => {
|
||||
closePdfViewer();
|
||||
});
|
||||
|
||||
async function closePdfViewer() {
|
||||
// Destroy PDF document to free memory
|
||||
if (pdfDoc) {
|
||||
try {
|
||||
await pdfDoc.destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying PDF:', e);
|
||||
}
|
||||
}
|
||||
|
||||
pdfDoc = null;
|
||||
pdfFilePath = null;
|
||||
isPdfViewerActive = false;
|
||||
|
||||
// Hide PDF viewer
|
||||
document.getElementById('pdf-viewer-container').classList.add('hidden');
|
||||
|
||||
// Show markdown tabs, tab bar, and toolbar
|
||||
document.getElementById('tab-bar').classList.remove('hidden');
|
||||
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('hidden'));
|
||||
document.querySelector('.toolbar').classList.remove('hidden');
|
||||
|
||||
// Activate the correct markdown tab
|
||||
if (tabManager) {
|
||||
const activeTab = document.querySelector(`.tab-content[data-tab-id="${tabManager.activeTabId}"]`);
|
||||
if (activeTab) activeTab.classList.add('active');
|
||||
// Refresh the active tab
|
||||
tabManager.updatePreview(tabManager.activeTabId);
|
||||
}
|
||||
|
||||
document.getElementById('status-text').textContent = 'Ready';
|
||||
}
|
||||
|
||||
// Handle PDF file open from main process
|
||||
ipcRenderer.on('open-pdf-file', (event, filePath) => {
|
||||
openPdfFile(filePath);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// PDF EDITOR TOOLBAR BUTTONS
|
||||
// ============================================
|
||||
|
||||
// Helper function to trigger PDF editor dialog
|
||||
function openPdfEditorDialog(operation) {
|
||||
// Use the opened PDF file if available, otherwise prompt
|
||||
if (pdfFilePath) {
|
||||
ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: pdfFilePath });
|
||||
} else {
|
||||
ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: null });
|
||||
}
|
||||
}
|
||||
|
||||
// PDF Editor toolbar button handlers
|
||||
document.getElementById('pdf-tb-merge')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('merge');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-split')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('split');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-compress')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('compress');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-rotate')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('rotate');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-delete')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('delete');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-reorder')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('reorder');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-watermark')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('watermark');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-encrypt')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('encrypt');
|
||||
});
|
||||
|
||||
document.getElementById('pdf-tb-decrypt')?.addEventListener('click', () => {
|
||||
openPdfEditorDialog('decrypt');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// DYNAMIC PANE RESIZER
|
||||
// ============================================
|
||||
|
||||
function initPaneResizer(tabId) {
|
||||
const resizer = document.getElementById(`pane-resizer-${tabId}`);
|
||||
const editorPane = document.getElementById(`editor-pane-${tabId}`);
|
||||
const previewPane = document.getElementById(`preview-pane-${tabId}`);
|
||||
const tabContent = document.getElementById(`tab-content-${tabId}`);
|
||||
|
||||
if (!resizer || !editorPane || !previewPane) return;
|
||||
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startEditorWidth = 0;
|
||||
|
||||
resizer.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
startX = e.clientX;
|
||||
startEditorWidth = editorPane.offsetWidth;
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const containerWidth = tabContent.offsetWidth;
|
||||
const deltaX = e.clientX - startX;
|
||||
let newEditorWidth = startEditorWidth + deltaX;
|
||||
|
||||
// Minimum widths
|
||||
const minWidth = 150;
|
||||
const maxWidth = containerWidth - minWidth - 6; // 6px for resizer
|
||||
|
||||
newEditorWidth = Math.max(minWidth, Math.min(maxWidth, newEditorWidth));
|
||||
|
||||
const editorPercent = (newEditorWidth / containerWidth) * 100;
|
||||
const previewPercent = 100 - editorPercent - 1; // 1% for resizer
|
||||
|
||||
editorPane.style.flex = `0 0 ${editorPercent}%`;
|
||||
previewPane.style.flex = `0 0 ${previewPercent}%`;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize resizer for first tab
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initPaneResizer(1);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// IMAGE/AUDIO/VIDEO CONVERTER HANDLERS
|
||||
// These require external tools (ImageMagick, FFmpeg)
|
||||
// ============================================
|
||||
|
||||
ipcRenderer.on('show-image-tool', (event, tool) => {
|
||||
alert(`Image ${tool} tool requires ImageMagick to be installed.\n\nPlease install ImageMagick from: https://imagemagick.org/\n\nThis feature will be available in a future update with built-in support.`);
|
||||
});
|
||||
|
||||
ipcRenderer.on('show-audio-tool', (event, tool) => {
|
||||
alert(`Audio ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`);
|
||||
});
|
||||
|
||||
ipcRenderer.on('show-video-tool', (event, tool) => {
|
||||
alert(`Video ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,969 @@
|
||||
/**
|
||||
* ConcreteInfo Theme for MarkdownConverter
|
||||
* Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09
|
||||
* Version: 3.0.0
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
CSS Variables - ConcreteInfo Theme
|
||||
============================================ */
|
||||
:root {
|
||||
/* Primary Colors from Logo Palette */
|
||||
--ci-dark-gray: #464646;
|
||||
--ci-medium-gray: #9a9696;
|
||||
--ci-accent: #e5461f;
|
||||
--ci-light-gray: #e3e3e3;
|
||||
--ci-black: #0d0b09;
|
||||
|
||||
/* Extended Palette */
|
||||
--ci-accent-hover: #c93a18;
|
||||
--ci-accent-light: rgba(229, 70, 31, 0.1);
|
||||
--ci-white: #ffffff;
|
||||
--ci-bg: #f5f5f5;
|
||||
--ci-border: #d0d0d0;
|
||||
|
||||
/* Semantic Colors */
|
||||
--ci-success: #28a745;
|
||||
--ci-warning: #ffc107;
|
||||
--ci-danger: #dc3545;
|
||||
--ci-info: #17a2b8;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 250ms ease;
|
||||
--transition-slow: 350ms ease;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ConcreteInfo Theme Application
|
||||
============================================ */
|
||||
body.theme-concreteinfo {
|
||||
background: var(--ci-bg);
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .toolbar {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .toolbar button {
|
||||
color: var(--ci-white);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .toolbar button:hover {
|
||||
background: var(--ci-accent);
|
||||
border-color: var(--ci-accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab-bar {
|
||||
background: var(--ci-dark-gray);
|
||||
border-bottom: 2px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab {
|
||||
background: var(--ci-medium-gray);
|
||||
color: var(--ci-white);
|
||||
border: none;
|
||||
margin: 2px;
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab:hover {
|
||||
background: var(--ci-accent-light);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab.active {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .editor-textarea {
|
||||
background: var(--ci-white);
|
||||
color: var(--ci-black);
|
||||
border: 1px solid var(--ci-border);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .preview-content {
|
||||
background: var(--ci-white);
|
||||
color: var(--ci-dark-gray);
|
||||
border-left: 3px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .status-bar {
|
||||
background: var(--ci-dark-gray);
|
||||
color: var(--ci-light-gray);
|
||||
border-top: 2px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Modern Modal Styles
|
||||
============================================ */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(13, 11, 9, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--ci-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal-content {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
color: var(--ci-white);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ci-white);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--ci-accent);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--spacing-lg);
|
||||
overflow-y: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--ci-light-gray);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
border-top: 1px solid var(--ci-border);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Button Styles
|
||||
============================================ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--ci-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ci-medium-gray);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--ci-accent);
|
||||
color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--ci-light-gray);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Form Controls
|
||||
============================================ */
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-weight: 500;
|
||||
color: var(--ci-dark-gray);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
border: 2px solid var(--ci-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--ci-white);
|
||||
color: var(--ci-dark-gray);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--ci-accent);
|
||||
box-shadow: 0 0 0 3px var(--ci-accent-light);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--ci-medium-gray);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
App Header with Logo
|
||||
============================================ */
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e1e4e8;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.app-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
height: 22px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
color: #24292e;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
color: #6a737d;
|
||||
font-size: 0.7rem;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
/* Dark theme header adjustments */
|
||||
body.theme-dark .app-header,
|
||||
body.theme-dracula .app-header,
|
||||
body.theme-onedark .app-header,
|
||||
body.theme-tokyonight .app-header,
|
||||
body.theme-monokai .app-header,
|
||||
body.theme-cobalt2 .app-header,
|
||||
body.theme-gruvbox-dark .app-header,
|
||||
body.theme-ayu-dark .app-header,
|
||||
body.theme-ayu-mirage .app-header,
|
||||
body.theme-palenight .app-header,
|
||||
body.theme-oceanic-next .app-header,
|
||||
body.theme-nord .app-header,
|
||||
body.theme-concrete-dark .app-header {
|
||||
background: #1f2937;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
body.theme-dark .app-title,
|
||||
body.theme-dracula .app-title,
|
||||
body.theme-onedark .app-title,
|
||||
body.theme-tokyonight .app-title,
|
||||
body.theme-monokai .app-title,
|
||||
body.theme-cobalt2 .app-title,
|
||||
body.theme-gruvbox-dark .app-title,
|
||||
body.theme-ayu-dark .app-title,
|
||||
body.theme-ayu-mirage .app-title,
|
||||
body.theme-palenight .app-title,
|
||||
body.theme-oceanic-next .app-title,
|
||||
body.theme-nord .app-title,
|
||||
body.theme-concrete-dark .app-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
body.theme-dark .app-version,
|
||||
body.theme-dracula .app-version,
|
||||
body.theme-onedark .app-version,
|
||||
body.theme-tokyonight .app-version,
|
||||
body.theme-monokai .app-version,
|
||||
body.theme-cobalt2 .app-version,
|
||||
body.theme-gruvbox-dark .app-version,
|
||||
body.theme-ayu-dark .app-version,
|
||||
body.theme-ayu-mirage .app-version,
|
||||
body.theme-palenight .app-version,
|
||||
body.theme-oceanic-next .app-version,
|
||||
body.theme-nord .app-version,
|
||||
body.theme-concrete-dark .app-version {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
body.theme-dark .app-logo,
|
||||
body.theme-dracula .app-logo,
|
||||
body.theme-onedark .app-logo,
|
||||
body.theme-tokyonight .app-logo,
|
||||
body.theme-monokai .app-logo,
|
||||
body.theme-cobalt2 .app-logo,
|
||||
body.theme-gruvbox-dark .app-logo,
|
||||
body.theme-ayu-dark .app-logo,
|
||||
body.theme-ayu-mirage .app-logo,
|
||||
body.theme-palenight .app-logo,
|
||||
body.theme-oceanic-next .app-logo,
|
||||
body.theme-nord .app-logo,
|
||||
body.theme-concrete-dark .app-logo {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Converter Cards
|
||||
============================================ */
|
||||
.converter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.converter-card {
|
||||
background: var(--ci-white);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
border: 2px solid var(--ci-border);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.converter-card:hover {
|
||||
border-color: var(--ci-accent);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.converter-card-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.converter-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--ci-dark-gray);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.converter-card-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--ci-medium-gray);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Progress Bar
|
||||
============================================ */
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--ci-light-gray);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--ci-accent) 0%, var(--ci-accent-hover) 100%);
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--transition-normal);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
File Drop Zone
|
||||
============================================ */
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--ci-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drop-zone:hover,
|
||||
.drop-zone.drag-over {
|
||||
border-color: var(--ci-accent);
|
||||
background: var(--ci-accent-light);
|
||||
}
|
||||
|
||||
.drop-zone-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--ci-medium-gray);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.drop-zone-text {
|
||||
color: var(--ci-dark-gray);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.drop-zone-hint {
|
||||
color: var(--ci-medium-gray);
|
||||
font-size: 0.875rem;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Syntax Highlighting for Editor
|
||||
============================================ */
|
||||
.editor-syntax .md-heading {
|
||||
color: var(--ci-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-syntax .md-bold {
|
||||
color: var(--ci-dark-gray);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-syntax .md-italic {
|
||||
color: var(--ci-dark-gray);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-syntax .md-code {
|
||||
background: var(--ci-light-gray);
|
||||
color: var(--ci-accent);
|
||||
font-family: var(--font-mono);
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.editor-syntax .md-link {
|
||||
color: var(--ci-info);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-syntax .md-list {
|
||||
color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.editor-syntax .md-blockquote {
|
||||
color: var(--ci-medium-gray);
|
||||
border-left: 3px solid var(--ci-accent);
|
||||
padding-left: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ASCII Art Generator Styles
|
||||
============================================ */
|
||||
.ascii-preview {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
background: var(--ci-black);
|
||||
color: #00ff00;
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ascii-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Table Generator Styles
|
||||
============================================ */
|
||||
.table-generator-grid {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
background: var(--ci-border);
|
||||
border: 1px solid var(--ci-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-generator-cell {
|
||||
background: var(--ci-white);
|
||||
padding: var(--spacing-sm);
|
||||
min-width: 80px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.table-generator-cell input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.table-generator-cell.header {
|
||||
background: var(--ci-light-gray);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PDF Viewer/Editor Styles
|
||||
============================================ */
|
||||
.pdf-viewer {
|
||||
background: var(--ci-dark-gray);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
background: var(--ci-white);
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--ci-light-gray);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Dark Mode for ConcreteInfo Theme
|
||||
============================================ */
|
||||
body.theme-concreteinfo-dark {
|
||||
--ci-bg: #1a1a1a;
|
||||
--ci-white: #2a2a2a;
|
||||
--ci-light-gray: #3a3a3a;
|
||||
--ci-border: #4a4a4a;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .editor-textarea,
|
||||
body.theme-concreteinfo-dark .preview-content {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .modal-content {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .modal-body {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .modal-footer {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Animations
|
||||
============================================ */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp var(--transition-normal);
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Responsive Design
|
||||
============================================ */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.converter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Concrete Dark Theme
|
||||
============================================ */
|
||||
body.theme-concrete-dark {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .toolbar {
|
||||
background: linear-gradient(135deg, #0d0b09 0%, #1a1a1a 100%);
|
||||
border-bottom: 3px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .toolbar button {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .toolbar button:hover {
|
||||
background: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .tab-bar {
|
||||
background: #0d0b09;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .tab {
|
||||
background: #2a2a2a;
|
||||
color: #e3e3e3;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .tab.active {
|
||||
background: #464646;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .editor-textarea,
|
||||
body.theme-concrete-dark .editor-pane {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .preview-content {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .status-bar {
|
||||
background: #0d0b09;
|
||||
color: #9a9696;
|
||||
border-top: 1px solid #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .export-dialog-content,
|
||||
body.theme-concrete-dark .batch-dialog-content {
|
||||
background: #2a2a2a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .export-dialog-header,
|
||||
body.theme-concrete-dark .batch-dialog-header {
|
||||
background: #1a1a1a;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .export-dialog-footer,
|
||||
body.theme-concrete-dark .batch-dialog-footer {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark input,
|
||||
body.theme-concrete-dark select,
|
||||
body.theme-concrete-dark textarea {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
border-color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark input:focus,
|
||||
body.theme-concrete-dark select:focus,
|
||||
body.theme-concrete-dark textarea:focus {
|
||||
border-color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .line-numbers {
|
||||
background: #0d0b09;
|
||||
color: #9a9696;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Concrete Light Theme
|
||||
============================================ */
|
||||
body.theme-concrete-light {
|
||||
background: #f5f5f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .toolbar {
|
||||
background: #ffffff;
|
||||
border-bottom: 3px solid #e5461f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body.theme-concrete-light .toolbar button {
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .toolbar button:hover {
|
||||
background: #e5461f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .tab-bar {
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .tab {
|
||||
background: #f5f5f5;
|
||||
color: #464646;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .tab.active {
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .editor-textarea,
|
||||
body.theme-concrete-light .editor-pane {
|
||||
background: #ffffff;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .preview-content {
|
||||
background: #ffffff;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .status-bar {
|
||||
background: #ffffff;
|
||||
color: #9a9696;
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .export-dialog-content,
|
||||
body.theme-concrete-light .batch-dialog-content {
|
||||
background: #ffffff;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .export-dialog-header,
|
||||
body.theme-concrete-light .batch-dialog-header {
|
||||
background: #f5f5f5;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .line-numbers {
|
||||
background: #f5f5f5;
|
||||
color: #9a9696;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Concrete Warm Theme
|
||||
============================================ */
|
||||
body.theme-concrete-warm {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .toolbar {
|
||||
background: linear-gradient(135deg, #464646 0%, #5a5a5a 100%);
|
||||
border-bottom: 3px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .toolbar button {
|
||||
color: #faf8f5;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .toolbar button:hover {
|
||||
background: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .tab-bar {
|
||||
background: #464646;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .tab {
|
||||
background: #6a6a6a;
|
||||
color: #faf8f5;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .tab.active {
|
||||
background: #faf8f5;
|
||||
color: #e5461f;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .editor-textarea,
|
||||
body.theme-concrete-warm .editor-pane {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .preview-content {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .status-bar {
|
||||
background: #f0ebe4;
|
||||
color: #9a9696;
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .export-dialog-content,
|
||||
body.theme-concrete-warm .batch-dialog-content {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .export-dialog-header,
|
||||
body.theme-concrete-warm .batch-dialog-header {
|
||||
background: #f0ebe4;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm h1,
|
||||
body.theme-concrete-warm h2,
|
||||
body.theme-concrete-warm h3,
|
||||
body.theme-concrete-warm h4,
|
||||
body.theme-concrete-warm h5,
|
||||
body.theme-concrete-warm h6 {
|
||||
color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm a {
|
||||
color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .line-numbers {
|
||||
background: #f0ebe4;
|
||||
color: #9a9696;
|
||||
}
|
||||
+1375
-87
File diff suppressed because it is too large
Load Diff
+296
-20
@@ -1031,52 +1031,82 @@ body.theme-github .status-bar {
|
||||
|
||||
.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: 90vh;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
width: 85%;
|
||||
max-width: 450px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.export-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.export-dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.export-dialog-body {
|
||||
padding: 20px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.export-section label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.export-section label.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.export-section label.checkbox-inline input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* PDF Editor buttons */
|
||||
#add-merge-file {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
background: #4a90d9;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#add-merge-file:hover {
|
||||
background: #357abd;
|
||||
}
|
||||
|
||||
.export-section select,
|
||||
.export-section input[type="text"],
|
||||
.export-section input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.export-section select:focus,
|
||||
@@ -1280,9 +1310,10 @@ body.theme-dark #add-metadata-field {
|
||||
|
||||
/* PDF Editor Specific Styles */
|
||||
.pdf-editor-content {
|
||||
max-width: 600px !important;
|
||||
max-height: 85vh;
|
||||
max-width: 450px !important;
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pdf-editor-body {
|
||||
@@ -1418,12 +1449,13 @@ body.theme-dark #export-dialog-close:hover {
|
||||
|
||||
.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;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
width: 85%;
|
||||
max-width: 450px;
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.batch-dialog-header {
|
||||
@@ -3152,6 +3184,250 @@ body.theme-concrete-warm .status-bar {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Sepia Theme - Warm reading theme
|
||||
======================================== */
|
||||
body.theme-sepia {
|
||||
background: #f4ecd8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .tab-bar {
|
||||
background: #e8dcc8;
|
||||
border-bottom-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .tab {
|
||||
background: #e8dcc8;
|
||||
border-color: #d4c4a8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .tab.active {
|
||||
background: #f4ecd8;
|
||||
border-bottom-color: #8b7355;
|
||||
}
|
||||
|
||||
body.theme-sepia .toolbar {
|
||||
background: #e8dcc8;
|
||||
border-bottom-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .toolbar button {
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .toolbar button:hover {
|
||||
background: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .editor-textarea {
|
||||
background: #f4ecd8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .pane:last-child {
|
||||
background: #f4ecd8;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content {
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content h1,
|
||||
body.theme-sepia .preview-content h2 {
|
||||
color: #3d2914;
|
||||
border-bottom-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content code {
|
||||
background: #e8dcc8;
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content pre {
|
||||
background: #e8dcc8;
|
||||
border-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content a {
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content blockquote {
|
||||
border-left-color: #8b7355;
|
||||
background: #e8dcc8;
|
||||
}
|
||||
|
||||
body.theme-sepia .status-bar {
|
||||
background: #e8dcc8;
|
||||
border-top-color: #d4c4a8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Paper Theme - Clean white paper
|
||||
======================================== */
|
||||
body.theme-paper {
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .tab-bar {
|
||||
background: #f8f8f8;
|
||||
border-bottom-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .tab {
|
||||
background: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.theme-paper .tab.active {
|
||||
background: #ffffff;
|
||||
border-bottom-color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .toolbar {
|
||||
background: #f8f8f8;
|
||||
border-bottom-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .toolbar button {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.theme-paper .toolbar button:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
body.theme-paper .editor-textarea {
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .pane:last-child {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content h1,
|
||||
body.theme-paper .preview-content h2 {
|
||||
color: #000;
|
||||
border-bottom-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content code {
|
||||
background: #f5f5f5;
|
||||
color: #c41a16;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content pre {
|
||||
background: #f5f5f5;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content a {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content blockquote {
|
||||
border-left-color: #ddd;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
body.theme-paper .status-bar {
|
||||
background: #f8f8f8;
|
||||
border-top-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Rose Pine Dawn - Warm rose-tinted light
|
||||
======================================== */
|
||||
body.theme-rosepine-dawn {
|
||||
background: #faf4ed;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .tab-bar {
|
||||
background: #f2e9e1;
|
||||
border-bottom-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .tab {
|
||||
background: #f2e9e1;
|
||||
border-color: #dfdad9;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .tab.active {
|
||||
background: #faf4ed;
|
||||
border-bottom-color: #907aa9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .toolbar {
|
||||
background: #f2e9e1;
|
||||
border-bottom-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .toolbar button {
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .toolbar button:hover {
|
||||
background: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .editor-textarea {
|
||||
background: #faf4ed;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .pane:last-child {
|
||||
background: #faf4ed;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content {
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content h1,
|
||||
body.theme-rosepine-dawn .preview-content h2 {
|
||||
color: #286983;
|
||||
border-bottom-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content code {
|
||||
background: #f2e9e1;
|
||||
color: #b4637a;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content pre {
|
||||
background: #f2e9e1;
|
||||
border-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content a {
|
||||
color: #56949f;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content blockquote {
|
||||
border-left-color: #907aa9;
|
||||
background: #f2e9e1;
|
||||
color: #797593;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .status-bar {
|
||||
background: #f2e9e1;
|
||||
border-top-color: #dfdad9;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
/* Hide UI elements for clean print output */
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Table Generator - MarkdownConverter</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ci-dark-gray: #464646;
|
||||
--ci-medium-gray: #9a9696;
|
||||
--ci-accent: #e5461f;
|
||||
--ci-light-gray: #e3e3e3;
|
||||
--ci-black: #0d0b09;
|
||||
--ci-white: #ffffff;
|
||||
--ci-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--ci-bg);
|
||||
color: var(--ci-dark-gray);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--ci-white);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--ci-white);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ci-dark-gray);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--ci-medium-gray);
|
||||
}
|
||||
|
||||
.form-input, .form-select {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.editable-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.editable-table th,
|
||||
.editable-table td {
|
||||
border: 1px solid var(--ci-light-gray);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editable-table th {
|
||||
background: var(--ci-dark-gray);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.editable-table input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.editable-table input:focus {
|
||||
outline: 2px solid var(--ci-accent);
|
||||
outline-offset: -2px;
|
||||
background: rgba(229, 70, 31, 0.05);
|
||||
}
|
||||
|
||||
.editable-table th input {
|
||||
color: var(--ci-white);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editable-table th input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
background: var(--ci-black);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-height: 250px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #00ff00;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: var(--ci-white);
|
||||
border-top: 1px solid var(--ci-light-gray);
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #c93a18;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ci-light-gray);
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--ci-accent);
|
||||
color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.quick-templates {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-chip {
|
||||
padding: 6px 12px;
|
||||
background: var(--ci-light-gray);
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.template-chip:hover {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Table Generator</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<div class="section-title">Table Settings</div>
|
||||
<div class="controls-row">
|
||||
<div class="control-group">
|
||||
<span class="control-label">Rows</span>
|
||||
<input type="number" id="rows" class="form-input" min="1" max="50" value="4" style="width: 80px;">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<span class="control-label">Columns</span>
|
||||
<input type="number" id="cols" class="form-input" min="1" max="20" value="4" style="width: 80px;">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<span class="control-label">Alignment</span>
|
||||
<select id="alignment" class="form-select">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group" style="justify-content: flex-end;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="has-header" checked>
|
||||
Include Header Row
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-templates">
|
||||
<button class="template-chip" data-rows="3" data-cols="2">2-Column (3 rows)</button>
|
||||
<button class="template-chip" data-rows="5" data-cols="3">3-Column (5 rows)</button>
|
||||
<button class="template-chip" data-rows="4" data-cols="4">4x4 Grid</button>
|
||||
<button class="template-chip" data-rows="10" data-cols="3">Data Table (10 rows)</button>
|
||||
<button class="template-chip" data-rows="2" data-cols="5">Wide Table</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Edit Table Content</div>
|
||||
<div class="table-container">
|
||||
<table class="editable-table" id="editable-table">
|
||||
<!-- Generated dynamically -->
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Markdown Preview</div>
|
||||
<div class="preview-container">
|
||||
<div id="preview" class="preview-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="footer-left">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-clear">Clear All</button>
|
||||
<button class="btn btn-outline btn-sm" id="btn-fill-sample">Fill Sample Data</button>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<button class="btn btn-secondary" id="btn-copy">Copy Markdown</button>
|
||||
<button class="btn btn-primary" id="btn-insert">Insert to Editor</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentRows = 4;
|
||||
let currentCols = 4;
|
||||
let hasHeader = true;
|
||||
let alignment = 'left';
|
||||
|
||||
// Generate the editable table
|
||||
function generateEditableTable() {
|
||||
const table = document.getElementById('editable-table');
|
||||
hasHeader = document.getElementById('has-header').checked;
|
||||
alignment = document.getElementById('alignment').value;
|
||||
|
||||
let html = '';
|
||||
|
||||
for (let i = 0; i < currentRows; i++) {
|
||||
html += '<tr>';
|
||||
for (let j = 0; j < currentCols; j++) {
|
||||
const isHeader = hasHeader && i === 0;
|
||||
const tag = isHeader ? 'th' : 'td';
|
||||
const placeholder = isHeader ? `Header ${j + 1}` : `Cell ${i},${j + 1}`;
|
||||
html += `<${tag}><input type="text" data-row="${i}" data-col="${j}" placeholder="${placeholder}"></${tag}>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
table.innerHTML = html;
|
||||
|
||||
// Add event listeners for live preview
|
||||
table.querySelectorAll('input').forEach(input => {
|
||||
input.addEventListener('input', updatePreview);
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
// Get table data
|
||||
function getTableData() {
|
||||
const data = [];
|
||||
for (let i = 0; i < currentRows; i++) {
|
||||
const row = [];
|
||||
for (let j = 0; j < currentCols; j++) {
|
||||
const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`);
|
||||
row.push(input ? input.value || '' : '');
|
||||
}
|
||||
data.push(row);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Generate markdown table
|
||||
function generateMarkdown(data) {
|
||||
if (data.length === 0 || data[0].length === 0) return '';
|
||||
|
||||
// Calculate column widths
|
||||
const colWidths = [];
|
||||
for (let j = 0; j < data[0].length; j++) {
|
||||
let maxWidth = 3; // Minimum width for separator
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
maxWidth = Math.max(maxWidth, (data[i][j] || '').length);
|
||||
}
|
||||
colWidths.push(maxWidth);
|
||||
}
|
||||
|
||||
// Generate alignment function
|
||||
const alignCell = (text, width) => {
|
||||
text = text || '';
|
||||
const padding = width - text.length;
|
||||
if (alignment === 'center') {
|
||||
const left = Math.floor(padding / 2);
|
||||
const right = padding - left;
|
||||
return ' '.repeat(left) + text + ' '.repeat(right);
|
||||
} else if (alignment === 'right') {
|
||||
return ' '.repeat(padding) + text;
|
||||
}
|
||||
return text + ' '.repeat(padding);
|
||||
};
|
||||
|
||||
// Generate separator based on alignment
|
||||
const getSeparator = (width) => {
|
||||
if (alignment === 'center') {
|
||||
return ':' + '-'.repeat(width - 2) + ':';
|
||||
} else if (alignment === 'right') {
|
||||
return '-'.repeat(width - 1) + ':';
|
||||
}
|
||||
return '-'.repeat(width);
|
||||
};
|
||||
|
||||
let markdown = '';
|
||||
const startRow = hasHeader ? 0 : -1;
|
||||
|
||||
for (let i = startRow; i < data.length; i++) {
|
||||
if (i === -1) {
|
||||
// Generate empty header for tables without header
|
||||
markdown += '| ' + colWidths.map(w => ' '.repeat(w)).join(' | ') + ' |\n';
|
||||
} else {
|
||||
const row = data[i];
|
||||
const cells = row.map((cell, j) => alignCell(cell, colWidths[j]));
|
||||
markdown += '| ' + cells.join(' | ') + ' |\n';
|
||||
}
|
||||
|
||||
// Add separator after first row
|
||||
if ((hasHeader && i === 0) || (!hasHeader && i === -1)) {
|
||||
const separators = colWidths.map(w => getSeparator(w));
|
||||
markdown += '| ' + separators.join(' | ') + ' |\n';
|
||||
}
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
// Update preview
|
||||
function updatePreview() {
|
||||
const data = getTableData();
|
||||
const markdown = generateMarkdown(data);
|
||||
document.getElementById('preview').textContent = markdown;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('rows').addEventListener('change', (e) => {
|
||||
currentRows = parseInt(e.target.value) || 4;
|
||||
generateEditableTable();
|
||||
});
|
||||
|
||||
document.getElementById('cols').addEventListener('change', (e) => {
|
||||
currentCols = parseInt(e.target.value) || 4;
|
||||
generateEditableTable();
|
||||
});
|
||||
|
||||
document.getElementById('alignment').addEventListener('change', () => {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
document.getElementById('has-header').addEventListener('change', () => {
|
||||
generateEditableTable();
|
||||
});
|
||||
|
||||
// Quick templates
|
||||
document.querySelectorAll('.template-chip').forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
currentRows = parseInt(chip.dataset.rows);
|
||||
currentCols = parseInt(chip.dataset.cols);
|
||||
document.getElementById('rows').value = currentRows;
|
||||
document.getElementById('cols').value = currentCols;
|
||||
generateEditableTable();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear all
|
||||
document.getElementById('btn-clear').addEventListener('click', () => {
|
||||
document.querySelectorAll('#editable-table input').forEach(input => {
|
||||
input.value = '';
|
||||
});
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Fill sample data
|
||||
document.getElementById('btn-fill-sample').addEventListener('click', () => {
|
||||
const sampleHeaders = ['Name', 'Description', 'Status', 'Priority', 'Due Date'];
|
||||
const sampleData = [
|
||||
['Task 1', 'First task description', 'In Progress', 'High', '2024-01-15'],
|
||||
['Task 2', 'Second task description', 'Completed', 'Medium', '2024-01-10'],
|
||||
['Task 3', 'Third task description', 'Pending', 'Low', '2024-01-20'],
|
||||
['Task 4', 'Fourth task description', 'In Review', 'High', '2024-01-18']
|
||||
];
|
||||
|
||||
for (let i = 0; i < currentRows; i++) {
|
||||
for (let j = 0; j < currentCols; j++) {
|
||||
const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`);
|
||||
if (input) {
|
||||
if (hasHeader && i === 0) {
|
||||
input.value = sampleHeaders[j % sampleHeaders.length];
|
||||
} else {
|
||||
const dataRow = hasHeader ? i - 1 : i;
|
||||
if (dataRow < sampleData.length && j < sampleData[dataRow].length) {
|
||||
input.value = sampleData[dataRow][j];
|
||||
} else {
|
||||
input.value = `Data ${i}-${j + 1}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Copy markdown
|
||||
document.getElementById('btn-copy').addEventListener('click', () => {
|
||||
const markdown = document.getElementById('preview').textContent;
|
||||
navigator.clipboard.writeText(markdown).then(() => {
|
||||
const btn = document.getElementById('btn-copy');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Copy Markdown';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
// Insert to editor
|
||||
document.getElementById('btn-insert').addEventListener('click', () => {
|
||||
const content = document.getElementById('preview').textContent;
|
||||
if (content && window.electronAPI) {
|
||||
window.electronAPI.send('insert-generated-content', '\n' + content + '\n');
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
generateEditableTable();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user