Phase 3 (Organize + robust save) + Phase 4 (Edit content); update docs
Engine: - organize.rs: build_document(plan) core — insert/delete/rotate/crop/extract/merge/split - watermark.rs: watermark + header/footer via PdfPages::watermark, CID Unicode fonts - qpdf.rs: QPDF CLI wrapper — repair corrupt files, re-encrypt after edits, ensure_openable - edit.rs: MOAT — list_objects + apply_edits (SetText VN, Transform, Delete, Add/ReplaceImage) - Tests: organize/watermark/qpdf_safety/edit roundtrip — 43/43 engine tests green UI (Tauri): "Tổ chức trang" mode (grid + drag-reorder + multi-select), "Sửa nội dung" mode (WYSIWYG object overlay, edit-in-place), global undo/redo, robust file open. Corpus: add encrypted.pdf + corrupt-truncated.pdf fixtures. Docs: mark Phase 1-4 done in roadmap; document QPDF setup + engine module layout in 05-dev-setup; add 10-13 phase summaries & user-test checklists; README status.
This commit is contained in:
+54
-5
@@ -30,11 +30,17 @@
|
||||
<button id="searchNext" title="Kết quả sau (Enter)">▶</button>
|
||||
<span class="sep"></span>
|
||||
<button id="copyPage" title="Copy text trang hiện tại">⧉ Copy</button>
|
||||
<span class="sep"></span>
|
||||
<button id="undoBtn" title="Hoàn tác (Ctrl+Z)" disabled>↶ Hoàn tác</button>
|
||||
<button id="redoBtn" title="Làm lại (Ctrl+Y)" disabled>↷ Làm lại</button>
|
||||
<span class="sep"></span>
|
||||
<button id="organizeModeBtn" title="Tổ chức trang">🗂 Tổ chức trang</button>
|
||||
<button id="editModeBtn" title="Sửa nội dung trang">✏️ Sửa nội dung</button>
|
||||
<div class="spacer"></div>
|
||||
<span id="status" class="status"></span>
|
||||
</header>
|
||||
|
||||
<div class="annobar">
|
||||
<div id="annobar" class="annobar">
|
||||
<span class="annolabel">Chú thích:</span>
|
||||
<button class="atool" data-tool="highlight" title="Tô sáng (kéo vùng)">🖍️ Tô sáng</button>
|
||||
<button class="atool" data-tool="underline" title="Gạch chân">U̲ Gạch chân</button>
|
||||
@@ -42,18 +48,48 @@
|
||||
<button class="atool" data-tool="square" title="Khung chữ nhật">▭ Khung</button>
|
||||
<button class="atool" data-tool="freetext" title="Hộp văn bản">🅣 Text box</button>
|
||||
<button class="atool" data-tool="note" title="Ghi chú dán">📌 Ghi chú</button>
|
||||
<button class="atool" data-tool="crop" title="Crop trang">✂️ Crop</button>
|
||||
<span class="sep"></span>
|
||||
<button id="annotColorBtn" class="atool" title="Màu chú thích"><span class="sw" id="annotSw"></span> Màu</button>
|
||||
<span class="sep"></span>
|
||||
<button id="undoBtn" title="Hoàn tác (Ctrl+Z)" disabled>↶ Hoàn tác</button>
|
||||
<button id="redoBtn" title="Làm lại (Ctrl+Y)" disabled>↷ Làm lại</button>
|
||||
<span class="spacer"></span>
|
||||
<span id="annotHint" class="status"></span>
|
||||
<button id="saveAnnots" disabled>💾 Lưu chú thích (<span id="annotCount">0</span>)</button>
|
||||
</div>
|
||||
|
||||
<div id="organizeBar" class="organizebar hidden">
|
||||
<span class="annolabel">Tổ chức trang:</span>
|
||||
<button id="orgInsert" title="Chèn trang">➕ Chèn</button>
|
||||
<button id="orgDelete" title="Xoá trang đã chọn (Delete)">🗑 Xoá</button>
|
||||
<button id="orgRotateL" title="Xoay trái">↺ Xoay trái</button>
|
||||
<button id="orgRotateR" title="Xoay phải">↻ Xoay phải</button>
|
||||
<button id="orgExtract" title="Trích trang đã chọn">📤 Trích</button>
|
||||
<button id="orgReplace" title="Thay trang đã chọn">🔁 Thay</button>
|
||||
<span class="sep"></span>
|
||||
<button id="orgMerge" title="Trộn nhiều file PDF">🔀 Trộn file</button>
|
||||
<button id="orgSplit" title="Tách thành nhiều file">✂ Tách file</button>
|
||||
<button id="orgWatermark" title="Thêm watermark">💧 Watermark</button>
|
||||
<button id="orgHeaderFooter" title="Thêm header/footer">📋 Header/Footer</button>
|
||||
<span class="spacer"></span>
|
||||
<span id="organizeHint" class="status"></span>
|
||||
<button id="orgSave" disabled>💾 Lưu thay đổi tổ chức trang</button>
|
||||
</div>
|
||||
|
||||
<div id="editBar" class="editbar hidden">
|
||||
<span class="annolabel">Sửa nội dung:</span>
|
||||
<button id="edAddText" title="Thêm chữ — bấm rồi click lên trang">🆎 Thêm chữ</button>
|
||||
<button id="edAddImage" title="Thêm ảnh — chọn ảnh rồi click lên trang">🖼 Thêm ảnh</button>
|
||||
<button id="edDelete" title="Xoá đối tượng đang chọn (Delete)" disabled>🗑 Xoá</button>
|
||||
<button id="edReplaceImage" title="Thay ảnh đang chọn" disabled>🔁 Thay ảnh</button>
|
||||
<span class="sep"></span>
|
||||
<label class="ed-prop">Cỡ chữ <input id="edFontSize" type="number" min="1" max="400" disabled></label>
|
||||
<button id="edColorBtn" class="atool" title="Màu chữ" disabled><span class="sw" id="edSw"></span> Màu</button>
|
||||
<span class="spacer"></span>
|
||||
<span id="editHint" class="status"></span>
|
||||
<button id="edSave" disabled>💾 Lưu thay đổi nội dung</button>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<aside class="sidebar">
|
||||
<aside id="sidebar" class="sidebar">
|
||||
<div class="tabs">
|
||||
<button id="tabThumbs" class="tab active">Thumbnails</button>
|
||||
<button id="tabOutline" class="tab">Outline</button>
|
||||
@@ -67,6 +103,19 @@
|
||||
<main id="viewport" class="viewport">
|
||||
<div id="pages" class="pages"></div>
|
||||
</main>
|
||||
|
||||
<main id="organizeGrid" class="organize-grid hidden"></main>
|
||||
|
||||
<main id="editStage" class="edit-stage hidden">
|
||||
<div id="editPageWrap" class="edit-page-wrap">
|
||||
<img id="editImg" class="edit-img" alt="trang đang sửa"/>
|
||||
<div id="editOverlay" class="edit-overlay"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="modalOverlay" class="modal-overlay hidden">
|
||||
<div id="modalBox" class="modal-box"></div>
|
||||
</div>
|
||||
|
||||
<script src="main.js"></script>
|
||||
|
||||
+1168
-19
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
:root { color-scheme: dark; --accent: #4d8bff; }
|
||||
* { box-sizing: border-box; }
|
||||
.hidden { display: none !important; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
@@ -12,6 +13,7 @@ body {
|
||||
/* Toolbar */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
@@ -47,6 +49,7 @@ button {
|
||||
button:hover { background: #595f63; }
|
||||
button:disabled { opacity: .4; cursor: default; }
|
||||
#copyPage { background: var(--accent); }
|
||||
#organizeModeBtn.active { background: var(--accent); }
|
||||
select.zoom {
|
||||
background: #1f2224; color: #eee; border: 1px solid #4a4f52;
|
||||
border-radius: 6px; padding: 4px 4px; font-size: 13px; min-width: 64px;
|
||||
@@ -63,6 +66,7 @@ input[type="search"] {
|
||||
/* Annotation toolbar */
|
||||
.annobar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
@@ -266,3 +270,143 @@ body.tool-active .annotlayer * { pointer-events: none; }
|
||||
outline: 1px solid rgba(255, 184, 0, .9);
|
||||
}
|
||||
.hl.current { background: rgba(255, 138, 0, .55); }
|
||||
|
||||
/* ===== Phase 3: Tổ chức trang ===== */
|
||||
.organizebar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
background: #2b2f31;
|
||||
border-bottom: 1px solid #1c1f21;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#orgSave { background: #2e7d32; }
|
||||
#orgSave:disabled { background: #555; }
|
||||
|
||||
.organize-grid {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
background: #3a3e41;
|
||||
}
|
||||
.org-card {
|
||||
width: 160px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
background: #2b2f31;
|
||||
}
|
||||
.org-card.selected { border-color: var(--accent); background: #2f3a4d; }
|
||||
.org-card.drag-over { outline: 3px dashed var(--accent); }
|
||||
.org-card .org-thumb-wrap {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.5);
|
||||
overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 60px;
|
||||
}
|
||||
.org-card img { display: block; width: 100%; transition: transform .15s; }
|
||||
.org-card .org-n { font-size: 12px; color: #ccc; margin-top: 4px; }
|
||||
.org-card .org-crop-mark {
|
||||
position: absolute; inset: 10%; border: 1.5px dashed #e53935; pointer-events: none;
|
||||
}
|
||||
|
||||
/* Modal dialog dùng chung cho Insert/Extract/Replace/Crop/Merge/Split/Watermark/Header-Footer */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 100;
|
||||
}
|
||||
.modal-box {
|
||||
background: #2b2f31; border-radius: 8px; padding: 16px 20px;
|
||||
min-width: 340px; max-width: 560px; max-height: 84vh; overflow: auto;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,.6); color: #eee;
|
||||
}
|
||||
.modal-box h3 { margin: 0 0 12px; font-size: 15px; color: #fff; }
|
||||
.modal-box label { display: block; font-size: 12px; color: #9aa0a6; margin: 10px 0 3px; }
|
||||
.modal-box input[type="text"], .modal-box input[type="number"], .modal-box select, .modal-box textarea {
|
||||
width: 100%; background: #1f2224; color: #eee; border: 1px solid #4a4f52;
|
||||
border-radius: 6px; padding: 6px 8px; font-size: 13px; box-sizing: border-box;
|
||||
}
|
||||
.modal-box .row { display: flex; gap: 8px; }
|
||||
.modal-box .row > * { flex: 1; }
|
||||
.modal-box .radiorow { display: flex; gap: 14px; align-items: center; font-size: 13px; margin: 4px 0; }
|
||||
.modal-box .foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
||||
.modal-box .foot button.primary { background: var(--accent); }
|
||||
.modal-box .anchor9 { display: grid; grid-template-columns: repeat(3, 32px); gap: 4px; }
|
||||
.modal-box .anchor9 button { width: 32px; height: 28px; padding: 0; background: #3a3f42; }
|
||||
.modal-box .anchor9 button.cur { background: var(--accent); }
|
||||
.modal-box .err { color: #ef5350; font-size: 12px; margin-top: 8px; }
|
||||
|
||||
/* ===== Phase 4: Sửa nội dung (Edit) ===== */
|
||||
#editModeBtn.active { background: var(--accent); }
|
||||
.editbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
background: #2b2f31;
|
||||
border-bottom: 1px solid #1c1f21;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.editbar .ed-prop { display: flex; align-items: center; gap: 4px; font-size: 13px; }
|
||||
.editbar .ed-prop input { width: 56px; }
|
||||
.editbar button.armed { background: var(--accent); }
|
||||
#edSave { background: #2e7d32; }
|
||||
#edSave:disabled { background: #555; }
|
||||
|
||||
.edit-stage {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: #444;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
user-select: none;
|
||||
}
|
||||
.edit-inline { user-select: text; }
|
||||
.edit-page-wrap { position: relative; box-shadow: 0 2px 12px rgba(0,0,0,.5); background: #fff; }
|
||||
.edit-img { display: block; }
|
||||
.edit-overlay { position: absolute; inset: 0; }
|
||||
.edit-overlay.armed { cursor: crosshair; }
|
||||
|
||||
.edit-box {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(77,139,255,.5);
|
||||
box-sizing: border-box;
|
||||
cursor: move;
|
||||
}
|
||||
.edit-box:hover { border-color: var(--accent); background: rgba(77,139,255,.08); }
|
||||
.edit-box.kind-text { border-style: dashed; }
|
||||
.edit-box.selected { border: 2px solid var(--accent); background: rgba(77,139,255,.12); }
|
||||
.edit-box .ed-handle {
|
||||
position: absolute;
|
||||
width: 10px; height: 10px;
|
||||
right: -6px; bottom: -6px;
|
||||
background: var(--accent);
|
||||
border: 1px solid #fff;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.edit-inline {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
border: 2px solid var(--accent);
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 1px 2px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user