Skip to content

Commit 2561964

Browse files
FlowingSPDGclaude
andauthored
Fix ListManager external window design (#198)
* Fix ListManager external window design * Refactor Tauri invoke calls to service layer and improve InputManager State display ## Service Layer Refactoring - **Created service architecture** for better maintainability - **Enhanced useVMixStatus hook** with selectVideoListItem() and openVideoListWindow() - **Added settingsService** for app settings, logging config, and file operations - **Added diagnosticsService** for debug operations - **Updated all components** to use service functions instead of direct invoke calls ### Benefits: - Better separation of concerns - Improved code reusability and testability - Centralized error handling - Type safety with proper interfaces ## InputManager State Label Redesign - **Replaced problematic round Chip** with clean status badge design - **Added colored status dot** (8px) + typography for better visual hierarchy - **Fixed sizing issues** - no more stretched/narrow appearance - **Improved accessibility** with proper color coding and responsive sizing - **Color scheme**: 🟢 Running (green), 🟡 Paused (orange), ⚪ Other (grey) ### Technical Changes: - Removed forced height constraints that caused distortion - Implemented proper flexbox layout with gap spacing - Added font weight differentiation for different states - Maintained UI density settings compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Implement centralized vMix service architecture to improve performance and reduce FMP time - Create dedicated vmixService.ts for centralized vMix API communication - Refactor SingleVideoList to use cached data from VMixStatusProvider instead of direct API calls - Update useVMixStatus hook to use vmixService for all Tauri invoke operations - Replace hardcoded event strings with VMIX_EVENTS constants - Eliminate duplicate API calls by leveraging backend's existing caching system - Improve page load performance by using immediately available cached data This leverages the backend's sophisticated caching with change detection and real-time updates, significantly improving First Meaningful Paint (FMP) time across all pages. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 642f214 commit 2561964

15 files changed

+685
-605
lines changed
395 KB
Binary file not shown.
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Box,
4+
Typography,
5+
IconButton,
6+
Collapse,
7+
Switch,
8+
FormControlLabel,
9+
} from '@mui/material';
10+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
11+
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
12+
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
13+
import VisibilityIcon from '@mui/icons-material/Visibility';
14+
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
15+
import { getDensitySpacing, UIDensity } from '../hooks/useUISettings.tsx';
16+
17+
interface VmixVideoListItem {
18+
key: string;
19+
number: number;
20+
title: string;
21+
input_type: string;
22+
state: string;
23+
selected: boolean;
24+
enabled: boolean;
25+
}
26+
27+
interface VmixVideoListInput {
28+
key: string;
29+
number: number;
30+
title: string;
31+
input_type: string;
32+
state: string;
33+
items: VmixVideoListItem[];
34+
selected_index: number | null;
35+
}
36+
37+
interface CompactVideoListViewProps {
38+
videoLists: VmixVideoListInput[];
39+
onItemSelected: (listKey: string, itemIndex: number) => void;
40+
onPopout?: (videoList: VmixVideoListInput) => void;
41+
showPathsToggle?: boolean;
42+
uiDensity?: UIDensity;
43+
initialExpandedLists?: Set<string>;
44+
}
45+
46+
const CompactVideoListView: React.FC<CompactVideoListViewProps> = ({
47+
videoLists,
48+
onItemSelected,
49+
onPopout,
50+
showPathsToggle = false,
51+
uiDensity = 'standard' as UIDensity,
52+
initialExpandedLists = new Set(),
53+
}) => {
54+
const [expandedLists, setExpandedLists] = useState<Set<string>>(initialExpandedLists);
55+
const [showFullPaths, setShowFullPaths] = useState(false);
56+
57+
const spacing = getDensitySpacing(uiDensity);
58+
59+
const toggleListExpansion = (listKey: string) => {
60+
setExpandedLists(prev => {
61+
const newSet = new Set(prev);
62+
if (newSet.has(listKey)) {
63+
newSet.delete(listKey);
64+
} else {
65+
newSet.add(listKey);
66+
}
67+
return newSet;
68+
});
69+
};
70+
71+
const getFileName = (filePath: string) => {
72+
return filePath.split(/[\\\/]/).pop() || 'Unknown File';
73+
};
74+
75+
// Generate stable key based on data content
76+
const generateItemKey = (item: VmixVideoListItem, index: number) => {
77+
return `${item.key}-${index}-${item.selected ? 'sel' : 'unsel'}-${item.enabled ? 'en' : 'dis'}`;
78+
};
79+
80+
// Generate stable key for video list
81+
const generateListKey = (videoList: VmixVideoListInput) => {
82+
const selectedCount = videoList.items.filter(i => i.selected).length;
83+
const enabledCount = videoList.items.filter(i => i.enabled).length;
84+
return `${videoList.key}-items${videoList.items.length}-sel${selectedCount}-en${enabledCount}`;
85+
};
86+
87+
return (
88+
<Box>
89+
{/* Show full paths toggle */}
90+
{showPathsToggle && (
91+
<Box display="flex" justifyContent="flex-end" alignItems="center" mb={1}>
92+
<FormControlLabel
93+
control={
94+
<Switch
95+
checked={showFullPaths}
96+
onChange={(e) => setShowFullPaths(e.target.checked)}
97+
size="small"
98+
icon={<VisibilityOffIcon fontSize="small" />}
99+
checkedIcon={<VisibilityIcon fontSize="small" />}
100+
/>
101+
}
102+
label="Show full paths"
103+
labelPlacement="start"
104+
sx={{ ml: 0, mr: 0 }}
105+
/>
106+
</Box>
107+
)}
108+
109+
{/* Video Lists */}
110+
{videoLists.length === 0 ? (
111+
<Typography color="text.secondary" sx={{ p: 2, textAlign: 'center' }}>
112+
No VideoList inputs found.
113+
</Typography>
114+
) : (
115+
<Box>
116+
{videoLists.map((videoList) => {
117+
const isExpanded = expandedLists.has(videoList.key);
118+
return (
119+
<Box key={generateListKey(videoList)} sx={{ mb: spacing.spacing }}>
120+
{/* vMix-style compact header */}
121+
<Box
122+
sx={{
123+
bgcolor: 'grey.800',
124+
color: 'white',
125+
p: spacing.cardPadding,
126+
borderRadius: '4px 4px 0 0',
127+
border: '1px solid',
128+
borderColor: 'grey.700',
129+
cursor: 'pointer',
130+
'&:hover': {
131+
bgcolor: 'grey.700',
132+
},
133+
}}
134+
onClick={() => toggleListExpansion(videoList.key)}
135+
>
136+
<Box display="flex" justifyContent="space-between" alignItems="center">
137+
<Box display="flex" alignItems="center" gap={1}>
138+
<Typography variant="body2" sx={{ fontSize: '0.8rem', fontWeight: 'medium' }}>
139+
{videoList.title}
140+
</Typography>
141+
<Box
142+
sx={{
143+
width: 8,
144+
height: 8,
145+
bgcolor: videoList.state === 'running' ? 'success.main' : 'grey.400',
146+
borderRadius: '50%'
147+
}}
148+
/>
149+
</Box>
150+
<Box display="flex" alignItems="center" gap={1}>
151+
<Typography variant="caption" sx={{ fontSize: '0.7rem', color: 'grey.300' }}>
152+
{videoList.items.length} items
153+
</Typography>
154+
{onPopout && (
155+
<IconButton
156+
size="small"
157+
onClick={(e) => {
158+
e.stopPropagation();
159+
onPopout(videoList);
160+
}}
161+
sx={{ color: 'white', p: 0.25 }}
162+
>
163+
<OpenInNewIcon fontSize="small" />
164+
</IconButton>
165+
)}
166+
<IconButton size="small" sx={{ color: 'white', p: 0.25 }}>
167+
{isExpanded ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
168+
</IconButton>
169+
</Box>
170+
</Box>
171+
</Box>
172+
173+
{/* vMix-style compact list */}
174+
<Collapse in={isExpanded}>
175+
<Box
176+
sx={{
177+
bgcolor: 'grey.900',
178+
border: '1px solid',
179+
borderColor: 'grey.700',
180+
borderTop: 'none',
181+
borderRadius: '0 0 4px 4px',
182+
maxHeight: '300px',
183+
overflow: 'auto',
184+
}}
185+
>
186+
{videoList.items.length === 0 ? (
187+
<Typography color="text.secondary" sx={{ p: 2, textAlign: 'center' }}>
188+
No items in this video list
189+
</Typography>
190+
) : (
191+
videoList.items.map((item, index) => {
192+
const itemKey = generateItemKey(item, index);
193+
const displayName = showFullPaths ? item.title : getFileName(item.title);
194+
195+
return (
196+
<Box
197+
key={itemKey}
198+
sx={{
199+
display: 'flex',
200+
alignItems: 'center',
201+
justifyContent: 'space-between',
202+
height: spacing.itemHeight,
203+
px: 1,
204+
py: 0.25,
205+
bgcolor: item.selected ? 'primary.dark' : 'transparent',
206+
borderBottom: '1px solid',
207+
borderColor: 'grey.800',
208+
cursor: 'pointer',
209+
'&:hover': {
210+
bgcolor: item.selected ? 'primary.dark' : 'grey.800',
211+
},
212+
'&:last-child': {
213+
borderBottom: 'none',
214+
},
215+
}}
216+
onClick={() => item.enabled && onItemSelected(videoList.key, index)}
217+
>
218+
{/* Status indicator (green square like vMix) */}
219+
<Box display="flex" alignItems="center" gap={1} sx={{ flex: 1, minWidth: 0 }}>
220+
<Box
221+
sx={{
222+
width: 12,
223+
height: 12,
224+
bgcolor: item.enabled ? 'success.main' : 'grey.600',
225+
borderRadius: '2px',
226+
flexShrink: 0,
227+
}}
228+
/>
229+
<Typography
230+
variant="body2"
231+
sx={{
232+
fontSize: spacing.fontSize,
233+
fontWeight: item.selected ? 'bold' : 'normal',
234+
color: item.selected ? 'white' : 'text.primary',
235+
overflow: 'hidden',
236+
textOverflow: 'ellipsis',
237+
whiteSpace: 'nowrap',
238+
}}
239+
>
240+
{displayName}
241+
</Typography>
242+
</Box>
243+
</Box>
244+
);
245+
})
246+
)}
247+
</Box>
248+
</Collapse>
249+
</Box>
250+
);
251+
})}
252+
</Box>
253+
)}
254+
</Box>
255+
);
256+
};
257+
258+
export default CompactVideoListView;

0 commit comments

Comments
 (0)