PDF pages: visual rotate/delete/reorder with action buttons

- Delete: click pages (red highlight), counter, Удалить выбранные (N) button
- Rotate: click pages (visual rotation preview 90/180/270), Повернуть button
- Reorder: drag-and-drop thumbnails, visual number update, Применить button
- CSS: rotation transforms, delete/drag states, action bar with buttons
- Direct execute functions (bypass main execute flow)
This commit is contained in:
treamz 2026-03-22 01:42:26 +03:00
parent 6bf2a0b6a1
commit 816753c925

View File

@ -203,6 +203,21 @@
.page-thumb-check { position:absolute; top:4px; right:4px; width:18px; height:18px; border-radius:50%; border:2px solid rgba(255,255,255,.5); background:rgba(0,0,0,.3); display:none; align-items:center; justify-content:center; }
.page-thumb.selected .page-thumb-check { display:flex; background:var(--accent); border-color:var(--accent); }
.page-thumb-check svg { width:12px; height:12px; }
.page-thumb.rotate-90 img { transform: rotate(90deg); }
.page-thumb.rotate-180 img { transform: rotate(180deg); }
.page-thumb.rotate-270 img { transform: rotate(270deg); }
.page-thumb.dragging { opacity: 0.4; border-color: var(--accent); }
.page-thumb.drag-over { border-color: var(--accent); transform: scale(1.05); }
.page-thumb.marked-delete { border-color: #ef4444; opacity: 0.6; }
.page-thumb.marked-delete .page-thumb-check { display: flex; background: #ef4444; border-color: #ef4444; }
.page-thumb.marked-delete .page-thumb-num { background: rgba(239,68,68,.8); }
.grid-action-bar { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.grid-action-btn { padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; border: none; cursor: pointer; font-family: 'Manrope', sans-serif; transition: all .15s; }
.grid-action-btn.danger { background: rgba(239,68,68,.1); color: #ef4444; border: 1px solid rgba(239,68,68,.3); }
.grid-action-btn.danger:hover { background: rgba(239,68,68,.2); }
.grid-action-btn.primary { background: var(--accent); color: #fff; }
.grid-action-btn.primary:hover { background: var(--accent-dim); }
.grid-action-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.page-grid-hint { font-size:11px; color:var(--text-muted); margin-top:6px; font-family:'JetBrains Mono',monospace; }
.file-card-remove:hover {
@ -727,27 +742,36 @@
<button class="angle-btn" data-angle="180">180°</button>
<button class="angle-btn" data-angle="270">270° (90°)</button>
</div>
<span class="option-label" style="margin-top:8px;">Страницы (нажмите для выбора)</span>
<span class="option-label" style="margin-top:8px;">Нажмите на страницы для поворота</span>
<div class="page-grid" id="rotatePageGrid"></div>
<div class="page-grid-hint" id="rotateHint">Загрузите файл для просмотра страниц</div>
<div class="grid-action-bar">
<button class="grid-action-btn primary" id="btnRotateSelected" disabled onclick="executeRotatePages()">Повернуть выбранные (<span id="rotateCount">0</span>)</button>
<span class="page-grid-hint" id="rotateHint"></span>
</div>
</div>
</div>
<!-- Delete sub-panel -->
<div id="subpanel-delete" class="sub-panel wa-card">
<div class="option-group">
<span class="option-label">Выберите страницы для удаления</span>
<span class="option-label">Нажмите на страницы для удаления</span>
<div class="page-grid" id="deletePageGrid"></div>
<div class="page-grid-hint" id="deleteHint">Загрузите файл для просмотра страниц</div>
<div class="grid-action-bar">
<button class="grid-action-btn danger" id="btnDeleteSelected" disabled onclick="executeDeletePages()">Удалить выбранные (<span id="deleteCount">0</span>)</button>
<span class="page-grid-hint" id="deleteHint"></span>
</div>
</div>
</div>
<!-- Reorder sub-panel -->
<div id="subpanel-reorder" class="sub-panel wa-card">
<div class="option-group">
<span class="option-label">Перетаскивайте страницы для изменения порядка</span>
<span class="option-label">Перетаскивайте страницы мышкой для нового порядка</span>
<div class="page-grid" id="reorderPageGrid"></div>
<div class="page-grid-hint" id="reorderHint">Загрузите файл для просмотра страниц</div>
<div class="grid-action-bar">
<button class="grid-action-btn primary" id="btnReorderApply" onclick="executeReorderPages()">Применить порядок</button>
<span class="page-grid-hint" id="reorderHint"></span>
</div>
</div>
</div>
</div>
@ -1676,27 +1700,88 @@
if (!grid) return;
if (!fileId || pages < 1) {
grid.innerHTML = '';
if (hint) hint.textContent = 'Выберите файл';
if (hint) hint.textContent = '';
return;
}
if (hint) hint.textContent = mode === 'delete' ? 'Нажмите на страницы для выбора' : mode === 'rotate' ? 'Нажмите для выбора страниц' : 'Перетаскивайте для изменения порядка';
if (hint) hint.textContent = '';
grid.innerHTML = '';
for (let i = 1; i <= pages; i++) {
for (let i = 1; i <= Math.min(pages, 100); i++) {
const thumb = document.createElement('div');
thumb.className = 'page-thumb';
thumb.dataset.page = i;
thumb.innerHTML = `<img src="/pdf/preview/${fileId}/${i}" alt="Page ${i}" loading="lazy"><div class="page-thumb-num">${i}</div><div class="page-thumb-check"><svg fill="none" stroke="#fff" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg></div>`;
thumb.innerHTML = '<img src="/pdf/preview/' + fileId + '/' + i + '" alt="' + i + '" loading="lazy"><div class="page-thumb-num">' + i + '</div><div class="page-thumb-check"><svg fill="none" stroke="#fff" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg></div>';
if (mode === 'delete' || mode === 'rotate') {
if (mode === 'delete') {
thumb.addEventListener('click', () => {
thumb.classList.toggle('selected');
thumb.classList.toggle('marked-delete');
updateDeleteCount();
});
} else if (mode === 'rotate') {
thumb.addEventListener('click', () => {
thumb.classList.toggle('selected');
// Visual rotation preview
if (thumb.classList.contains('selected')) {
const angle = state.rotateAngle || 90;
thumb.classList.add('rotate-' + angle);
} else {
thumb.classList.remove('rotate-90', 'rotate-180', 'rotate-270');
}
updateRotateCount();
});
} else if (mode === 'reorder') {
// Drag and drop
thumb.draggable = true;
thumb.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', i);
thumb.classList.add('dragging');
});
thumb.addEventListener('dragend', () => thumb.classList.remove('dragging'));
thumb.addEventListener('dragover', e => { e.preventDefault(); thumb.classList.add('drag-over'); });
thumb.addEventListener('dragleave', () => thumb.classList.remove('drag-over'));
thumb.addEventListener('drop', e => {
e.preventDefault();
thumb.classList.remove('drag-over');
const fromPage = e.dataTransfer.getData('text/plain');
const fromEl = grid.querySelector('[data-page="' + fromPage + '"]');
if (fromEl && fromEl !== thumb) {
// Swap positions in DOM
const parent = thumb.parentNode;
const thumbNext = thumb.nextSibling;
if (fromEl.nextSibling === thumb) {
parent.insertBefore(thumb, fromEl);
} else {
parent.insertBefore(fromEl, thumb);
parent.insertBefore(thumb, thumbNext);
}
// Update visual numbers
Array.from(grid.children).forEach((el, idx) => {
el.querySelector('.page-thumb-num').textContent = idx + 1;
});
}
});
}
grid.appendChild(thumb);
}
}
function updateDeleteCount() {
const count = getSelectedPages('deletePageGrid').length;
const el = document.getElementById('deleteCount');
const btn = document.getElementById('btnDeleteSelected');
if (el) el.textContent = count;
if (btn) btn.disabled = count === 0;
}
function updateRotateCount() {
const count = getSelectedPages('rotatePageGrid').length;
const el = document.getElementById('rotateCount');
const btn = document.getElementById('btnRotateSelected');
if (el) el.textContent = count;
if (btn) btn.disabled = count === 0;
}
function getSelectedPages(gridId) {
const grid = document.getElementById(gridId);
if (!grid) return [];
@ -1705,6 +1790,60 @@
// Direct execute for page operations (from grid action buttons)
window.executeDeletePages = async function() {
const pages = getSelectedPages('deletePageGrid');
if (!pages.length) { showError('Выберите страницы'); return; }
const fileId = state.selectedFileId;
if (!fileId) { showError('Выберите файл'); return; }
clearError();
try {
const res = await fetch('/pdf/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ fileId, pages: pages.join(',') }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
showResult(data);
} catch(e) { showError(e.message); }
};
window.executeRotatePages = async function() {
const pages = getSelectedPages('rotatePageGrid');
const fileId = state.selectedFileId;
if (!fileId) { showError('Выберите файл'); return; }
clearError();
try {
const res = await fetch('/pdf/rotate', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ fileId, pages: pages.length ? pages.join(',') : 'all', angle: state.rotateAngle || 90 }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
showResult(data);
} catch(e) { showError(e.message); }
};
window.executeReorderPages = async function() {
const grid = document.getElementById('reorderPageGrid');
if (!grid) return;
const order = Array.from(grid.querySelectorAll('.page-thumb')).map(t => parseInt(t.dataset.page));
const fileId = state.selectedFileId;
if (!fileId) { showError('Выберите файл'); return; }
clearError();
try {
const res = await fetch('/pdf/reorder', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ fileId, order }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
showResult(data);
} catch(e) { showError(e.message); }
};
function showResult(data) {
if (data.downloadUrl) {
document.getElementById('downloadLink').href = data.downloadUrl;
document.getElementById('downloadLink').style.display = '';
if (data.size) document.getElementById('resultSize').textContent = formatSize(data.size);
stepConfig.style.display = 'none';
stepResult.style.display = 'block';
}
}
})();
</script>