diff --git a/web/index.html b/web/index.html index de68df7..a619773 100644 --- a/web/index.html +++ b/web/index.html @@ -1,14 +1,16 @@ - + - - + + - AnyDrop + + + AnyDrop — Universal transfer - +
diff --git a/web/src/components/DevicePairingPanel.tsx b/web/src/components/DevicePairingPanel.tsx index c2bbe12..4fcaffc 100644 --- a/web/src/components/DevicePairingPanel.tsx +++ b/web/src/components/DevicePairingPanel.tsx @@ -19,7 +19,6 @@ export default function DevicePairingPanel({ const [inputCode, setInputCode] = useState(""); const [pairError, setPairError] = useState(null); - // When modal opens in "show" mode, always request a fresh code useEffect(() => { if (showModal && mode === "show") { let gid = groupId; @@ -32,7 +31,6 @@ export default function DevicePairingPanel({ } }, [showModal, mode]); - // Detect pair-code-not-found errors useEffect(() => { if (error && error.includes("appairage")) { setPairError(error); @@ -51,11 +49,9 @@ export default function DevicePairingPanel({ if (inputCode.length >= 6) { setPairError(null); onResolveCode(inputCode); - // Don't close — wait for response or error } }; - // Auto-close on successful pairing (groupId changed after resolve) const currentGroupId = useProfileStore((s) => s.groupId); useEffect(() => { if (mode === "enter" && showModal && currentGroupId && currentGroupId !== groupId) { @@ -67,68 +63,78 @@ export default function DevicePairingPanel({ <> {showModal && (
e.stopPropagation()} > +
+ Pair device +
+

+ Link your devices +

+ {/* Tab switcher */} -
+
{mode === "show" ? ( <> -

- Entrez ce code sur votre autre appareil +

+ Enter this code on your other device.

{pairCode ? ( -

+

{pairCode}

) : ( -
-
+
+
)} -

- Le code expire dans 5 minutes. - L'appairage est permanent. +

+ Code expires in 5 minutes. Pairing is permanent.

) : ( <> -

- Entrez le code affiché sur l'autre appareil +

+ Enter the code shown on the other device.

{ if (e.key === "Enter") handleSubmitCode(); }} /> {pairError && ( -

{pairError}

+

{pairError}

)} )}
diff --git a/web/src/components/DropZone.tsx b/web/src/components/DropZone.tsx index fd29740..8f0d390 100644 --- a/web/src/components/DropZone.tsx +++ b/web/src/components/DropZone.tsx @@ -56,7 +56,6 @@ export default function DropZone({ onFilesSelected, disabled }: DropZoneProps) { if (files.length > 0) { onFilesSelected(files); } - // Reset to allow re-selecting the same file e.target.value = ""; }; @@ -68,26 +67,29 @@ export default function DropZone({ onFilesSelected, disabled }: DropZoneProps) { onDrop={handleDrop} onClick={handleClick} className={` - border-2 border-dashed rounded-2xl p-8 - flex flex-col items-center justify-center gap-3 - transition-all duration-200 cursor-pointer - min-h-[160px] + border border-dashed rounded-sm px-6 py-8 + flex flex-col items-center justify-center gap-2 + transition-colors duration-fast ease-crisp cursor-pointer ${isDragging - ? "border-brand-400 bg-brand-500/10 scale-[1.02]" + ? "border-signal bg-signal-quiet" : disabled - ? "border-slate-700 bg-slate-900/50 cursor-not-allowed opacity-50" - : "border-slate-700 hover:border-slate-500 bg-slate-900/30 hover:bg-slate-900/50" + ? "border-paper-edge bg-paper-deep/40 cursor-not-allowed opacity-60" + : "border-paper-edge bg-paper hover:border-ink hover:bg-paper-deep/40" } `} > -
{isDragging ? "📥" : "📁"}
-

+ + {isDragging ? "Release" : "Drop"} + +

{disabled - ? "Sélectionnez d'abord un appareil" + ? "Select a device first" : isDragging - ? "Déposez vos fichiers ici" - : "Glissez des fichiers ici ou cliquez pour sélectionner" - } + ? "Release to send" + : "Drop files here"} +

+

+ or click to choose

= { - phone: "📱", - tablet: "📱", - laptop: "💻", - desktop: "🖥️", +const DEVICE_GLYPH: Record = { + phone: "phone", + tablet: "tablet", + laptop: "laptop", + desktop: "desk", }; const sizeClasses = { - sm: { container: "w-12 h-12", icon: "text-xl", img: "w-12 h-12" }, - md: { container: "w-16 h-16", icon: "text-2xl", img: "w-16 h-16" }, - lg: { container: "w-20 h-20", icon: "text-3xl", img: "w-20 h-20" }, + sm: { container: "w-12 h-12", label: "text-[10px]" }, + md: { container: "w-16 h-16", label: "text-[11px]" }, + lg: { container: "w-20 h-20", label: "text-xs" }, }; -export default function PeerAvatar({ displayName, deviceType, avatar, online = true, onClick, isSelected, size = "md" }: PeerAvatarProps) { +export default function PeerAvatar({ + displayName, + deviceType, + avatar, + online = true, + onClick, + isSelected, + size = "md", +}: PeerAvatarProps) { const s = sizeClasses[size]; const isOffline = !online; @@ -31,9 +39,9 @@ export default function PeerAvatar({ displayName, deviceType, avatar, online = t diff --git a/web/src/components/PeerList.tsx b/web/src/components/PeerList.tsx index 3529f2a..b70fa54 100644 --- a/web/src/components/PeerList.tsx +++ b/web/src/components/PeerList.tsx @@ -11,18 +11,18 @@ export default function PeerList({ onPeerSelect }: PeerListProps) { if (peers.length === 0) { return ( -
-
📡
-

En attente d'appareils...

-

- Ouvrez AnyDrop sur un autre appareil connecté au même Wi-Fi, - ou appairez vos appareils ci-dessous. +

+
+ +
+

Listening for devices

+

+ Open AnyDrop on another device on this network, or pair a device below.

); } - // Sort: online first, then offline const sorted = [...peers].sort((a, b) => { const aOnline = a.online !== false ? 1 : 0; const bOnline = b.online !== false ? 1 : 0; @@ -30,18 +30,20 @@ export default function PeerList({ onPeerSelect }: PeerListProps) { }); return ( -
- {sorted.map((peer) => ( - onPeerSelect(peer.peerId)} - /> - ))} +
+
+ {sorted.map((peer) => ( + onPeerSelect(peer.peerId)} + /> + ))} +
); } diff --git a/web/src/components/ProfileSetup.tsx b/web/src/components/ProfileSetup.tsx index fdbd7f7..d8dc0ca 100644 --- a/web/src/components/ProfileSetup.tsx +++ b/web/src/components/ProfileSetup.tsx @@ -1,7 +1,7 @@ import { useState, useRef } from "react"; import { useProfileStore } from "../stores/useProfileStore"; -const MAX_AVATAR_SIZE = 80_000; // ~80KB after base64 +const MAX_AVATAR_SIZE = 80_000; function resizeImage(file: File, maxSize: number): Promise { return new Promise((resolve, reject) => { @@ -10,7 +10,6 @@ function resizeImage(file: File, maxSize: number): Promise { img.onload = () => { URL.revokeObjectURL(url); const canvas = document.createElement("canvas"); - // Crop to square, max 128px const side = Math.min(img.width, img.height); const sx = (img.width - side) / 2; const sy = (img.height - side) / 2; @@ -21,7 +20,6 @@ function resizeImage(file: File, maxSize: number): Promise { const ctx = canvas.getContext("2d")!; ctx.drawImage(img, sx, sy, side, side, 0, 0, outSize, outSize); - // Try JPEG at decreasing quality until small enough let quality = 0.8; let dataUrl = canvas.toDataURL("image/jpeg", quality); while (dataUrl.length > maxSize && quality > 0.2) { @@ -30,7 +28,6 @@ function resizeImage(file: File, maxSize: number): Promise { } if (dataUrl.length > maxSize) { - // Shrink further outSize = 64; canvas.width = outSize; canvas.height = outSize; @@ -70,28 +67,45 @@ export default function ProfileSetup({ onDone, isEditing }: ProfileSetupProps) { onDone(); }; + const wrapperClass = isEditing + ? "fixed inset-0 z-50 flex items-center justify-center bg-ink/40 p-4" + : "min-h-screen flex items-center justify-center p-4"; + return ( -
-
-

- {isEditing ? "Modifier le profil" : "Votre appareil"} +
+
+ {!isEditing && ( + <> +

+ AnyDrop +

+

+ Universal transfer +

+ + )} +
+ {isEditing ? "Edit" : "Set up"} +
+

+ Your device

{/* Avatar */} -
+
- {/* Remove photo */} {preview && ( )} {/* Name */} -
- +
+ setName(e.target.value)} maxLength={30} - placeholder="ex: iPhone d'Arthur" - className="w-full px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-xl - text-white text-sm placeholder:text-slate-500 - focus:outline-none focus:border-brand-500 transition-colors" + placeholder="e.g. Arthur's iPhone" + className="w-full px-3 py-2.5 bg-paper border border-paper-edge rounded-sm + text-ink text-sm placeholder:text-ink-faint + focus:outline-none focus:border-ink transition-colors + duration-fast ease-crisp" autoFocus onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
- {/* Submit */}
diff --git a/web/src/components/PublicRoomPanel.tsx b/web/src/components/PublicRoomPanel.tsx index 4ca07b2..a3699ad 100644 --- a/web/src/components/PublicRoomPanel.tsx +++ b/web/src/components/PublicRoomPanel.tsx @@ -24,13 +24,14 @@ export default function PublicRoomPanel({ onCreateRoom }: PublicRoomPanelProps) <> {showModal && ( @@ -53,58 +54,66 @@ function PublicRoomModal({ url: string | null; onClose: () => void; }) { + const [copied, setCopied] = useState(false); + const copyToClipboard = () => { - if (url) navigator.clipboard.writeText(url); + if (!url) return; + navigator.clipboard.writeText(url); + setCopied(true); + setTimeout(() => setCopied(false), 1500); }; return (
e.stopPropagation()} > -

Lien public

+
+ Public link +
+

+ Receive from anyone +

{!code || !url ? ( -

Création en cours...

+

Generating…

) : ( <> -
-
- +
+
+
-
-

+

+

{code.toUpperCase()}

-

- Partagez ce lien pour recevoir des fichiers de n'importe qui. - Expire dans 10 minutes. +

+ Anyone with this code or link can send you files. Expires in 10 minutes.

)}
diff --git a/web/src/components/ReceiveDialog.tsx b/web/src/components/ReceiveDialog.tsx index 4ba5998..01f1a11 100644 --- a/web/src/components/ReceiveDialog.tsx +++ b/web/src/components/ReceiveDialog.tsx @@ -1,10 +1,10 @@ import type { IncomingRequest } from "../stores/useStore"; function formatSize(bytes: number): string { - if (bytes < 1024) return `${bytes} o`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} Ko`; - if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} Mo`; - return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} Go`; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } interface ReceiveDialogProps { @@ -15,57 +15,64 @@ interface ReceiveDialogProps { export default function ReceiveDialog({ request, onAccept, onReject }: ReceiveDialogProps) { const totalSize = request.files.reduce((sum, f) => sum + f.size, 0); + const isTextOnly = request.text && request.files.length === 0; return ( -
-
-

- Transfert entrant +
+
+
+ Incoming +
+

+ {isTextOnly ? "Text received" : "Transfer request"}

-

- {request.displayName} veut vous envoyer : +

+ From {request.displayName}

{request.files.length > 0 && ( -
+
{request.files.map((file) => ( -
- {file.name} - {formatSize(file.size)} +
+ {file.name} + + {formatSize(file.size)} +
))} {request.files.length > 1 && ( -
- {request.files.length} fichiers - {formatSize(totalSize)} +
+ {request.files.length} files + {formatSize(totalSize)}
)}
)} {request.text && ( -
-

Texte :

-

+

+
Text
+

{request.text}

)} -
+
diff --git a/web/src/components/TextShareModal.tsx b/web/src/components/TextShareModal.tsx index 552bafe..377ecae 100644 --- a/web/src/components/TextShareModal.tsx +++ b/web/src/components/TextShareModal.tsx @@ -17,35 +17,45 @@ export default function TextShareModal({ onSend, onClose }: TextShareModalProps) }; return ( -
-
-

Envoyer du texte

+
+
e.stopPropagation()} + > +
+ Compose +
+

Send text