From 2be390f4263606930cd1243393ef7d2d8576acc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Tue, 1 Sep 2026 14:25:28 +0200 Subject: [PATCH] [client] Fix the Files tab layout and add sending from the Files page The Files rows overflowed the fixed-width panel instead of truncating. Radix wraps ScrollArea.Viewport children in a div carrying an inline `min-width:100%; display:table`, and a table box is shrink-to-fit, so it grew to the widest row's intrinsic width and never gave `truncate` a finite bound. Since overflowX is hidden when only the vertical scrollbar is enabled, the result was clipped content rather than a scrollbar. A scoped `.nb-scroll-clamp` rule overrides that display (the inline style needs `!important`), leaving other ScrollAreas untouched. Each transfer row is now three lines -- file name, to/from peer, then time and size -- with the file name and the peer label both truncating and revealing the full text on hover via TruncatedText. The status label keeps its own right-hand column, vertically centred against the block, so a refusal or failure stays scannable. The Files page could not start a transfer at all: the send buttons lived only on the peer detail panel, and an empty history replaced the whole page, hiding the header. The header now always renders and carries one primary Send button opening a two-step picker -- payload first, then the peer list with search. One entry point means the peer list is built in one place instead of two, and one button leaves the search field room to survive the longer translations. Clipboard sends showed nothing before leaving the machine. Both the picker and the peer panel now preview the text first, and both send the previewed string rather than re-reading the clipboard, which could have changed between the preview and the confirmation. The picker disables its clipboard entry when the clipboard is empty, so the failure surfaces before the recipient is chosen. PeerSendActions is keyed on the peer so a staged confirmation cannot carry over to a different recipient. Deleting a history entry now asks first, reusing the app-wide useConfirm hook. The wording states that received files stay on disk, matching what DeleteTransfer does -- it drops the history record and cancels a live transfer, and never touches delivered files. The dropdown is modal={false} because DropdownMenuItem calls preventDefault, which keeps a modal menu's focus trap alive against the dialog. --- .../src/components/ClipboardPreview.tsx | 40 +++ .../src/components/buttons/Button.tsx | 5 + client/ui/frontend/src/globals.css | 13 + .../src/modules/main/advanced/files/Files.tsx | 188 ++++++++---- .../main/advanced/files/SendPeerPicker.tsx | 275 ++++++++++++++++++ .../main/advanced/peers/PeerDetailPanel.tsx | 92 ++++-- .../src/modules/main/advanced/peers/Peers.tsx | 2 +- client/ui/i18n/locales/de/common.json | 24 ++ client/ui/i18n/locales/en/common.json | 32 ++ client/ui/i18n/locales/es/common.json | 24 ++ client/ui/i18n/locales/fr/common.json | 24 ++ client/ui/i18n/locales/hu/common.json | 24 ++ client/ui/i18n/locales/it/common.json | 24 ++ client/ui/i18n/locales/ja/common.json | 24 ++ client/ui/i18n/locales/pt/common.json | 24 ++ client/ui/i18n/locales/ru/common.json | 24 ++ client/ui/i18n/locales/zh-CN/common.json | 24 ++ 17 files changed, 785 insertions(+), 78 deletions(-) create mode 100644 client/ui/frontend/src/components/ClipboardPreview.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/files/SendPeerPicker.tsx 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)) + } + > + + + {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. + + {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 (
-
- - -
+ {pendingText === null ? ( +
+ + +
+ ) : ( +
+ +
+ + +
+
+ )} {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": "放开即发送" },