Просмотр исходного кода

fix(virtual-printer): clipboard fallback for HTTP (non-secure) context

  The Tailscale FQDN copy button used only `navigator.clipboard.writeText`,
  which browsers block when `window.isSecureContext === false` — i.e. when
  Bambuddy is reached over HTTP on a LAN / tailnet IP, which is the
  common case. My catch block swallowed the error and the generic
  "Failed to update settings" toast fired instead of actually copying.

  Add a legacy `document.execCommand('copy')` fallback via a hidden
  textarea for non-secure contexts. New i18n key
  `virtualPrinter.toast.copyFailed` added to all 8 locales for the
  (rare) both-paths-fail case.
maziggy 1 месяц назад
Родитель
Сommit
a00ce61064

+ 55 - 5
frontend/src/components/VirtualPrinterCard.tsx

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import {
 import {
   Loader2, Check, AlertTriangle, Eye, EyeOff, Info,
   Loader2, Check, AlertTriangle, Eye, EyeOff, Info,
-  ChevronDown, ChevronRight, ArrowRightLeft, Trash2, ShieldCheck,
+  ChevronDown, ChevronRight, ArrowRightLeft, Trash2, ShieldCheck, Copy,
 } from 'lucide-react';
 } from 'lucide-react';
 import { api, multiVirtualPrinterApi } from '../api/client';
 import { api, multiVirtualPrinterApi } from '../api/client';
 import type { VirtualPrinterConfig } from '../api/client';
 import type { VirtualPrinterConfig } from '../api/client';
@@ -47,6 +47,45 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
   const [showAccessCode, setShowAccessCode] = useState(false);
   const [showAccessCode, setShowAccessCode] = useState(false);
   const [pendingAction, setPendingAction] = useState<string | null>(null);
   const [pendingAction, setPendingAction] = useState<string | null>(null);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+  const [fqdnCopied, setFqdnCopied] = useState(false);
+
+  const handleCopyFqdn = async (e: React.MouseEvent) => {
+    e.stopPropagation();
+    const fqdn = printer.status?.tailscale_fqdn;
+    if (!fqdn) return;
+    let ok = false;
+    // Modern API — only works in secure contexts (HTTPS / localhost).
+    if (navigator.clipboard && window.isSecureContext) {
+      try {
+        await navigator.clipboard.writeText(fqdn);
+        ok = true;
+      } catch {
+        // fall through to legacy
+      }
+    }
+    // Legacy fallback for HTTP (common when Bambuddy is reached over LAN / tailnet IP).
+    if (!ok) {
+      try {
+        const ta = document.createElement('textarea');
+        ta.value = fqdn;
+        ta.style.position = 'fixed';
+        ta.style.opacity = '0';
+        document.body.appendChild(ta);
+        ta.select();
+        ok = document.execCommand('copy');
+        document.body.removeChild(ta);
+      } catch {
+        ok = false;
+      }
+    }
+    if (ok) {
+      setFqdnCopied(true);
+      showToast(t('printers.copied'));
+      setTimeout(() => setFqdnCopied(false), 2000);
+    } else {
+      showToast(t('virtualPrinter.toast.copyFailed'), 'error');
+    }
+  };
 
 
   // Sync local state when props change (e.g., after backend auto-disable)
   // Sync local state when props change (e.g., after backend auto-disable)
   useEffect(() => {
   useEffect(() => {
@@ -222,6 +261,17 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
             <span className="flex items-center gap-1 text-xs text-green-400/70 flex-shrink-0">
             <span className="flex items-center gap-1 text-xs text-green-400/70 flex-shrink-0">
               <ShieldCheck className="w-3 h-3" />
               <ShieldCheck className="w-3 h-3" />
               <span className="font-mono text-[10px]">{printer.status.tailscale_fqdn}</span>
               <span className="font-mono text-[10px]">{printer.status.tailscale_fqdn}</span>
+              <button
+                onClick={handleCopyFqdn}
+                className="p-0.5 rounded hover:bg-bambu-dark-tertiary transition-colors"
+                title={fqdnCopied ? t('printers.copied') : t('printers.copyToClipboard')}
+              >
+                {fqdnCopied ? (
+                  <Check className="w-3 h-3 text-bambu-green" />
+                ) : (
+                  <Copy className="w-3 h-3" />
+                )}
+              </button>
             </span>
             </span>
           )}
           )}
           <div className="ml-auto flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
           <div className="ml-auto flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
@@ -298,8 +348,8 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
             {/* Auto-dispatch toggle - only for print_queue mode */}
             {/* Auto-dispatch toggle - only for print_queue mode */}
             {localMode === 'print_queue' && (
             {localMode === 'print_queue' && (
               <div className="pt-2 border-t border-bambu-dark-tertiary">
               <div className="pt-2 border-t border-bambu-dark-tertiary">
-                <div className="flex items-center justify-between">
-                  <div>
+                <div className="flex items-center justify-between gap-3">
+                  <div className="min-w-0">
                     <div className="text-white text-sm font-medium">{t('virtualPrinter.autoDispatch.title')}</div>
                     <div className="text-white text-sm font-medium">{t('virtualPrinter.autoDispatch.title')}</div>
                     <div className="text-[10px] text-bambu-gray">{t('virtualPrinter.autoDispatch.description')}</div>
                     <div className="text-[10px] text-bambu-gray">{t('virtualPrinter.autoDispatch.description')}</div>
                   </div>
                   </div>
@@ -327,8 +377,8 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
 
 
             {/* Tailscale toggle */}
             {/* Tailscale toggle */}
             <div className="pt-2 border-t border-bambu-dark-tertiary">
             <div className="pt-2 border-t border-bambu-dark-tertiary">
-              <div className="flex items-center justify-between">
-                <div>
+              <div className="flex items-center justify-between gap-3">
+                <div className="min-w-0">
                   <div className="text-white text-sm font-medium">{t('virtualPrinter.tailscaleDisabled.title')}</div>
                   <div className="text-white text-sm font-medium">{t('virtualPrinter.tailscaleDisabled.title')}</div>
                   <div className="text-[10px] text-bambu-gray">{t('virtualPrinter.tailscaleDisabled.description')}</div>
                   <div className="text-[10px] text-bambu-gray">{t('virtualPrinter.tailscaleDisabled.description')}</div>
                 </div>
                 </div>

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -3985,6 +3985,7 @@ export default {
       updated: 'Virtuelle Druckereinstellungen aktualisiert',
       updated: 'Virtuelle Druckereinstellungen aktualisiert',
       failedToUpdate: 'Einstellungen konnten nicht aktualisiert werden',
       failedToUpdate: 'Einstellungen konnten nicht aktualisiert werden',
       tailscaleNotAvailable: 'Tailscale ist auf diesem Host nicht installiert. Installiere Tailscale zuerst und versuche es dann erneut.',
       tailscaleNotAvailable: 'Tailscale ist auf diesem Host nicht installiert. Installiere Tailscale zuerst und versuche es dann erneut.',
+      copyFailed: 'Kopieren fehlgeschlagen — bitte Text manuell markieren',
       accessCodeRequired: 'Bitte zuerst einen Zugangscode setzen',
       accessCodeRequired: 'Bitte zuerst einen Zugangscode setzen',
       targetPrinterRequired: 'Bitte zuerst einen Zieldrucker auswählen',
       targetPrinterRequired: 'Bitte zuerst einen Zieldrucker auswählen',
       bindIpRequired: 'Bitte zuerst eine Bind-IP setzen',
       bindIpRequired: 'Bitte zuerst eine Bind-IP setzen',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -3993,6 +3993,7 @@ export default {
       updated: 'Virtual printer settings updated',
       updated: 'Virtual printer settings updated',
       failedToUpdate: 'Failed to update settings',
       failedToUpdate: 'Failed to update settings',
       tailscaleNotAvailable: 'Tailscale is not installed on this host. Install Tailscale first, then try again.',
       tailscaleNotAvailable: 'Tailscale is not installed on this host. Install Tailscale first, then try again.',
+      copyFailed: 'Failed to copy — try selecting the text manually',
       accessCodeRequired: 'Please set an access code first',
       accessCodeRequired: 'Please set an access code first',
       targetPrinterRequired: 'Please select a target printer first',
       targetPrinterRequired: 'Please select a target printer first',
       bindIpRequired: 'Please set a bind IP first',
       bindIpRequired: 'Please set a bind IP first',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -3908,6 +3908,7 @@ export default {
       updated: 'Réglages virtuels mis à jour',
       updated: 'Réglages virtuels mis à jour',
       failedToUpdate: 'Échec mise à jour',
       failedToUpdate: 'Échec mise à jour',
       tailscaleNotAvailable: 'Tailscale n\'est pas installé sur cet hôte. Installez Tailscale puis réessayez.',
       tailscaleNotAvailable: 'Tailscale n\'est pas installé sur cet hôte. Installez Tailscale puis réessayez.',
+      copyFailed: 'Échec de la copie — veuillez sélectionner le texte manuellement',
       accessCodeRequired: 'Code d\'accès requis',
       accessCodeRequired: 'Code d\'accès requis',
       targetPrinterRequired: 'Imprimante cible requise',
       targetPrinterRequired: 'Imprimante cible requise',
       bindIpRequired: 'Veuillez d\'abord définir une adresse IP',
       bindIpRequired: 'Veuillez d\'abord définir une adresse IP',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -3907,6 +3907,7 @@ export default {
       updated: 'Impostazioni stampante virtuale aggiornate',
       updated: 'Impostazioni stampante virtuale aggiornate',
       failedToUpdate: 'Aggiornamento impostazioni fallito',
       failedToUpdate: 'Aggiornamento impostazioni fallito',
       tailscaleNotAvailable: 'Tailscale non è installato su questo host. Installa prima Tailscale, poi riprova.',
       tailscaleNotAvailable: 'Tailscale non è installato su questo host. Installa prima Tailscale, poi riprova.',
+      copyFailed: 'Copia non riuscita — seleziona il testo manualmente',
       accessCodeRequired: 'Imposta prima un codice accesso',
       accessCodeRequired: 'Imposta prima un codice accesso',
       targetPrinterRequired: 'Seleziona prima una stampante target',
       targetPrinterRequired: 'Seleziona prima una stampante target',
       bindIpRequired: 'Impostare prima un indirizzo IP',
       bindIpRequired: 'Impostare prima un indirizzo IP',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -3946,6 +3946,7 @@ export default {
       updated: '仮想プリンター設定を更新しました',
       updated: '仮想プリンター設定を更新しました',
       failedToUpdate: '設定の更新に失敗しました',
       failedToUpdate: '設定の更新に失敗しました',
       tailscaleNotAvailable: 'このホストにTailscaleがインストールされていません。先にTailscaleをインストールしてから再試行してください。',
       tailscaleNotAvailable: 'このホストにTailscaleがインストールされていません。先にTailscaleをインストールしてから再試行してください。',
+      copyFailed: 'コピーに失敗しました — テキストを手動で選択してください',
       accessCodeRequired: '先にアクセスコードを設定してください',
       accessCodeRequired: '先にアクセスコードを設定してください',
       targetPrinterRequired: '先にターゲットプリンターを選択してください',
       targetPrinterRequired: '先にターゲットプリンターを選択してください',
       bindIpRequired: '先にバインドIPを設定してください',
       bindIpRequired: '先にバインドIPを設定してください',

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -3921,6 +3921,7 @@ export default {
       updated: 'Configurações da impressora virtual atualizadas',
       updated: 'Configurações da impressora virtual atualizadas',
       failedToUpdate: 'Falha ao atualizar as configurações',
       failedToUpdate: 'Falha ao atualizar as configurações',
       tailscaleNotAvailable: 'Tailscale não está instalado neste host. Instale o Tailscale primeiro e tente novamente.',
       tailscaleNotAvailable: 'Tailscale não está instalado neste host. Instale o Tailscale primeiro e tente novamente.',
+      copyFailed: 'Falha ao copiar — selecione o texto manualmente',
       accessCodeRequired: 'Defina um código de acesso primeiro',
       accessCodeRequired: 'Defina um código de acesso primeiro',
       targetPrinterRequired: 'Selecione uma impressora alvo primeiro',
       targetPrinterRequired: 'Selecione uma impressora alvo primeiro',
       bindIpRequired: 'Defina um IP de ligação primeiro',
       bindIpRequired: 'Defina um IP de ligação primeiro',

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -3973,6 +3973,7 @@ export default {
       updated: '虚拟打印机设置已更新',
       updated: '虚拟打印机设置已更新',
       failedToUpdate: '更新设置失败',
       failedToUpdate: '更新设置失败',
       tailscaleNotAvailable: '此主机上未安装 Tailscale。请先安装 Tailscale,然后重试。',
       tailscaleNotAvailable: '此主机上未安装 Tailscale。请先安装 Tailscale,然后重试。',
+      copyFailed: '复制失败 — 请手动选中文本',
       accessCodeRequired: '请先设置访问码',
       accessCodeRequired: '请先设置访问码',
       targetPrinterRequired: '请先选择目标打印机',
       targetPrinterRequired: '请先选择目标打印机',
       bindIpRequired: '请先设置绑定 IP',
       bindIpRequired: '请先设置绑定 IP',

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -3973,6 +3973,7 @@ export default {
       updated: '虛擬印表機設定已更新',
       updated: '虛擬印表機設定已更新',
       failedToUpdate: '更新設定失敗',
       failedToUpdate: '更新設定失敗',
       tailscaleNotAvailable: '此主機上未安裝 Tailscale。請先安裝 Tailscale,然後重試。',
       tailscaleNotAvailable: '此主機上未安裝 Tailscale。請先安裝 Tailscale,然後重試。',
+      copyFailed: '複製失敗 — 請手動選取文字',
       accessCodeRequired: '請先設定存取碼',
       accessCodeRequired: '請先設定存取碼',
       targetPrinterRequired: '請先選擇目標印表機',
       targetPrinterRequired: '請先選擇目標印表機',
       bindIpRequired: '請先設定繫結 IP',
       bindIpRequired: '請先設定繫結 IP',

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-BcG_mHJy.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Bm0EQe4Z.js"></script>
+    <script type="module" crossorigin src="/assets/index-BcG_mHJy.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DGaysySO.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DGaysySO.css">
   </head>
   </head>
   <body>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов