CopyButton.tsx 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import { useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Copy, Check } from 'lucide-react';
  4. interface CopyButtonProps {
  5. value: string;
  6. /** i18n key for the resting tooltip. */
  7. titleKey?: string;
  8. /** i18n key for the tooltip while the tick is showing. */
  9. copiedTitleKey?: string;
  10. className?: string;
  11. iconClassName?: string;
  12. }
  13. /**
  14. * Copy-to-clipboard button with the plain-HTTP fallback (#1174).
  15. *
  16. * Lifted out of PrinterInfoModal when the Docker update instructions needed
  17. * the same control (#2664). The fallback is the whole reason this is shared
  18. * rather than re-written per call site: navigator.clipboard is gated behind
  19. * the secure-context requirement, so on a LAN install reached over plain HTTP
  20. * — which is most Bambuddy installs — the API is simply undefined, and a
  21. * naive implementation swallows the failure with no tick and nothing copied.
  22. */
  23. export function CopyButton({
  24. value,
  25. titleKey = 'printers.copyToClipboard',
  26. copiedTitleKey = 'printers.copied',
  27. className = 'ml-2 p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors',
  28. iconClassName = 'w-3.5 h-3.5',
  29. }: CopyButtonProps) {
  30. const { t } = useTranslation();
  31. const [copied, setCopied] = useState(false);
  32. const handleCopy = async () => {
  33. try {
  34. if (navigator.clipboard && window.isSecureContext) {
  35. await navigator.clipboard.writeText(value);
  36. } else {
  37. // Legacy execCommand path via an off-screen textarea, matching the
  38. // pattern used by CameraTokensPage's plaintext-token modal.
  39. const ta = document.createElement('textarea');
  40. ta.value = value;
  41. ta.style.position = 'fixed';
  42. ta.style.opacity = '0';
  43. document.body.appendChild(ta);
  44. try {
  45. ta.select();
  46. const ok = document.execCommand('copy');
  47. if (!ok) return;
  48. } finally {
  49. document.body.removeChild(ta);
  50. }
  51. }
  52. setCopied(true);
  53. setTimeout(() => setCopied(false), 2000);
  54. } catch {
  55. // Both paths failed (no clipboard API, no execCommand). Leave the icon
  56. // unchanged so the user knows nothing was copied.
  57. }
  58. };
  59. return (
  60. <button
  61. onClick={handleCopy}
  62. className={className}
  63. title={copied ? t(copiedTitleKey) : t(titleKey)}
  64. >
  65. {copied ? <Check className={`${iconClassName} text-bambu-green`} /> : <Copy className={iconClassName} />}
  66. </button>
  67. );
  68. }