Skip to content

Commit b14c12e

Browse files
committed
feat: added export/import gig
1 parent 7feca6b commit b14c12e

3 files changed

Lines changed: 54 additions & 54 deletions

File tree

CODEMAP.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,14 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
105105
- 236:export async function deleteSetlist(id: string): Promise<void>
106106
- **deps**: ../types
107107

108-
### exportService.ts (232 lines)
108+
### exportService.ts (251 lines)
109109
- 45:export interface SetlistImportResult
110110
- 136:export async function exportSong(song: SongData): Promise<void>
111111
- 142:export async function importSong(file: File): Promise<SongData>
112112
- 151:export async function exportSetlist(
113113
- 210:export async function importSetlist(file: File): Promise<SetlistImportResult>
114114
- 216:export async function importSetlistFromUrl(
115+
- 235:export async function exportGig(
115116
- **deps**: ../types,../utils/encoding
116117

117118
### metronomeScheduler.ts (179 lines)
@@ -272,7 +273,7 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
272273
- 37:export function MidiSettingsDialog({ onClose }: MidiSettingsDialogProps)
273274
- **deps**: ../../services/midiService,../../stores/useMidiStore,../../stores/useToastStore
274275

275-
### Sidebar.tsx (1080 lines)
276+
### Sidebar.tsx (1060 lines)
276277
- 25:export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, collapsed = false, onToggleCollapse, o...
277278
- **deps**: ../Markers/MarkerList,../../services/exportService,../../stores/useModeStore,../../stores/useSetlistStore,../../stores/useSongStore,../../stores/useTabStore,../../stores/useToastStore,../../utils/formatTime,../../utils/iconSizes
278279

src/components/Layout/Sidebar.tsx

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useSongStore } from '../../stores/useSongStore';
33
import { useSetlistStore } from '../../stores/useSetlistStore';
44
import { useModeStore } from '../../stores/useModeStore';
55
import { MarkerList } from '../Markers/MarkerList';
6-
import { exportSong, importSong, exportSetlist, importSetlist } from '../../services/exportService';
6+
import { exportSong, importSong, exportSetlist, importSetlist, exportGig } from '../../services/exportService';
77
import { useTabStore } from '../../stores/useTabStore';
88
import { useToastStore } from '../../stores/useToastStore';
99
import { UrlImportDialog } from './UrlImportDialog';
@@ -25,13 +25,11 @@ interface SidebarProps {
2525
export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, collapsed = false, onToggleCollapse, onAddMarker }: SidebarProps) {
2626
const [sectionsOpen, setSectionsOpen] = useState(true);
2727
const [setlistOpen, setSetlistOpen] = useState(true);
28-
const [setlistName, setSetlistName] = useState('');
2928
const [editingPauseId, setEditingPauseId] = useState<string | null>(null);
3029
const [editingPauseValue, setEditingPauseValue] = useState('');
3130
const [dragIndex, setDragIndex] = useState<number | null>(null);
3231
const [dropIndex, setDropIndex] = useState<number | null>(null);
3332
const [showImportExport, setShowImportExport] = useState(false);
34-
const [setlistExportMode, setSetlistExportMode] = useState(false);
3533
const [showUrlImport, setShowUrlImport] = useState(false);
3634
const importExportRef = useRef<HTMLDivElement>(null);
3735

@@ -64,7 +62,6 @@ export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, col
6462
const handleClick = (e: MouseEvent) => {
6563
if (importExportRef.current && !importExportRef.current.contains(e.target as Node)) {
6664
setShowImportExport(false);
67-
setSetlistExportMode(false);
6865
}
6966
};
7067
document.addEventListener('mousedown', handleClick);
@@ -950,7 +947,6 @@ const handleImportSetlist = () => {
950947
<button
951948
onClick={() => {
952949
setShowImportExport((v) => !v);
953-
setSetlistExportMode(false);
954950
}}
955951
className='w-full px-2 py-1.5 text-xs font-mono rounded transition-colors
956952
bg-slate-700 hover:bg-slate-600 text-slate-300'
@@ -983,73 +979,57 @@ const handleImportSetlist = () => {
983979
<Upload size={ICON_SIZE.ACTION} className='inline-block' /> Import Song
984980
</button>
985981
<div className='border-t border-slate-700 my-1' />
986-
{!setlistExportMode ? (
987-
<button
982+
983+
<button
988984
onClick={() => {
989-
handleExportSetlist();
985+
handleImportSetlist();
990986
setShowImportExport(false);
991987
}}
992-
disabled={songOrder.length === 0}
993988
className='w-full text-left px-3 py-1.5 text-xs font-mono
994-
text-slate-300 hover:bg-slate-700 transition-colors
995-
disabled:opacity-30 disabled:cursor-not-allowed'
989+
text-slate-300 hover:bg-slate-700 transition-colors'
996990
>
997-
<Download size={ICON_SIZE.ACTION} className='inline-block' /> Export Setlist
991+
<Upload size={ICON_SIZE.ACTION} className='inline-block' /> Import Gig/Setlist
998992
</button>
999-
) : (
1000-
<div className='px-3 py-1.5 flex flex-col gap-1.5'>
1001-
<input
1002-
type='text'
1003-
placeholder='Setlist name...'
1004-
value={setlistName}
1005-
onChange={(e) => setSetlistName(e.target.value)}
1006-
onKeyDown={(e) => {
1007-
if (e.key === 'Enter' && setlistName.trim()) {
1008-
handleExportSetlist();
1009-
setShowImportExport(false);
1010-
setSetlistExportMode(false);
1011-
}
1012-
}}
1013-
autoFocus
1014-
className='bg-slate-900 text-slate-200 text-xs rounded px-2
1015-
py-1 border border-slate-600 focus:border-indigo-500
1016-
outline-none font-mono w-full'
1017-
/>
1018-
<button
1019-
onClick={() => {
1020-
handleExportSetlist();
1021-
setShowImportExport(false);
1022-
setSetlistExportMode(false);
1023-
}}
1024-
disabled={!setlistName.trim()}
1025-
className='px-2 py-1 text-xs font-mono bg-indigo-600
1026-
hover:bg-indigo-500 text-white rounded
1027-
transition-colors disabled:opacity-30
1028-
disabled:cursor-not-allowed'
1029-
>
1030-
<Download size={ICON_SIZE.ACTION} className='inline-block' /> Export
1031-
</button>
1032-
</div>
1033-
)}
1034993
<button
1035994
onClick={() => {
1036-
handleImportSetlist();
995+
setShowUrlImport(true);
1037996
setShowImportExport(false);
1038997
}}
1039998
className='w-full text-left px-3 py-1.5 text-xs font-mono
1040999
text-slate-300 hover:bg-slate-700 transition-colors'
10411000
>
1042-
<Upload size={ICON_SIZE.ACTION} className='inline-block' /> Import Setlist
1001+
<Upload size={ICON_SIZE.ACTION} className='inline-block' /> Import from URL
10431002
</button>
1003+
10441004
<button
10451005
onClick={() => {
1046-
setShowUrlImport(true);
1006+
handleExportSetlist();
10471007
setShowImportExport(false);
10481008
}}
1009+
disabled={songOrder.length === 0}
10491010
className='w-full text-left px-3 py-1.5 text-xs font-mono
1050-
text-slate-300 hover:bg-slate-700 transition-colors'
1011+
text-slate-300 hover:bg-slate-700 transition-colors
1012+
disabled:opacity-30 disabled:cursor-not-allowed'
10511013
>
1052-
<Upload size={ICON_SIZE.ACTION} className='inline-block' /> Import from URL
1014+
<Download size={ICON_SIZE.ACTION} className='inline-block' /> Export Setlist
1015+
</button>
1016+
1017+
<button
1018+
onClick={async () => {
1019+
const allItems = allSetlists.map((sl) => ({
1020+
name: sl.name,
1021+
items: sl.items,
1022+
}));
1023+
await exportGig(allItems, songs);
1024+
addToast('Exported gig', 'success');
1025+
setShowImportExport(false);
1026+
}}
1027+
disabled={allSetlists.length === 0}
1028+
className='w-full text-left px-3 py-1.5 text-xs font-mono
1029+
text-slate-300 hover:bg-slate-700 transition-colors
1030+
disabled:opacity-30 disabled:cursor-not-allowed'
1031+
>
1032+
<Download size={ICON_SIZE.ACTION} className='inline-block' /> Export Gig
10531033
</button>
10541034
</div>
10551035
)}

src/services/exportService.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,23 @@ export async function importSetlistFromUrl(
230230

231231
const raw = await response.json();
232232
return restoreSetlistData(raw);
233+
}
234+
235+
export async function exportGig(
236+
setlists: { name: string; items: SetlistItem[] }[],
237+
songs: SongData[],
238+
): Promise<void> {
239+
// Deduplicate songs (same song can be in multiple setlists)
240+
const uniqueSongs = new Map(songs.map((s) => [s.id, s]));
241+
const bundles = await Promise.all(
242+
[...uniqueSongs.values()].map((song) => bundleSong(song)),
243+
);
244+
245+
const data: SetlistExportV2 = {
246+
version: 2,
247+
setlists,
248+
songs: bundles,
249+
};
250+
251+
downloadJson(data, 'gig.setlist.json');
233252
}

0 commit comments

Comments
 (0)