feat(界面设计): 精简图片列表,修复预览导入
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-31 13:48:54 +08:00
1 parent 5fce4f0a71
commit d7d3942e1a
6 files changed
+55 -44

No files matched your search

-1
View File
@@ -358,7 +358,6 @@
<div id="createImageQueue" hidden> <div id="createImageQueue" hidden>
<div class="form-row"><label for="createImageOrder">排序</label><select id="createImageOrder"><option value="name-asc">名称升序</option><option value="name-desc">名称降序</option><option value="time-asc">修改时间升序</option><option value="time-desc">修改时间降序</option><option value="added">添加顺序</option><option value="custom">自定顺序</option></select></div> <div class="form-row"><label for="createImageOrder">排序</label><select id="createImageOrder"><option value="name-asc">名称升序</option><option value="name-desc">名称降序</option><option value="time-asc">修改时间升序</option><option value="time-desc">修改时间降序</option><option value="added">添加顺序</option><option value="custom">自定顺序</option></select></div>
<div id="createImageList" class="create-image-list"></div> <div id="createImageList" class="create-image-list"></div>
<div class="create-image-actions"><button id="createImageUp" class="button" type="button">上移</button><button id="createImageDown" class="button" type="button">下移</button><button id="createImageRemove" class="button" type="button">移除</button></div>
</div> </div>
<div id="createSizeRow" class="form-row"><label for="createSize">纸张</label><select id="createSize"><option value="image" hidden disabled>随图</option><option value="a4" selected>A4</option><option value="a3">A3</option><option value="a5">A5</option><option value="custom">自定</option></select></div> <div id="createSizeRow" class="form-row"><label for="createSize">纸张</label><select id="createSize"><option value="image" hidden disabled>随图</option><option value="a4" selected>A4</option><option value="a3">A3</option><option value="a5">A5</option><option value="custom">自定</option></select></div>
<div id="createDirectionRow" class="form-row"><span>方向</span><div class="form-options"><label><input id="createPortrait" type="radio" name="direction" value="portrait" checked><span>竖向</span></label><label><input id="createLandscape" type="radio" name="direction" value="landscape"><span>横向</span></label></div></div> <div id="createDirectionRow" class="form-row"><span>方向</span><div class="form-options"><label><input id="createPortrait" type="radio" name="direction" value="portrait" checked><span>竖向</span></label><label><input id="createLandscape" type="radio" name="direction" value="landscape"><span>横向</span></label></div></div>
+19 -12
View File
@@ -1692,14 +1692,10 @@ button:not(:disabled):hover {
.create-image-entry { .create-image-entry {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: 3ch 48px minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 8px;
width: 100%; width: 100%;
padding: 6px 8px;
border: 0;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
text-align: left;
} }
.create-image-entry:last-child { .create-image-entry:last-child {
@@ -1711,7 +1707,24 @@ button:not(:disabled):hover {
color: var(--accent-strong); color: var(--accent-strong);
} }
.create-image-entry:focus-visible { .create-image-select {
display: grid;
grid-template-columns: 3ch 48px minmax(0, 1fr);
align-items: center;
gap: 8px;
min-width: 0;
padding: 6px 8px;
border: 0;
background: transparent;
color: inherit;
text-align: left;
}
.create-image-entry .remove-button {
margin-right: 8px;
}
.create-image-select:focus-visible {
outline-offset: -2px; outline-offset: -2px;
} }
@@ -1747,7 +1760,6 @@ button:not(:disabled):hover {
.create-image-details { .create-image-details {
display: grid; display: grid;
min-width: 0; min-width: 0;
gap: 4px;
} }
.create-image-details > span { .create-image-details > span {
@@ -1756,11 +1768,6 @@ button:not(:disabled):hover {
white-space: nowrap; white-space: nowrap;
} }
.create-image-details > time {
color: var(--text-muted);
font-size: 12px;
}
.encryption-current { .encryption-current {
display: flex; display: flex;
align-items: center; align-items: center;
+20 -22
View File
@@ -510,9 +510,6 @@ const el = {
createImageAdd: document.querySelector("#createImageAdd"), createImageAdd: document.querySelector("#createImageAdd"),
createImageCount: document.querySelector("#createImageCount"), createImageCount: document.querySelector("#createImageCount"),
createImageOrder: document.querySelector("#createImageOrder"), createImageOrder: document.querySelector("#createImageOrder"),
createImageUp: document.querySelector("#createImageUp"),
createImageDown: document.querySelector("#createImageDown"),
createImageRemove: document.querySelector("#createImageRemove"),
createPortrait: document.querySelector("#createPortrait"), createPortrait: document.querySelector("#createPortrait"),
createLandscape: document.querySelector("#createLandscape"), createLandscape: document.querySelector("#createLandscape"),
createSize: document.querySelector("#createSize"), createSize: document.querySelector("#createSize"),
@@ -1695,12 +1692,12 @@ el.createImageAdd.addEventListener("click", () => {
el.createImages.addEventListener("change", addCreateImages); el.createImages.addEventListener("change", addCreateImages);
el.createImages.addEventListener("cancel", () => { createImagePicker = null; }); el.createImages.addEventListener("cancel", () => { createImagePicker = null; });
el.createImageOrder.addEventListener("change", () => { state.createImageQueue.order = el.createImageOrder.value; sortCreateImages(); }); el.createImageOrder.addEventListener("change", () => { state.createImageQueue.order = el.createImageOrder.value; sortCreateImages(); });
el.createImageUp.addEventListener("click", () => moveCreateImage(-1));
el.createImageDown.addEventListener("click", () => moveCreateImage(1));
el.createImageRemove.addEventListener("click", removeCreateImage);
el.createImageList.addEventListener("click", event => { el.createImageList.addEventListener("click", event => {
const row = event.target.closest("[data-image-id]"); const row = event.target.closest("[data-image-id]");
if (row) { state.createImageQueue.selected = Number(row.dataset.imageId); updateCreateImageSelection(); } if (!row || state.imageImporting) return;
state.createImageQueue.selected = Number(row.dataset.imageId);
if (event.target.closest(".remove-button")) removeCreateImage();
else updateCreateImageSelection();
}); });
el.createImageList.addEventListener("keydown", event => { el.createImageList.addEventListener("keydown", event => {
const row = event.target.closest("[data-image-id]"), queue = state.createImageQueue; const row = event.target.closest("[data-image-id]"), queue = state.createImageQueue;
@@ -3920,20 +3917,16 @@ function sortCreateImages(queue = state.createImageQueue) {
function updateCreateImageSelection() { function updateCreateImageSelection() {
const queue = state.createImageQueue; const queue = state.createImageQueue;
const index = queue?.entries.findIndex(entry => entry.id === queue.selected) ?? -1;
el.createImageUp.disabled = state.imageImporting || index <= 0;
el.createImageDown.disabled = state.imageImporting || index < 0 || index === queue.entries.length - 1;
el.createImageRemove.disabled = state.imageImporting || index < 0;
for (const row of el.createImageList.children) { for (const row of el.createImageList.children) {
row.dataset.selected = String(Number(row.dataset.imageId) === queue?.selected); row.dataset.selected = String(Number(row.dataset.imageId) === queue?.selected);
row.tabIndex = row.dataset.selected === "true" ? 0 : -1; row.querySelector(".create-image-select").tabIndex = row.dataset.selected === "true" ? 0 : -1;
row.disabled = state.imageImporting; for (const button of row.querySelectorAll("button")) button.disabled = state.imageImporting;
row.draggable = !state.imageImporting; row.draggable = !state.imageImporting;
} }
} }
function focusCreateImage() { function focusCreateImage() {
(el.createImageList.querySelector('[data-selected="true"]') || el.createImageAdd).focus(); (el.createImageList.querySelector('[data-selected="true"] .create-image-select') || el.createImageAdd).focus();
} }
function reorderCreateImage(from, to) { function reorderCreateImage(from, to) {
@@ -3978,20 +3971,23 @@ function renderCreateImages() {
} }
}, { root: el.createImageList, rootMargin: "64px" }); }, { root: el.createImageList, rootMargin: "64px" });
for (const [index, entry] of queue.entries.entries()) { for (const [index, entry] of queue.entries.entries()) {
const row = document.createElement("button"), number = document.createElement("span"), preview = document.createElement("span"), details = document.createElement("span"); const row = document.createElement("div"), select = document.createElement("button"), remove = document.createElement("button"), number = document.createElement("span"), preview = document.createElement("span"), details = document.createElement("span");
row.type = "button";
row.className = "create-image-entry"; row.className = "create-image-entry";
row.dataset.imageId = entry.id; row.dataset.imageId = entry.id;
row.title = entry.file.name; select.type = remove.type = "button";
select.className = "create-image-select";
select.title = entry.file.name;
remove.className = "icon-button remove-button";
remove.title = "移除";
number.textContent = String(index + 1); number.textContent = String(index + 1);
preview.className = "create-image-preview"; preview.className = "create-image-preview";
preview.textContent = entry.file.name.split(".").at(-1).toUpperCase(); preview.textContent = entry.file.name.split(".").at(-1).toUpperCase();
details.className = "create-image-details"; details.className = "create-image-details";
const name = document.createElement("span"), time = document.createElement("time"); const name = document.createElement("span");
name.textContent = entry.file.name; name.textContent = entry.file.name;
time.textContent = new Date(entry.file.lastModified).toLocaleString(); details.append(name);
details.append(name, time); select.append(number, preview, details);
row.append(number, preview, details); row.append(select, remove);
el.createImageList.append(row); el.createImageList.append(row);
if (entry.url) showCreateImagePreview(entry, row); if (entry.url) showCreateImagePreview(entry, row);
else if (!entry.previewed) queue.observer.observe(row); else if (!entry.previewed) queue.observer.observe(row);
@@ -4013,7 +4009,9 @@ async function loadCreateImagePreview(tab, queue, entry) {
entry.previewed = true; entry.previewed = true;
let bitmap; let bitmap;
try { try {
bitmap = await createImageBitmap(entry.file, { resizeWidth: 64 }); const source = entry.file instanceof Blob ? entry.file : new Blob([await entry.file.arrayBuffer()]);
if (tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return;
bitmap = await createImageBitmap(source, { resizeWidth: 64 });
const canvas = document.createElement("canvas"), scale = Math.min(64 / bitmap.width, 64 / bitmap.height, 1); const canvas = document.createElement("canvas"), scale = Math.min(64 / bitmap.width, 64 / bitmap.height, 1);
canvas.width = Math.max(1, Math.round(bitmap.width * scale)); canvas.width = Math.max(1, Math.round(bitmap.width * scale));
canvas.height = Math.max(1, Math.round(bitmap.height * scale)); canvas.height = Math.max(1, Math.round(bitmap.height * scale));
+3 -5
View File
@@ -1,3 +1,5 @@
import { setInputFiles } from "./ofdgo_util.js";
let engineModule = null; let engineModule = null;
let engineScheduler = null; let engineScheduler = null;
let engineSequence = 0; let engineSequence = 0;
@@ -823,11 +825,7 @@ class NativeHost {
try { try {
const files = (await this.call("open", {multiple:input.multiple, directory:input.webkitdirectory, accept:input.accept})).map(value => new NativeFile(value)); const files = (await this.call("open", {multiple:input.multiple, directory:input.webkitdirectory, accept:input.accept})).map(value => new NativeFile(value));
if (!files.length) return; if (!files.length) return;
const transfer = new DataTransfer(); setInputFiles(input, files);
for (const file of files) transfer.items.add(new File([], file.name, {type:file.type}));
delete input.files;
input.files = transfer.files;
Object.defineProperty(input, "files", {configurable:true, get:() => input.value ? files : []});
input.dispatchEvent(new Event("input", {bubbles:true})); input.dispatchEvent(new Event("input", {bubbles:true}));
input.dispatchEvent(new Event("change", {bubbles:true})); input.dispatchEvent(new Event("change", {bubbles:true}));
} catch (error) { if (error.name !== "AbortError") onError(error); } } catch (error) { if (error.name !== "AbortError") onError(error); }
+10 -3
View File
@@ -1,3 +1,12 @@
export function setInputFiles(input, files) {
const view = input.ownerDocument.defaultView, transfer = new view.DataTransfer();
const native = files.some(file => !(file instanceof view.File));
for (const file of files) transfer.items.add(file instanceof view.File ? file : new view.File([], file.name, {type: file.type}));
delete input.files;
input.files = transfer.files;
if (native) Object.defineProperty(input, "files", {configurable: true, get: () => input.value ? files : []});
}
export class DocumentDialogs { export class DocumentDialogs {
constructor({ document: root = document, targets = [], titlebar = null } = {}) { constructor({ document: root = document, targets = [], titlebar = null } = {}) {
Object.assign(this, { document: root, targets, titlebar, owner: null, records: new Map(), tabs: new Map(), released: new WeakSet(), inert: new Map(), styles: new Map() }); Object.assign(this, { document: root, targets, titlebar, owner: null, records: new Map(), tabs: new Map(), released: new WeakSet(), inert: new Map(), styles: new Map() });
@@ -258,9 +267,7 @@ export class DocumentDialogs {
if (item.value !== undefined && node.tagName !== "SELECT") node.value = item.value; if (item.value !== undefined && node.tagName !== "SELECT") node.value = item.value;
for (const key of ["checked", "indeterminate", "selected"]) if (item[key] !== undefined) node[key] = item[key]; for (const key of ["checked", "indeterminate", "selected"]) if (item[key] !== undefined) node[key] = item[key];
if (item.files !== undefined) { if (item.files !== undefined) {
const transfer = new this.document.defaultView.DataTransfer(); setInputFiles(node, item.files);
for (const file of item.files) transfer.items.add(file);
node.files = transfer.files;
} }
if (item.selectionStart != null && typeof node.setSelectionRange === "function") node.setSelectionRange(item.selectionStart, item.selectionEnd, item.selectionDirection); if (item.selectionStart != null && typeof node.setSelectionRange === "function") node.setSelectionRange(item.selectionStart, item.selectionEnd, item.selectionDirection);
node.scrollTop = item.scrollTop; node.scrollTop = item.scrollTop;
+3 -1
View File
@@ -226,6 +226,7 @@ func (s *fileStore) file(id string) (map[string]any, error) {
} }
handle := s.descriptor(id) handle := s.descriptor(id)
size := int64(len(entry.data)) size := int64(len(entry.data))
var modified int64
if entry.data == nil { if entry.data == nil {
file, err := basefile.Open(entry.path) file, err := basefile.Open(entry.path)
if err != nil { if err != nil {
@@ -237,11 +238,12 @@ func (s *fileStore) file(id string) (map[string]any, error) {
return nil, err return nil, err
} }
size = info.Size() size = info.Size()
modified = info.ModTime().UnixMilli()
id = rand.Text() id = rand.Text()
entry = &fileEntry{path: entry.path, name: entry.name, file: file} entry = &fileEntry{path: entry.path, name: entry.name, file: file}
s.entries[id] = entry s.entries[id] = entry
} }
return map[string]any{"nativeFile": id, "name": entry.name, "size": size, "url": "/__native/data/" + id, "handle": handle}, nil return map[string]any{"nativeFile": id, "name": entry.name, "size": size, "lastModified": modified, "url": "/__native/data/" + id, "handle": handle}, nil
} }
// resolve 按授权标识读取文件并保持随机读取句柄 // resolve 按授权标识读取文件并保持随机读取句柄