feat: add rename channel
This commit is contained in:
@@ -365,3 +365,54 @@
|
|||||||
.app-root--sidebar-expanded .stage--with-sidebar {
|
.app-root--sidebar-expanded .stage--with-sidebar {
|
||||||
margin-left: var(--sidebar-width-expanded);
|
margin-left: var(--sidebar-width-expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Item (editing state) ---------- */
|
||||||
|
|
||||||
|
.sidebar-item--editing {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--sidebar-item-gap);
|
||||||
|
|
||||||
|
aspect-ratio: 1;
|
||||||
|
height: calc(var(--header-height) * 0.7);
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item-input {
|
||||||
|
width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
max-width: 0;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
opacity var(--dur-fast) var(--ease-standard),
|
||||||
|
max-width var(--dur-base) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-rail--expanded .sidebar-item-input {
|
||||||
|
width: auto;
|
||||||
|
opacity: 1;
|
||||||
|
max-width: 200px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type SidebarItem<T extends string = string> = {
|
|||||||
id: T;
|
id: T;
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
editable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SidebarAppOption = {
|
export type SidebarAppOption = {
|
||||||
@@ -17,11 +18,75 @@ function SidebarButton<T extends string>({
|
|||||||
item,
|
item,
|
||||||
active,
|
active,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onRename,
|
||||||
}: {
|
}: {
|
||||||
item: SidebarItem<T>;
|
item: SidebarItem<T>;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
|
onRename?: (name: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState(item.label);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editing) setInputValue(item.label);
|
||||||
|
}, [item.label, editing]);
|
||||||
|
|
||||||
|
function startEditing(e: React.MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!onRename) return;
|
||||||
|
|
||||||
|
setInputValue(item.label);
|
||||||
|
setEditing(true);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
inputRef.current?.select();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitRename() {
|
||||||
|
setEditing(false);
|
||||||
|
|
||||||
|
const trimmed = inputValue.trim();
|
||||||
|
|
||||||
|
if (!trimmed || trimmed === item.label) {
|
||||||
|
setInputValue(item.label);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRename?.(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: React.KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') inputRef.current?.blur();
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setInputValue(item.label);
|
||||||
|
setEditing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
return (
|
||||||
|
<div className="sidebar-item sidebar-item--editing">
|
||||||
|
<span className="sidebar-icon">{item.icon}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="sidebar-item-input"
|
||||||
|
value={inputValue}
|
||||||
|
maxLength={8}
|
||||||
|
onChange={(e) => setInputValue(e.target.value.slice(0, 8))}
|
||||||
|
onBlur={commitRename}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
aria-label="Rename channel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
@@ -31,7 +96,14 @@ function SidebarButton<T extends string>({
|
|||||||
className={'sidebar-item' + (active ? ' sidebar-item--active' : '')}
|
className={'sidebar-item' + (active ? ' sidebar-item--active' : '')}
|
||||||
>
|
>
|
||||||
<span className="sidebar-icon">{item.icon}</span>
|
<span className="sidebar-icon">{item.icon}</span>
|
||||||
<span className="sidebar-label">{item.label}</span>
|
|
||||||
|
<span
|
||||||
|
className="sidebar-label"
|
||||||
|
onDoubleClick={onRename ? startEditing : undefined}
|
||||||
|
title={onRename ? `${item.label} (double click to rename)` : undefined}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -230,6 +302,7 @@ export function Sidebar<T extends string>({
|
|||||||
selectedAppId,
|
selectedAppId,
|
||||||
onAppChange,
|
onAppChange,
|
||||||
onAppRename,
|
onAppRename,
|
||||||
|
onItemRename,
|
||||||
expanded,
|
expanded,
|
||||||
onToggle,
|
onToggle,
|
||||||
}: {
|
}: {
|
||||||
@@ -241,6 +314,7 @@ export function Sidebar<T extends string>({
|
|||||||
selectedAppId?: string;
|
selectedAppId?: string;
|
||||||
onAppChange: (id: string) => void;
|
onAppChange: (id: string) => void;
|
||||||
onAppRename: (name: string) => void;
|
onAppRename: (name: string) => void;
|
||||||
|
onItemRename?: (id: T, name: string) => void;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -298,6 +372,9 @@ export function Sidebar<T extends string>({
|
|||||||
item={item}
|
item={item}
|
||||||
active={activeId === item.id}
|
active={activeId === item.id}
|
||||||
onSelect={() => onSelect(item.id)}
|
onSelect={() => onSelect(item.id)}
|
||||||
|
onRename={
|
||||||
|
item.editable && onItemRename ? (name) => onItemRename(item.id, name) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,66 +47,18 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
|||||||
{ id: 'delay', label: 'Delay', icon: <DelayIcon /> },
|
{ id: 'delay', label: 'Delay', icon: <DelayIcon /> },
|
||||||
{ id: 'matrix', label: 'Matrix', icon: <MatrixIcon /> },
|
{ id: 'matrix', label: 'Matrix', icon: <MatrixIcon /> },
|
||||||
{ id: 'geq', label: 'Graphic Equalizer', icon: <SlidersVerticalIcon /> },
|
{ id: 'geq', label: 'Graphic Equalizer', icon: <SlidersVerticalIcon /> },
|
||||||
{
|
{ id: 'ina', label: inputStates[InputChannel.InA].name, icon: <TextIcon>In A</TextIcon>, editable: true },
|
||||||
id: 'ina',
|
{ id: 'inb', label: inputStates[InputChannel.InB].name, icon: <TextIcon>In B</TextIcon>, editable: true },
|
||||||
label: inputStates[InputChannel.InA].name,
|
{ id: 'inc', label: inputStates[InputChannel.InC].name, icon: <TextIcon>In C</TextIcon>, editable: true },
|
||||||
icon: <TextIcon>In A</TextIcon>,
|
{ id: 'ind', label: inputStates[InputChannel.InD].name, icon: <TextIcon>In D</TextIcon>, editable: true },
|
||||||
},
|
{ id: 'out1', label: outputStates[OutputChannel.Out1].name, icon: <TextIcon>Out 1</TextIcon>, editable: true },
|
||||||
{
|
{ id: 'out2', label: outputStates[OutputChannel.Out2].name, icon: <TextIcon>Out 2</TextIcon>, editable: true },
|
||||||
id: 'inb',
|
{ id: 'out3', label: outputStates[OutputChannel.Out3].name, icon: <TextIcon>Out 3</TextIcon>, editable: true },
|
||||||
label: inputStates[InputChannel.InB].name,
|
{ id: 'out4', label: outputStates[OutputChannel.Out4].name, icon: <TextIcon>Out 4</TextIcon>, editable: true },
|
||||||
icon: <TextIcon>In B</TextIcon>,
|
{ id: 'out5', label: outputStates[OutputChannel.Out5].name, icon: <TextIcon>Out 5</TextIcon>, editable: true },
|
||||||
},
|
{ id: 'out6', label: outputStates[OutputChannel.Out6].name, icon: <TextIcon>Out 6</TextIcon>, editable: true },
|
||||||
{
|
{ id: 'out7', label: outputStates[OutputChannel.Out7].name, icon: <TextIcon>Out 7</TextIcon>, editable: true },
|
||||||
id: 'inc',
|
{ id: 'out8', label: outputStates[OutputChannel.Out8].name, icon: <TextIcon>Out 8</TextIcon>, editable: true },
|
||||||
label: inputStates[InputChannel.InC].name,
|
|
||||||
icon: <TextIcon>In C</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ind',
|
|
||||||
label: inputStates[InputChannel.InD].name,
|
|
||||||
icon: <TextIcon>In D</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out1',
|
|
||||||
label: outputStates[OutputChannel.Out1].name,
|
|
||||||
icon: <TextIcon>Out 1</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out2',
|
|
||||||
label: outputStates[OutputChannel.Out2].name,
|
|
||||||
icon: <TextIcon>Out 2</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out3',
|
|
||||||
label: outputStates[OutputChannel.Out3].name,
|
|
||||||
icon: <TextIcon>Out 3</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out4',
|
|
||||||
label: outputStates[OutputChannel.Out4].name,
|
|
||||||
icon: <TextIcon>Out 4</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out5',
|
|
||||||
label: outputStates[OutputChannel.Out5].name,
|
|
||||||
icon: <TextIcon>Out 5</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out6',
|
|
||||||
label: outputStates[OutputChannel.Out6].name,
|
|
||||||
icon: <TextIcon>Out 6</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out7',
|
|
||||||
label: outputStates[OutputChannel.Out7].name,
|
|
||||||
icon: <TextIcon>Out 7</TextIcon>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'out8',
|
|
||||||
label: outputStates[OutputChannel.Out8].name,
|
|
||||||
icon: <TextIcon>Out 8</TextIcon>,
|
|
||||||
},
|
|
||||||
{ id: 'miscellaneous', label: 'Miscellaneous', icon: <BoltIcon /> },
|
{ id: 'miscellaneous', label: 'Miscellaneous', icon: <BoltIcon /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -136,6 +88,14 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
|||||||
setPresetLoading(false);
|
setPresetLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onItemRename = async (id: SidebarId, name: string) => {
|
||||||
|
const channel = channelMap[id];
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
const padded = name.slice(0, 8).padEnd(8, '\0');
|
||||||
|
await api.setChannelName(channel, padded);
|
||||||
|
};
|
||||||
|
|
||||||
const renderChannel = (channel: { Input: InputChannel } | { Output: OutputChannel }) => (
|
const renderChannel = (channel: { Input: InputChannel } | { Output: OutputChannel }) => (
|
||||||
<ChannelSection
|
<ChannelSection
|
||||||
dsp={dsp}
|
dsp={dsp}
|
||||||
@@ -236,6 +196,7 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
|||||||
selectedAppId={presets[selectedPreset]?.id}
|
selectedAppId={presets[selectedPreset]?.id}
|
||||||
onAppChange={onAppChange}
|
onAppChange={onAppChange}
|
||||||
onAppRename={api.setCurrentPresetName}
|
onAppRename={api.setCurrentPresetName}
|
||||||
|
onItemRename={onItemRename}
|
||||||
expanded={sidebarExpanded}
|
expanded={sidebarExpanded}
|
||||||
onToggle={() => setSidebarExpanded((v) => !v)}
|
onToggle={() => setSidebarExpanded((v) => !v)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user