LEDPage.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. import { useState, useEffect, useCallback } from 'react'
  2. import { Link } from 'react-router-dom'
  3. import { toast } from 'sonner'
  4. import { Button } from '@/components/ui/button'
  5. import {
  6. Card,
  7. CardContent,
  8. CardDescription,
  9. CardHeader,
  10. CardTitle,
  11. } from '@/components/ui/card'
  12. import { Label } from '@/components/ui/label'
  13. import { Separator } from '@/components/ui/separator'
  14. import { Switch } from '@/components/ui/switch'
  15. import { Slider } from '@/components/ui/slider'
  16. import {
  17. Select,
  18. SelectContent,
  19. SelectItem,
  20. SelectTrigger,
  21. SelectValue,
  22. } from '@/components/ui/select'
  23. import { Input } from '@/components/ui/input'
  24. import { ColorPicker } from '@/components/ui/color-picker'
  25. // Types
  26. interface LedConfig {
  27. provider: 'none' | 'wled' | 'dw_leds'
  28. wled_ip?: string
  29. num_leds?: number
  30. gpio_pin?: number
  31. }
  32. interface DWLedsStatus {
  33. connected: boolean
  34. power_on: boolean
  35. brightness: number
  36. speed: number
  37. intensity: number
  38. current_effect: number
  39. current_palette: number
  40. num_leds: number
  41. gpio_pin: number
  42. colors: string[]
  43. error?: string
  44. }
  45. interface EffectSettings {
  46. effect_id: number
  47. palette_id: number
  48. speed: number
  49. intensity: number
  50. color1: string
  51. color2: string
  52. color3: string
  53. }
  54. export function LEDPage() {
  55. const [ledConfig, setLedConfig] = useState<LedConfig | null>(null)
  56. const [isLoading, setIsLoading] = useState(true)
  57. // DW LEDs state
  58. const [dwStatus, setDwStatus] = useState<DWLedsStatus | null>(null)
  59. const [effects, setEffects] = useState<[number, string][]>([])
  60. const [palettes, setPalettes] = useState<[number, string][]>([])
  61. const [brightness, setBrightness] = useState(35)
  62. const [speed, setSpeed] = useState(128)
  63. const [intensity, setIntensity] = useState(128)
  64. const [selectedEffect, setSelectedEffect] = useState('')
  65. const [selectedPalette, setSelectedPalette] = useState('')
  66. const [color1, setColor1] = useState('#ff0000')
  67. const [color2, setColor2] = useState('#000000')
  68. const [color3, setColor3] = useState('#0000ff')
  69. // Effect automation state
  70. const [idleEffect, setIdleEffect] = useState<EffectSettings | null>(null)
  71. const [playingEffect, setPlayingEffect] = useState<EffectSettings | null>(null)
  72. const [idleTimeoutEnabled, setIdleTimeoutEnabled] = useState(false)
  73. const [idleTimeoutMinutes, setIdleTimeoutMinutes] = useState(30)
  74. // Fetch LED configuration
  75. useEffect(() => {
  76. const fetchConfig = async () => {
  77. try {
  78. const response = await fetch('/get_led_config')
  79. const data = await response.json()
  80. // Map backend response fields to our interface
  81. setLedConfig({
  82. provider: data.provider || 'none',
  83. wled_ip: data.wled_ip,
  84. num_leds: data.dw_led_num_leds,
  85. gpio_pin: data.dw_led_gpio_pin,
  86. })
  87. } catch (error) {
  88. console.error('Error fetching LED config:', error)
  89. } finally {
  90. setIsLoading(false)
  91. }
  92. }
  93. fetchConfig()
  94. }, [])
  95. // Initialize DW LEDs when provider is dw_leds
  96. useEffect(() => {
  97. if (ledConfig?.provider === 'dw_leds') {
  98. fetchDWLedsStatus()
  99. fetchEffectsAndPalettes()
  100. fetchEffectSettings()
  101. fetchIdleTimeout()
  102. }
  103. }, [ledConfig])
  104. const fetchDWLedsStatus = async () => {
  105. try {
  106. const response = await fetch('/api/dw_leds/status')
  107. const data = await response.json()
  108. setDwStatus(data)
  109. if (data.connected) {
  110. setBrightness(data.brightness || 35)
  111. setSpeed(data.speed || 128)
  112. setIntensity(data.intensity || 128)
  113. setSelectedEffect(String(data.current_effect || 0))
  114. setSelectedPalette(String(data.current_palette || 0))
  115. if (data.colors) {
  116. setColor1(data.colors[0] || '#ff0000')
  117. setColor2(data.colors[1] || '#000000')
  118. setColor3(data.colors[2] || '#0000ff')
  119. }
  120. }
  121. } catch (error) {
  122. console.error('Error fetching DW LEDs status:', error)
  123. }
  124. }
  125. const fetchEffectsAndPalettes = async () => {
  126. try {
  127. const [effectsRes, palettesRes] = await Promise.all([
  128. fetch('/api/dw_leds/effects'),
  129. fetch('/api/dw_leds/palettes'),
  130. ])
  131. const effectsData = await effectsRes.json()
  132. const palettesData = await palettesRes.json()
  133. if (effectsData.effects) {
  134. const sorted = [...effectsData.effects].sort((a, b) => a[1].localeCompare(b[1]))
  135. setEffects(sorted)
  136. }
  137. if (palettesData.palettes) {
  138. const sorted = [...palettesData.palettes].sort((a, b) => a[1].localeCompare(b[1]))
  139. setPalettes(sorted)
  140. }
  141. } catch (error) {
  142. console.error('Error fetching effects/palettes:', error)
  143. }
  144. }
  145. const fetchEffectSettings = async () => {
  146. try {
  147. const response = await fetch('/api/dw_leds/get_effect_settings')
  148. const data = await response.json()
  149. setIdleEffect(data.idle_effect || null)
  150. setPlayingEffect(data.playing_effect || null)
  151. } catch (error) {
  152. console.error('Error fetching effect settings:', error)
  153. }
  154. }
  155. const fetchIdleTimeout = async () => {
  156. try {
  157. const response = await fetch('/api/dw_leds/idle_timeout')
  158. const data = await response.json()
  159. setIdleTimeoutEnabled(data.enabled || false)
  160. setIdleTimeoutMinutes(data.minutes || 30)
  161. } catch (error) {
  162. console.error('Error fetching idle timeout:', error)
  163. }
  164. }
  165. const handlePowerToggle = async () => {
  166. try {
  167. const response = await fetch('/api/dw_leds/power', {
  168. method: 'POST',
  169. headers: { 'Content-Type': 'application/json' },
  170. body: JSON.stringify({ state: 2 }), // Toggle
  171. })
  172. const data = await response.json()
  173. if (data.connected) {
  174. toast.success(`Power ${data.power_on ? 'ON' : 'OFF'}`)
  175. await fetchDWLedsStatus()
  176. } else {
  177. toast.error(data.error || 'Failed to toggle power')
  178. }
  179. } catch (error) {
  180. toast.error('Failed to toggle power')
  181. }
  182. }
  183. const handleBrightnessChange = useCallback(async (value: number[]) => {
  184. setBrightness(value[0])
  185. }, [])
  186. const handleBrightnessCommit = async (value: number[]) => {
  187. try {
  188. const response = await fetch('/api/dw_leds/brightness', {
  189. method: 'POST',
  190. headers: { 'Content-Type': 'application/json' },
  191. body: JSON.stringify({ value: value[0] }),
  192. })
  193. const data = await response.json()
  194. if (data.connected) {
  195. toast.success(`Brightness: ${value[0]}%`)
  196. }
  197. } catch (error) {
  198. toast.error('Failed to set brightness')
  199. }
  200. }
  201. const handleSpeedChange = useCallback((value: number[]) => {
  202. setSpeed(value[0])
  203. }, [])
  204. const handleSpeedCommit = async (value: number[]) => {
  205. try {
  206. await fetch('/api/dw_leds/speed', {
  207. method: 'POST',
  208. headers: { 'Content-Type': 'application/json' },
  209. body: JSON.stringify({ speed: value[0] }),
  210. })
  211. toast.success(`Speed: ${value[0]}`)
  212. } catch (error) {
  213. toast.error('Failed to set speed')
  214. }
  215. }
  216. const handleIntensityChange = useCallback((value: number[]) => {
  217. setIntensity(value[0])
  218. }, [])
  219. const handleIntensityCommit = async (value: number[]) => {
  220. try {
  221. await fetch('/api/dw_leds/intensity', {
  222. method: 'POST',
  223. headers: { 'Content-Type': 'application/json' },
  224. body: JSON.stringify({ intensity: value[0] }),
  225. })
  226. toast.success(`Intensity: ${value[0]}`)
  227. } catch (error) {
  228. toast.error('Failed to set intensity')
  229. }
  230. }
  231. const handleEffectChange = async (value: string) => {
  232. setSelectedEffect(value)
  233. try {
  234. const response = await fetch('/api/dw_leds/effect', {
  235. method: 'POST',
  236. headers: { 'Content-Type': 'application/json' },
  237. body: JSON.stringify({ effect_id: parseInt(value) }),
  238. })
  239. const data = await response.json()
  240. if (data.connected) {
  241. toast.success('Effect changed')
  242. if (data.power_on !== undefined) {
  243. setDwStatus((prev) => prev ? { ...prev, power_on: data.power_on } : null)
  244. }
  245. }
  246. } catch (error) {
  247. toast.error('Failed to set effect')
  248. }
  249. }
  250. const handlePaletteChange = async (value: string) => {
  251. setSelectedPalette(value)
  252. try {
  253. const response = await fetch('/api/dw_leds/palette', {
  254. method: 'POST',
  255. headers: { 'Content-Type': 'application/json' },
  256. body: JSON.stringify({ palette_id: parseInt(value) }),
  257. })
  258. const data = await response.json()
  259. if (data.connected) {
  260. toast.success('Palette changed')
  261. }
  262. } catch (error) {
  263. toast.error('Failed to set palette')
  264. }
  265. }
  266. const handleColorChange = async (slot: 1 | 2 | 3, value: string) => {
  267. if (slot === 1) setColor1(value)
  268. else if (slot === 2) setColor2(value)
  269. else setColor3(value)
  270. // Debounce color changes
  271. try {
  272. const hexToRgb = (hex: string) => {
  273. const r = parseInt(hex.slice(1, 3), 16)
  274. const g = parseInt(hex.slice(3, 5), 16)
  275. const b = parseInt(hex.slice(5, 7), 16)
  276. return [r, g, b]
  277. }
  278. const payload: Record<string, number[]> = {}
  279. payload[`color${slot}`] = hexToRgb(value)
  280. await fetch('/api/dw_leds/colors', {
  281. method: 'POST',
  282. headers: { 'Content-Type': 'application/json' },
  283. body: JSON.stringify(payload),
  284. })
  285. } catch (error) {
  286. console.error('Failed to set color:', error)
  287. }
  288. }
  289. const saveCurrentEffectSettings = async (type: 'idle' | 'playing') => {
  290. try {
  291. const settings = {
  292. type,
  293. effect_id: parseInt(selectedEffect) || 0,
  294. palette_id: parseInt(selectedPalette) || 0,
  295. speed,
  296. intensity,
  297. color1,
  298. color2,
  299. color3,
  300. }
  301. await fetch('/api/dw_leds/save_effect_settings', {
  302. method: 'POST',
  303. headers: { 'Content-Type': 'application/json' },
  304. body: JSON.stringify(settings),
  305. })
  306. toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} effect saved`)
  307. await fetchEffectSettings()
  308. } catch (error) {
  309. toast.error(`Failed to save ${type} effect`)
  310. }
  311. }
  312. const clearEffectSettings = async (type: 'idle' | 'playing') => {
  313. try {
  314. await fetch('/api/dw_leds/clear_effect_settings', {
  315. method: 'POST',
  316. headers: { 'Content-Type': 'application/json' },
  317. body: JSON.stringify({ type }),
  318. })
  319. toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} effect cleared`)
  320. await fetchEffectSettings()
  321. } catch (error) {
  322. toast.error(`Failed to clear ${type} effect`)
  323. }
  324. }
  325. const saveIdleTimeout = async (enabled?: boolean, minutes?: number) => {
  326. const finalEnabled = enabled !== undefined ? enabled : idleTimeoutEnabled
  327. const finalMinutes = minutes !== undefined ? minutes : idleTimeoutMinutes
  328. try {
  329. await fetch('/api/dw_leds/idle_timeout', {
  330. method: 'POST',
  331. headers: { 'Content-Type': 'application/json' },
  332. body: JSON.stringify({ enabled: finalEnabled, minutes: finalMinutes }),
  333. })
  334. toast.success(`Idle timeout ${finalEnabled ? 'enabled' : 'disabled'}`)
  335. } catch (error) {
  336. toast.error('Failed to save idle timeout')
  337. }
  338. }
  339. const handleIdleTimeoutToggle = async (checked: boolean) => {
  340. setIdleTimeoutEnabled(checked)
  341. await saveIdleTimeout(checked, idleTimeoutMinutes)
  342. }
  343. const formatEffectSettings = (settings: EffectSettings | null) => {
  344. if (!settings) return 'Not configured'
  345. const effectName = effects.find((e) => e[0] === settings.effect_id)?.[1] || settings.effect_id
  346. const paletteName = palettes.find((p) => p[0] === settings.palette_id)?.[1] || settings.palette_id
  347. return `${effectName} | ${paletteName} | Speed: ${settings.speed} | Intensity: ${settings.intensity}`
  348. }
  349. // Loading state
  350. if (isLoading) {
  351. return (
  352. <div className="flex items-center justify-center min-h-[60vh]">
  353. <span className="material-icons-outlined animate-spin text-4xl text-muted-foreground">
  354. sync
  355. </span>
  356. </div>
  357. )
  358. }
  359. // Not configured state
  360. if (!ledConfig || ledConfig.provider === 'none') {
  361. return (
  362. <div className="flex flex-col items-center justify-center min-h-[60vh] gap-6 text-center px-4">
  363. <div className="p-4 rounded-full bg-muted">
  364. <span className="material-icons-outlined text-5xl text-muted-foreground">
  365. lightbulb
  366. </span>
  367. </div>
  368. <div className="space-y-2">
  369. <h1 className="text-2xl font-bold">LED Controller Not Configured</h1>
  370. <p className="text-muted-foreground max-w-md">
  371. Configure your LED controller (WLED or DW LEDs) in the Settings page to control your lights.
  372. </p>
  373. </div>
  374. <Button asChild className="gap-2">
  375. <Link to="/settings?section=led">
  376. <span className="material-icons-outlined">settings</span>
  377. Go to Settings
  378. </Link>
  379. </Button>
  380. </div>
  381. )
  382. }
  383. // WLED iframe view
  384. if (ledConfig.provider === 'wled' && ledConfig.wled_ip) {
  385. return (
  386. <div className="flex flex-col w-full h-[calc(100vh-180px)] py-4">
  387. <iframe
  388. src={`http://${ledConfig.wled_ip}`}
  389. className="w-full h-full rounded-lg border border-border"
  390. title="WLED Control"
  391. />
  392. </div>
  393. )
  394. }
  395. // DW LEDs control panel
  396. return (
  397. <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-6 px-4">
  398. {/* Page Header */}
  399. <div className="space-y-1">
  400. <h1 className="text-3xl font-bold tracking-tight">LED Control</h1>
  401. <p className="text-muted-foreground">DW LEDs - GPIO controlled LED strip</p>
  402. </div>
  403. <Separator />
  404. {/* Main Control Grid - 2 columns on large screens */}
  405. <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
  406. {/* Left Column - Primary Controls */}
  407. <div className="lg:col-span-2 space-y-6">
  408. {/* Power & Status Card */}
  409. <Card>
  410. <CardContent className="pt-6">
  411. <div className="flex flex-col sm:flex-row items-center gap-6">
  412. {/* Power Button - Large and prominent */}
  413. <div className="flex flex-col items-center gap-3">
  414. <button
  415. onClick={handlePowerToggle}
  416. className={`w-24 h-24 rounded-full flex items-center justify-center transition-all shadow-lg ${
  417. dwStatus?.power_on
  418. ? 'bg-green-500 hover:bg-green-600 shadow-green-500/30'
  419. : 'bg-muted hover:bg-muted/80'
  420. }`}
  421. >
  422. <span className={`material-icons text-4xl ${dwStatus?.power_on ? 'text-white' : 'text-muted-foreground'}`}>
  423. power_settings_new
  424. </span>
  425. </button>
  426. <span className={`text-sm font-medium ${dwStatus?.power_on ? 'text-green-600' : 'text-muted-foreground'}`}>
  427. {dwStatus?.power_on ? 'ON' : 'OFF'}
  428. </span>
  429. </div>
  430. {/* Status & Brightness */}
  431. <div className="flex-1 w-full space-y-4">
  432. {/* Connection Status */}
  433. <div className={`flex items-center gap-2 text-sm ${dwStatus?.connected ? 'text-green-600' : 'text-destructive'}`}>
  434. <span className="material-icons-outlined text-base">
  435. {dwStatus?.connected ? 'check_circle' : 'error'}
  436. </span>
  437. {dwStatus?.connected
  438. ? `${dwStatus.num_leds} LEDs on GPIO ${dwStatus.gpio_pin}`
  439. : 'Not connected'}
  440. </div>
  441. {/* Brightness Slider */}
  442. <div className="space-y-2">
  443. <div className="flex justify-between">
  444. <Label className="flex items-center gap-2">
  445. <span className="material-icons-outlined text-base text-muted-foreground">brightness_6</span>
  446. Brightness
  447. </Label>
  448. <span className="text-sm font-medium">{brightness}%</span>
  449. </div>
  450. <Slider
  451. value={[brightness]}
  452. onValueChange={handleBrightnessChange}
  453. onValueCommit={handleBrightnessCommit}
  454. max={100}
  455. step={1}
  456. />
  457. </div>
  458. </div>
  459. </div>
  460. </CardContent>
  461. </Card>
  462. {/* Effects Card */}
  463. <Card>
  464. <CardHeader className="pb-3">
  465. <CardTitle className="text-lg flex items-center gap-2">
  466. <span className="material-icons-outlined text-muted-foreground">auto_awesome</span>
  467. Effects & Palettes
  468. </CardTitle>
  469. </CardHeader>
  470. <CardContent className="space-y-6">
  471. {/* Effect & Palette Selects */}
  472. <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
  473. <div className="space-y-2">
  474. <Label>Effect</Label>
  475. <Select value={selectedEffect} onValueChange={handleEffectChange}>
  476. <SelectTrigger>
  477. <SelectValue placeholder="Select effect..." />
  478. </SelectTrigger>
  479. <SelectContent>
  480. {effects.map(([id, name]) => (
  481. <SelectItem key={id} value={String(id)}>
  482. {name}
  483. </SelectItem>
  484. ))}
  485. </SelectContent>
  486. </Select>
  487. </div>
  488. <div className="space-y-2">
  489. <Label>Palette</Label>
  490. <Select value={selectedPalette} onValueChange={handlePaletteChange}>
  491. <SelectTrigger>
  492. <SelectValue placeholder="Select palette..." />
  493. </SelectTrigger>
  494. <SelectContent>
  495. {palettes.map(([id, name]) => (
  496. <SelectItem key={id} value={String(id)}>
  497. {name}
  498. </SelectItem>
  499. ))}
  500. </SelectContent>
  501. </Select>
  502. </div>
  503. </div>
  504. {/* Speed and Intensity in styled boxes */}
  505. <div className="grid grid-cols-2 gap-4">
  506. <div className="p-4 bg-muted/50 rounded-lg space-y-3">
  507. <div className="flex justify-between items-center">
  508. <Label className="flex items-center gap-2">
  509. <span className="material-icons-outlined text-base text-muted-foreground">speed</span>
  510. Speed
  511. </Label>
  512. <span className="text-sm font-medium">{speed}</span>
  513. </div>
  514. <Slider
  515. value={[speed]}
  516. onValueChange={handleSpeedChange}
  517. onValueCommit={handleSpeedCommit}
  518. max={255}
  519. step={1}
  520. />
  521. </div>
  522. <div className="p-4 bg-muted/50 rounded-lg space-y-3">
  523. <div className="flex justify-between items-center">
  524. <Label className="flex items-center gap-2">
  525. <span className="material-icons-outlined text-base text-muted-foreground">tungsten</span>
  526. Intensity
  527. </Label>
  528. <span className="text-sm font-medium">{intensity}</span>
  529. </div>
  530. <Slider
  531. value={[intensity]}
  532. onValueChange={handleIntensityChange}
  533. onValueCommit={handleIntensityCommit}
  534. max={255}
  535. step={1}
  536. />
  537. </div>
  538. </div>
  539. </CardContent>
  540. </Card>
  541. </div>
  542. {/* Right Column - Colors & Quick Settings */}
  543. <div className="flex flex-col gap-6">
  544. {/* Colors Card */}
  545. <Card className="flex-1 flex flex-col">
  546. <CardHeader className="pb-3">
  547. <CardTitle className="text-lg flex items-center gap-2">
  548. <span className="material-icons-outlined text-muted-foreground">palette</span>
  549. Colors
  550. </CardTitle>
  551. </CardHeader>
  552. <CardContent className="flex-1 flex items-center justify-center">
  553. <div className="flex justify-around w-full">
  554. <div className="flex flex-col items-center gap-2">
  555. <ColorPicker
  556. value={color1}
  557. onChange={(color) => handleColorChange(1, color)}
  558. />
  559. <span className="text-xs text-muted-foreground">Primary</span>
  560. </div>
  561. <div className="flex flex-col items-center gap-2">
  562. <ColorPicker
  563. value={color2}
  564. onChange={(color) => handleColorChange(2, color)}
  565. />
  566. <span className="text-xs text-muted-foreground">Secondary</span>
  567. </div>
  568. <div className="flex flex-col items-center gap-2">
  569. <ColorPicker
  570. value={color3}
  571. onChange={(color) => handleColorChange(3, color)}
  572. />
  573. <span className="text-xs text-muted-foreground">Accent</span>
  574. </div>
  575. </div>
  576. </CardContent>
  577. </Card>
  578. {/* Auto Turn Off */}
  579. <Card className="flex-1 flex flex-col">
  580. <CardHeader className="pb-3">
  581. <CardTitle className="text-lg flex items-center gap-2">
  582. <span className="material-icons-outlined text-muted-foreground">schedule</span>
  583. Auto Turn Off
  584. </CardTitle>
  585. </CardHeader>
  586. <CardContent className="flex-1 flex flex-col justify-center space-y-4">
  587. <div className="flex items-center justify-between">
  588. <span className="text-sm text-muted-foreground">Enable timeout</span>
  589. <Switch
  590. checked={idleTimeoutEnabled}
  591. onCheckedChange={handleIdleTimeoutToggle}
  592. />
  593. </div>
  594. {idleTimeoutEnabled && (
  595. <div className="flex items-center gap-2">
  596. <Input
  597. type="number"
  598. value={idleTimeoutMinutes}
  599. onChange={(e) => setIdleTimeoutMinutes(parseInt(e.target.value) || 30)}
  600. min={1}
  601. max={1440}
  602. className="w-20"
  603. />
  604. <span className="text-sm text-muted-foreground flex-1">minutes</span>
  605. <Button size="sm" onClick={() => saveIdleTimeout()}>
  606. Save
  607. </Button>
  608. </div>
  609. )}
  610. </CardContent>
  611. </Card>
  612. </div>
  613. </div>
  614. {/* Automation Settings - Full Width */}
  615. <Card>
  616. <CardHeader className="pb-3">
  617. <CardTitle className="text-lg flex items-center gap-2">
  618. <span className="material-icons-outlined text-muted-foreground">smart_toy</span>
  619. Effect Automation
  620. </CardTitle>
  621. <CardDescription>
  622. Save current settings to automatically apply when table state changes
  623. </CardDescription>
  624. </CardHeader>
  625. <CardContent>
  626. <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
  627. {/* Playing Effect */}
  628. <div className="p-4 bg-muted/50 rounded-lg space-y-3">
  629. <div className="flex items-center justify-between">
  630. <div className="flex items-center gap-2">
  631. <span className="material-icons text-green-600">play_circle</span>
  632. <span className="font-medium">While Playing</span>
  633. </div>
  634. </div>
  635. <div className="text-xs text-muted-foreground p-2 bg-background rounded border min-h-[40px]">
  636. {formatEffectSettings(playingEffect)}
  637. </div>
  638. <div className="flex gap-2">
  639. <Button
  640. size="sm"
  641. onClick={() => saveCurrentEffectSettings('playing')}
  642. className="flex-1 gap-1"
  643. >
  644. <span className="material-icons text-sm">save</span>
  645. Save Current
  646. </Button>
  647. <Button
  648. size="sm"
  649. variant="outline"
  650. onClick={() => clearEffectSettings('playing')}
  651. >
  652. Clear
  653. </Button>
  654. </div>
  655. </div>
  656. {/* Idle Effect */}
  657. <div className="p-4 bg-muted/50 rounded-lg space-y-3">
  658. <div className="flex items-center justify-between">
  659. <div className="flex items-center gap-2">
  660. <span className="material-icons text-blue-600">bedtime</span>
  661. <span className="font-medium">When Idle</span>
  662. </div>
  663. </div>
  664. <div className="text-xs text-muted-foreground p-2 bg-background rounded border min-h-[40px]">
  665. {formatEffectSettings(idleEffect)}
  666. </div>
  667. <div className="flex gap-2">
  668. <Button
  669. size="sm"
  670. onClick={() => saveCurrentEffectSettings('idle')}
  671. className="flex-1 gap-1"
  672. >
  673. <span className="material-icons text-sm">save</span>
  674. Save Current
  675. </Button>
  676. <Button
  677. size="sm"
  678. variant="outline"
  679. onClick={() => clearEffectSettings('idle')}
  680. >
  681. Clear
  682. </Button>
  683. </div>
  684. </div>
  685. </div>
  686. </CardContent>
  687. </Card>
  688. </div>
  689. )
  690. }