diff --git a/client/ui/frontend/src/components/ClipboardPreview.tsx b/client/ui/frontend/src/components/ClipboardPreview.tsx
new file mode 100644
index 000000000..626a5f4e1
--- /dev/null
+++ b/client/ui/frontend/src/components/ClipboardPreview.tsx
@@ -0,0 +1,40 @@
+import { useTranslation } from "react-i18next";
+import { cn } from "@/lib/cn";
+import { formatBytes } from "@/lib/formatters";
+
+// Enough to recognise which snippet is on the clipboard without turning the
+// preview into a text editor; the box clamps to three lines on top of this.
+const PREVIEW_CHARS = 280;
+
+export const ClipboardPreview = ({ text, className }: { text: string; className?: string }) => {
+ const { t } = useTranslation();
+ const clipped = text.length > PREVIEW_CHARS;
+ const bytes = new TextEncoder().encode(text).length;
+
+ return (
+
+
+ {clipped ? `${text.slice(0, PREVIEW_CHARS)}…` : text}
+
+
+ {formatBytes(bytes)}
+
+
+ );
+};
diff --git a/client/ui/frontend/src/components/buttons/Button.tsx b/client/ui/frontend/src/components/buttons/Button.tsx
index 6b151c17b..c1a5a4d9f 100644
--- a/client/ui/frontend/src/components/buttons/Button.tsx
+++ b/client/ui/frontend/src/components/buttons/Button.tsx
@@ -91,6 +91,11 @@ const buttonVariants = cva(
danger: [
"dark:bg-red-600 dark:text-red-100 dark:hover:border-red-800/50 hover:dark:bg-red-700 dark:focus:bg-red-700 dark:focus:ring-red-700/20",
],
+ yellow: [
+ "enabled:bg-yellow-300 enabled:text-yellow-900 enabled:hover:bg-yellow-200 enabled:focus:ring-yellow-300/50",
+ "dark:ring-offset-neutral-950/50 dark:focus:ring-yellow-300/40",
+ "enabled:dark:bg-yellow-300 enabled:dark:text-yellow-900 enabled:dark:hover:bg-yellow-200 disabled:dark:bg-nb-gray-900",
+ ],
},
size: {
xs: "px-3.5 py-2.5 text-xs",
diff --git a/client/ui/frontend/src/globals.css b/client/ui/frontend/src/globals.css
index 84ddfdcfe..e07da9de2 100644
--- a/client/ui/frontend/src/globals.css
+++ b/client/ui/frontend/src/globals.css
@@ -43,3 +43,16 @@ body {
.wails-no-draggable {
--wails-draggable: no-drag;
}
+
+/*
+ * Radix wraps ScrollArea.Viewport children in a div with
+ * `min-width:100%; display:table`. A table box is shrink-to-fit, so it grows
+ * to the widest row's intrinsic width instead of stopping at 100%, which
+ * defeats `truncate` on a long file name and pushes the row out sideways.
+ * The window is a fixed width and cannot be resized, so the content box is
+ * capped here and the rows truncate as intended.
+ */
+.nb-scroll-clamp [data-radix-scroll-area-viewport] > div {
+ display: block !important;
+ max-width: 100%;
+}
diff --git a/client/ui/frontend/src/modules/main/advanced/files/Files.tsx b/client/ui/frontend/src/modules/main/advanced/files/Files.tsx
index 295a7691e..9e9e68d79 100644
--- a/client/ui/frontend/src/modules/main/advanced/files/Files.tsx
+++ b/client/ui/frontend/src/modules/main/advanced/files/Files.tsx
@@ -7,16 +7,18 @@ import {
ArrowUpIcon,
BanIcon,
CheckIcon,
+ ChevronDownIcon,
CopyIcon,
FolderDownIcon,
FolderOpenIcon,
MoreVerticalIcon,
+ SendIcon,
ShieldCheckIcon,
Trash2Icon,
XIcon,
} from "lucide-react";
import { FileDrop } from "@bindings/services";
-import type { FileDropTransfer } from "@bindings/services/models.js";
+import type { FileDropTransfer, PeerStatus } from "@bindings/services/models.js";
import { cn } from "@/lib/cn";
import { formatBytes } from "@/lib/formatters";
import {
@@ -31,6 +33,9 @@ import { EmptyState } from "@/components/empty-state/EmptyState";
import { NoResults } from "@/components/empty-state/NoResults";
import { Button } from "@/components/buttons/Button";
import { Tooltip } from "@/components/Tooltip";
+import { TruncatedText } from "@/components/TruncatedText";
+import { useConfirm } from "@/contexts/DialogContext";
+import { SendPeerPicker } from "@/modules/main/advanced/files/SendPeerPicker";
import {
DropdownMenu,
DropdownMenuContent,
@@ -133,15 +138,7 @@ export const Files = () => {
});
}, [rest, t]);
- if (transfers !== null && transfers.length === 0) {
- return (
-
- );
- }
+ const isEmpty = transfers !== null && transfers.length === 0;
return (
@@ -154,11 +151,21 @@ export const Files = () => {
onChange={(e) => setSearch(e.target.value)}
/>
+
- {filtered.length === 0 ? (
+ {isEmpty ? (
+
+ ) : filtered.length === 0 ? (
) : (
-
+
{pending.map((tr) => (
@@ -200,6 +207,64 @@ export const Files = () => {
);
};
+const SendActions = ({ onSent }: { onSent: () => void }) => {
+ const { t } = useTranslation();
+ const [error, setError] = useState
(null);
+
+ const sendFiles = async (peer: PeerStatus) => {
+ setError(null);
+ try {
+ const paths = await FileDrop.PickFiles();
+ if (!paths || paths.length === 0) return;
+ await FileDrop.Send(peer.pubKey, paths, "");
+ onSent();
+ } catch (e) {
+ setError(String(e));
+ }
+ };
+
+ // Sends the text the picker previewed, not a fresh read: the clipboard may
+ // have changed between showing it and confirming the recipient.
+ const sendClipboard = async (peer: PeerStatus, text: string) => {
+ setError(null);
+ if (!text) {
+ setError(t("peers.details.sendClipboard.empty"));
+ return;
+ }
+ try {
+ await FileDrop.Send(peer.pubKey, [], text);
+ onSent();
+ } catch (e) {
+ setError(String(e));
+ }
+ };
+
+ return (
+
+
+ void (mode === "files" ? sendFiles(peer) : sendClipboard(peer, clipboard))
+ }
+ >
+
+
+ {t("files.send.trigger")}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
type RowProps = {
transfer: FileDropTransfer;
onChanged: () => void;
@@ -232,15 +297,18 @@ const PendingOfferRow = ({ transfer, onChanged }: RowProps) => {
)}
>
-
+
-
- {transferTitle(transfer)}
-
+
+
+
{" · "}
{formatBytes(transfer.totalSize)}
@@ -286,14 +354,9 @@ const TransferRow = ({ transfer, onChanged }: RowProps) => {
? Math.min(100, Math.floor((transfer.transferred / transfer.totalSize) * 100))
: null;
- // The subtitle carries who and how big; the outcome sits on the right next
- // to the time, so a glance down that column reads the results.
- const subtitleParts = [
- transfer.outgoing
- ? t("files.row.to", { peer: transfer.peerName })
- : t("files.row.from", { peer: transfer.peerName }),
- ];
- if (!isText) subtitleParts.push(formatBytes(transfer.totalSize));
+ const peerLabel = transfer.outgoing
+ ? t("files.row.to", { peer: transfer.peerName })
+ : t("files.row.from", { peer: transfer.peerName });
let stateLabel: string;
if (progress !== null) {
@@ -335,39 +398,47 @@ const TransferRow = ({ transfer, onChanged }: RowProps) => {
aria-hidden={"true"}
/>
)}
-
-
+
- {transferTitle(transfer)}
-
-
- {subtitleParts.join(" · ")}
+ />
+
+
+ {time}
+ {!isText && (
+
+ {" · "}
+ {formatBytes(transfer.totalSize)}
+
+ )}
- {/* Time over outcome, capped: the window is a fixed 900px and some
- translations of the state labels are long enough to eat the file
- name if they share one line with the timestamp. */}
+ {/* Own column, capped: the window is a fixed 900px and some
+ translations of the state labels are long enough to eat the
+ file name if they are given free rein. */}
- {time}
- {stateLabel}
+ {stateLabel}
{/* The window is a fixed 900px wide and cannot be resized, so the
- row actions overlay the outcome column on hover instead of
+ row actions overlay the status column on hover instead of
reserving width that the file name would otherwise lose. */}
{
const { t } = useTranslation();
+ const confirm = useConfirm();
const setRule = async (rule: PeerRule) => {
await FileDrop.SetPeerRule(transfer.peerKey, rule);
onChanged();
};
+ const remove = async () => {
+ const ok = await confirm({
+ title: t("files.delete.title"),
+ description: t("files.delete.message"),
+ confirmLabel: t("common.delete"),
+ danger: true,
+ });
+ if (!ok) return;
+ await FileDrop.Delete(transfer.id);
+ onChanged();
+ };
+
return (
-
+ // modal={false}: the item handler opens a confirm dialog, and a modal
+ // dropdown would keep the focus trap that the dialog then fights over.
+
>
)}
- {
- await FileDrop.Delete(transfer.id);
- onChanged();
- }}
- >
+ void remove()}>
{t("files.action.delete")}
diff --git a/client/ui/frontend/src/modules/main/advanced/files/SendPeerPicker.tsx b/client/ui/frontend/src/modules/main/advanced/files/SendPeerPicker.tsx
new file mode 100644
index 000000000..739c6e744
--- /dev/null
+++ b/client/ui/frontend/src/modules/main/advanced/files/SendPeerPicker.tsx
@@ -0,0 +1,275 @@
+import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import * as Popover from "@radix-ui/react-popover";
+import * as ScrollArea from "@radix-ui/react-scroll-area";
+import { Command } from "cmdk";
+import { ChevronLeftIcon, ClipboardIcon, Search, SendIcon } from "lucide-react";
+import { FileDrop } from "@bindings/services";
+import type { PeerStatus } from "@bindings/services/models.js";
+import { cn } from "@/lib/cn";
+import { shortenDns } from "@/lib/formatters";
+import { useStatus } from "@/contexts/StatusContext";
+import { ClipboardPreview } from "@/components/ClipboardPreview";
+import { dotClass, peerStatusLabelKey } from "@/modules/main/advanced/peers/Peers";
+
+export type SendMode = "files" | "clipboard";
+
+type Props = {
+ children: ReactNode;
+ onPick: (peer: PeerStatus, mode: SendMode, clipboard: string) => void;
+};
+
+const itemClass = cn(
+ "my-0.5 flex cursor-default items-center gap-2",
+ "rounded-md px-2 py-2 outline-none",
+ "text-xs text-nb-gray-200",
+ "data-[selected=true]:bg-nb-gray-850 data-[selected=true]:text-nb-gray-50",
+);
+
+const noticeClass = "px-3 py-4 text-center text-[0.7rem] text-nb-gray-400";
+
+const modeLabelKey = (mode: SendMode): string =>
+ mode === "files" ? "peers.details.sendFile" : "peers.details.sendClipboard";
+
+// What to send first, then to whom: the payload is the cheap, two-option
+// decision, so it gets out of the way before the list that needs searching.
+export const SendPeerPicker = ({ children, onPick }: Props) => {
+ const [open, setOpen] = useState(false);
+ const [mode, setMode] = useState(null);
+ const [clipboard, setClipboard] = useState("");
+ const { status } = useStatus();
+
+ // Read on open so the clipboard entry can be disabled and previewed before
+ // the peer is chosen, instead of failing after it.
+ useEffect(() => {
+ if (!open) return;
+ let cancelled = false;
+ FileDrop.ClipboardText()
+ .then((text) => {
+ if (!cancelled) setClipboard(text ?? "");
+ })
+ .catch(() => {
+ if (!cancelled) setClipboard("");
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [open]);
+
+ // A peer without an overlay address has nothing to dial. Connection state
+ // is deliberately not a filter: an idle peer is the normal resting state
+ // under lazy connections, and the transfer's own packets wake it.
+ const peers = useMemo(
+ () =>
+ (status?.peers ?? [])
+ .filter((p) => p.ip !== "")
+ .sort((a, b) =>
+ (a.fqdn || a.ip).toLowerCase().localeCompare((b.fqdn || b.ip).toLowerCase()),
+ ),
+ [status?.peers],
+ );
+
+ const setOpenState = (next: boolean) => {
+ setOpen(next);
+ if (!next) setMode(null);
+ };
+
+ const choose = (peer: PeerStatus) => {
+ if (!mode) return;
+ const picked = mode;
+ const text = clipboard;
+ setOpenState(false);
+ onPick(peer, picked, text);
+ };
+
+ return (
+
+ {children}
+
+
+ {mode ? (
+ setMode(null)}
+ onSelect={choose}
+ />
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+const ModeStep = ({
+ clipboard,
+ onChoose,
+}: {
+ clipboard: string;
+ onChoose: (mode: SendMode) => void;
+}) => {
+ const { t } = useTranslation();
+ const rootRef = useRef(null);
+ const hasClipboard = clipboard !== "";
+
+ // This step has no Command.Input, and cmdk drives arrows/Enter from the
+ // focused root, so focus it on mount to keep the flow keyboard-navigable.
+ useEffect(() => {
+ rootRef.current?.focus();
+ }, []);
+
+ return (
+
+
+ onChoose("files")}
+ className={itemClass}
+ >
+
+ {t("peers.details.sendFile")}
+
+ onChoose("clipboard")}
+ className={cn(itemClass, "data-[disabled=true]:opacity-40")}
+ >
+
+
+ {t("peers.details.sendClipboard")}
+
+ {!hasClipboard && (
+
+ {t("peers.details.sendClipboard.empty")}
+
+ )}
+
+
+
+ );
+};
+
+const PeerStep = ({
+ mode,
+ clipboard,
+ peers,
+ onBack,
+ onSelect,
+}: {
+ mode: SendMode;
+ clipboard: string;
+ peers: PeerStatus[];
+ onBack: () => void;
+ onSelect: (peer: PeerStatus) => void;
+}) => {
+ const { t } = useTranslation();
+
+ return (
+ {
+ // Escape steps back to the mode choice rather than discarding
+ // the whole flow; a second Escape then closes the popover.
+ if (e.key === "Escape") {
+ e.preventDefault();
+ e.stopPropagation();
+ onBack();
+ }
+ }}
+ >
+
+
+
+ {t(modeLabelKey(mode))}
+
+
+ {mode === "clipboard" && }
+
+
+
+
+
+
+
+
+
+ {t("files.send.empty")}
+
+ {peers.length === 0 && (
+ {t("files.send.noPeers")}
+ )}
+ {peers.map((peer) => (
+ onSelect(peer)}
+ className={itemClass}
+ >
+
+
+ {shortenDns(peer.fqdn) || peer.ip}
+
+
+ {t(peerStatusLabelKey(peer.connStatus))}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
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 71ccbf278..0605206c4 100644
--- a/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx
+++ b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx
@@ -38,6 +38,7 @@ import {
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 { useNavSection } from "@/contexts/NavSectionContext";
import { cn } from "@/lib/cn";
import { CopyToClipboard } from "@/components/CopyToClipboard";
@@ -255,7 +256,10 @@ 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. */}
+
{
const { setSection } = useNavSection();
const { setSelected } = usePeerDetail();
const [error, setError] = useState(null);
+ const [pendingText, setPendingText] = useState(null);
// Deliberately not gated on connStatus: an idle peer is the normal resting
// state under lazy connections, and the outgoing packets of the transfer are
// exactly what wakes it. Only a peer without an overlay address has nothing
@@ -306,7 +311,9 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => {
}
};
- const sendClipboard = async () => {
+ // 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 () => {
setError(null);
try {
const text = await FileDrop.ClipboardText();
@@ -314,7 +321,20 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => {
setError(t("peers.details.sendClipboard.empty"));
return;
}
- await FileDrop.Send(peer.pubKey, [], text);
+ setPendingText(text);
+ } catch (e) {
+ setError(String(e));
+ }
+ };
+
+ // Sends the staged text rather than re-reading, so what was confirmed is
+ // what goes out.
+ const confirmClipboard = async () => {
+ if (pendingText === null) return;
+ setError(null);
+ try {
+ await FileDrop.Send(peer.pubKey, [], pendingText);
+ setPendingText(null);
finishSend();
} catch (e) {
setError(String(e));
@@ -323,26 +343,52 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => {
return (
-
- void sendFiles()}
- >
-
- {t("peers.details.sendFile")}
-
- void sendClipboard()}
- >
-
- {t("peers.details.sendClipboard")}
-
-
+ {pendingText === null ? (
+
+ void sendFiles()}
+ >
+
+ {t("peers.details.sendFile")}
+
+ void stageClipboard()}
+ >
+
+ {t("peers.details.sendClipboard")}
+
+
+ ) : (
+
+
+
+ void confirmClipboard()}
+ >
+
+ {t("peers.details.sendClipboard")}
+
+ {
+ setPendingText(null);
+ setError(null);
+ }}
+ >
+ {t("common.cancel")}
+
+
+
+ )}
{error &&
{error}
}
);
diff --git a/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx b/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx
index 4ae61b8b4..d20e909f3 100644
--- a/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx
+++ b/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx
@@ -31,7 +31,7 @@ import { PeerFilters, type StatusFilter } from "./PeerFilters";
const isOnline = (connStatus: string) => connStatus === "Connected";
-const dotClass = (connStatus: string): string => {
+export const dotClass = (connStatus: string): string => {
switch (connStatus) {
case "Connected":
return "bg-green-400";
diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json
index 91317185f..aef295f1b 100644
--- a/client/ui/i18n/locales/de/common.json
+++ b/client/ui/i18n/locales/de/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Löschen"
},
+ "files.delete.title": {
+ "message": "Aus dem Verlauf entfernen?"
+ },
+ "files.delete.message": {
+ "message": "Dieser Eintrag wird aus dem Übertragungsverlauf entfernt.\nBereits empfangene Dateien bleiben auf der Festplatte."
+ },
+ "files.send.search": {
+ "message": "Peers suchen"
+ },
+ "files.send.empty": {
+ "message": "Kein passender Peer"
+ },
+ "files.send.noPeers": {
+ "message": "Keine Peers verfügbar"
+ },
+ "files.send.trigger": {
+ "message": "Senden…"
+ },
+ "files.send.back": {
+ "message": "Zurück"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Vorschau der Zwischenablage"
+ },
"peers.dropToSend": {
"message": "Zum Senden ablegen"
},
diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json
index 710654320..beb61da63 100644
--- a/client/ui/i18n/locales/en/common.json
+++ b/client/ui/i18n/locales/en/common.json
@@ -1943,6 +1943,38 @@
"message": "Delete",
"description": "Remove the entry from the history."
},
+ "files.delete.title": {
+ "message": "Remove from history?",
+ "description": "Confirmation-dialog title for removing a transfer from the file history."
+ },
+ "files.delete.message": {
+ "message": "This entry is removed from the transfer history.\nAlready received files are kept on disk.",
+ "description": "Confirmation body for removing a history entry; states that received files stay on disk. Contains a line break (\\n) — keep it."
+ },
+ "files.send.search": {
+ "message": "Search peers",
+ "description": "Placeholder of the peer-picker search field."
+ },
+ "files.send.empty": {
+ "message": "No matching peer",
+ "description": "Shown when the peer-picker search matches nothing."
+ },
+ "files.send.noPeers": {
+ "message": "No peers available",
+ "description": "Shown in the peer picker when the peer list is empty."
+ },
+ "files.send.trigger": {
+ "message": "Send…",
+ "description": "Label of the single send button in the Files header. Keep short."
+ },
+ "files.send.back": {
+ "message": "Back",
+ "description": "Returns from the what-to-send step to the peer list. Keep short."
+ },
+ "files.send.clipboardPreview": {
+ "message": "Clipboard preview",
+ "description": "Accessible label of the clipboard-text preview shown before sending."
+ },
"peers.dropToSend": {
"message": "Drop to send",
"description": "Overlay shown while dragging files over a peer row."
diff --git a/client/ui/i18n/locales/es/common.json b/client/ui/i18n/locales/es/common.json
index 28a4b2d35..b1cff6f17 100644
--- a/client/ui/i18n/locales/es/common.json
+++ b/client/ui/i18n/locales/es/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Eliminar"
},
+ "files.delete.title": {
+ "message": "¿Quitar del historial?"
+ },
+ "files.delete.message": {
+ "message": "Esta entrada se quitará del historial de transferencias.\nLos archivos ya recibidos se conservan en el disco."
+ },
+ "files.send.search": {
+ "message": "Buscar peers"
+ },
+ "files.send.empty": {
+ "message": "Ningún peer coincide"
+ },
+ "files.send.noPeers": {
+ "message": "No hay peers disponibles"
+ },
+ "files.send.trigger": {
+ "message": "Enviar…"
+ },
+ "files.send.back": {
+ "message": "Atrás"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Vista previa del portapapeles"
+ },
"peers.dropToSend": {
"message": "Suelta para enviar"
},
diff --git a/client/ui/i18n/locales/fr/common.json b/client/ui/i18n/locales/fr/common.json
index ace8b1269..24ef53356 100644
--- a/client/ui/i18n/locales/fr/common.json
+++ b/client/ui/i18n/locales/fr/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Supprimer"
},
+ "files.delete.title": {
+ "message": "Retirer de l’historique ?"
+ },
+ "files.delete.message": {
+ "message": "Cette entrée est retirée de l’historique des transferts.\nLes fichiers déjà reçus sont conservés sur le disque."
+ },
+ "files.send.search": {
+ "message": "Rechercher des pairs"
+ },
+ "files.send.empty": {
+ "message": "Aucun pair correspondant"
+ },
+ "files.send.noPeers": {
+ "message": "Aucun pair disponible"
+ },
+ "files.send.trigger": {
+ "message": "Envoyer…"
+ },
+ "files.send.back": {
+ "message": "Retour"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Aperçu du presse-papiers"
+ },
"peers.dropToSend": {
"message": "Déposer pour envoyer"
},
diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json
index 8a886182b..816252103 100644
--- a/client/ui/i18n/locales/hu/common.json
+++ b/client/ui/i18n/locales/hu/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Törlés"
},
+ "files.delete.title": {
+ "message": "Eltávolítás az előzményekből?"
+ },
+ "files.delete.message": {
+ "message": "A bejegyzés törlődik az átviteli előzményekből.\nA már fogadott fájlok a lemezen maradnak."
+ },
+ "files.send.search": {
+ "message": "Gépek keresése"
+ },
+ "files.send.empty": {
+ "message": "Nincs találat"
+ },
+ "files.send.noPeers": {
+ "message": "Nincs elérhető gép"
+ },
+ "files.send.trigger": {
+ "message": "Küldés…"
+ },
+ "files.send.back": {
+ "message": "Vissza"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Vágólap előnézete"
+ },
"peers.dropToSend": {
"message": "Engedd el a küldéshez"
},
diff --git a/client/ui/i18n/locales/it/common.json b/client/ui/i18n/locales/it/common.json
index 3c95e3afa..74412a093 100644
--- a/client/ui/i18n/locales/it/common.json
+++ b/client/ui/i18n/locales/it/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Elimina"
},
+ "files.delete.title": {
+ "message": "Rimuovere dalla cronologia?"
+ },
+ "files.delete.message": {
+ "message": "Questa voce viene rimossa dalla cronologia dei trasferimenti.\nI file già ricevuti vengono conservati su disco."
+ },
+ "files.send.search": {
+ "message": "Cerca peer"
+ },
+ "files.send.empty": {
+ "message": "Nessun peer corrispondente"
+ },
+ "files.send.noPeers": {
+ "message": "Nessun peer disponibile"
+ },
+ "files.send.trigger": {
+ "message": "Invia…"
+ },
+ "files.send.back": {
+ "message": "Indietro"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Anteprima degli appunti"
+ },
"peers.dropToSend": {
"message": "Rilascia per inviare"
},
diff --git a/client/ui/i18n/locales/ja/common.json b/client/ui/i18n/locales/ja/common.json
index c05724a15..8d31daf03 100644
--- a/client/ui/i18n/locales/ja/common.json
+++ b/client/ui/i18n/locales/ja/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "削除"
},
+ "files.delete.title": {
+ "message": "履歴から削除しますか?"
+ },
+ "files.delete.message": {
+ "message": "この項目は転送履歴から削除されます。\n受信済みのファイルはディスクに残ります。"
+ },
+ "files.send.search": {
+ "message": "ピアを検索"
+ },
+ "files.send.empty": {
+ "message": "一致するピアがありません"
+ },
+ "files.send.noPeers": {
+ "message": "利用可能なピアがありません"
+ },
+ "files.send.trigger": {
+ "message": "送信…"
+ },
+ "files.send.back": {
+ "message": "戻る"
+ },
+ "files.send.clipboardPreview": {
+ "message": "クリップボードのプレビュー"
+ },
"peers.dropToSend": {
"message": "ドロップして送信"
},
diff --git a/client/ui/i18n/locales/pt/common.json b/client/ui/i18n/locales/pt/common.json
index d3ef2166a..01cacd084 100644
--- a/client/ui/i18n/locales/pt/common.json
+++ b/client/ui/i18n/locales/pt/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Excluir"
},
+ "files.delete.title": {
+ "message": "Remover do histórico?"
+ },
+ "files.delete.message": {
+ "message": "Esta entrada é removida do histórico de transferências.\nOs ficheiros já recebidos são mantidos no disco."
+ },
+ "files.send.search": {
+ "message": "Pesquisar peers"
+ },
+ "files.send.empty": {
+ "message": "Nenhum peer correspondente"
+ },
+ "files.send.noPeers": {
+ "message": "Nenhum peer disponível"
+ },
+ "files.send.trigger": {
+ "message": "Enviar…"
+ },
+ "files.send.back": {
+ "message": "Voltar"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Pré-visualização da área de transferência"
+ },
"peers.dropToSend": {
"message": "Solte para enviar"
},
diff --git a/client/ui/i18n/locales/ru/common.json b/client/ui/i18n/locales/ru/common.json
index dd85936dd..99069e7a9 100644
--- a/client/ui/i18n/locales/ru/common.json
+++ b/client/ui/i18n/locales/ru/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Удалить"
},
+ "files.delete.title": {
+ "message": "Удалить из истории?"
+ },
+ "files.delete.message": {
+ "message": "Запись будет удалена из истории передач.\nУже полученные файлы останутся на диске."
+ },
+ "files.send.search": {
+ "message": "Поиск узлов"
+ },
+ "files.send.empty": {
+ "message": "Нет подходящих узлов"
+ },
+ "files.send.noPeers": {
+ "message": "Нет доступных узлов"
+ },
+ "files.send.trigger": {
+ "message": "Отправить…"
+ },
+ "files.send.back": {
+ "message": "Назад"
+ },
+ "files.send.clipboardPreview": {
+ "message": "Предпросмотр буфера обмена"
+ },
"peers.dropToSend": {
"message": "Отпустите для отправки"
},
diff --git a/client/ui/i18n/locales/zh-CN/common.json b/client/ui/i18n/locales/zh-CN/common.json
index f7528f48c..17adbc366 100644
--- a/client/ui/i18n/locales/zh-CN/common.json
+++ b/client/ui/i18n/locales/zh-CN/common.json
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "删除"
},
+ "files.delete.title": {
+ "message": "从历史记录中移除?"
+ },
+ "files.delete.message": {
+ "message": "此条目将从传输历史记录中移除。\n已接收的文件会保留在磁盘上。"
+ },
+ "files.send.search": {
+ "message": "搜索对等节点"
+ },
+ "files.send.empty": {
+ "message": "没有匹配的对等节点"
+ },
+ "files.send.noPeers": {
+ "message": "没有可用的对等节点"
+ },
+ "files.send.trigger": {
+ "message": "发送…"
+ },
+ "files.send.back": {
+ "message": "返回"
+ },
+ "files.send.clipboardPreview": {
+ "message": "剪贴板预览"
+ },
"peers.dropToSend": {
"message": "放开即发送"
},