feat: added region color customization feature
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
.color-palette-popup {
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
|
||||
padding: 8px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.color-palette-popup button {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.color-palette-none {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 0 2px;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
background: #383838;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.color-palette-none:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.color-palette-none.active {
|
||||
box-shadow: 0 0 0 2px #ffffff, 0 0 0 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.color-palette-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(24, 24px);
|
||||
grid-auto-rows: 24px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.color-palette-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-palette-swatch:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.color-palette-swatch.active {
|
||||
box-shadow: 0 0 0 2px #ffffff, 0 0 0 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { FaUndoAlt } from 'react-icons/fa';
|
||||
import { LOGIC_REGION_COLOR_SWATCHES } from '../../constants/regionColorPalette';
|
||||
import './ColorPalettePopup.css';
|
||||
|
||||
interface ColorPalettePopupProps {
|
||||
selectedColor?: string;
|
||||
onSelect: (color: string | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ColorPalettePopup: React.FC<ColorPalettePopupProps> = ({
|
||||
selectedColor,
|
||||
onSelect,
|
||||
className = '',
|
||||
}) => (
|
||||
<div className={`color-palette-popup ${className}`.trim()} role="menu" aria-label="Color palette">
|
||||
<button
|
||||
type="button"
|
||||
className={`color-palette-none${selectedColor === undefined ? ' active' : ''}`}
|
||||
onClick={() => onSelect(null)}
|
||||
aria-label="Reset color"
|
||||
title="Reset color"
|
||||
>
|
||||
<FaUndoAlt aria-hidden="true" />
|
||||
</button>
|
||||
<div className="color-palette-grid">
|
||||
{LOGIC_REGION_COLOR_SWATCHES.flat().map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={`color-palette-swatch${selectedColor === color ? ' active' : ''}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => onSelect(color)}
|
||||
title={color}
|
||||
aria-label={`Select color ${color}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ColorPalettePopup;
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as KGDropdown } from './KGDropdown';
|
||||
export { default as ColorPalettePopup } from './ColorPalettePopup';
|
||||
export { default as Playhead } from './Playhead';
|
||||
export { default as FileImportModal } from './FileImportModal';
|
||||
export { default as LoadingOverlay } from './LoadingOverlay';
|
||||
|
||||
@@ -16,6 +16,10 @@ export interface RegionUI {
|
||||
barNumber: number;
|
||||
length: number;
|
||||
name: string;
|
||||
color?: string;
|
||||
trackColor?: string;
|
||||
effectiveColor?: string;
|
||||
isAudioRegion?: boolean;
|
||||
}
|
||||
|
||||
export interface RegionPreviewContentStyle {
|
||||
|
||||
@@ -247,6 +247,22 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.piano-roll-menu-item-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.piano-roll-more-menu {
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.piano-roll-region-color-popup {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
z-index: 1600;
|
||||
}
|
||||
|
||||
.piano-roll-automation-toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -101,7 +101,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const isAudioWaveform = currentMode === 'audio-waveform';
|
||||
const isAudioOnly = isAudioWaveform || isSpectrogram;
|
||||
const isHybrid = currentMode === 'hybrid';
|
||||
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showKGOnePanel, showEventListPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds, automationRedrawVersion, refreshProjectState, setBpm } = useProjectStore();
|
||||
const { maxBars, tracks, updateTrack, updateRegionProperties, timeSignature, showChatBox, showKGOnePanel, showEventListPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds, selectedRegionIds, automationRedrawVersion, refreshProjectState, setBpm } = useProjectStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Tool state for piano roll
|
||||
@@ -167,6 +167,14 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const activeInstrument = useMemo<InstrumentType>(() => (
|
||||
parentMidiTrack instanceof KGMidiTrack ? parentMidiTrack.getInstrument() : 'acoustic_grand_piano'
|
||||
), [parentMidiTrack]);
|
||||
const activeEditableRegionId = audioRegion?.getId() ?? activeRegion?.getId() ?? null;
|
||||
const selectedRegionColor = useMemo(() => {
|
||||
if (audioRegion) {
|
||||
return audioRegion.getColor();
|
||||
}
|
||||
|
||||
return activeRegion?.getColor();
|
||||
}, [activeRegion, audioRegion]);
|
||||
const parsedSheetQuantization = useMemo(
|
||||
() => parseSheetQuantization(sheetQuantization),
|
||||
[sheetQuantization]
|
||||
@@ -492,6 +500,20 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegionColorSelect = useCallback(async (color: string | null) => {
|
||||
const allTrackRegionIds = new Set(tracks.flatMap(track => track.getRegions().map(region => region.getId())));
|
||||
const selectedProjectRegionIds = selectedRegionIds.filter(regionId => allTrackRegionIds.has(regionId));
|
||||
const targetRegionIds = activeEditableRegionId && selectedProjectRegionIds.includes(activeEditableRegionId)
|
||||
? selectedProjectRegionIds
|
||||
: activeEditableRegionId
|
||||
? [activeEditableRegionId]
|
||||
: [];
|
||||
|
||||
for (const regionId of targetRegionIds) {
|
||||
await updateRegionProperties(regionId, { color });
|
||||
}
|
||||
}, [activeEditableRegionId, selectedRegionIds, tracks, updateRegionProperties]);
|
||||
|
||||
const handleDetectChords = useCallback(async () => {
|
||||
if (!audioRegion && !activeRegion) {
|
||||
await showAlert('Open a MIDI or audio region before detecting chords.');
|
||||
@@ -1597,6 +1619,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
detectingChords={isDetectingChords}
|
||||
onDetectTempo={audioRegion ? handleDetectTempo : undefined}
|
||||
detectingTempo={isDetectingTempo}
|
||||
selectedRegionColor={selectedRegionColor}
|
||||
onRegionColorSelect={activeEditableRegionId ? (color) => { void handleRegionColorSelect(color); } : undefined}
|
||||
/>
|
||||
|
||||
<NoteAttributeBar selectedNotes={selectedNotes} isSpectrogram={isAudioOnly} activeRegion={activeRegion} />
|
||||
|
||||
@@ -30,6 +30,16 @@ vi.mock('../common', () => ({
|
||||
</button>
|
||||
);
|
||||
},
|
||||
ColorPalettePopup: ({ onSelect }: { onSelect: (value: string | null) => void }) => (
|
||||
<div>
|
||||
<button type="button" aria-label="Reset color" onClick={() => onSelect(null)}>
|
||||
Reset
|
||||
</button>
|
||||
<button type="button" onClick={() => onSelect('#3C8AC4')}>
|
||||
Color Swatch
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../core/KGCore', () => ({
|
||||
@@ -431,4 +441,23 @@ describe('PianoRollToolbar', () => {
|
||||
|
||||
expect(screen.getByRole('button', { name: '和声小调' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the region color action in the more menu and emits the chosen color', () => {
|
||||
const onRegionColorSelect = vi.fn();
|
||||
|
||||
renderWithLocale(
|
||||
<PianoRollToolbar
|
||||
{...baseProps}
|
||||
mode="midi-edit"
|
||||
onRegionColorSelect={onRegionColorSelect}
|
||||
selectedRegionColor="#3C8AC4"
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'More options' }));
|
||||
fireEvent.click(screen.getByText('Region Color...'));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Reset color' }));
|
||||
|
||||
expect(onRegionColorSelect).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FaMousePointer, FaPencilAlt } from 'react-icons/fa';
|
||||
import { TbArrowBarToUp } from 'react-icons/tb';
|
||||
import { KGDropdown } from '../common';
|
||||
import { ColorPalettePopup, KGDropdown } from '../common';
|
||||
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
import {
|
||||
@@ -50,6 +50,8 @@ interface PianoRollToolbarProps {
|
||||
detectingChords?: boolean;
|
||||
onDetectTempo?: () => void | Promise<void>;
|
||||
detectingTempo?: boolean;
|
||||
selectedRegionColor?: string;
|
||||
onRegionColorSelect?: (color: string | null) => void;
|
||||
}
|
||||
|
||||
const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
@@ -92,6 +94,8 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
detectingChords = false,
|
||||
onDetectTempo,
|
||||
detectingTempo = false,
|
||||
selectedRegionColor,
|
||||
onRegionColorSelect,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const showMidiControls = mode !== 'spectrogram' && mode !== 'audio-waveform' && !sheetMusicViewEnabled;
|
||||
@@ -151,6 +155,7 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
}, [showZoomSlider]);
|
||||
|
||||
const [showMoreMenu, setShowMoreMenu] = React.useState(false);
|
||||
const [showRegionColorPalette, setShowRegionColorPalette] = React.useState(false);
|
||||
const specMenuRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -158,6 +163,7 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (specMenuRef.current && !specMenuRef.current.contains(e.target as Node)) {
|
||||
setShowMoreMenu(false);
|
||||
setShowRegionColorPalette(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
@@ -386,7 +392,29 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
...
|
||||
</button>
|
||||
{showMoreMenu && (
|
||||
<div className="quant-dropdown" style={{ right: 0, left: 'auto', width: 'auto', whiteSpace: 'nowrap' }}>
|
||||
<div className="quant-dropdown piano-roll-more-menu" style={{ right: 0, left: 'auto', width: 'auto', whiteSpace: 'nowrap' }}>
|
||||
{onRegionColorSelect && (
|
||||
<div className="piano-roll-menu-item-wrapper">
|
||||
<div
|
||||
className="quant-option"
|
||||
onClick={() => setShowRegionColorPalette(open => !open)}
|
||||
>
|
||||
{t('pianoRoll.regionColor')}
|
||||
</div>
|
||||
{showRegionColorPalette && (
|
||||
<div className="piano-roll-region-color-popup">
|
||||
<ColorPalettePopup
|
||||
selectedColor={selectedRegionColor}
|
||||
onSelect={(color) => {
|
||||
onRegionColorSelect(color);
|
||||
setShowRegionColorPalette(false);
|
||||
setShowMoreMenu(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{onDetectChords && (
|
||||
<div
|
||||
className={`quant-option${detectingChords ? ' disabled' : ''}`}
|
||||
@@ -394,6 +422,7 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
if (detectingChords) {
|
||||
return;
|
||||
}
|
||||
setShowRegionColorPalette(false);
|
||||
setShowMoreMenu(false);
|
||||
void onDetectChords();
|
||||
}}
|
||||
@@ -409,6 +438,7 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
if (detectingTempo) {
|
||||
return;
|
||||
}
|
||||
setShowRegionColorPalette(false);
|
||||
setShowMoreMenu(false);
|
||||
void onDetectTempo();
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* Track Region Styles */
|
||||
.track-region {
|
||||
position: absolute;
|
||||
background-color: #4a6b8a;
|
||||
border: 2px solid #5a7b9a;
|
||||
background-color: var(--region-header-bg, #4a6b8a);
|
||||
border: 2px solid var(--region-border-color, #5a7b9a);
|
||||
border-radius: 3px;
|
||||
height: calc(100%);
|
||||
margin: 0px;
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.region-header {
|
||||
background-color: #5a7b9a;
|
||||
background-color: var(--region-header-bg, #5a7b9a);
|
||||
color: #fff;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
@@ -61,19 +61,19 @@
|
||||
}
|
||||
|
||||
.track-region:hover .region-header {
|
||||
background-color: #6a8baa;
|
||||
background-color: var(--region-header-hover-bg, #6a8baa);
|
||||
}
|
||||
|
||||
.region-content {
|
||||
height: calc(100% - 18px);
|
||||
background-color: #87CEFA; /* Light blue */
|
||||
background-color: var(--region-content-bg, #4B9A41); /* Default MIDI region color */
|
||||
width: 100%;
|
||||
position: relative; /* Allow overlayed controls */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.region-content.audio-region-content {
|
||||
background-color: #90EE90; /* Light green for audio regions */
|
||||
background-color: var(--region-content-bg, #39649E); /* Default audio region color */
|
||||
}
|
||||
|
||||
.region-preview-content {
|
||||
@@ -91,8 +91,8 @@
|
||||
|
||||
/* Audio region overrides */
|
||||
.track-region.audio-region {
|
||||
background-color: #3a6b4a;
|
||||
border-color: #4a8b5a;
|
||||
background-color: var(--region-header-bg, #3a6b4a);
|
||||
border-color: var(--region-border-color, #4a8b5a);
|
||||
}
|
||||
|
||||
.track-region.audio-region.selected {
|
||||
@@ -109,11 +109,11 @@
|
||||
}
|
||||
|
||||
.track-region.audio-region .region-header {
|
||||
background-color: #4a8b5a;
|
||||
background-color: var(--region-header-bg, #4a8b5a);
|
||||
}
|
||||
|
||||
.track-region.audio-region:hover .region-header {
|
||||
background-color: #5a9b6a;
|
||||
background-color: var(--region-header-hover-bg, #5a9b6a);
|
||||
}
|
||||
|
||||
/* Left-side button cluster inside region-content */
|
||||
|
||||
@@ -259,6 +259,49 @@
|
||||
color: #101010;
|
||||
}
|
||||
|
||||
.track-settings-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
margin-top: 2px;
|
||||
min-width: 120px;
|
||||
padding: 4px 0;
|
||||
border: 1px solid #444;
|
||||
border-radius: 3px;
|
||||
background: #2d2d2d;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.track-settings-menu-item {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 6px 10px !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
color: #e0e0e0 !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.2 !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.track-settings-menu-item:hover {
|
||||
background: #444 !important;
|
||||
}
|
||||
|
||||
.track-settings-color-popup {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(100% + 6px);
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.track-grid.automation-active {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useProjectStore } from '../../stores/projectStore';
|
||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||
import { CHORD_REGION_IMPORT_MIME_TYPE } from '../../util/chordRegionImportUtil';
|
||||
import { TrackType } from '../../core/track/KGTrack';
|
||||
import { buildRegionSurfaceColors, resolveRegionColor } from '../../util/regionColor';
|
||||
|
||||
interface RegionResizePreviewBaseline {
|
||||
regionId: string;
|
||||
@@ -795,12 +796,22 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const effectiveColor = region.effectiveColor
|
||||
?? resolveRegionColor(coreRegion?.getColor(), track.getColor(), !!audioRegion || !!region.isAudioRegion);
|
||||
const surfaceColors = buildRegionSurfaceColors(effectiveColor);
|
||||
|
||||
return (
|
||||
<RegionItem
|
||||
key={region.id}
|
||||
id={region.id}
|
||||
name={region.name}
|
||||
style={getRegionStyle(region)}
|
||||
style={{
|
||||
...getRegionStyle(region),
|
||||
['--region-border-color' as string]: surfaceColors.borderColor,
|
||||
['--region-header-bg' as string]: surfaceColors.headerColor,
|
||||
['--region-header-hover-bg' as string]: surfaceColors.headerHoverColor,
|
||||
['--region-content-bg' as string]: surfaceColors.contentColor,
|
||||
}}
|
||||
barNumber={region.barNumber}
|
||||
length={region.length}
|
||||
trackIndex={index}
|
||||
|
||||
@@ -12,6 +12,7 @@ const storeState = {
|
||||
removeTrack: vi.fn(),
|
||||
toggleInstrumentSelectionForTrack: vi.fn(),
|
||||
importAudioToTrack: vi.fn(),
|
||||
updateTrackProperties: vi.fn(),
|
||||
tracks: [] as KGAudioTrack[],
|
||||
activeTrackAutomationTrackId: null as string | null,
|
||||
activeTrackAutomationType: null as string | null,
|
||||
@@ -21,8 +22,13 @@ const storeState = {
|
||||
let fileImportModalProps: Record<string, unknown> | null = null;
|
||||
|
||||
vi.mock('../../stores/projectStore', () => ({
|
||||
useProjectStore: (selector?: (state: typeof storeState) => unknown) => (
|
||||
selector ? selector(storeState) : storeState
|
||||
useProjectStore: Object.assign(
|
||||
(selector?: (state: typeof storeState) => unknown) => (
|
||||
selector ? selector(storeState) : storeState
|
||||
),
|
||||
{
|
||||
getState: () => storeState,
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -73,6 +79,7 @@ describe('TrackInfoItem audio import', () => {
|
||||
storeState.removeTrack.mockReset();
|
||||
storeState.toggleInstrumentSelectionForTrack.mockReset();
|
||||
storeState.importAudioToTrack.mockReset();
|
||||
storeState.updateTrackProperties.mockReset();
|
||||
storeState.setTrackAutomationView.mockReset();
|
||||
vi.mocked(showConfirm).mockReset();
|
||||
});
|
||||
@@ -127,4 +134,32 @@ describe('TrackInfoItem audio import', () => {
|
||||
translate('track.controls.settings.deleteTrackConfirm', { name: '钢琴' }, 'zh_cn')
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the track color entry and clears the color override', async () => {
|
||||
const audioTrack = new KGAudioTrack('Audio Track', 1);
|
||||
audioTrack.setTrackIndex(0);
|
||||
audioTrack.setColor('#3C8AC4');
|
||||
storeState.tracks = [audioTrack];
|
||||
storeState.updateTrackProperties.mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<TrackInfoItem
|
||||
track={audioTrack}
|
||||
index={0}
|
||||
isDragging={false}
|
||||
isDragOver={false}
|
||||
onTrackNameEdit={vi.fn()}
|
||||
onDragStart={vi.fn()}
|
||||
onDragOver={vi.fn()}
|
||||
onDrop={vi.fn()}
|
||||
onDragEnd={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '更多操作' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '轨道颜色...' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Reset color' }));
|
||||
|
||||
expect(storeState.updateTrackProperties).toHaveBeenCalledWith(1, { color: null });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TbPiano } from 'react-icons/tb';
|
||||
import { TbDots } from 'react-icons/tb';
|
||||
import { FaFileAudio } from 'react-icons/fa';
|
||||
import KGDropdown from '../common/KGDropdown';
|
||||
import ColorPalettePopup from '../common/ColorPalettePopup';
|
||||
import FileImportModal from '../common/FileImportModal';
|
||||
import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
|
||||
import { DEBUG_MODE } from '../../constants/uiConstants';
|
||||
@@ -91,6 +92,7 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
const [showSettingsDropdown, setShowSettingsDropdown] = useState(false);
|
||||
const [showAudioImportModal, setShowAudioImportModal] = useState(false);
|
||||
const [showAutomationDropdown, setShowAutomationDropdown] = useState(false);
|
||||
const [showTrackColorPalette, setShowTrackColorPalette] = useState(false);
|
||||
const settingsDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const automationDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const suppressDragRef = useRef(false);
|
||||
@@ -115,6 +117,7 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
!settingsDropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowSettingsDropdown(false);
|
||||
setShowTrackColorPalette(false);
|
||||
}
|
||||
if (
|
||||
showAutomationDropdown &&
|
||||
@@ -336,6 +339,9 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
const handleSettingsButtonClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setShowSettingsDropdown(!showSettingsDropdown);
|
||||
if (showSettingsDropdown) {
|
||||
setShowTrackColorPalette(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutomationButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -387,6 +393,17 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrackColorSelect = async (color: string | null) => {
|
||||
try {
|
||||
await useProjectStore.getState().updateTrackProperties(track.getId(), { color });
|
||||
} catch (error) {
|
||||
console.error('Failed to update track color:', error);
|
||||
} finally {
|
||||
setShowTrackColorPalette(false);
|
||||
setShowSettingsDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`track-info ${isDragOver ? 'drag-over' : ''} ${isDragging ? 'dragging' : ''} ${isSelected ? 'selected' : ''}`}
|
||||
@@ -545,18 +562,38 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
>
|
||||
<TbDots />
|
||||
</button>
|
||||
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10000 }}>
|
||||
<KGDropdown
|
||||
options={[{ label: t('track.controls.settings.deleteTrack'), value: 'Delete Track' }]}
|
||||
value={''}
|
||||
onChange={handleSettingsAction}
|
||||
label={t('track.controls.moreActions')}
|
||||
hideButton={true}
|
||||
isOpen={showSettingsDropdown}
|
||||
onToggle={setShowSettingsDropdown}
|
||||
className="settings-dropdown"
|
||||
/>
|
||||
</div>
|
||||
{showSettingsDropdown && (
|
||||
<div className="track-settings-menu">
|
||||
<button
|
||||
type="button"
|
||||
className="track-settings-menu-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowTrackColorPalette((open) => !open);
|
||||
}}
|
||||
>
|
||||
{t('track.controls.settings.trackColor')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="track-settings-menu-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleSettingsAction('Delete Track');
|
||||
}}
|
||||
>
|
||||
{t('track.controls.settings.deleteTrack')}
|
||||
</button>
|
||||
{showTrackColorPalette && (
|
||||
<div className="track-settings-color-popup">
|
||||
<ColorPalettePopup
|
||||
selectedColor={track.getColor()}
|
||||
onSelect={handleTrackColorSelect}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user