Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/md/Advanced-options-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,16 +295,16 @@ ChmUI [
; default values for annotations in PDF documents (introduced in version 3.3)
Annotations [
; highlight annotation color
HighlightColor = #ffff00
HighlightColor = #facc15

; underline annotation color
UnderlineColor = #00ff00
UnderlineColor = #22c55e

; squiggly annotation color (introduced in version 3.5)
SquigglyColor = #ff00ff
SquigglyColor = #ec4899

; strike out annotation color (introduced in version 3.5)
StrikeOutColor = #ff0000
StrikeOutColor = #ef4444

; text color of free text annotation (introduced in version 3.5)
FreeTextColor =
Expand Down
Binary file added gfx/reload-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions gfx/reload-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions prettysumatra/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor con Estilo Heroicons</title>

<style>
:root {
--icon-color: #4b5563; /* Gris pizarra (text-gray-600 en Tailwind) */
}

body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
}

.toolbar {
display: flex;
align-items: center;
gap: 0.25rem;
background: white;
padding: 0.5rem;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.02);
border: 1px solid #e2e8f0;
}

.toolbar-divider {
width: 1px;
height: 24px;
background-color: #e2e8f0;
margin: 0 0.5rem;
}

/* --- Botones --- */
.toolbar-btn {
background: transparent;
border: none;
padding: 0.5rem;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--icon-color);
transition: all 0.2s ease;
position: relative;
}

.toolbar-btn:hover {
background-color: #f1f5f9;
color: #0f172a;
}

/* Estándar de tamaño Heroicons (w-6 h-6 en Tailwind) */
.hero-icon {
width: 24px;
height: 24px;
}

/* --- Línea de Color Inferior --- */
.btn-color::after {
content: '';
position: absolute;
bottom: 4px;
width: 16px;
height: 3px;
border-radius: 2px;
background-color: var(--active-color);
}

/* Reducimos el margen para acomodar la línea inferior */
.btn-color .hero-icon {
margin-bottom: 4px;
}

/* Variables de color de Tailwind */
.btn-highlight { --active-color: #fbbf24; } /* yellow-400 */
.btn-underline { --active-color: #3b82f6; } /* blue-500 */
.btn-strike { --active-color: #ef4444; } /* red-500 */

/* Botón del picker (Ajuste para que el icono principal y el chevron se vean bien) */
.btn-picker {
gap: 2px;
padding-right: 0.25rem;
}

.btn-picker .hero-chevron {
width: 16px;
height: 16px;
stroke-width: 2; /* Un poco más grueso al ser más pequeño */
}
</style>
</head>
<body>

<div class="toolbar">

<!-- Resaltador -->
<button class="toolbar-btn btn-color btn-highlight" aria-label="Resaltar">
<!-- Todos usan el formato base de Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="hero-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="m9 11-6 6v3h9l3-3" />
<path stroke-linecap="round" stroke-linejoin="round" d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
</svg>
</button>

<!-- Subrayado -->
<button class="toolbar-btn btn-color btn-underline" aria-label="Subrayar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="hero-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 4v6a6 6 0 0 0 12 0V4" />
</svg>
</button>

<!-- Tachado -->
<button class="toolbar-btn btn-color btn-strike" aria-label="Tachar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="hero-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6M3 12h18" />
</svg>
</button>

<!-- Separador -->
<div class="toolbar-divider" aria-hidden="true"></div>

<!-- Selector Neutro (Swatch circular + Chevron) -->
<button class="toolbar-btn btn-picker" aria-label="Seleccionar color">
<!-- Swatch circular customizado bajo normas Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="hero-icon">
<!-- Círculo exterior (borde del swatch) -->
<circle cx="12" cy="12" r="8" stroke-linecap="round" stroke-linejoin="round" />
<!-- Pequeño detalle interior para darle más entidad de herramienta de color (opcional, como un reflejo) -->
<path stroke-linecap="round" stroke-linejoin="round" d="M15.5 8.5A4.5 4.5 0 0 0 8.5 15.5" opacity="0.4" />
</svg>

<!-- Heroicon Chevron Down original -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="hero-chevron">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>

</div>

</body>
</html>
139 changes: 120 additions & 19 deletions prettysumatra/webui/home.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,74 @@
<html lang="es" data-theme="light">
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
// Monkeypatch addEventListener early to neutralize webview JS keyboard handlers (Home page).
(function(){
const blockedKeys = new Set(['f','o','p','s','b','k','t','a','c','v','x','z','y','r','n']);
function isEditableElement(el){ if(!el) return false; const t=(el.tagName||'').toUpperCase(); return t==='INPUT'||t==='TEXTAREA'||el.isContentEditable; }

const origAdd = EventTarget.prototype.addEventListener;
const origRemove = EventTarget.prototype.removeEventListener;
const perTargetMap = new WeakMap();
function shouldWrapForKeyboard(type){ return typeof type === 'string' && /^(keydown|keypress|keyup)$/.test(type); }
function makeWrapper(listener){
return function(e){
try{
const active = document.activeElement;
const editable = isEditableElement(active);
if ((e.ctrlKey||e.metaKey) && !editable) {
const k = (e.key||'').toLowerCase();
if (blockedKeys.has(k)) return;
}
if (!editable && e.key === 'F3') return;
} catch (_) {}
return listener.apply(this, arguments);
};
}
EventTarget.prototype.addEventListener = function(type, listener, options){
try{
if (shouldWrapForKeyboard(type) && typeof listener === 'function') {
let map = perTargetMap.get(this);
if (!map) { map = new Map(); perTargetMap.set(this, map); }
if (map.has(listener)) return origAdd.call(this, type, map.get(listener), options);
const wrapped = makeWrapper(listener);
map.set(listener, wrapped);
return origAdd.call(this, type, wrapped, options);
}
} catch (e) {}
return origAdd.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function(type, listener, options){
try{
if (shouldWrapForKeyboard(type) && typeof listener === 'function') {
const map = perTargetMap.get(this);
const wrapped = map && map.get(listener);
if (wrapped) { map.delete(listener); return origRemove.call(this, type, wrapped, options); }
}
} catch (e) {}
return origRemove.call(this, type, listener, options);
};

function homeCtrlFHandler(e){
try{
const active = document.activeElement;
if ((e.ctrlKey||e.metaKey) && (e.key||'').toLowerCase() === 'f' && !isEditableElement(active)) {
const s = document.getElementById('searchInput');
if (s) { s.focus(); s.select && s.select(); e.stopPropagation(); }
}
if ((e.key === 'F3') && !isEditableElement(document.activeElement)) {
if (e.shiftKey) document.getElementById('searchPrev')?.click(); else document.getElementById('searchNext')?.click();
e.stopPropagation();
}
} catch (_) {}
}
window.addEventListener('keydown', homeCtrlFHandler, { capture: true });
})();
</script>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: blob:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'none';">
<title>PrettySumatraPDF - Home</title>
<link rel="stylesheet" href="vendor/fontawesome/all.min.css">
<link rel="stylesheet" href="vendor/material-icons/material-icons.css">
<link rel="stylesheet" href="vendor/inter/inter.css">
<style>
:root {
Expand Down Expand Up @@ -55,7 +119,7 @@
.hero-title span { background: var(--brand-gradient); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.hero-subtitle { font-size: 1.15rem; color: var(--text-muted); max-width: 550px; margin-bottom: 36px; line-height: 1.5; }
.search-container { width: 100%; max-width: 650px; position: relative; margin-bottom: 32px; }
.search-container i.search-icon { position: absolute; left: 24px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 1.3rem; z-index: 2; }
.search-container .search-icon { position: absolute; left: 24px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 1.3rem; z-index: 2; }
.search-input { width: 100%; height: 64px; background: var(--bg-input); backdrop-filter: blur(10px) saturate(130%); -webkit-backdrop-filter: blur(10px) saturate(130%); border: 1px solid var(--border-input); border-radius: var(--radius-xl); padding: 0 24px 0 60px; font-family: inherit; font-size: 1.15rem; color: var(--text-main); box-shadow: var(--shadow-sm); transition: var(--transition); }
.search-input::placeholder { color: var(--text-muted); font-weight: 500; }
.search-input:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 4px var(--brand-glow), var(--shadow-md); background: var(--bg-surface); }
Expand Down Expand Up @@ -108,37 +172,37 @@
<div class="hero">
<img class="hero-logo" src="assets/SumatraPDF-256x256x32.png" alt="SumatraPDF">
<div class="hero-title">Pretty<span>Sumatra</span>PDF</div>
<div class="hero-subtitle">Tu biblioteca digital, más elegante que nunca. Busca, abre o retoma tus lecturas al instante.</div>
<div class="hero-subtitle">Your digital library, more elegant than ever. Search, open, or resume your reading instantly.</div>
<div class="search-container">
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" id="searchInput" class="search-input" placeholder="Buscar documentos por nombre..." autocomplete="off">
<span class="material-symbols-rounded search-icon">search</span>
<input type="text" id="searchInput" class="search-input" placeholder="Search documents by name..." autocomplete="off">
</div>
<div class="quick-actions">
<button class="btn-action btn-primary-action" onclick="post('openFile')">
<i class="fa-solid fa-folder-open"></i> Explorar Archivos
<button class="btn-action btn-primary-action" onclick="if(window.external&&typeof window.external.invoke==='function')window.external.invoke(JSON.stringify({type:'command',name:'openFile',payload:{},ts:Date.now()}))" aria-label="Explore files">
<span class="material-symbols-rounded">folder_open</span> Explore files
</button>
<button class="btn-action" onclick="post('reopenLast')">
<i class="fa-solid fa-clock-rotate-left"></i> Retomar Último
<button class="btn-action" onclick="if(window.external&&typeof window.external.invoke==='function')window.external.invoke(JSON.stringify({type:'command',name:'reopenLast',payload:{},ts:Date.now()}))" aria-label="Resume last document">
<span class="material-symbols-rounded">history</span> Resume last
</button>
</div>
</div>

<div class="recent-section">
<div class="section-header">
<div class="section-title"><i class="fa-regular fa-clock"></i> Documentos Recientes</div>
<div class="section-title"><span class="material-symbols-rounded">access_time</span> Recent Documents</div>
</div>
<div class="file-grid" id="recentGrid">
<div class="file-card skeleton-card"><div class="file-card-header"><div class="file-icon"><i class="fa-solid fa-file"></i></div></div><div class="file-info"><div class="skel-block" style="height: 18px; width: 70%;"></div><div class="skel-block" style="height: 12px; width: 40%;"></div></div></div>
<div class="file-card skeleton-card" style="animation-delay: 0.2s"><div class="file-card-header"><div class="file-icon"><i class="fa-solid fa-file"></i></div></div><div class="file-info"><div class="skel-block" style="height: 18px; width: 85%;"></div><div class="skel-block" style="height: 12px; width: 50%;"></div></div></div>
<div class="file-card skeleton-card"><div class="file-card-header"><div class="file-icon"><span class="material-symbols-rounded">insert_drive_file</span></div></div><div class="file-info"><div class="skel-block" style="height: 18px; width: 70%;"></div><div class="skel-block" style="height: 12px; width: 40%;"></div></div></div>
<div class="file-card skeleton-card" style="animation-delay: 0.2s"><div class="file-card-header"><div class="file-icon"><span class="material-symbols-rounded">insert_drive_file</span></div></div><div class="file-info"><div class="skel-block" style="height: 18px; width: 85%;"></div><div class="skel-block" style="height: 12px; width: 50%;"></div></div></div>
</div>
</div>

<script>
function post(name, payload = {}) { if (window.external && typeof window.external.invoke === 'function') window.external.invoke(JSON.stringify({ type: 'command', name, payload, ts: Date.now() })); }
// `post` helper removed — use direct host invoke instead when needed

const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') post('searchFiles', { query: e.target.value });
if (e.key === 'Enter') { if (window.external && typeof window.external.invoke === 'function') window.external.invoke(JSON.stringify({ type: 'command', name: 'searchFiles', payload: { query: e.target.value }, ts: Date.now() })); }
});
searchInput.addEventListener('input', (e) => {
const q = e.target.value.trim();
Expand All @@ -154,12 +218,12 @@
function renderRecentFiles(files) {
const grid = document.getElementById('recentGrid'); grid.innerHTML = '';
if (!files || files.length === 0) {
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 60px 20px; color: var(--text-muted); animation: slideUp 0.5s;"><i class="fa-regular fa-folder-open" style="font-size: 3rem; opacity: 0.5; margin-bottom: 16px;"></i><h3 style="font-weight: 600; margin-bottom: 8px;">No hay archivos recientes</h3><p style="font-size: 0.95rem;">Los documentos que abras aparecerán aquí mágicamente.</p></div>`; return;
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 60px 20px; color: var(--text-muted); animation: slideUp 0.5s;"><span class="material-symbols-rounded" style="font-size: 3rem; opacity: 0.5; margin-bottom: 16px;">folder_open</span><h3 style="font-weight: 600; margin-bottom: 8px;">No recent files</h3><p style="font-size: 0.95rem;">Documents you open will appear here.</p></div>`; return;
}
files.forEach((file, index) => {
const card = document.createElement('div'); card.className = 'file-card'; card.style.animation = `slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) ${index * 0.05}s both`;
card.onclick = () => post('openRecent', { path: file.path });
card.innerHTML = `<div class="file-card-header"><div class="file-icon"><i class="fa-solid fa-file-pdf"></i></div><i class="fa-solid fa-arrow-right file-action-icon"></i></div><div class="file-info"><div class="file-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div><div class="file-path" title="${escapeHtml(file.path)}">${escapeHtml(file.path)}</div></div>`;
card.onclick = () => { if (window.external && typeof window.external.invoke === 'function') window.external.invoke(JSON.stringify({ type: 'command', name: 'openRecent', payload: { path: file.path }, ts: Date.now() })); };
card.innerHTML = `<div class="file-card-header"><div class="file-icon"><span class="material-symbols-rounded">picture_as_pdf</span></div><span class="material-symbols-rounded file-action-icon">arrow_forward</span></div><div class="file-info"><div class="file-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div><div class="file-path" title="${escapeHtml(file.path)}">${escapeHtml(file.path)}</div></div>`;
grid.appendChild(card);
});
}
Expand All @@ -178,7 +242,44 @@
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
};

post('homePageReady');
if (window.external && typeof window.external.invoke === 'function') window.external.invoke(JSON.stringify({ type: 'command', name: 'homePageReady', payload: {}, ts: Date.now() }));
</script>
<script>
// Shortcut guard for home page search: block global shortcuts and focus search on Ctrl+F
(function() {
function isEditableElement(el) {
if (!el) return false;
const tag = (el.tagName || '').toUpperCase();
return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
}

window.addEventListener('keydown', (e) => {
try {
const active = document.activeElement;
const editable = isEditableElement(active);
if ((e.ctrlKey || e.metaKey) && !editable) {
const k = (e.key || '').toLowerCase();
const blocked = ['f','o','p','s','b','k','t','a','c','v','x','z','y','r','n'];
if (blocked.indexOf(k) >= 0) {
e.preventDefault(); e.stopPropagation();
if (k === 'f') {
const s = document.getElementById('searchInput');
if (s) { s.focus(); if (typeof s.select === 'function') s.select(); }
}
}
}

if (!editable && e.key === 'F3') {
e.preventDefault(); e.stopPropagation();
// On Home there is no per-document next/prev; trigger a search (Enter)
const s = document.getElementById('searchInput');
if (s && s.value && s.value.trim()) {
if (window.external && typeof window.external.invoke === 'function') window.external.invoke(JSON.stringify({ type: 'command', name: 'searchFiles', payload: { query: s.value }, ts: Date.now() }));
}
}
} catch (err) { try { console.error('home shortcut-guard error', err); } catch(e){} }
}, { capture: true });
})();
</script>
</body>
</html>
Loading
Loading