initial public release.
This commit is contained in:
@@ -0,0 +1,789 @@
|
||||
import { create } from 'zustand';
|
||||
import { KGCore } from '../core/KGCore';
|
||||
import { KGTrack } from '../core/track/KGTrack';
|
||||
import { KGProject, type KeySignature } from '../core/KGProject';
|
||||
import type { TimeSignature } from '../types/projectTypes';
|
||||
import { KGMidiTrack, type InstrumentType } from '../core/track/KGMidiTrack';
|
||||
import { beatsToTimeString } from '../util/timeUtil';
|
||||
import { KGAudioInterface } from '../core/audio-interface/KGAudioInterface';
|
||||
import { KGPianoRollState } from '../core/state/KGPianoRollState';
|
||||
import { KGMidiNote } from '../core/midi/KGMidiNote';
|
||||
import { KGRegion } from '../core/region/KGRegion';
|
||||
import { AddTrackCommand, RemoveTrackCommand, ReorderTracksCommand, UpdateTrackCommand, type TrackUpdateProperties, PasteRegionsCommand, PasteNotesCommand, ChangeProjectPropertyCommand } from '../core/commands';
|
||||
import { ConfigManager } from '../core/config/ConfigManager';
|
||||
import { upgradeProjectToLatest } from '../core/project-upgrader/KGProjectUpgrader';
|
||||
|
||||
/**
|
||||
* Update CSS custom property for time signature numerator
|
||||
* This ensures UI calculations are synchronized with project settings
|
||||
*/
|
||||
function updateTimeSignatureCSS(timeSignature: TimeSignature): void {
|
||||
document.documentElement.style.setProperty('--time-signature-numerator', timeSignature.numerator.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CSS custom property for max number of bars
|
||||
* Keeps layout widths in sync with project max bars
|
||||
*/
|
||||
function updateMaxBarsCSS(maxBars: number): void {
|
||||
document.documentElement.style.setProperty('--max-number-of-bars', maxBars.toString());
|
||||
}
|
||||
|
||||
// Define the store state interface
|
||||
interface ProjectState {
|
||||
// State
|
||||
projectName: string;
|
||||
tracks: KGTrack[];
|
||||
currentStatus: string;
|
||||
maxBars: number;
|
||||
timeSignature: TimeSignature;
|
||||
bpm: number;
|
||||
keySignature: KeySignature;
|
||||
playheadPosition: number; // in beats
|
||||
isPlaying: boolean;
|
||||
currentTime: string; // formatted time string
|
||||
|
||||
// Selection state for UI reactivity
|
||||
selectedNoteIds: string[];
|
||||
selectedRegionIds: string[];
|
||||
selectedTrackId: string | null;
|
||||
|
||||
// Piano roll state
|
||||
showPianoRoll: boolean;
|
||||
activeRegionId: string | null;
|
||||
|
||||
// ChatBox state
|
||||
showChatBox: boolean;
|
||||
|
||||
// Instrument selection panel state
|
||||
showInstrumentSelection: boolean;
|
||||
instrumentSelectionTrackId: string | null;
|
||||
|
||||
// Settings state
|
||||
showSettings: boolean;
|
||||
|
||||
// Undo/redo state
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
undoDescription: string | null;
|
||||
redoDescription: string | null;
|
||||
|
||||
// Actions
|
||||
setProjectName: (name: string) => void;
|
||||
addTrack: () => Promise<void>;
|
||||
removeTrack: (id: number) => Promise<void>;
|
||||
updateTrack: (track: KGTrack) => Promise<void>;
|
||||
updateTrackProperties: (trackId: number, properties: TrackUpdateProperties) => Promise<void>;
|
||||
setTrackInstrument: (trackId: number, instrument: InstrumentType) => Promise<void>;
|
||||
reorderTracks: (sourceIndex: number, destinationIndex: number) => void;
|
||||
setStatus: (status: string) => void;
|
||||
removeStatus: () => void;
|
||||
refreshStatus: () => void;
|
||||
loadProject: (project: KGProject | null) => Promise<void>;
|
||||
setPlayheadPosition: (position: number) => void;
|
||||
startPlaying: () => Promise<void>;
|
||||
stopPlaying: () => Promise<void>;
|
||||
setBpm: (bpm: number) => void;
|
||||
setMaxBars: (maxBars: number) => void;
|
||||
setTimeSignature: (timeSignature: TimeSignature) => void;
|
||||
setKeySignature: (keySignature: KeySignature) => void;
|
||||
|
||||
// Selection actions
|
||||
syncSelectionFromCore: () => void;
|
||||
clearAllSelections: () => void;
|
||||
setSelectedTrack: (trackId: string | null) => void;
|
||||
|
||||
// Piano roll actions
|
||||
setShowPianoRoll: (show: boolean) => void;
|
||||
setActiveRegionId: (regionId: string | null) => void;
|
||||
|
||||
// Project state cleanup
|
||||
cleanupProjectState: () => void;
|
||||
|
||||
// ChatBox actions
|
||||
setShowChatBox: (show: boolean) => void;
|
||||
toggleChatBox: () => void;
|
||||
|
||||
// Instrument selection panel actions
|
||||
openInstrumentSelectionForTrack: (trackId: string) => void;
|
||||
toggleInstrumentSelectionForTrack: (trackId: string) => void;
|
||||
closeInstrumentSelection: () => void;
|
||||
|
||||
// Settings actions
|
||||
setShowSettings: (show: boolean) => void;
|
||||
toggleSettings: () => void;
|
||||
|
||||
// Copy/paste actions
|
||||
pasteRegionsAtTrack: (trackId: string, position: number) => void;
|
||||
pasteNotesToActiveRegion: (regionId: string, position: number) => void;
|
||||
|
||||
// Undo/redo actions
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
syncUndoRedoState: () => void;
|
||||
|
||||
// Project state refresh actions
|
||||
refreshProjectState: () => void;
|
||||
|
||||
// Initialization actions
|
||||
initializeFromConfig: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Create the store
|
||||
export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
const currentProject = KGCore.instance().getCurrentProject();
|
||||
|
||||
// Initialize CSS variable for time signature on store creation
|
||||
updateTimeSignatureCSS(currentProject.getTimeSignature());
|
||||
// Initialize CSS variable for max bars on store creation
|
||||
updateMaxBarsCSS(currentProject.getMaxBars());
|
||||
|
||||
// Get initial ChatBox state from config
|
||||
const configManager = ConfigManager.instance();
|
||||
const initialChatBoxState = configManager.getIsInitialized()
|
||||
? (configManager.get('chatbox.default_open') as boolean) ?? false
|
||||
: false;
|
||||
|
||||
// Set up playhead update callback to keep store in sync during playback
|
||||
KGCore.instance().setPlayheadUpdateCallback((position: number) => {
|
||||
const { bpm, timeSignature } = get();
|
||||
set({
|
||||
playheadPosition: position,
|
||||
currentTime: beatsToTimeString(position, bpm, timeSignature)
|
||||
});
|
||||
});
|
||||
|
||||
// Keep store isPlaying in sync when core auto-stops (e.g., at maxBars)
|
||||
KGCore.instance().setPlaybackStateChangeCallback((isPlaying: boolean) => {
|
||||
set({ isPlaying });
|
||||
});
|
||||
|
||||
// Set up selection sync callback
|
||||
const syncSelectionFromCore = () => {
|
||||
const core = KGCore.instance();
|
||||
const selectedItems = core.getSelectedItems();
|
||||
|
||||
const noteIds = selectedItems
|
||||
.filter(item => item instanceof KGMidiNote)
|
||||
.map(item => item.getId());
|
||||
const regionIds = selectedItems
|
||||
.filter(item => item instanceof KGRegion)
|
||||
.map(item => item.getId());
|
||||
|
||||
set({ selectedNoteIds: noteIds, selectedRegionIds: regionIds });
|
||||
};
|
||||
|
||||
// Register the sync callback with KGCore
|
||||
KGCore.instance().onSelectionChanged(syncSelectionFromCore);
|
||||
|
||||
// Initial selection sync
|
||||
syncSelectionFromCore();
|
||||
|
||||
// Set up command history sync callback
|
||||
const syncUndoRedoState = () => {
|
||||
const core = KGCore.instance();
|
||||
set({
|
||||
canUndo: core.canUndo(),
|
||||
canRedo: core.canRedo(),
|
||||
undoDescription: core.getUndoDescription(),
|
||||
redoDescription: core.getRedoDescription()
|
||||
});
|
||||
};
|
||||
|
||||
// Register the undo/redo sync callback with KGCore
|
||||
KGCore.instance().setOnCommandHistoryChanged(syncUndoRedoState);
|
||||
|
||||
// Initial undo/redo state sync
|
||||
syncUndoRedoState();
|
||||
|
||||
// Ensure a default track exists on initial app start
|
||||
// Also auto-select it and open instrument selection panel
|
||||
let initialSelectedTrackId: string | null = null;
|
||||
let initialShowInstrumentSelection = false;
|
||||
let initialInstrumentSelectionTrackId: string | null = null;
|
||||
try {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
if (project.getTracks().length === 0) {
|
||||
const addDefaultTrackCommand = new AddTrackCommand(undefined, 'Melody');
|
||||
KGCore.instance().executeCommand(addDefaultTrackCommand);
|
||||
const createdId = String(addDefaultTrackCommand.getTrackId());
|
||||
initialSelectedTrackId = createdId;
|
||||
initialShowInstrumentSelection = true;
|
||||
initialInstrumentSelectionTrackId = createdId;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating default track on startup:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
// Initial state
|
||||
projectName: currentProject.getName(),
|
||||
tracks: currentProject.getTracks() as KGTrack[],
|
||||
currentStatus: KGCore.instance().getStatus() || 'Unknown',
|
||||
maxBars: currentProject.getMaxBars(),
|
||||
timeSignature: currentProject.getTimeSignature(),
|
||||
bpm: currentProject.getBpm(),
|
||||
keySignature: currentProject.getKeySignature(),
|
||||
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
||||
isPlaying: KGCore.instance().getIsPlaying(),
|
||||
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
|
||||
|
||||
// Initial selection state
|
||||
selectedNoteIds: [],
|
||||
selectedRegionIds: [],
|
||||
selectedTrackId: initialSelectedTrackId,
|
||||
|
||||
// Initial piano roll state
|
||||
showPianoRoll: false,
|
||||
activeRegionId: null,
|
||||
|
||||
// Initial ChatBox state
|
||||
showChatBox: initialChatBoxState,
|
||||
|
||||
// Initial Instrument Selection panel state
|
||||
showInstrumentSelection: initialShowInstrumentSelection,
|
||||
instrumentSelectionTrackId: initialInstrumentSelectionTrackId,
|
||||
|
||||
// Initial Settings state
|
||||
showSettings: false,
|
||||
|
||||
// Initial undo/redo state
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
undoDescription: null,
|
||||
redoDescription: null,
|
||||
|
||||
// Actions
|
||||
setProjectName: (name: string) => {
|
||||
try {
|
||||
// Create and execute the change project property command
|
||||
const command = new ChangeProjectPropertyCommand({ name });
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state
|
||||
set({ projectName: name });
|
||||
|
||||
console.log(`Set project name to "${name}"`);
|
||||
} catch (error) {
|
||||
console.error('Error setting project name:', error);
|
||||
get().setStatus('Failed to set project name');
|
||||
}
|
||||
},
|
||||
|
||||
addTrack: async () => {
|
||||
try {
|
||||
// Create and execute the add track command
|
||||
const command = new AddTrackCommand();
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state with a new array reference to trigger re-render
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
||||
|
||||
console.log(`Added track ${command.getTrackId()}`);
|
||||
} catch (error) {
|
||||
console.error('Error adding track:', error);
|
||||
get().setStatus('Failed to add track');
|
||||
}
|
||||
},
|
||||
|
||||
removeTrack: async (id: number) => {
|
||||
try {
|
||||
// Create and execute the remove track command
|
||||
const command = new RemoveTrackCommand(id);
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state with a new array reference to trigger re-render
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
||||
|
||||
console.log(`Removed track ${id}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing track:', error);
|
||||
get().setStatus('Failed to remove track');
|
||||
}
|
||||
},
|
||||
|
||||
updateTrack: async (updatedTrack: KGTrack) => {
|
||||
const { tracks } = get();
|
||||
|
||||
try {
|
||||
// Find the old track to compare instruments
|
||||
const oldTrack = tracks.find(track => track.getId() === updatedTrack.getId());
|
||||
|
||||
// Type guard to check if track is KGMidiTrack
|
||||
const isMidiTrack = (track: KGTrack): track is KGMidiTrack => {
|
||||
return track.constructor.name === 'KGMidiTrack' && 'getInstrument' in track;
|
||||
};
|
||||
|
||||
// Check if instrument changed (only for MIDI tracks)
|
||||
const shouldUpdateInstrument =
|
||||
isMidiTrack(updatedTrack) &&
|
||||
oldTrack &&
|
||||
isMidiTrack(oldTrack) &&
|
||||
updatedTrack.getInstrument() !== oldTrack.getInstrument();
|
||||
|
||||
// Update instrument in audio interface if changed
|
||||
if (shouldUpdateInstrument && isMidiTrack(updatedTrack)) {
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
const newInstrument = updatedTrack.getInstrument();
|
||||
audioInterface.setTrackInstrument(updatedTrack.getId().toString(), newInstrument);
|
||||
console.log(`Updated track ${updatedTrack.getId()} instrument to ${newInstrument}`);
|
||||
}
|
||||
|
||||
// Find and update the track
|
||||
const updatedTracks = tracks.map(track =>
|
||||
track.getId() === updatedTrack.getId() ? updatedTrack : track
|
||||
);
|
||||
|
||||
// Update the core model
|
||||
KGCore.instance().getCurrentProject().setTracks(updatedTracks);
|
||||
|
||||
// Update the store
|
||||
set({ tracks: updatedTracks });
|
||||
|
||||
console.log(`Updated track ${updatedTrack.getId()}`);
|
||||
} catch (error) {
|
||||
console.error('Error updating track:', error);
|
||||
get().setStatus('Failed to update track');
|
||||
}
|
||||
},
|
||||
|
||||
updateTrackProperties: async (trackId: number, properties: TrackUpdateProperties) => {
|
||||
try {
|
||||
// Create and execute the update track command
|
||||
const command = new UpdateTrackCommand(trackId, properties);
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state with the current project state
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
||||
|
||||
console.log(`Updated track ${trackId} properties`);
|
||||
} catch (error) {
|
||||
console.error('Error updating track properties:', error);
|
||||
get().setStatus('Failed to update track properties');
|
||||
}
|
||||
},
|
||||
|
||||
setTrackInstrument: async (trackId: number, instrument: InstrumentType) => {
|
||||
try {
|
||||
// Use the new updateTrackProperties method with command pattern
|
||||
await get().updateTrackProperties(trackId, { instrument });
|
||||
|
||||
console.log(`Set track ${trackId} instrument to ${instrument}`);
|
||||
} catch (error) {
|
||||
console.error('Error setting track instrument:', error);
|
||||
get().setStatus('Failed to change instrument');
|
||||
}
|
||||
},
|
||||
|
||||
reorderTracks: (sourceIndex: number, destinationIndex: number) => {
|
||||
try {
|
||||
// Create and execute the reorder tracks command
|
||||
const command = new ReorderTracksCommand(sourceIndex, destinationIndex);
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state with the current project state
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
||||
|
||||
console.log(`Reordered track from index ${sourceIndex} to ${destinationIndex}`);
|
||||
} catch (error) {
|
||||
console.error('Error reordering tracks:', error);
|
||||
get().setStatus('Failed to reorder tracks');
|
||||
}
|
||||
},
|
||||
|
||||
setStatus: (status: string) => {
|
||||
KGCore.instance().setStatus(status);
|
||||
set({ currentStatus: status });
|
||||
},
|
||||
|
||||
removeStatus: () => {
|
||||
KGCore.instance().setStatus('');
|
||||
set({ currentStatus: '' });
|
||||
},
|
||||
|
||||
refreshStatus: () => {
|
||||
set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' });
|
||||
},
|
||||
|
||||
loadProject: async (project: KGProject | null = null) => {
|
||||
try {
|
||||
const { setPlayheadPosition } = get();
|
||||
|
||||
// Upgrade incoming project data to latest structure version (only when provided explicitly)
|
||||
if (project) {
|
||||
project = upgradeProjectToLatest(project);
|
||||
}
|
||||
|
||||
// Get project from KGCore if null
|
||||
const projectToLoad = project || KGCore.instance().getCurrentProject();
|
||||
|
||||
// Set the project in KGCore if one was provided
|
||||
if (project) {
|
||||
KGCore.instance().setCurrentProject(projectToLoad);
|
||||
}
|
||||
|
||||
// Ensure a default "Melody" track exists for empty projects
|
||||
if (projectToLoad.getTracks().length === 0) {
|
||||
const addDefaultTrackCommand = new AddTrackCommand(undefined, 'Melody');
|
||||
KGCore.instance().executeCommand(addDefaultTrackCommand);
|
||||
}
|
||||
|
||||
// Reset playhead to 0 when loading a project
|
||||
setPlayheadPosition(0);
|
||||
|
||||
// Get project properties
|
||||
const maxBars = projectToLoad.getMaxBars();
|
||||
const timeSignature = projectToLoad.getTimeSignature();
|
||||
const bpm = projectToLoad.getBpm();
|
||||
const keySignature = projectToLoad.getKeySignature();
|
||||
const tracks = projectToLoad.getTracks();
|
||||
|
||||
// Setup audio synths for all tracks
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
|
||||
// Clear any existing synths first
|
||||
tracks.forEach(track => {
|
||||
audioInterface.removeTrackSynth(track.getId().toString());
|
||||
});
|
||||
|
||||
// Create synths for all tracks (with their stored volumes)
|
||||
tracks.forEach(track => {
|
||||
const trackId = track.getId().toString();
|
||||
// Get instrument from track model if it's a MIDI track
|
||||
let instrument: InstrumentType = 'acoustic_grand_piano'; // Default fallback
|
||||
if (track.constructor.name === 'KGMidiTrack' && 'getInstrument' in track) {
|
||||
instrument = (track as KGMidiTrack).getInstrument();
|
||||
}
|
||||
audioInterface.createTrackSynth(trackId, instrument);
|
||||
// Volume is applied during bus creation; ensure sync if bus already existed
|
||||
audioInterface.setTrackVolume(trackId, track.getVolume());
|
||||
});
|
||||
|
||||
// Update CSS variable for time signature numerator
|
||||
updateTimeSignatureCSS(timeSignature);
|
||||
// Update CSS variable for max bars
|
||||
updateMaxBarsCSS(maxBars);
|
||||
|
||||
// Log project loading info
|
||||
console.log(`Project max bars: ${maxBars}`);
|
||||
console.log(`Setup audio synths for ${tracks.length} tracks`);
|
||||
|
||||
// Update the store state to reflect the loaded project
|
||||
// Force a new array reference for tracks to trigger React/Zustand re-render
|
||||
set({
|
||||
projectName: projectToLoad.getName(),
|
||||
tracks: [...tracks],
|
||||
maxBars,
|
||||
timeSignature,
|
||||
bpm,
|
||||
keySignature,
|
||||
playheadPosition: 0, // Ensure store state is also updated
|
||||
currentTime: beatsToTimeString(0, bpm, timeSignature) // Reset time display
|
||||
});
|
||||
|
||||
// After loading a project, auto-select the first track and open Instrument Selection
|
||||
const firstTrack = tracks[0];
|
||||
if (firstTrack) {
|
||||
const firstTrackIdStr = firstTrack.getId().toString();
|
||||
set({
|
||||
selectedTrackId: firstTrackIdStr,
|
||||
showInstrumentSelection: true,
|
||||
instrumentSelectionTrackId: firstTrackIdStr
|
||||
});
|
||||
}
|
||||
|
||||
// Reset piano roll state for new/loaded project
|
||||
KGPianoRollState.instance().setLastEditedNoteLength(1);
|
||||
|
||||
// Add a status message
|
||||
KGCore.instance().setStatus(`Project "${projectToLoad.getName()}" loaded with audio setup`);
|
||||
set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' });
|
||||
} catch (error) {
|
||||
console.error('Error loading project:', error);
|
||||
get().setStatus('Failed to load project');
|
||||
}
|
||||
},
|
||||
|
||||
setPlayheadPosition: (position: number) => {
|
||||
const { bpm, timeSignature } = get();
|
||||
KGCore.instance().setPlayheadPosition(position);
|
||||
set({
|
||||
playheadPosition: position,
|
||||
currentTime: beatsToTimeString(position, bpm, timeSignature)
|
||||
});
|
||||
},
|
||||
|
||||
startPlaying: async () => {
|
||||
await KGCore.instance().startPlaying();
|
||||
set({ isPlaying: true });
|
||||
},
|
||||
|
||||
stopPlaying: async () => {
|
||||
await KGCore.instance().stopPlaying();
|
||||
set({ isPlaying: false });
|
||||
},
|
||||
|
||||
setBpm: (bpm: number) => {
|
||||
try {
|
||||
// Create and execute the change project property command
|
||||
const command = new ChangeProjectPropertyCommand({ bpm });
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state
|
||||
set({ bpm });
|
||||
|
||||
console.log(`Set BPM to ${bpm}`);
|
||||
} catch (error) {
|
||||
console.error('Error setting BPM:', error);
|
||||
get().setStatus('Failed to set BPM');
|
||||
}
|
||||
},
|
||||
|
||||
setMaxBars: (maxBars: number) => {
|
||||
try {
|
||||
// Create and execute the change project property command
|
||||
const command = new ChangeProjectPropertyCommand({ maxBars });
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state
|
||||
set({ maxBars });
|
||||
// Sync CSS var so layout adjusts immediately
|
||||
updateMaxBarsCSS(maxBars);
|
||||
|
||||
console.log(`Set max bars to ${maxBars}`);
|
||||
} catch (error) {
|
||||
console.error('Error setting max bars:', error);
|
||||
get().setStatus('Failed to set max bars');
|
||||
}
|
||||
},
|
||||
|
||||
setTimeSignature: (timeSignature: TimeSignature) => {
|
||||
try {
|
||||
// Create and execute the change project property command
|
||||
const command = new ChangeProjectPropertyCommand({ timeSignature });
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state
|
||||
set({ timeSignature });
|
||||
updateTimeSignatureCSS(timeSignature);
|
||||
|
||||
console.log(`Set time signature to ${timeSignature.numerator}/${timeSignature.denominator}`);
|
||||
} catch (error) {
|
||||
console.error('Error setting time signature:', error);
|
||||
get().setStatus('Failed to set time signature');
|
||||
}
|
||||
},
|
||||
|
||||
setKeySignature: (keySignature: KeySignature) => {
|
||||
try {
|
||||
// Create and execute the change project property command
|
||||
const command = new ChangeProjectPropertyCommand({ keySignature });
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store state
|
||||
set({ keySignature });
|
||||
|
||||
console.log(`Set key signature to ${keySignature}`);
|
||||
} catch (error) {
|
||||
console.error('Error setting key signature:', error);
|
||||
get().setStatus('Failed to set key signature');
|
||||
}
|
||||
},
|
||||
|
||||
// Selection actions
|
||||
syncSelectionFromCore,
|
||||
|
||||
clearAllSelections: () => {
|
||||
KGCore.instance().clearSelectedItems();
|
||||
// Note: syncSelectionFromCore will be called automatically via callback
|
||||
},
|
||||
|
||||
setSelectedTrack: (trackId: string | null) => {
|
||||
const { showInstrumentSelection } = get();
|
||||
// Update selected track id
|
||||
set({ selectedTrackId: trackId });
|
||||
// If instrument panel is open and a track is selected, retarget the panel
|
||||
if (showInstrumentSelection && trackId) {
|
||||
set({ instrumentSelectionTrackId: trackId });
|
||||
}
|
||||
},
|
||||
|
||||
// Piano roll actions
|
||||
setShowPianoRoll: (show: boolean) => {
|
||||
set({ showPianoRoll: show });
|
||||
},
|
||||
|
||||
setActiveRegionId: (regionId: string | null) => {
|
||||
set({ activeRegionId: regionId });
|
||||
},
|
||||
|
||||
// Project state cleanup - used when starting new/loading projects
|
||||
cleanupProjectState: () => {
|
||||
// Close piano roll if it's visible
|
||||
set({ showPianoRoll: false });
|
||||
|
||||
// Clear active region
|
||||
set({ activeRegionId: null });
|
||||
|
||||
// Clear any selected items
|
||||
KGCore.instance().clearSelectedItems();
|
||||
// Note: syncSelectionFromCore will be called automatically via callback
|
||||
|
||||
console.log("Cleaned up project state: closed piano roll, cleared active region, cleared selections");
|
||||
},
|
||||
|
||||
// ChatBox action implementations
|
||||
setShowChatBox: (show: boolean) => {
|
||||
set({ showChatBox: show });
|
||||
},
|
||||
|
||||
toggleChatBox: () => {
|
||||
const { showChatBox } = get();
|
||||
set({ showChatBox: !showChatBox });
|
||||
},
|
||||
|
||||
// Instrument selection panel actions
|
||||
openInstrumentSelectionForTrack: (trackId: string) => {
|
||||
set({ showInstrumentSelection: true, instrumentSelectionTrackId: trackId });
|
||||
},
|
||||
toggleInstrumentSelectionForTrack: (trackId: string) => {
|
||||
set({ showInstrumentSelection: true, instrumentSelectionTrackId: trackId });
|
||||
},
|
||||
closeInstrumentSelection: () => {
|
||||
set({ showInstrumentSelection: false, instrumentSelectionTrackId: null });
|
||||
},
|
||||
|
||||
// Settings action implementations
|
||||
setShowSettings: (show: boolean) => {
|
||||
set({ showSettings: show });
|
||||
},
|
||||
|
||||
toggleSettings: () => {
|
||||
const { showSettings } = get();
|
||||
set({ showSettings: !showSettings });
|
||||
},
|
||||
|
||||
// Copy/paste actions
|
||||
pasteRegionsAtTrack: (trackId: string, position: number) => {
|
||||
// Use command pattern for region pasting with undo support
|
||||
const command = PasteRegionsCommand.fromClipboard(trackId, position);
|
||||
|
||||
if (!command) {
|
||||
console.log('No regions to paste');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store to trigger re-render
|
||||
const { tracks } = get();
|
||||
const updatedTracks = [...tracks];
|
||||
set({ tracks: updatedTracks });
|
||||
|
||||
console.log(`Executed PasteRegionsCommand: pasted regions to track ${trackId} using command pattern`);
|
||||
} catch (error) {
|
||||
console.error('Error pasting regions:', error);
|
||||
}
|
||||
},
|
||||
|
||||
pasteNotesToActiveRegion: (regionId: string, position: number) => {
|
||||
// Use command pattern for note pasting with undo support
|
||||
const command = PasteNotesCommand.fromClipboard(regionId, position);
|
||||
|
||||
if (!command) {
|
||||
console.log('No notes to paste');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
// Update the store to trigger re-render
|
||||
const { tracks } = get();
|
||||
const updatedTracks = [...tracks];
|
||||
set({ tracks: updatedTracks });
|
||||
|
||||
console.log(`Executed PasteNotesCommand: pasted notes to region ${regionId} using command pattern`);
|
||||
} catch (error) {
|
||||
console.error('Error pasting notes:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Undo/redo actions
|
||||
undo: () => {
|
||||
const core = KGCore.instance();
|
||||
if (core.undo()) {
|
||||
// Use centralized refresh method
|
||||
get().refreshProjectState();
|
||||
console.log('Undo completed');
|
||||
}
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
const core = KGCore.instance();
|
||||
if (core.redo()) {
|
||||
// Use centralized refresh method
|
||||
get().refreshProjectState();
|
||||
console.log('Redo completed');
|
||||
}
|
||||
},
|
||||
|
||||
syncUndoRedoState: () => {
|
||||
const core = KGCore.instance();
|
||||
set({
|
||||
canUndo: core.canUndo(),
|
||||
canRedo: core.canRedo(),
|
||||
undoDescription: core.getUndoDescription(),
|
||||
redoDescription: core.getRedoDescription()
|
||||
});
|
||||
},
|
||||
|
||||
// Centralized project state refresh method
|
||||
// Used by undo/redo and external operations (like XML tools) to sync UI with core model
|
||||
refreshProjectState: () => {
|
||||
const core = KGCore.instance();
|
||||
const project = core.getCurrentProject();
|
||||
|
||||
// Force new array reference to trigger React re-renders
|
||||
set({
|
||||
projectName: project.getName(),
|
||||
tracks: [...project.getTracks()] as KGTrack[], // Force new array reference - key for re-rendering!
|
||||
maxBars: project.getMaxBars(),
|
||||
timeSignature: project.getTimeSignature(),
|
||||
bpm: project.getBpm(),
|
||||
keySignature: project.getKeySignature()
|
||||
});
|
||||
|
||||
// Sync CSS variables that affect layout
|
||||
updateTimeSignatureCSS(project.getTimeSignature());
|
||||
updateMaxBarsCSS(project.getMaxBars());
|
||||
|
||||
// Sync all related state
|
||||
const actions = get();
|
||||
actions.syncUndoRedoState();
|
||||
actions.syncSelectionFromCore();
|
||||
},
|
||||
|
||||
// Initialize store with configuration values
|
||||
initializeFromConfig: async () => {
|
||||
const configManager = ConfigManager.instance();
|
||||
if (configManager.getIsInitialized()) {
|
||||
const defaultChatBoxOpen = (configManager.get('chatbox.default_open') as boolean) ?? false;
|
||||
set({ showChatBox: defaultChatBoxOpen });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user