diff --git a/frontend/app/components/TerminalTyper.tsx b/frontend/app/components/TerminalTyper.tsx index f1da62b..1d6f85f 100644 --- a/frontend/app/components/TerminalTyper.tsx +++ b/frontend/app/components/TerminalTyper.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { LINKS } from "@/lib/links"; +import { copyText } from "@/lib/clipboard"; export interface TerminalLinks { dockerImage?: string; @@ -254,10 +255,10 @@ export default function TerminalTyper({ mode = "docker-run", links = {}, casaosM .join("\n"); } - navigator.clipboard.writeText(textToCopy).then(() => { + copyText(textToCopy).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); - }); + }).catch(() => {}); }; const copyCasaOs = () => { @@ -265,10 +266,10 @@ export default function TerminalTyper({ mode = "docker-run", links = {}, casaosM const text = CASAOS_LINES.filter(line => line.type === "output") .map(line => line.text) .join("\n"); - navigator.clipboard.writeText(text).then(() => { + copyText(text).then(() => { setCopiedCasaOs(true); setTimeout(() => setCopiedCasaOs(false), 2000); - }); + }).catch(() => {}); }; useEffect(() => { diff --git a/frontend/app/files/[token]/CopyLinkButton.tsx b/frontend/app/files/[token]/CopyLinkButton.tsx index 3fdba94..bc5f7fa 100644 --- a/frontend/app/files/[token]/CopyLinkButton.tsx +++ b/frontend/app/files/[token]/CopyLinkButton.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { copyText } from "@/lib/clipboard"; export default function CopyLinkButton({ token }: { token: string }) { const [copied, setCopied] = useState(false); @@ -8,21 +9,11 @@ export default function CopyLinkButton({ token }: { token: string }) { async function copy() { const url = `${window.location.origin}/files/${token}`; try { - await navigator.clipboard.writeText(url); + await copyText(url); setCopied(true); setTimeout(() => setCopied(false), 2500); } catch { - // Fallback for browsers that deny clipboard access - const el = document.createElement("textarea"); - el.value = url; - el.style.position = "fixed"; - el.style.opacity = "0"; - document.body.appendChild(el); - el.select(); - document.execCommand("copy"); - document.body.removeChild(el); - setCopied(true); - setTimeout(() => setCopied(false), 2500); + // copy completely unavailable (e.g. sandboxed iframe) — do nothing } } diff --git a/frontend/lib/clipboard.ts b/frontend/lib/clipboard.ts new file mode 100644 index 0000000..944a9d3 --- /dev/null +++ b/frontend/lib/clipboard.ts @@ -0,0 +1,33 @@ +/** + * Copies text to the clipboard. + * + * Tries the modern Clipboard API first (requires secure context / HTTPS). + * Falls back to the legacy textarea + execCommand technique, which works on + * plain HTTP and private-IP origins where navigator.clipboard is unavailable. + */ +export async function copyText(text: string): Promise { + // Modern path — secure context (HTTPS or localhost) + if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text) + return + } catch { + // Fall through to legacy method + } + } + + // Legacy fallback — works on HTTP / private IPs + const el = document.createElement('textarea') + el.value = text + // Keep it out of the viewport so it doesn't flash + el.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none;' + document.body.appendChild(el) + el.focus() + el.select() + try { + // eslint-disable-next-line @typescript-eslint/no-deprecated + document.execCommand('copy') + } finally { + document.body.removeChild(el) + } +}