fix(UI):Optimize mobile layout
The copy in the extraction method has been added with a fallback mechanism to improve adaptability
This commit is contained in:
@@ -30,7 +30,7 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
|
||||
className="py-12"
|
||||
aria-label="File Transfer Application"
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="w-full max-w-none">
|
||||
{/* sr-only--screen-only: visually hidden */}
|
||||
<h2 className={cn("sr-only", "text-3xl font-bold mb-8 text-center")}>
|
||||
{messages.text.home.h2_screenOnly}
|
||||
|
||||
@@ -181,7 +181,7 @@ const ClipboardApp = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
|
||||
<div className="w-full mx-auto px-1 sm:px-1 py-3 sm:py-8 md:max-w-4xl md:container">
|
||||
<FullScreenDropZone isDragging={isDragging} messages={messages} />
|
||||
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
|
||||
<Button
|
||||
@@ -205,15 +205,15 @@ const ClipboardApp = () => {
|
||||
{messages.text.ClipboardApp.html.retrieveTab}
|
||||
</Button>
|
||||
</div>
|
||||
<Card className="border-8 shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Card className="border-4 sm:border-8 shadow-md">
|
||||
<CardHeader className="px-3 sm:px-6 py-3 sm:py-6">
|
||||
<CardTitle className="text-lg sm:text-xl">
|
||||
{activeTab === "send"
|
||||
? messages.text.ClipboardApp.html.shareTitle_dis
|
||||
: messages.text.ClipboardApp.html.retrieveTitle_dis}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="px-3 sm:px-6">
|
||||
{activeTab === "send" ? (
|
||||
<SendTabPanel
|
||||
messages={messages}
|
||||
@@ -224,7 +224,6 @@ const ClipboardApp = () => {
|
||||
processRoomIdInput={processRoomIdInput}
|
||||
joinRoom={joinRoom}
|
||||
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
|
||||
|
||||
shareMessage={shareMessage}
|
||||
currentValidatedShareRoomId={shareRoomId}
|
||||
handleLeaveSenderRoom={handleLeaveSenderRoom}
|
||||
@@ -236,7 +235,6 @@ const ClipboardApp = () => {
|
||||
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
|
||||
joinRoom={joinRoom}
|
||||
retrieveJoinRoomBtnRef={retrieveJoinRoomBtnRef}
|
||||
|
||||
richTextToPlainText={richTextToPlainText}
|
||||
handleDownloadFile={handleDownloadFile}
|
||||
requestFile={requestFile}
|
||||
@@ -251,13 +249,13 @@ const ClipboardApp = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
{activeTab === "send" && shareLink && messages && (
|
||||
<Card className="border-2 shadow-md mt-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Card className="border-2 sm:border-4 shadow-md mt-2 sm:mt-4">
|
||||
<CardHeader className="pb-3 sm:pb-6">
|
||||
<CardTitle className="text-base sm:text-lg">
|
||||
{messages.text.ClipboardApp.html.RetrieveMethodTitle}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-0 px-3 sm:px-6">
|
||||
<QRCodeComponent RoomID={shareRoomId} shareLink={shareLink} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -273,56 +273,66 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 min-w-0 flex-shrink-0">
|
||||
{progress && progress.progress < 1 ? ( //Show progress or completed
|
||||
<TransferProgress
|
||||
message={
|
||||
mode === "sender"
|
||||
? messages.text.FileListDisplay.sending_dis
|
||||
: messages.text.FileListDisplay.receiving_dis
|
||||
}
|
||||
progress={progress}
|
||||
/>
|
||||
<div className="w-full sm:w-auto">
|
||||
<TransferProgress
|
||||
message={
|
||||
mode === "sender"
|
||||
? messages.text.FileListDisplay.sending_dis
|
||||
: messages.text.FileListDisplay.receiving_dis
|
||||
}
|
||||
progress={progress}
|
||||
/>
|
||||
</div>
|
||||
) : showCompletion ? (
|
||||
<span className="mr-2 text-sm text-green-500">
|
||||
<span className="text-sm text-green-500 whitespace-nowrap">
|
||||
{messages.text.FileListDisplay.finish_dis}
|
||||
</span>
|
||||
) : null}
|
||||
{mode === "receiver" &&
|
||||
onRequest &&
|
||||
onDownload && ( //Request && Download
|
||||
<FileTransferButton
|
||||
onRequest={() => onRequest(item)}
|
||||
isCurrentFileTransferring={
|
||||
|
||||
<div className="flex items-center gap-1 sm:gap-2 flex-wrap">
|
||||
{mode === "receiver" &&
|
||||
onRequest &&
|
||||
onDownload && ( //Request && Download
|
||||
<FileTransferButton
|
||||
onRequest={() => onRequest(item)}
|
||||
isCurrentFileTransferring={
|
||||
progress
|
||||
? progress.progress > 0 && progress.progress < 1
|
||||
: false
|
||||
}
|
||||
isOtherFileTransferring={isAnyFileTransferring && !progress}
|
||||
isSavedToDisk={saveType ? saveType[item.fileId] : false}
|
||||
/>
|
||||
)}
|
||||
{/* display download Num*/}
|
||||
{mode === "sender" && (
|
||||
<span className="text-xs sm:text-sm whitespace-nowrap">
|
||||
{messages.text.FileListDisplay.downloadNum_dis}: {downloadCount}
|
||||
</span>
|
||||
)}
|
||||
{mode === "sender" && onDelete && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onDelete(item);
|
||||
}}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={
|
||||
progress
|
||||
? progress.progress > 0 && progress.progress < 1
|
||||
? progress?.progress > 0 && progress.progress < 1
|
||||
: false
|
||||
}
|
||||
isOtherFileTransferring={isAnyFileTransferring && !progress}
|
||||
isSavedToDisk={saveType ? saveType[item.fileId] : false}
|
||||
/>
|
||||
className="text-xs sm:text-sm px-2 sm:px-3"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">
|
||||
{messages.text.FileListDisplay.delete_dis}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{/* display download Num*/}
|
||||
{mode === "sender" && (
|
||||
<span className="mr-2 text-sm">
|
||||
{messages.text.FileListDisplay.downloadNum_dis}: {downloadCount}
|
||||
</span>
|
||||
)}
|
||||
{mode === "sender" && onDelete && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onDelete(item);
|
||||
}}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={
|
||||
progress ? progress?.progress > 0 && progress.progress < 1 : false
|
||||
}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />{" "}
|
||||
{messages.text.FileListDisplay.delete_dis}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -340,23 +350,32 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
|
||||
: `${item.name} ${formatSize}`;
|
||||
|
||||
return (
|
||||
<div key={item.name} className="flex items-center justify-between mb-1">
|
||||
<div
|
||||
key={item.name}
|
||||
className="flex flex-col sm:flex-row sm:items-center gap-2 mb-2 p-2 sm:p-3 border border-gray-100 rounded-lg"
|
||||
>
|
||||
<Tooltip content={tooltipContent}>
|
||||
<span className="mr-2 truncate max-w-sm">
|
||||
{isFolder ? "📁 " : ""}
|
||||
{item.name.length > filenameDisplayLen
|
||||
? `${item.name.slice(0, filenameDisplayLen - 3)}...`
|
||||
: item.name}
|
||||
{isFolder
|
||||
? `${formatFolderDis(
|
||||
messages!.text.FileListDisplay.folder_dis_template,
|
||||
item.fileCount || 0,
|
||||
formatSize
|
||||
)}`
|
||||
: ` ${formatSize}`}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="block truncate text-sm sm:text-base">
|
||||
{isFolder ? "📁 " : ""}
|
||||
{item.name.length > filenameDisplayLen
|
||||
? `${item.name.slice(0, filenameDisplayLen - 3)}...`
|
||||
: item.name}
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm text-gray-500">
|
||||
{isFolder
|
||||
? `${formatFolderDis(
|
||||
messages!.text.FileListDisplay.folder_dis_template,
|
||||
item.fileCount || 0,
|
||||
formatSize
|
||||
)}`
|
||||
: ` ${formatSize}`}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{renderItemActions(item)}
|
||||
<div className="w-full sm:w-auto sm:flex-shrink-0">
|
||||
{renderItemActions(item)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -120,47 +120,52 @@ export function RetrieveTabPanel({
|
||||
? messages.text.ClipboardApp.roomStatus.connected_dis
|
||||
: messages.text.ClipboardApp.roomStatus.receiverEmptyMsg)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-center gap-2 mb-3">
|
||||
<ReadClipboardButton
|
||||
title={messages.text.ClipboardApp.html.readClipboard_dis}
|
||||
onRead={setRetrieveRoomIdInput}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Retrieve Room ID"
|
||||
value={retrieveRoomIdInput}
|
||||
onChange={(e) => setRetrieveRoomIdInput(e.target.value)}
|
||||
placeholder={
|
||||
messages.text.ClipboardApp.html.retrieveRoomId_placeholder
|
||||
}
|
||||
className="flex-grow min-w-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => joinRoom(false, retrieveRoomIdInput)}
|
||||
ref={retrieveJoinRoomBtnRef}
|
||||
disabled={isReceiverInRoom || !retrieveRoomIdInput.trim()}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.joinRoom_dis}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLeaveRoom}
|
||||
disabled={!isReceiverInRoom || isAnyFileTransferring}
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
<div className="space-y-3 mb-4">
|
||||
{/* Room ID input section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<ReadClipboardButton
|
||||
title={messages.text.ClipboardApp.html.readClipboard_dis}
|
||||
onRead={setRetrieveRoomIdInput}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Retrieve Room ID"
|
||||
value={retrieveRoomIdInput}
|
||||
onChange={(e) => setRetrieveRoomIdInput(e.target.value)}
|
||||
placeholder={
|
||||
messages.text.ClipboardApp.html.retrieveRoomId_placeholder
|
||||
}
|
||||
className="flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
className="flex-1 order-1"
|
||||
onClick={() => joinRoom(false, retrieveRoomIdInput)}
|
||||
ref={retrieveJoinRoomBtnRef}
|
||||
disabled={isReceiverInRoom || !retrieveRoomIdInput.trim()}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.joinRoom_dis}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLeaveRoom}
|
||||
disabled={!isReceiverInRoom || isAnyFileTransferring}
|
||||
className="w-full sm:w-auto px-4 order-2"
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{retrievedContent && (
|
||||
<div className="my-3 p-2 border rounded bg-gray-50">
|
||||
<RichTextEditor
|
||||
value={retrievedContent}
|
||||
onChange={() => {
|
||||
/* Read-only */
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<div className="my-3 p-3 border rounded-md">
|
||||
<div className="bg-white p-3 rounded border border-gray-200 text-sm leading-relaxed">
|
||||
<div dangerouslySetInnerHTML={{ __html: retrievedContent }} />
|
||||
</div>
|
||||
<div className="flex justify-start">
|
||||
<WriteClipboardButton
|
||||
title={messages.text.ClipboardApp.html.Copy_dis}
|
||||
textToCopy={richTextToPlainText(retrievedContent)}
|
||||
|
||||
@@ -101,7 +101,7 @@ export function SendTabPanel({
|
||||
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg)}
|
||||
</div>
|
||||
<RichTextEditor value={shareContent} onChange={updateShareContent} />
|
||||
<div className="flex flex-wrap gap-2 my-3">
|
||||
<div className="flex flex-col sm:flex-row gap-2 my-3">
|
||||
<ReadClipboardButton
|
||||
title={messages.text.ClipboardApp.html.Paste_dis}
|
||||
onRead={updateShareContent}
|
||||
@@ -121,46 +121,59 @@ export function SendTabPanel({
|
||||
onDelete={removeFileToSend}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-center gap-2 mb-3">
|
||||
<span className="text-sm whitespace-nowrap">
|
||||
{messages.text.ClipboardApp.html.inputRoomId_tips}
|
||||
</span>
|
||||
<Input
|
||||
aria-label="Share Room ID"
|
||||
value={inputFieldValue} // Bind to local state
|
||||
onChange={handleInputChange}
|
||||
onPaste={handlePaste}
|
||||
className="flex-grow min-w-0"
|
||||
/>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => joinRoom(true, inputFieldValue.trim())} // Attempt to join using the current input field value
|
||||
disabled={isSenderInRoom || !inputFieldValue.trim()}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.joinRoom_dis}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<AnimatedButton
|
||||
className="flex-1"
|
||||
onClick={generateShareLinkAndBroadcast}
|
||||
loadingText={messages.text.ClipboardApp.html.SyncSending_loadingText}
|
||||
disabled={
|
||||
!isSenderInRoom ||
|
||||
(sendFiles.length === 0 && shareContent.trim() === "") ||
|
||||
!currentValidatedShareRoomId.trim() ||
|
||||
isAnyFileTransferring
|
||||
} // Ensure there is a validated room ID before allowing sharing
|
||||
>
|
||||
{messages.text.ClipboardApp.html.SyncSending_dis}
|
||||
</AnimatedButton>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLeaveSenderRoom}
|
||||
disabled={!isSenderInRoom || isAnyFileTransferring}
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
<div className="space-y-3 mb-4">
|
||||
{/* Room ID input section */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600">
|
||||
{messages.text.ClipboardApp.html.inputRoomId_tips}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Input
|
||||
aria-label="Share Room ID"
|
||||
value={inputFieldValue}
|
||||
onChange={handleInputChange}
|
||||
onPaste={handlePaste}
|
||||
className="flex-1 min-w-0"
|
||||
placeholder={
|
||||
messages.text.ClipboardApp.html.retrieveRoomId_placeholder
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className="w-full sm:w-auto px-4"
|
||||
onClick={() => joinRoom(true, inputFieldValue.trim())}
|
||||
disabled={isSenderInRoom || !inputFieldValue.trim()}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.joinRoom_dis}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<AnimatedButton
|
||||
className="flex-1 order-1"
|
||||
onClick={generateShareLinkAndBroadcast}
|
||||
loadingText={
|
||||
messages.text.ClipboardApp.html.SyncSending_loadingText
|
||||
}
|
||||
disabled={
|
||||
!isSenderInRoom ||
|
||||
(sendFiles.length === 0 && shareContent.trim() === "") ||
|
||||
!currentValidatedShareRoomId.trim() ||
|
||||
isAnyFileTransferring
|
||||
}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.SyncSending_dis}
|
||||
</AnimatedButton>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLeaveSenderRoom}
|
||||
disabled={!isSenderInRoom || isAnyFileTransferring}
|
||||
className="w-full sm:w-auto px-4 order-2"
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{shareMessage && (
|
||||
<p className="mt-3 text-sm text-blue-600">{shareMessage}</p>
|
||||
|
||||
@@ -30,6 +30,19 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
|
||||
const copyToClipboard = async () => {
|
||||
if (!qrRef.current) return;
|
||||
|
||||
// Check for Clipboard API support for images
|
||||
if (
|
||||
!navigator.clipboard ||
|
||||
!navigator.clipboard.write ||
|
||||
!window.ClipboardItem
|
||||
) {
|
||||
console.warn(
|
||||
"Clipboard API for images not supported. Falling back to download."
|
||||
);
|
||||
downloadQRCode();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const svgElement = qrRef.current.querySelector("svg");
|
||||
if (!svgElement) return;
|
||||
@@ -44,21 +57,33 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngFile = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((blob) => resolve(blob!), "image/png")
|
||||
const pngBlob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, "image/png")
|
||||
);
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
"image/png": pngFile,
|
||||
}),
|
||||
]);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
|
||||
if (pngBlob) {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
"image/png": pngBlob,
|
||||
}),
|
||||
]);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
} else {
|
||||
throw new Error("Canvas to Blob conversion failed");
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
// If image loading fails, fall back to download
|
||||
console.error(
|
||||
"Image loading for QR code failed. Falling back to download."
|
||||
);
|
||||
downloadQRCode();
|
||||
};
|
||||
img.src = "data:image/svg+xml;base64," + btoa(svgData);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy QR code: ", err);
|
||||
alert("Failed to copy QR code. Please try again.");
|
||||
console.error("Failed to copy QR code, falling back to download: ", err);
|
||||
downloadQRCode(); // Fallback to download on any error
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
@@ -95,73 +120,99 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
return (
|
||||
<div className="bg-blue-100 p-4 rounded-md">
|
||||
<p className="text-blue-700 mb-4">{messages.text.RetrieveMethod.P}</p>
|
||||
<div className="bg-blue-50 p-2 sm:p-4 rounded-lg border border-blue-200">
|
||||
<p className="text-blue-700 mb-3 sm:mb-4 text-sm sm:text-base">
|
||||
{messages.text.RetrieveMethod.P}
|
||||
</p>
|
||||
|
||||
{/* Use flex-col instead of list for better control on mobile layout */}
|
||||
<div className="flex flex-col space-y-4">
|
||||
{/* Mobile-first responsive layout */}
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* RoomID section */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>{messages.text.RetrieveMethod.RoomId_tips + RoomID}</span>
|
||||
<WriteClipboardButton
|
||||
title={messages.text.RetrieveMethod.copyRoomId_tips}
|
||||
textToCopy={RoomID}
|
||||
/>
|
||||
<div className="bg-white p-2 sm:p-3 rounded-lg border border-blue-100">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
{messages.text.RetrieveMethod.RoomId_tips}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<code className="flex-1 bg-gray-100 px-2 py-1 rounded text-sm font-mono break-all">
|
||||
{RoomID}
|
||||
</code>
|
||||
<WriteClipboardButton
|
||||
title={messages.text.RetrieveMethod.copyRoomId_tips}
|
||||
textToCopy={RoomID}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* URL section */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="break-all">
|
||||
{messages.text.RetrieveMethod.url_tips + shareLink}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<WriteClipboardButton
|
||||
title={messages.text.RetrieveMethod.copyUrl_tips}
|
||||
textToCopy={shareLink}
|
||||
/>
|
||||
<div className="bg-white p-2 sm:p-3 rounded-lg border border-blue-100">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
{messages.text.RetrieveMethod.url_tips}
|
||||
</p>
|
||||
<div className="bg-gray-100 px-2 py-2 rounded text-xs sm:text-sm break-all font-mono">
|
||||
{shareLink}
|
||||
</div>
|
||||
<div className="flex justify-start">
|
||||
<WriteClipboardButton
|
||||
title={messages.text.RetrieveMethod.copyUrl_tips}
|
||||
textToCopy={shareLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QR Code section */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div>{messages.text.RetrieveMethod.scanQR_tips}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{messages.text.RetrieveMethod.Copied_dis}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />{" "}
|
||||
{messages.text.RetrieveMethod.Copy_QR_dis}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={downloadQRCode}
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />{" "}
|
||||
{messages.text.RetrieveMethod.download_QR_dis}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-2 sm:p-3 rounded-lg border border-blue-100">
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
{messages.text.RetrieveMethod.scanQR_tips}
|
||||
</p>
|
||||
|
||||
{/* QR Code display area */}
|
||||
<div className="mt-4 flex justify-center">
|
||||
<div className="inline-block border-2 p-4 bg-white rounded-lg">
|
||||
<div ref={qrRef}>
|
||||
<QRCodeSVG value={shareLink} />
|
||||
{/* QR Code display area - moved up for better mobile UX */}
|
||||
<div className="flex justify-center">
|
||||
<div className="inline-block border-2 p-2 sm:p-4 bg-gray-50 rounded-lg">
|
||||
<div ref={qrRef}>
|
||||
<QRCodeSVG
|
||||
value={shareLink}
|
||||
size={120}
|
||||
className="sm:w-32 sm:h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QR Code action buttons */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{messages.text.RetrieveMethod.Copied_dis}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
{messages.text.RetrieveMethod.Copy_QR_dis}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={downloadQRCode}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{messages.text.RetrieveMethod.download_QR_dis}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,17 +67,44 @@ export const useClipboardActions = (): ClipboardActions => {
|
||||
async (textToCopy: string) => {
|
||||
setError(null);
|
||||
setIsCopied(false);
|
||||
if (!navigator.clipboard) {
|
||||
setError(clipboardMessages.copyError || "Clipboard API not available.");
|
||||
return;
|
||||
|
||||
// Modern API: navigator.clipboard.writeText
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
return; // Success
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text with navigator.clipboard: ", err);
|
||||
// Fallback will be attempted below
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: document.execCommand('copy')
|
||||
let textArea: HTMLTextAreaElement | null = null;
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
textArea = document.createElement("textarea");
|
||||
textArea.value = textToCopy;
|
||||
textArea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
|
||||
textArea.style.left = "-9999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
const successful = document.execCommand("copy");
|
||||
if (successful) {
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
} else {
|
||||
throw new Error("document.execCommand failed");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
console.error("Fallback copy method failed: ", err);
|
||||
setError(clipboardMessages.copyError || "Failed to copy.");
|
||||
} finally {
|
||||
if (textArea) {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
},
|
||||
[clipboardMessages.copyError]
|
||||
|
||||
Reference in New Issue
Block a user