feat: add remote URL attachment support and project editing endpoint
This commit is contained in:
@@ -248,10 +248,14 @@
|
||||
.project-detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.project-actions-overflow {
|
||||
display: none;
|
||||
}
|
||||
.project-star-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -18,7 +18,7 @@ export class FormManager {
|
||||
|
||||
if (content && contentCount) {
|
||||
content.addEventListener("input", () => {
|
||||
contentCount.textContent = `${content.value.length}/2000`;
|
||||
contentCount.textContent = `${content.value.length}/125000`;
|
||||
});
|
||||
}
|
||||
if (title && titleCount) {
|
||||
|
||||
@@ -63,6 +63,14 @@ export class ProfileEditor {
|
||||
hiddenInput.value = values.join(",");
|
||||
};
|
||||
|
||||
const seeded = hiddenInput.value;
|
||||
hiddenInput.value = "";
|
||||
seeded
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.forEach(addPlatform);
|
||||
|
||||
platformsInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -239,6 +239,9 @@ export default class ContainerTerminalElement extends FloatingWindow {
|
||||
_contextItems() {
|
||||
const slug = this.getAttribute("project-slug");
|
||||
return [
|
||||
{ label: "Copy", disabled: !(this.term && this.term.hasSelection()), onSelect: () => this._copySelection() },
|
||||
{ label: "Paste", onSelect: () => this._paste() },
|
||||
{ separator: true },
|
||||
{ label: "Sync files", onSelect: () => this._lifecycle("sync", "Files synced") },
|
||||
{ label: "Restart", onSelect: () => this._lifecycle("restart", "Restart requested") },
|
||||
{ label: "Terminate", danger: true, onSelect: () => this._terminate() },
|
||||
@@ -279,6 +282,52 @@ export default class ContainerTerminalElement extends FloatingWindow {
|
||||
this.close();
|
||||
}
|
||||
|
||||
_copySelection() {
|
||||
if (!this.term || !this.term.hasSelection()) return;
|
||||
this._clipboardWrite(this.term.getSelection());
|
||||
}
|
||||
|
||||
async _paste() {
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||
this._toast("clipboard read unavailable", "error");
|
||||
return;
|
||||
}
|
||||
let text = "";
|
||||
try {
|
||||
text = await navigator.clipboard.readText();
|
||||
} catch (error) {
|
||||
this._toast("paste failed", "error");
|
||||
return;
|
||||
}
|
||||
if (text && this._ws && this._ws.readyState === 1) this._ws.send(text);
|
||||
if (this.term) this.term.focus();
|
||||
}
|
||||
|
||||
_clipboardWrite(text) {
|
||||
if (!text) return;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).catch(() => this._clipboardWriteFallback(text));
|
||||
} else {
|
||||
this._clipboardWriteFallback(text);
|
||||
}
|
||||
}
|
||||
|
||||
_clipboardWriteFallback(text) {
|
||||
const area = document.createElement("textarea");
|
||||
area.value = text;
|
||||
area.style.position = "fixed";
|
||||
area.style.opacity = "0";
|
||||
document.body.appendChild(area);
|
||||
area.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
} catch (error) {
|
||||
document.body.removeChild(area);
|
||||
return;
|
||||
}
|
||||
document.body.removeChild(area);
|
||||
}
|
||||
|
||||
_navigate(url) {
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
||||
@@ -297,7 +297,11 @@ export default class DeviiTerminalElement extends FloatingWindow {
|
||||
}
|
||||
|
||||
_contextItems() {
|
||||
const selection = window.getSelection ? String(window.getSelection()) : "";
|
||||
return [
|
||||
{ label: "Copy", disabled: !selection && !this.input.value, onSelect: () => this._copySelection() },
|
||||
{ label: "Paste", onSelect: () => this._paste() },
|
||||
{ separator: true },
|
||||
{ label: "Minimize", onSelect: () => this.minimizeWindow() },
|
||||
{ label: "Normalize", onSelect: () => this.normalizeWindow() },
|
||||
{ separator: true },
|
||||
@@ -305,6 +309,47 @@ export default class DeviiTerminalElement extends FloatingWindow {
|
||||
];
|
||||
}
|
||||
|
||||
_copySelection() {
|
||||
const selection = window.getSelection ? String(window.getSelection()) : "";
|
||||
const text = selection || this.input.value;
|
||||
if (!text) return;
|
||||
this._clipboardWrite(text);
|
||||
}
|
||||
|
||||
async _paste() {
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||
this._error("clipboard read unavailable.");
|
||||
return;
|
||||
}
|
||||
let text = "";
|
||||
try {
|
||||
text = await navigator.clipboard.readText();
|
||||
} catch (error) {
|
||||
this._error("paste failed.");
|
||||
return;
|
||||
}
|
||||
this._insertInput(text);
|
||||
}
|
||||
|
||||
_insertInput(text) {
|
||||
if (!text) return;
|
||||
const start = this.input.selectionStart != null ? this.input.selectionStart : this.input.value.length;
|
||||
const end = this.input.selectionEnd != null ? this.input.selectionEnd : this.input.value.length;
|
||||
this.input.value = this.input.value.slice(0, start) + text + this.input.value.slice(end);
|
||||
const caret = start + text.length;
|
||||
this.input.focus({ preventScroll: true });
|
||||
this.input.setSelectionRange(caret, caret);
|
||||
}
|
||||
|
||||
_clipboardWrite(text) {
|
||||
if (!text) return;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).catch(() => this._fallbackCopy(text, () => {}));
|
||||
} else {
|
||||
this._fallbackCopy(text, () => {});
|
||||
}
|
||||
}
|
||||
|
||||
_fontKey() {
|
||||
return `${FONT_KEY}:${userScope()}`;
|
||||
}
|
||||
@@ -583,8 +628,27 @@ export default class DeviiTerminalElement extends FloatingWindow {
|
||||
}
|
||||
|
||||
_append(node) {
|
||||
const pinned = this._atBottom();
|
||||
this.output.appendChild(node);
|
||||
this.output.scrollTop = this.output.scrollHeight;
|
||||
this._scrollOnMediaLoad(node, pinned);
|
||||
}
|
||||
|
||||
_atBottom() {
|
||||
const slack = 40;
|
||||
return this.output.scrollHeight - this.output.scrollTop - this.output.clientHeight <= slack;
|
||||
}
|
||||
|
||||
_scrollOnMediaLoad(node, pinned) {
|
||||
const images = node.querySelectorAll ? node.querySelectorAll("img") : [];
|
||||
images.forEach((image) => {
|
||||
if (image.complete) return;
|
||||
const settle = () => {
|
||||
if (pinned) this.output.scrollTop = this.output.scrollHeight;
|
||||
};
|
||||
image.addEventListener("load", settle, { once: true });
|
||||
image.addEventListener("error", settle, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
_onOutputClick(event) {
|
||||
|
||||
Reference in New Issue
Block a user