diff --git a/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx index 0605206c4..4ab80ff7f 100644 --- a/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx +++ b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx @@ -23,6 +23,7 @@ import { ClipboardIcon, ClockIcon, Copy as CopyIcon, + FileIcon, GaugeIcon, HandshakeIcon, KeyRoundIcon, @@ -32,13 +33,19 @@ import { MonitorIcon, Radio, RefreshCwIcon, - SendIcon, + UploadIcon, WaypointsIcon, } from "lucide-react"; import { FileDrop } from "@bindings/services"; import type { PeerStatus } from "@bindings/services/models.js"; import { Button } from "@/components/buttons/Button"; import { ClipboardPreview } from "@/components/ClipboardPreview"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; import { useNavSection } from "@/contexts/NavSectionContext"; import { cn } from "@/lib/cn"; import { CopyToClipboard } from "@/components/CopyToClipboard"; @@ -256,10 +263,6 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => { - {/* Keyed on the peer so a staged clipboard - confirmation cannot carry over to a different - recipient when the selection changes. */} - { /> + {/* Keyed on the peer so a staged clipboard confirmation + cannot carry over to a different recipient when the + selection changes. */} + )} @@ -286,6 +293,8 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { const { t } = useTranslation(); const { setSection } = useNavSection(); const { setSelected } = usePeerDetail(); + const [menuOpen, setMenuOpen] = useState(false); + const [clipboard, setClipboard] = useState(""); const [error, setError] = useState(null); const [pendingText, setPendingText] = useState(null); // Deliberately not gated on connStatus: an idle peer is the normal resting @@ -294,12 +303,28 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { // to dial. const canSend = peer.ip !== ""; + useEffect(() => { + if (!menuOpen) return; + let cancelled = false; + FileDrop.ClipboardText() + .then((text) => { + if (!cancelled) setClipboard(text ?? ""); + }) + .catch(() => { + if (!cancelled) setClipboard(""); + }); + return () => { + cancelled = true; + }; + }, [menuOpen]); + const finishSend = () => { setSelected(null); setSection("files"); }; const sendFiles = async () => { + setMenuOpen(false); setError(null); try { const paths = await FileDrop.PickFiles(); @@ -313,18 +338,14 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { // Clipboard text is shown for confirmation first: sending it blind gave no // way to tell what was about to leave the machine. - const stageClipboard = async () => { + const stageClipboard = () => { + setMenuOpen(false); setError(null); - try { - const text = await FileDrop.ClipboardText(); - if (!text) { - setError(t("peers.details.sendClipboard.empty")); - return; - } - setPendingText(text); - } catch (e) { - setError(String(e)); + if (!clipboard) { + setError(t("peers.details.sendClipboard.empty")); + return; } + setPendingText(clipboard); }; // Sends the staged text rather than re-reading, so what was confirmed is @@ -342,40 +363,65 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { }; return ( -
+
{pendingText === null ? ( -
- - +
+ + + + + e.stopPropagation()} + > + void sendFiles()}> +
+ + + {t("peers.details.sendMenu.file")} + +
+
+ +
+ + + {t("peers.details.sendMenu.clipboard")} + + {!clipboard && ( + + {t("peers.details.sendClipboard.empty")} + + )} +
+
+
+
) : (
-
- +
+
)} diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index aef295f1b..295a9be0a 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Zwischenablage ist leer" }, + "peers.details.send": { + "message": "Senden" + }, + "peers.details.sendMenu.file": { + "message": "Datei…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Zwischenablage" + }, "settings.tabs.fileSharing": { "message": "Dateifreigabe" }, diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index beb61da63..3832f504f 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -1991,6 +1991,18 @@ "message": "Clipboard is empty", "description": "Error when the clipboard has no text." }, + "peers.details.send": { + "message": "Send", + "description": "Label of the single send button at the bottom of the peer details panel; opens the what-to-send menu. Keep short." + }, + "peers.details.sendMenu.file": { + "message": "File…", + "description": "Send menu entry opening the file picker. Keep short." + }, + "peers.details.sendMenu.clipboard": { + "message": "Clipboard", + "description": "Send menu entry sending the clipboard text. Keep short." + }, "settings.tabs.fileSharing": { "message": "File sharing", "description": "Settings tab for file transfer policy." diff --git a/client/ui/i18n/locales/es/common.json b/client/ui/i18n/locales/es/common.json index b1cff6f17..f2e5f07b8 100644 --- a/client/ui/i18n/locales/es/common.json +++ b/client/ui/i18n/locales/es/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "El portapapeles está vacío" }, + "peers.details.send": { + "message": "Enviar" + }, + "peers.details.sendMenu.file": { + "message": "Archivo…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Portapapeles" + }, "settings.tabs.fileSharing": { "message": "Compartir archivos" }, diff --git a/client/ui/i18n/locales/fr/common.json b/client/ui/i18n/locales/fr/common.json index 24ef53356..117bbb984 100644 --- a/client/ui/i18n/locales/fr/common.json +++ b/client/ui/i18n/locales/fr/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Le presse-papiers est vide" }, + "peers.details.send": { + "message": "Envoyer" + }, + "peers.details.sendMenu.file": { + "message": "Fichier…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Presse-papiers" + }, "settings.tabs.fileSharing": { "message": "Partage de fichiers" }, diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index 816252103..8f494f4e3 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "A vágólap üres" }, + "peers.details.send": { + "message": "Küldés" + }, + "peers.details.sendMenu.file": { + "message": "Fájl…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Vágólap" + }, "settings.tabs.fileSharing": { "message": "Fájlmegosztás" }, diff --git a/client/ui/i18n/locales/it/common.json b/client/ui/i18n/locales/it/common.json index 74412a093..999a53d4a 100644 --- a/client/ui/i18n/locales/it/common.json +++ b/client/ui/i18n/locales/it/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Gli appunti sono vuoti" }, + "peers.details.send": { + "message": "Invia" + }, + "peers.details.sendMenu.file": { + "message": "File…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Appunti" + }, "settings.tabs.fileSharing": { "message": "Condivisione file" }, diff --git a/client/ui/i18n/locales/ja/common.json b/client/ui/i18n/locales/ja/common.json index 8d31daf03..6d4f82e19 100644 --- a/client/ui/i18n/locales/ja/common.json +++ b/client/ui/i18n/locales/ja/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "クリップボードは空です" }, + "peers.details.send": { + "message": "送信" + }, + "peers.details.sendMenu.file": { + "message": "ファイル…" + }, + "peers.details.sendMenu.clipboard": { + "message": "クリップボード" + }, "settings.tabs.fileSharing": { "message": "ファイル共有" }, diff --git a/client/ui/i18n/locales/pt/common.json b/client/ui/i18n/locales/pt/common.json index 01cacd084..caafd4dd2 100644 --- a/client/ui/i18n/locales/pt/common.json +++ b/client/ui/i18n/locales/pt/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "A área de transferência está vazia" }, + "peers.details.send": { + "message": "Enviar" + }, + "peers.details.sendMenu.file": { + "message": "Arquivo…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Área de transferência" + }, "settings.tabs.fileSharing": { "message": "Compartilhamento de arquivos" }, diff --git a/client/ui/i18n/locales/ru/common.json b/client/ui/i18n/locales/ru/common.json index 99069e7a9..e844e1f1b 100644 --- a/client/ui/i18n/locales/ru/common.json +++ b/client/ui/i18n/locales/ru/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Буфер обмена пуст" }, + "peers.details.send": { + "message": "Отправить" + }, + "peers.details.sendMenu.file": { + "message": "Файл…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Буфер обмена" + }, "settings.tabs.fileSharing": { "message": "Обмен файлами" }, diff --git a/client/ui/i18n/locales/uk/common.json b/client/ui/i18n/locales/uk/common.json index 619154c5d..4aa07c734 100644 --- a/client/ui/i18n/locales/uk/common.json +++ b/client/ui/i18n/locales/uk/common.json @@ -1493,6 +1493,15 @@ "peers.details.sendClipboard.empty": { "message": "Буфер обміну порожній" }, + "peers.details.send": { + "message": "Надіслати" + }, + "peers.details.sendMenu.file": { + "message": "Файл…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Буфер обміну" + }, "settings.tabs.fileSharing": { "message": "Обмін файлами" }, diff --git a/client/ui/i18n/locales/zh-CN/common.json b/client/ui/i18n/locales/zh-CN/common.json index 17adbc366..8bece8f90 100644 --- a/client/ui/i18n/locales/zh-CN/common.json +++ b/client/ui/i18n/locales/zh-CN/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "剪贴板为空" }, + "peers.details.send": { + "message": "发送" + }, + "peers.details.sendMenu.file": { + "message": "文件…" + }, + "peers.details.sendMenu.clipboard": { + "message": "剪贴板" + }, "settings.tabs.fileSharing": { "message": "文件共享" },