SwitchbarPopover.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { useState } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { useTranslation } from 'react-i18next';
  4. import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap, Radio, Eye } from 'lucide-react';
  5. import { api } from '../api/client';
  6. import type { SmartPlug } from '../api/client';
  7. import { ConfirmModal } from './ConfirmModal';
  8. interface SwitchbarPopoverProps {
  9. onClose: () => void;
  10. }
  11. function SwitchItem({ plug }: { plug: SmartPlug }) {
  12. const queryClient = useQueryClient();
  13. const [confirmAction, setConfirmAction] = useState<'on' | 'off' | null>(null);
  14. // Fetch current status
  15. const { data: status, isLoading: statusLoading } = useQuery({
  16. queryKey: ['smart-plug-status', plug.id],
  17. queryFn: () => api.getSmartPlugStatus(plug.id),
  18. refetchInterval: 10000, // Refresh every 10 seconds when popover is open
  19. });
  20. // Control mutation
  21. const controlMutation = useMutation({
  22. mutationFn: (action: 'on' | 'off') => api.controlSmartPlug(plug.id, action),
  23. onSuccess: () => {
  24. queryClient.invalidateQueries({ queryKey: ['smart-plug-status', plug.id] });
  25. },
  26. });
  27. const isOn = status?.state === 'ON';
  28. // For MQTT plugs, consider reachable if we have power data
  29. const hasMqttData = plug.plug_type === 'mqtt' && (status?.energy?.power !== null && status?.energy?.power !== undefined);
  30. const isReachable = (status?.reachable ?? false) || hasMqttData;
  31. const isPending = controlMutation.isPending;
  32. const isMqtt = plug.plug_type === 'mqtt';
  33. const handleConfirm = () => {
  34. if (confirmAction) {
  35. controlMutation.mutate(confirmAction);
  36. setConfirmAction(null);
  37. }
  38. };
  39. return (
  40. <>
  41. <div className="flex items-center justify-between py-2 px-3 hover:bg-bambu-dark-tertiary rounded-lg transition-colors">
  42. <div className="flex items-center gap-2">
  43. <div className={`p-1.5 rounded ${
  44. isMqtt
  45. ? (isReachable ? 'bg-teal-500/20' : 'bg-red-500/20')
  46. : (isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20')
  47. }`}>
  48. {isMqtt ? (
  49. <Radio className={`w-4 h-4 ${isReachable ? 'text-teal-400' : 'text-red-400'}`} />
  50. ) : (
  51. <Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
  52. )}
  53. </div>
  54. <div>
  55. <p className="text-sm text-white font-medium">{plug.name}</p>
  56. <div className="flex items-center gap-1 text-xs">
  57. {statusLoading ? (
  58. <Loader2 className="w-3 h-3 text-bambu-gray animate-spin" />
  59. ) : isMqtt ? (
  60. /* MQTT plugs show power and monitor-only indicator */
  61. isReachable ? (
  62. <>
  63. <Zap className="w-3 h-3 text-teal-400" />
  64. <span className="text-teal-400">{Math.round(status?.energy?.power ?? 0)}W</span>
  65. <span className="text-bambu-gray mx-1">|</span>
  66. <Eye className="w-3 h-3 text-bambu-gray" />
  67. <span className="text-bambu-gray">Monitor</span>
  68. </>
  69. ) : (
  70. <>
  71. <WifiOff className="w-3 h-3 text-status-error" />
  72. <span className="text-status-error">Waiting</span>
  73. </>
  74. )
  75. ) : isReachable ? (
  76. <>
  77. <Wifi className="w-3 h-3 text-status-ok" />
  78. <span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
  79. {status?.energy?.power !== null && status?.energy?.power !== undefined && (
  80. <>
  81. <span className="text-bambu-gray mx-1">|</span>
  82. <Zap className="w-3 h-3 text-yellow-400" />
  83. <span className="text-yellow-400">{Math.round(status.energy.power)}W</span>
  84. </>
  85. )}
  86. </>
  87. ) : (
  88. <>
  89. <WifiOff className="w-3 h-3 text-status-error" />
  90. <span className="text-status-error">Offline</span>
  91. </>
  92. )}
  93. </div>
  94. </div>
  95. </div>
  96. {/* Hide on/off buttons for MQTT plugs (monitor-only) */}
  97. {!isMqtt && (
  98. <div className="flex gap-1">
  99. <button
  100. onClick={() => setConfirmAction('on')}
  101. disabled={!isReachable || isPending}
  102. className={`p-1.5 rounded transition-colors ${
  103. isOn
  104. ? 'bg-bambu-green text-white'
  105. : 'bg-bambu-dark text-bambu-gray hover:text-white'
  106. } disabled:opacity-50 disabled:cursor-not-allowed`}
  107. title="Turn On"
  108. >
  109. {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
  110. </button>
  111. <button
  112. onClick={() => setConfirmAction('off')}
  113. disabled={!isReachable || isPending}
  114. className={`p-1.5 rounded transition-colors ${
  115. !isOn && isReachable
  116. ? 'bg-bambu-dark-tertiary text-white'
  117. : 'bg-bambu-dark text-bambu-gray hover:text-white'
  118. } disabled:opacity-50 disabled:cursor-not-allowed`}
  119. title="Turn Off"
  120. >
  121. {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
  122. </button>
  123. </div>
  124. )}
  125. </div>
  126. {confirmAction && (
  127. <ConfirmModal
  128. title={`Turn ${confirmAction === 'on' ? 'On' : 'Off'} Smart Plug`}
  129. message={`Are you sure you want to turn ${confirmAction === 'on' ? 'on' : 'off'} "${plug.name}"?`}
  130. confirmText={confirmAction === 'on' ? 'Turn On' : 'Turn Off'}
  131. variant={confirmAction === 'off' ? 'warning' : 'default'}
  132. onConfirm={handleConfirm}
  133. onCancel={() => setConfirmAction(null)}
  134. />
  135. )}
  136. </>
  137. );
  138. }
  139. export function SwitchbarPopover({ onClose }: SwitchbarPopoverProps) {
  140. const { t } = useTranslation();
  141. // Fetch all smart plugs
  142. const { data: plugs, isLoading } = useQuery({
  143. queryKey: ['smart-plugs'],
  144. queryFn: api.getSmartPlugs,
  145. });
  146. // Filter to only show plugs with show_in_switchbar enabled
  147. const switchbarPlugs = plugs?.filter(p => p.show_in_switchbar) || [];
  148. return (
  149. <div
  150. className="absolute bottom-full left-0 mb-2 w-72 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-xl z-50"
  151. onMouseLeave={onClose}
  152. >
  153. {/* Header */}
  154. <div className="px-4 py-3 border-b border-bambu-dark-tertiary">
  155. <h3 className="text-sm font-semibold text-white flex items-center gap-2">
  156. <Plug className="w-4 h-4 text-bambu-green" />
  157. Smart Switches
  158. </h3>
  159. </div>
  160. {/* Content */}
  161. <div className="p-2 max-h-80 overflow-y-auto">
  162. {isLoading ? (
  163. <div className="flex items-center justify-center py-8">
  164. <Loader2 className="w-6 h-6 text-bambu-gray animate-spin" />
  165. </div>
  166. ) : switchbarPlugs.length === 0 ? (
  167. <div className="text-center py-6 px-4">
  168. <Plug className="w-8 h-8 text-bambu-gray mx-auto mb-2" />
  169. <p className="text-sm text-bambu-gray">{t('smartPlugs.noSwitchesInSwitchbar')}</p>
  170. <p className="text-xs text-bambu-gray mt-1">
  171. {t('smartPlugs.enableSwitchbarHint')}
  172. </p>
  173. </div>
  174. ) : (
  175. <div className="space-y-1">
  176. {switchbarPlugs.map(plug => (
  177. <SwitchItem key={plug.id} plug={plug} />
  178. ))}
  179. </div>
  180. )}
  181. </div>
  182. </div>
  183. );
  184. }