mirror of
https://github.com/TommyFang2077/dsh-desktop.git
synced 2026-08-17 09:06:36 +08:00
GTK/wl-paste/xclip fallbacks plus a paste-event path so ModLens can take over image paste instead of only synthesizing drop. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.8 KiB
JavaScript
180 lines
5.8 KiB
JavaScript
(function () {
|
|
const IMAGE_PATH = /^(?:file:\/\/|(?:\/|\.{1,2}\/)).+\.(?:png|jpe?g|gif|webp|bmp|tiff?)$/i;
|
|
const IMAGE_MIME = /^image\/(png|jpe?g|gif|webp|bmp|tiff?)$/i;
|
|
|
|
function looksLikeImagePath(text) {
|
|
const first = (text || "").trim().split(/\s+/, 1)[0] || "";
|
|
return IMAGE_PATH.test(first);
|
|
}
|
|
|
|
function addImageFile(out, seen, file) {
|
|
if (!file || !file.size) return;
|
|
const type = file.type || "";
|
|
if (type && !IMAGE_MIME.test(type)) return;
|
|
if (!type && !IMAGE_PATH.test(file.name || "")) return;
|
|
const key = (file.name || "") + ":" + file.size + ":" + type;
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
out.push(file);
|
|
}
|
|
|
|
function imageFilesFromEvent(e) {
|
|
const out = [];
|
|
const seen = new Set();
|
|
const data = e.clipboardData || e.dataTransfer;
|
|
if (!data) return out;
|
|
const items = data.items ? Array.from(data.items) : [];
|
|
for (const item of items) {
|
|
if (item.kind === "file" || (item.type && item.type.indexOf("image/") === 0)) {
|
|
try {
|
|
addImageFile(out, seen, item.getAsFile());
|
|
} catch (err) {}
|
|
}
|
|
}
|
|
if (data.files) {
|
|
for (const file of data.files) addImageFile(out, seen, file);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function composer() {
|
|
const el = document.activeElement;
|
|
if (el && (el.tagName === "TEXTAREA" || el.tagName === "INPUT" || el.isContentEditable)) {
|
|
return el;
|
|
}
|
|
return document.querySelector("form textarea, textarea, [contenteditable='true']");
|
|
}
|
|
|
|
function insertText(target, text) {
|
|
const el = target && (target.tagName === "TEXTAREA" || target.tagName === "INPUT")
|
|
? target
|
|
: composer();
|
|
if (!el || (el.tagName !== "TEXTAREA" && el.tagName !== "INPUT")) return;
|
|
el.focus();
|
|
let inserted = false;
|
|
try {
|
|
inserted = document.execCommand("insertText", false, text);
|
|
} catch (err) {
|
|
inserted = false;
|
|
}
|
|
if (!inserted) {
|
|
const proto = el.tagName === "TEXTAREA"
|
|
? window.HTMLTextAreaElement.prototype
|
|
: window.HTMLInputElement.prototype;
|
|
const setter = Object.getOwnPropertyDescriptor(proto, "value").set;
|
|
const start = el.selectionStart || el.value.length;
|
|
const end = el.selectionEnd || start;
|
|
setter.call(el, el.value.slice(0, start) + text + el.value.slice(end));
|
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
function dataTransferOf(files) {
|
|
const dt = new DataTransfer();
|
|
for (const file of files) dt.items.add(file);
|
|
return dt;
|
|
}
|
|
|
|
function dispatchWithData(type, target, dt, key) {
|
|
let ev;
|
|
try {
|
|
if (type === "paste") {
|
|
ev = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: dt });
|
|
} else {
|
|
ev = new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt });
|
|
}
|
|
} catch (err) {
|
|
ev = new Event(type, { bubbles: true, cancelable: true });
|
|
}
|
|
try {
|
|
Object.defineProperty(ev, key, { value: dt, configurable: true });
|
|
} catch (err) {}
|
|
target.dispatchEvent(ev);
|
|
return ev;
|
|
}
|
|
|
|
function currentModelLabel() {
|
|
const buttons = document.querySelectorAll("button[aria-label]");
|
|
for (const button of buttons) {
|
|
const label = button.getAttribute("aria-label") || "";
|
|
if (/选择模型|select model|current model/i.test(label)) return label;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function shouldTakeoverPaste() {
|
|
const label = currentModelLabel();
|
|
return fetch("/modlens/paste?model=" + encodeURIComponent(label))
|
|
.then(function (res) {
|
|
if (!res.ok) return false;
|
|
return res.json().then(function (body) {
|
|
return body && body.takeover === true;
|
|
});
|
|
})
|
|
.catch(function () {
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function uploadModlens(files) {
|
|
return Promise.all(
|
|
files.map(function (file) {
|
|
return file.arrayBuffer().then(function (buffer) {
|
|
return fetch("/modlens/paste", { method: "POST", body: buffer }).then(function (res) {
|
|
if (!res.ok) throw new Error("modlens paste failed " + res.status);
|
|
return res.json();
|
|
});
|
|
});
|
|
})
|
|
).then(function (results) {
|
|
const text = results.map(function (r) { return r.path; }).filter(Boolean).join(" ");
|
|
if (text) insertText(composer(), text + " ");
|
|
});
|
|
}
|
|
|
|
function deliverNative(files) {
|
|
const prev = window.__dshDesktopIngesting;
|
|
window.__dshDesktopIngesting = true;
|
|
try {
|
|
const dt = dataTransferOf(files);
|
|
const target = composer() || document;
|
|
const paste = dispatchWithData("paste", target, dt, "clipboardData");
|
|
if (paste.defaultPrevented) return;
|
|
dispatchWithData("drop", document, dt, "dataTransfer");
|
|
} finally {
|
|
window.__dshDesktopIngesting = prev;
|
|
}
|
|
}
|
|
|
|
window.__dshDesktopLooksLikeImagePath = looksLikeImagePath;
|
|
window.__dshDesktopImageFilesFromEvent = imageFilesFromEvent;
|
|
|
|
window.__dshDesktopIngestFiles = function (files) {
|
|
if (!files || !files.length) return Promise.resolve();
|
|
return shouldTakeoverPaste()
|
|
.then(function (takeover) {
|
|
if (takeover) return uploadModlens(files);
|
|
deliverNative(files);
|
|
})
|
|
.catch(function (err) {
|
|
console.error("dsh-desktop paste image failed", err);
|
|
});
|
|
};
|
|
|
|
window.__dshDesktopPasteFiles = function (items) {
|
|
try {
|
|
const files = [];
|
|
for (const item of items) {
|
|
const bin = atob(item.b64);
|
|
const bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
files.push(new File([bytes], item.name, { type: item.type }));
|
|
}
|
|
return window.__dshDesktopIngestFiles(files);
|
|
} catch (err) {
|
|
console.error("dsh-desktop paste image failed", err);
|
|
return Promise.resolve();
|
|
}
|
|
};
|
|
})();
|