diff --git a/assets/webui/index.html b/assets/webui/index.html index 17debce..250b909 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -358,7 +358,6 @@
方向
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index 132d8df..9d1e126 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -1692,14 +1692,10 @@ button:not(:disabled):hover { .create-image-entry { position: relative; display: grid; - grid-template-columns: 3ch 48px minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr) auto; align-items: center; - gap: 8px; width: 100%; - padding: 6px 8px; - border: 0; border-bottom: 1px solid var(--line); - text-align: left; } .create-image-entry:last-child { @@ -1711,7 +1707,24 @@ button:not(:disabled):hover { 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; } @@ -1747,7 +1760,6 @@ button:not(:disabled):hover { .create-image-details { display: grid; min-width: 0; - gap: 4px; } .create-image-details > span { @@ -1756,11 +1768,6 @@ button:not(:disabled):hover { white-space: nowrap; } -.create-image-details > time { - color: var(--text-muted); - font-size: 12px; -} - .encryption-current { display: flex; align-items: center; diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index c7fa165..8570a33 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -510,9 +510,6 @@ const el = { createImageAdd: document.querySelector("#createImageAdd"), createImageCount: document.querySelector("#createImageCount"), createImageOrder: document.querySelector("#createImageOrder"), - createImageUp: document.querySelector("#createImageUp"), - createImageDown: document.querySelector("#createImageDown"), - createImageRemove: document.querySelector("#createImageRemove"), createPortrait: document.querySelector("#createPortrait"), createLandscape: document.querySelector("#createLandscape"), createSize: document.querySelector("#createSize"), @@ -1695,12 +1692,12 @@ el.createImageAdd.addEventListener("click", () => { el.createImages.addEventListener("change", addCreateImages); el.createImages.addEventListener("cancel", () => { createImagePicker = null; }); 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 => { 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 => { const row = event.target.closest("[data-image-id]"), queue = state.createImageQueue; @@ -3920,20 +3917,16 @@ function sortCreateImages(queue = state.createImageQueue) { function updateCreateImageSelection() { 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) { row.dataset.selected = String(Number(row.dataset.imageId) === queue?.selected); - row.tabIndex = row.dataset.selected === "true" ? 0 : -1; - row.disabled = state.imageImporting; + row.querySelector(".create-image-select").tabIndex = row.dataset.selected === "true" ? 0 : -1; + for (const button of row.querySelectorAll("button")) button.disabled = state.imageImporting; row.draggable = !state.imageImporting; } } 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) { @@ -3978,20 +3971,23 @@ function renderCreateImages() { } }, { root: el.createImageList, rootMargin: "64px" }); 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"); - row.type = "button"; + 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.className = "create-image-entry"; 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); preview.className = "create-image-preview"; preview.textContent = entry.file.name.split(".").at(-1).toUpperCase(); details.className = "create-image-details"; - const name = document.createElement("span"), time = document.createElement("time"); + const name = document.createElement("span"); name.textContent = entry.file.name; - time.textContent = new Date(entry.file.lastModified).toLocaleString(); - details.append(name, time); - row.append(number, preview, details); + details.append(name); + select.append(number, preview, details); + row.append(select, remove); el.createImageList.append(row); if (entry.url) showCreateImagePreview(entry, row); else if (!entry.previewed) queue.observer.observe(row); @@ -4013,7 +4009,9 @@ async function loadCreateImagePreview(tab, queue, entry) { entry.previewed = true; let bitmap; 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); canvas.width = Math.max(1, Math.round(bitmap.width * scale)); canvas.height = Math.max(1, Math.round(bitmap.height * scale)); diff --git a/assets/webui/ofdgo_host.js b/assets/webui/ofdgo_host.js index a6e8d2c..1cec77f 100644 --- a/assets/webui/ofdgo_host.js +++ b/assets/webui/ofdgo_host.js @@ -1,3 +1,5 @@ +import { setInputFiles } from "./ofdgo_util.js"; + let engineModule = null; let engineScheduler = null; let engineSequence = 0; @@ -823,11 +825,7 @@ class NativeHost { try { const files = (await this.call("open", {multiple:input.multiple, directory:input.webkitdirectory, accept:input.accept})).map(value => new NativeFile(value)); if (!files.length) return; - const transfer = new DataTransfer(); - 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 : []}); + setInputFiles(input, files); input.dispatchEvent(new Event("input", {bubbles:true})); input.dispatchEvent(new Event("change", {bubbles:true})); } catch (error) { if (error.name !== "AbortError") onError(error); } diff --git a/assets/webui/ofdgo_util.js b/assets/webui/ofdgo_util.js index 31727d1..3f3bf61 100644 --- a/assets/webui/ofdgo_util.js +++ b/assets/webui/ofdgo_util.js @@ -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 { 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() }); @@ -258,9 +267,7 @@ export class DocumentDialogs { 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]; if (item.files !== undefined) { - const transfer = new this.document.defaultView.DataTransfer(); - for (const file of item.files) transfer.items.add(file); - node.files = transfer.files; + setInputFiles(node, item.files); } if (item.selectionStart != null && typeof node.setSelectionRange === "function") node.setSelectionRange(item.selectionStart, item.selectionEnd, item.selectionDirection); node.scrollTop = item.scrollTop; diff --git a/internal/webview/webview_files.go b/internal/webview/webview_files.go index 8f93db2..818f684 100644 --- a/internal/webview/webview_files.go +++ b/internal/webview/webview_files.go @@ -226,6 +226,7 @@ func (s *fileStore) file(id string) (map[string]any, error) { } handle := s.descriptor(id) size := int64(len(entry.data)) + var modified int64 if entry.data == nil { file, err := basefile.Open(entry.path) if err != nil { @@ -237,11 +238,12 @@ func (s *fileStore) file(id string) (map[string]any, error) { return nil, err } size = info.Size() + modified = info.ModTime().UnixMilli() id = rand.Text() entry = &fileEntry{path: entry.path, name: entry.name, file: file} 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 按授权标识读取文件并保持随机读取句柄