VirtualPrinterSettings.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. import { useState, useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  4. import { Loader2, Check, AlertTriangle, Printer, Eye, EyeOff, Info, ChevronDown, ExternalLink, ArrowRightLeft } from 'lucide-react';
  5. import { api, virtualPrinterApi } from '../api/client';
  6. import { Card, CardContent, CardHeader } from './Card';
  7. import { Button } from './Button';
  8. import { useToast } from '../contexts/ToastContext';
  9. type LocalMode = 'archive' | 'review' | 'queue' | 'proxy';
  10. // Legacy wire values shipped before the UI labels were aligned with the wire
  11. // format. The backend normalizes these on read but a freshly-loaded settings
  12. // payload can still carry an old value if it pre-dates the migration. Map
  13. // to canonical so the button highlight matches the saved mode (#1429).
  14. function normalizeMode(value: string | undefined): LocalMode {
  15. if (value === 'immediate') return 'archive';
  16. if (value === 'print_queue') return 'queue';
  17. if (value === 'queue' || value === 'archive' || value === 'review' || value === 'proxy') return value;
  18. return 'archive';
  19. }
  20. export function VirtualPrinterSettings() {
  21. const { t } = useTranslation();
  22. const queryClient = useQueryClient();
  23. const { showToast } = useToast();
  24. const [localEnabled, setLocalEnabled] = useState(false);
  25. const [localAccessCode, setLocalAccessCode] = useState('');
  26. const [localMode, setLocalMode] = useState<LocalMode>('archive');
  27. const [localModel, setLocalModel] = useState('BL-P001');
  28. const [localTargetPrinterId, setLocalTargetPrinterId] = useState<number | null>(null);
  29. const [localRemoteInterfaceIp, setLocalRemoteInterfaceIp] = useState('');
  30. const [showAccessCode, setShowAccessCode] = useState(false);
  31. const [pendingAction, setPendingAction] = useState<'toggle' | 'accessCode' | 'mode' | 'model' | 'targetPrinter' | 'remoteInterface' | null>(null);
  32. // Fetch current settings
  33. const { data: settings, isLoading } = useQuery({
  34. queryKey: ['virtual-printer-settings'],
  35. queryFn: virtualPrinterApi.getSettings,
  36. refetchInterval: 10000, // Refresh every 10 seconds for status updates
  37. });
  38. // Fetch available models
  39. const { data: modelsData } = useQuery({
  40. queryKey: ['virtual-printer-models'],
  41. queryFn: virtualPrinterApi.getModels,
  42. });
  43. // Fetch printers for proxy mode dropdown
  44. const { data: printers } = useQuery({
  45. queryKey: ['printers'],
  46. queryFn: api.getPrinters,
  47. });
  48. // Fetch network interfaces for IP override (all modes when enabled)
  49. const { data: networkInterfaces } = useQuery({
  50. queryKey: ['network-interfaces'],
  51. queryFn: () => api.getNetworkInterfaces().then(res => res.interfaces),
  52. enabled: localEnabled,
  53. });
  54. // Initialize local state from settings
  55. useEffect(() => {
  56. if (settings) {
  57. setLocalEnabled(settings.enabled);
  58. setLocalMode(normalizeMode(settings.mode));
  59. setLocalModel(settings.model);
  60. setLocalTargetPrinterId(settings.target_printer_id);
  61. setLocalRemoteInterfaceIp(settings.remote_interface_ip || '');
  62. }
  63. }, [settings]);
  64. // Update mutation
  65. const updateMutation = useMutation({
  66. mutationFn: (data: { enabled?: boolean; access_code?: string; mode?: LocalMode; model?: string; target_printer_id?: number; remote_interface_ip?: string }) =>
  67. virtualPrinterApi.updateSettings(data),
  68. onSuccess: () => {
  69. queryClient.invalidateQueries({ queryKey: ['virtual-printer-settings'] });
  70. showToast(t('virtualPrinter.toast.updated'));
  71. setPendingAction(null);
  72. },
  73. onError: (error: Error) => {
  74. showToast(error.message || t('virtualPrinter.toast.failedToUpdate'), 'error');
  75. // Revert local state on error
  76. if (settings) {
  77. setLocalEnabled(settings.enabled);
  78. setLocalMode(normalizeMode(settings.mode));
  79. setLocalModel(settings.model);
  80. setLocalTargetPrinterId(settings.target_printer_id);
  81. }
  82. setPendingAction(null);
  83. },
  84. });
  85. const handleToggleEnabled = () => {
  86. const newEnabled = !localEnabled;
  87. // Validation depends on mode
  88. if (newEnabled) {
  89. if (localMode === 'proxy') {
  90. // Proxy mode requires target printer
  91. if (!localTargetPrinterId) {
  92. showToast(t('virtualPrinter.toast.targetPrinterRequired'), 'error');
  93. return;
  94. }
  95. } else {
  96. // Other modes require access code
  97. if (!localAccessCode && !settings?.access_code_set) {
  98. showToast(t('virtualPrinter.toast.accessCodeRequired'), 'error');
  99. return;
  100. }
  101. }
  102. }
  103. setLocalEnabled(newEnabled);
  104. setPendingAction('toggle');
  105. updateMutation.mutate({
  106. enabled: newEnabled,
  107. access_code: localMode !== 'proxy' ? (localAccessCode || undefined) : undefined,
  108. mode: localMode,
  109. target_printer_id: localMode === 'proxy' ? (localTargetPrinterId ?? undefined) : undefined,
  110. });
  111. };
  112. const handleAccessCodeChange = () => {
  113. if (!localAccessCode) {
  114. showToast(t('virtualPrinter.toast.accessCodeEmpty'), 'error');
  115. return;
  116. }
  117. if (localAccessCode.length !== 8) {
  118. showToast(t('virtualPrinter.toast.accessCodeLength'), 'error');
  119. return;
  120. }
  121. setPendingAction('accessCode');
  122. updateMutation.mutate({
  123. access_code: localAccessCode,
  124. });
  125. setLocalAccessCode(''); // Clear after saving
  126. };
  127. const handleModeChange = (mode: LocalMode) => {
  128. setLocalMode(mode);
  129. setPendingAction('mode');
  130. updateMutation.mutate({ mode });
  131. };
  132. const handleTargetPrinterChange = (printerId: number) => {
  133. setLocalTargetPrinterId(printerId);
  134. setPendingAction('targetPrinter');
  135. updateMutation.mutate({
  136. target_printer_id: printerId,
  137. });
  138. };
  139. const handleModelChange = (model: string) => {
  140. setLocalModel(model);
  141. setPendingAction('model');
  142. updateMutation.mutate({ model });
  143. };
  144. const handleRemoteInterfaceChange = (ip: string) => {
  145. setLocalRemoteInterfaceIp(ip);
  146. setPendingAction('remoteInterface');
  147. updateMutation.mutate({ remote_interface_ip: ip });
  148. };
  149. if (isLoading) {
  150. return (
  151. <Card>
  152. <CardContent className="py-8 flex justify-center">
  153. <Loader2 className="w-6 h-6 animate-spin text-bambu-green" />
  154. </CardContent>
  155. </Card>
  156. );
  157. }
  158. const status = settings?.status;
  159. const isRunning = status?.running || false;
  160. return (
  161. <div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
  162. {/* Left Column - Settings */}
  163. <div className="space-y-6 lg:w-[480px] lg:flex-shrink-0">
  164. <Card>
  165. <CardHeader>
  166. <div className="flex items-center justify-between">
  167. <div className="flex items-center gap-2">
  168. <Printer className="w-5 h-5 text-bambu-green" />
  169. <h2 className="text-lg font-semibold text-white">{t('virtualPrinter.title')}</h2>
  170. </div>
  171. {status && (
  172. <div className={`flex items-center gap-2 text-sm ${isRunning ? 'text-green-700 dark:text-green-400' : 'text-bambu-gray'}`}>
  173. <span className={`w-2 h-2 rounded-full ${isRunning ? 'bg-green-400 animate-pulse' : 'bg-gray-500'}`} />
  174. {isRunning ? t('virtualPrinter.running') : t('virtualPrinter.stopped')}
  175. </div>
  176. )}
  177. </div>
  178. </CardHeader>
  179. <CardContent className="space-y-4">
  180. <p className="text-sm text-bambu-gray">
  181. {localMode === 'proxy'
  182. ? t('virtualPrinter.description.proxy')
  183. : t('virtualPrinter.description.default')}
  184. </p>
  185. {/* Enable/Disable Toggle */}
  186. <div className="flex items-center justify-between py-3 border-t border-bambu-dark-tertiary">
  187. <div>
  188. <div className="text-white font-medium">{t('virtualPrinter.enable.title')}</div>
  189. <div className="text-sm text-bambu-gray">
  190. {isRunning ? (
  191. localMode === 'proxy'
  192. ? t('virtualPrinter.enable.proxyingTo', { name: printers?.find(p => p.id === localTargetPrinterId)?.name || 'printer' })
  193. : t('virtualPrinter.enable.visibleInSlicer')
  194. ) : t('virtualPrinter.enable.notActive')}
  195. </div>
  196. </div>
  197. <button
  198. onClick={handleToggleEnabled}
  199. disabled={pendingAction === 'toggle'}
  200. className={`relative w-12 h-6 rounded-full transition-colors ${
  201. localEnabled ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
  202. } ${pendingAction === 'toggle' ? 'opacity-50' : ''}`}
  203. >
  204. <span
  205. className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform ${
  206. localEnabled ? 'translate-x-6' : ''
  207. }`}
  208. />
  209. </button>
  210. </div>
  211. {/* Printer Model - only for non-proxy modes */}
  212. {localMode !== 'proxy' && (
  213. <div className="py-3 border-t border-bambu-dark-tertiary">
  214. <div className="text-white font-medium mb-2">{t('virtualPrinter.model.title')}</div>
  215. <div className="text-sm text-bambu-gray mb-3">
  216. {t('virtualPrinter.model.description')}
  217. </div>
  218. <div className="relative">
  219. <select
  220. value={localModel}
  221. onChange={(e) => handleModelChange(e.target.value)}
  222. disabled={pendingAction === 'model'}
  223. className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed pr-10"
  224. >
  225. {modelsData?.models && Object.entries(modelsData.models)
  226. .sort(([, a], [, b]) => (a as string).localeCompare(b as string))
  227. .map(([code, name]) => (
  228. <option key={code} value={code}>
  229. {name}
  230. </option>
  231. ))}
  232. </select>
  233. <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  234. </div>
  235. {localEnabled && isRunning && (
  236. <p className="text-xs text-bambu-gray mt-2">
  237. <Info className="w-3 h-3 inline mr-1" />
  238. {t('virtualPrinter.model.restartWarning')}
  239. </p>
  240. )}
  241. </div>
  242. )}
  243. {/* Access Code - only for non-proxy modes */}
  244. {localMode !== 'proxy' && (
  245. <div className="py-3 border-t border-bambu-dark-tertiary">
  246. <div className="text-white font-medium mb-2">{t('virtualPrinter.accessCode.title')}</div>
  247. <div className="text-sm text-bambu-gray mb-3">
  248. {settings?.access_code_set ? (
  249. <span className="flex items-center gap-1 text-green-700 dark:text-green-400">
  250. <Check className="w-4 h-4" />
  251. {t('virtualPrinter.accessCode.isSet')}
  252. </span>
  253. ) : (
  254. <span className="flex items-center gap-1 text-yellow-700 dark:text-yellow-400">
  255. <AlertTriangle className="w-4 h-4" />
  256. {t('virtualPrinter.accessCode.notSet')}
  257. </span>
  258. )}
  259. </div>
  260. <div className="flex gap-2">
  261. <div className="relative flex-1">
  262. <input
  263. type={showAccessCode ? 'text' : 'password'}
  264. value={localAccessCode}
  265. onChange={(e) => setLocalAccessCode(e.target.value)}
  266. placeholder={settings?.access_code_set ? t('virtualPrinter.accessCode.placeholderChange') : t('virtualPrinter.accessCode.placeholder')}
  267. maxLength={8}
  268. className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white placeholder-bambu-gray pr-10 font-mono"
  269. />
  270. <button
  271. onClick={() => setShowAccessCode(!showAccessCode)}
  272. className="absolute right-2 top-1/2 -translate-y-1/2 text-bambu-gray hover:text-white"
  273. >
  274. {showAccessCode ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
  275. </button>
  276. </div>
  277. <Button
  278. onClick={handleAccessCodeChange}
  279. disabled={!localAccessCode || pendingAction === 'accessCode'}
  280. variant="primary"
  281. >
  282. {pendingAction === 'accessCode' ? <Loader2 className="w-4 h-4 animate-spin" /> : t('common.save')}
  283. </Button>
  284. </div>
  285. <p className="text-xs text-bambu-gray mt-2">
  286. {t('virtualPrinter.accessCode.hint')}
  287. {localAccessCode && (
  288. <span className={localAccessCode.length === 8 ? 'text-green-700 dark:text-green-400' : 'text-yellow-700 dark:text-yellow-400'}>
  289. {' '}{t('virtualPrinter.accessCode.charCount', { count: localAccessCode.length })}
  290. </span>
  291. )}
  292. </p>
  293. </div>
  294. )}
  295. {/* Target Printer - only for proxy mode */}
  296. {localMode === 'proxy' && (
  297. <div className="py-3 border-t border-bambu-dark-tertiary">
  298. <div className="text-white font-medium mb-2">{t('virtualPrinter.targetPrinter.title')}</div>
  299. <div className="text-sm text-bambu-gray mb-3">
  300. {localTargetPrinterId ? (
  301. <span className="flex items-center gap-1 text-green-700 dark:text-green-400">
  302. <Check className="w-4 h-4" />
  303. {t('virtualPrinter.targetPrinter.configured')}
  304. </span>
  305. ) : (
  306. <span className="flex items-center gap-1 text-yellow-700 dark:text-yellow-400">
  307. <AlertTriangle className="w-4 h-4" />
  308. {t('virtualPrinter.targetPrinter.notConfigured')}
  309. </span>
  310. )}
  311. </div>
  312. <div className="relative">
  313. <select
  314. value={localTargetPrinterId ?? ''}
  315. onChange={(e) => {
  316. const id = parseInt(e.target.value, 10);
  317. if (!isNaN(id)) {
  318. handleTargetPrinterChange(id);
  319. }
  320. }}
  321. disabled={pendingAction === 'targetPrinter'}
  322. className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed pr-10"
  323. >
  324. <option value="">{t('virtualPrinter.targetPrinter.placeholder')}</option>
  325. {printers?.map((printer) => (
  326. <option key={printer.id} value={printer.id}>
  327. {printer.name} ({printer.ip_address})
  328. </option>
  329. ))}
  330. </select>
  331. <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  332. </div>
  333. <p className="text-xs text-bambu-gray mt-2">
  334. {t('virtualPrinter.targetPrinter.hint')}
  335. </p>
  336. {!printers?.length && (
  337. <p className="text-xs text-yellow-700 dark:text-yellow-400 mt-2">
  338. <AlertTriangle className="w-3 h-3 inline mr-1" />
  339. {t('virtualPrinter.targetPrinter.noPrinters')}
  340. </p>
  341. )}
  342. </div>
  343. )}
  344. {/* Remote Interface - available for all modes (IP override / SSDP proxy) */}
  345. {localEnabled && (
  346. <div className="py-3 border-t border-bambu-dark-tertiary">
  347. <div className="text-white font-medium mb-2">{t('virtualPrinter.remoteInterface.title')}</div>
  348. <div className="text-sm text-bambu-gray mb-3">
  349. {localRemoteInterfaceIp ? (
  350. <span className="flex items-center gap-1 text-green-700 dark:text-green-400">
  351. <Check className="w-4 h-4" />
  352. {t('virtualPrinter.remoteInterface.configured')}
  353. </span>
  354. ) : (
  355. <span className="flex items-center gap-1 text-bambu-gray">
  356. <Info className="w-4 h-4" />
  357. {t('virtualPrinter.remoteInterface.optional')}
  358. </span>
  359. )}
  360. </div>
  361. <div className="relative">
  362. <select
  363. value={localRemoteInterfaceIp}
  364. onChange={(e) => handleRemoteInterfaceChange(e.target.value)}
  365. disabled={pendingAction === 'remoteInterface'}
  366. className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed pr-10"
  367. >
  368. <option value="">{t('virtualPrinter.remoteInterface.placeholder')}</option>
  369. {networkInterfaces?.map((iface) => (
  370. <option key={iface.ip} value={iface.ip}>
  371. {iface.name} ({iface.ip}) - {iface.subnet}
  372. </option>
  373. ))}
  374. </select>
  375. <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  376. </div>
  377. <p className="text-xs text-bambu-gray mt-2">
  378. {t('virtualPrinter.remoteInterface.hint')}
  379. </p>
  380. </div>
  381. )}
  382. {/* Mode */}
  383. <div className="py-3 border-t border-bambu-dark-tertiary">
  384. <div className="text-white font-medium mb-2">{t('virtualPrinter.mode.title')}</div>
  385. <div className="grid grid-cols-2 gap-3">
  386. <button
  387. onClick={() => handleModeChange('archive')}
  388. disabled={pendingAction === 'mode'}
  389. className={`p-3 rounded-lg border text-left transition-colors ${
  390. localMode === 'archive'
  391. ? 'border-bambu-green bg-bambu-green/10'
  392. : 'border-bambu-dark-tertiary hover:border-bambu-gray'
  393. }`}
  394. >
  395. <div className="text-white font-medium">{t('virtualPrinter.mode.archive')}</div>
  396. <div className="text-xs text-bambu-gray">{t('virtualPrinter.mode.archiveDesc')}</div>
  397. </button>
  398. <button
  399. onClick={() => handleModeChange('review')}
  400. disabled={pendingAction === 'mode'}
  401. className={`p-3 rounded-lg border text-left transition-colors ${
  402. localMode === 'review'
  403. ? 'border-bambu-green bg-bambu-green/10'
  404. : 'border-bambu-dark-tertiary hover:border-bambu-gray'
  405. }`}
  406. >
  407. <div className="text-white font-medium">{t('virtualPrinter.mode.review')}</div>
  408. <div className="text-xs text-bambu-gray">{t('virtualPrinter.mode.reviewDesc')}</div>
  409. </button>
  410. <button
  411. onClick={() => handleModeChange('queue')}
  412. disabled={pendingAction === 'mode'}
  413. className={`p-3 rounded-lg border text-left transition-colors ${
  414. localMode === 'queue'
  415. ? 'border-bambu-green bg-bambu-green/10'
  416. : 'border-bambu-dark-tertiary hover:border-bambu-gray'
  417. }`}
  418. >
  419. <div className="text-white font-medium">{t('virtualPrinter.mode.queue')}</div>
  420. <div className="text-xs text-bambu-gray">{t('virtualPrinter.mode.queueDesc')}</div>
  421. </button>
  422. <button
  423. onClick={() => handleModeChange('proxy')}
  424. disabled={pendingAction === 'mode'}
  425. className={`p-3 rounded-lg border text-left transition-colors ${
  426. localMode === 'proxy'
  427. ? 'border-blue-500 bg-blue-500/10'
  428. : 'border-bambu-dark-tertiary hover:border-bambu-gray'
  429. }`}
  430. >
  431. <div className="flex items-center gap-1.5 text-white font-medium">
  432. <ArrowRightLeft className="w-4 h-4" />
  433. {t('virtualPrinter.mode.proxy')}
  434. </div>
  435. <div className="text-xs text-bambu-gray">{t('virtualPrinter.mode.proxyDesc')}</div>
  436. </button>
  437. </div>
  438. </div>
  439. </CardContent>
  440. </Card>
  441. </div>
  442. {/* Right Column - Info & Status */}
  443. <div className="space-y-6 lg:w-[480px] lg:flex-shrink-0">
  444. {/* Setup Required Warning */}
  445. <Card className="border-l-4 border-l-yellow-500">
  446. <CardContent className="py-4">
  447. <div className="flex items-start gap-3">
  448. <AlertTriangle className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
  449. <div className="text-sm">
  450. <p className="text-white font-medium mb-2">
  451. {t('virtualPrinter.setupRequired.title')}
  452. </p>
  453. <p className="text-bambu-gray mb-3">
  454. {t('virtualPrinter.setupRequired.description')}
  455. </p>
  456. <a
  457. href="https://wiki.bambuddy.cool/features/virtual-printer/"
  458. target="_blank"
  459. rel="noopener noreferrer"
  460. className="inline-flex items-center gap-2 px-4 py-2 bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-500/50 rounded-md text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors"
  461. >
  462. <ExternalLink className="w-4 h-4" />
  463. {t('virtualPrinter.setupRequired.readGuide')}
  464. </a>
  465. </div>
  466. </div>
  467. </CardContent>
  468. </Card>
  469. {/* How it works */}
  470. <Card>
  471. <CardContent className="py-4">
  472. <div className="flex items-start gap-3">
  473. <Info className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
  474. <div className="text-sm text-bambu-gray">
  475. <p className="mb-2">
  476. <strong className="text-white">{localMode === 'proxy' ? t('virtualPrinter.howItWorks.titleProxy') : t('virtualPrinter.howItWorks.title')}:</strong>
  477. </p>
  478. {localMode === 'proxy' ? (
  479. <ol className="list-decimal list-inside space-y-1">
  480. <li>{t('virtualPrinter.howItWorks.proxyStep1')}</li>
  481. <li>{t('virtualPrinter.howItWorks.proxyStep2')}</li>
  482. <li>{t('virtualPrinter.howItWorks.proxyStep3')}</li>
  483. <li>{t('virtualPrinter.howItWorks.proxyStep4')}</li>
  484. <li>{t('virtualPrinter.howItWorks.proxyStep5')}</li>
  485. </ol>
  486. ) : (
  487. <ol className="list-decimal list-inside space-y-1">
  488. <li>{t('virtualPrinter.howItWorks.step1')}</li>
  489. <li>{t('virtualPrinter.howItWorks.step2')}</li>
  490. <li>{t('virtualPrinter.howItWorks.step3')}</li>
  491. <li>{t('virtualPrinter.howItWorks.step4')}</li>
  492. <li>{t('virtualPrinter.howItWorks.step5')}</li>
  493. <li>{t('virtualPrinter.howItWorks.step6')}</li>
  494. </ol>
  495. )}
  496. </div>
  497. </div>
  498. </CardContent>
  499. </Card>
  500. {/* Status Details (when running) */}
  501. {status && isRunning && (
  502. <Card>
  503. <CardHeader>
  504. <h3 className="text-md font-semibold text-white">{t('virtualPrinter.status.title')}</h3>
  505. </CardHeader>
  506. <CardContent>
  507. {status.mode === 'proxy' && status.proxy ? (
  508. <div className="grid grid-cols-2 gap-4 text-sm">
  509. <div>
  510. <div className="text-bambu-gray">{t('virtualPrinter.status.targetPrinter')}</div>
  511. <div className="text-white">
  512. {printers?.find(p => p.id === localTargetPrinterId)?.name || status.proxy.target_host}
  513. </div>
  514. <div className="text-xs text-bambu-gray font-mono">{status.proxy.target_host}</div>
  515. </div>
  516. <div>
  517. <div className="text-bambu-gray">{t('virtualPrinter.status.mode')}</div>
  518. <div className="text-white flex items-center gap-1.5">
  519. <ArrowRightLeft className="w-4 h-4" />
  520. {t('virtualPrinter.mode.proxy')}
  521. </div>
  522. </div>
  523. <div>
  524. <div className="text-bambu-gray">{t('virtualPrinter.status.ftpPort')}</div>
  525. <div className="text-white font-mono">{status.proxy.ftp_port}</div>
  526. </div>
  527. <div>
  528. <div className="text-bambu-gray">{t('virtualPrinter.status.mqttPort')}</div>
  529. <div className="text-white font-mono">{status.proxy.mqtt_port}</div>
  530. </div>
  531. <div>
  532. <div className="text-bambu-gray">{t('virtualPrinter.status.ftpConnections')}</div>
  533. <div className="text-white">{status.proxy.ftp_connections ?? 0}</div>
  534. </div>
  535. <div>
  536. <div className="text-bambu-gray">{t('virtualPrinter.status.mqttConnections')}</div>
  537. <div className="text-white">{status.proxy.mqtt_connections ?? 0}</div>
  538. </div>
  539. </div>
  540. ) : (
  541. <div className="grid grid-cols-2 gap-4 text-sm">
  542. <div>
  543. <div className="text-bambu-gray">{t('virtualPrinter.status.printerName')}</div>
  544. <div className="text-white">{status.name}</div>
  545. </div>
  546. <div>
  547. <div className="text-bambu-gray">{t('virtualPrinter.status.model')}</div>
  548. <div className="text-white">{status.model_name || status.model}</div>
  549. </div>
  550. <div>
  551. <div className="text-bambu-gray">{t('virtualPrinter.status.serialNumber')}</div>
  552. <div className="text-white font-mono">{status.serial}</div>
  553. </div>
  554. <div>
  555. <div className="text-bambu-gray">{t('virtualPrinter.status.mode')}</div>
  556. <div className="text-white capitalize">{status.mode}</div>
  557. </div>
  558. <div>
  559. <div className="text-bambu-gray">{t('virtualPrinter.status.pendingFiles')}</div>
  560. <div className="text-white">{status.pending_files}</div>
  561. </div>
  562. </div>
  563. )}
  564. </CardContent>
  565. </Card>
  566. )}
  567. </div>
  568. </div>
  569. );
  570. }