From a4daf681bc71561ab8566ebcebfe799877999282 Mon Sep 17 00:00:00 2001 From: William Ross Date: Tue, 6 Jan 2026 19:57:49 -0700 Subject: [PATCH] Add song list feature for organizing and sharing charts - Add Lists tab in toolbar for managing song lists - Create, edit, and delete named lists with descriptions - Add songs to lists from chart sidebar or multi-select in browse view - Export lists to .bridgelist files (gzip compressed JSON) - Import .bridgelist files shared by others - View list details with cached metadata for offline preview - Persist lists to songlists.json in user data directory Fix: Use backend-generated list ID to ensure export and persistence work correctly --- src-angular/app/app-routing.module.ts | 4 + src-angular/app/app.component.ts | 5 +- src-angular/app/app.module.ts | 8 + .../chart-sidebar.component.html | 7 + .../chart-sidebar/chart-sidebar.component.ts | 8 + .../status-bar/status-bar.component.html | 8 +- .../browse/status-bar/status-bar.component.ts | 13 ++ .../settings/settings.component.html | 2 +- .../add-to-list-modal.component.html | 96 ++++++++ .../add-to-list-modal.component.ts | 141 ++++++++++++ .../songlist-detail.component.html | 196 ++++++++++++++++ .../songlist-detail.component.ts | 174 +++++++++++++++ .../songlists/songlists.component.html | 183 +++++++++++++++ .../songlists/songlists.component.ts | 108 +++++++++ .../components/toolbar/toolbar.component.html | 1 + .../app/core/services/songlist.service.ts | 210 ++++++++++++++++++ src-electron/IpcHandler.ts | 24 ++ src-electron/ipc/SongListHandler.ipc.ts | 209 +++++++++++++++++ src-electron/preload.ts | 12 + src-shared/Paths.ts | 1 + src-shared/interfaces/ipc.interface.ts | 30 ++- src-shared/interfaces/songlist.interface.ts | 83 +++++++ 22 files changed, 1519 insertions(+), 4 deletions(-) create mode 100644 src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.html create mode 100644 src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.ts create mode 100644 src-angular/app/components/songlists/songlist-detail/songlist-detail.component.html create mode 100644 src-angular/app/components/songlists/songlist-detail/songlist-detail.component.ts create mode 100644 src-angular/app/components/songlists/songlists.component.html create mode 100644 src-angular/app/components/songlists/songlists.component.ts create mode 100644 src-angular/app/core/services/songlist.service.ts create mode 100644 src-electron/ipc/SongListHandler.ipc.ts create mode 100644 src-shared/interfaces/songlist.interface.ts diff --git a/src-angular/app/app-routing.module.ts b/src-angular/app/app-routing.module.ts index c33b5cf..a687006 100644 --- a/src-angular/app/app-routing.module.ts +++ b/src-angular/app/app-routing.module.ts @@ -3,12 +3,16 @@ import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' import { BrowseComponent } from './components/browse/browse.component' import { SettingsComponent } from './components/settings/settings.component' +import { SongListsComponent } from './components/songlists/songlists.component' +import { SongListDetailComponent } from './components/songlists/songlist-detail/songlist-detail.component' import { ToolsComponent } from './components/tools/tools.component' import { TabPersistStrategy } from './core/tab-persist.strategy' const routes: Routes = [ { path: 'browse', component: BrowseComponent, data: { shouldReuse: true } }, { path: 'library', redirectTo: '/browse' }, + { path: 'lists', component: SongListsComponent, data: { shouldReuse: true } }, + { path: 'lists/:id', component: SongListDetailComponent }, { path: 'tools', component: ToolsComponent, data: { shouldReuse: true } }, { path: 'settings', component: SettingsComponent, data: { shouldReuse: true } }, { path: 'about', redirectTo: '/browse' }, diff --git a/src-angular/app/app.component.ts b/src-angular/app/app.component.ts index 4cec7c2..497757f 100644 --- a/src-angular/app/app.component.ts +++ b/src-angular/app/app.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core' import { SettingsService } from './core/services/settings.service' +import { SongListService } from './core/services/songlist.service' @Component({ selector: 'app-root', @@ -12,9 +13,11 @@ export class AppComponent { settingsLoaded = false - constructor(settingsService: SettingsService) { + constructor(settingsService: SettingsService, songListService: SongListService) { // Ensure settings are loaded before rendering the application settingsService.loadSettings().then(() => this.settingsLoaded = true) + // Load song lists in the background (not critical for initial render) + songListService.loadLists() document.addEventListener('keydown', event => { if (event.ctrlKey && (event.key === '+' || event.key === '-' || event.key === '=' || event.key === '0')) { diff --git a/src-angular/app/app.module.ts b/src-angular/app/app.module.ts index 8511388..fca8442 100644 --- a/src-angular/app/app.module.ts +++ b/src-angular/app/app.module.ts @@ -16,7 +16,11 @@ import { SearchBarComponent } from './components/browse/search-bar/search-bar.co import { DownloadsModalComponent } from './components/browse/status-bar/downloads-modal/downloads-modal.component' import { StatusBarComponent } from './components/browse/status-bar/status-bar.component' import { SettingsComponent } from './components/settings/settings.component' +import { SongListsComponent } from './components/songlists/songlists.component' +import { SongListDetailComponent } from './components/songlists/songlist-detail/songlist-detail.component' +import { AddToListModalComponent } from './components/songlists/add-to-list-modal/add-to-list-modal.component' import { ToolbarComponent } from './components/toolbar/toolbar.component' +import { ToolsComponent } from './components/tools/tools.component' import { RemoveStyleTagsPipe } from './core/pipes/remove-style-tags.pipe' @NgModule({ @@ -35,6 +39,10 @@ import { RemoveStyleTagsPipe } from './core/pipes/remove-style-tags.pipe' DownloadsModalComponent, RemoveStyleTagsPipe, SettingsComponent, + SongListsComponent, + SongListDetailComponent, + AddToListModalComponent, + ToolsComponent, ], bootstrap: [AppComponent], imports: [ BrowserModule, diff --git a/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.html b/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.html index 84f0118..779860b 100644 --- a/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.html +++ b/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.html @@ -143,6 +143,12 @@
+ +
} diff --git a/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.ts b/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.ts index 61a06c8..212d233 100644 --- a/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.ts +++ b/src-angular/app/components/browse/chart-sidebar/chart-sidebar.component.ts @@ -9,6 +9,7 @@ import { SettingsService } from 'src-angular/app/core/services/settings.service' import { ChartData } from 'src-shared/interfaces/search.interface' import { setlistNames } from 'src-shared/setlist-names' import { difficulties, difficultyDisplay, driveLink, hasIssues, instruments, msToRoughTime, removeStyleTags, shortInstrumentDisplay } from 'src-shared/UtilFunctions' +import { AddToListModalComponent } from '../../songlists/add-to-list-modal/add-to-list-modal.component' @Component({ selector: 'app-chart-sidebar', @@ -20,6 +21,7 @@ export class ChartSidebarComponent implements OnInit { @ViewChild('menu') menu: ElementRef @ViewChild('libraryDirectoryErrorModal') libraryDirectoryErrorModal: ElementRef + @ViewChild('addToListModal') addToListModal: AddToListModalComponent public shortInstrumentDisplay = shortInstrumentDisplay public difficultyDisplay = difficultyDisplay @@ -292,4 +294,10 @@ export class ChartSidebarComponent implements OnInit { } }) } + + onAddToListClicked() { + if (this.selectedChart) { + this.addToListModal.open([this.selectedChart]) + } + } } diff --git a/src-angular/app/components/browse/status-bar/status-bar.component.html b/src-angular/app/components/browse/status-bar/status-bar.component.html index 1eff9da..30517ad 100644 --- a/src-angular/app/components/browse/status-bar/status-bar.component.html +++ b/src-angular/app/components/browse/status-bar/status-bar.component.html @@ -2,10 +2,14 @@
{{ searchService.songsResponse.found | number: '1.0-0' }} Result{{ searchService.songsResponse.found === 1 ? '' : 's' }}
-
+
+
{{ downloadService.currentDownloadText }} @@ -33,4 +37,6 @@ + +
diff --git a/src-angular/app/components/browse/status-bar/status-bar.component.ts b/src-angular/app/components/browse/status-bar/status-bar.component.ts index 7214364..05ffc97 100644 --- a/src-angular/app/components/browse/status-bar/status-bar.component.ts +++ b/src-angular/app/components/browse/status-bar/status-bar.component.ts @@ -6,6 +6,7 @@ import { removeStyleTags } from '../../../../../src-shared/UtilFunctions.js' import { DownloadService } from '../../../core/services/download.service' import { SearchService } from '../../../core/services/search.service' import { SelectionService } from '../../../core/services/selection.service' +import { AddToListModalComponent } from '../../songlists/add-to-list-modal/add-to-list-modal.component' @Component({ selector: 'app-status-bar', @@ -15,6 +16,7 @@ import { SelectionService } from '../../../core/services/selection.service' export class StatusBarComponent { @ViewChild('downloadsModal', { static: false }) downloadsModalComponent: ElementRef + @ViewChild('addToListModal') addToListModal: AddToListModalComponent constructor( public downloadService: DownloadService, @@ -46,4 +48,15 @@ export class StatusBarComponent { clearCompleted() { this.downloadService.cancelAllCompleted() } + + addSelectedToList() { + const selectedGroupIds = this.selectedGroupIds + const selectedCharts = this.searchService.groupedSongs.filter(gs => selectedGroupIds.includes(gs[0].groupId)) + + const uniqueCharts = _.uniqBy(selectedCharts, gs => `${removeStyleTags(gs[0].artist ?? 'Unknown Artist') + } - ${removeStyleTags(gs[0].name ?? 'Unknown Name') + } (${removeStyleTags(gs[0].charter ?? 'Unknown Charter')})`).map(gs => gs[0]) + + this.addToListModal.open(uniqueCharts) + } } diff --git a/src-angular/app/components/settings/settings.component.html b/src-angular/app/components/settings/settings.component.html index 442e835..06e92c2 100644 --- a/src-angular/app/components/settings/settings.component.html +++ b/src-angular/app/components/settings/settings.component.html @@ -42,7 +42,7 @@
{{ '{name}, {artist}, {album}, {genre}, {year}, {charter}' }}

Example: -
"{artist}/{name} ({charter})" will create chart folders that look like "{name} ({charter})" inside subfolders that look like "{artist}"
+
"{{ '{artist}' }}/{{ '{name}' }} ({{ '{charter}' }})" will create chart folders that look like "{{ '{name}' }} ({{ '{charter}' }})" inside subfolders that look like "{{ '{artist}' }}"
diff --git a/src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.html b/src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.html new file mode 100644 index 0000000..0d6d683 --- /dev/null +++ b/src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.html @@ -0,0 +1,96 @@ + + + + + + + + + + + diff --git a/src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.ts b/src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.ts new file mode 100644 index 0000000..3d2308c --- /dev/null +++ b/src-angular/app/components/songlists/add-to-list-modal/add-to-list-modal.component.ts @@ -0,0 +1,141 @@ +import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { FormControl, Validators } from '@angular/forms' + +import { SongList } from '../../../../../src-shared/interfaces/songlist.interface.js' +import { ChartData } from '../../../../../src-shared/interfaces/search.interface.js' +import { SongListService } from '../../../core/services/songlist.service' + +@Component({ + selector: 'app-add-to-list-modal', + templateUrl: './add-to-list-modal.component.html', + standalone: false, +}) +export class AddToListModalComponent implements OnInit, OnDestroy { + @ViewChild('addToListModal') modal: ElementRef + @ViewChild('createNewListModal') createNewListModal: ElementRef + + lists: SongList[] = [] + charts: ChartData[] = [] + selectedListIds = new Set() + + newListName = new FormControl('', { nonNullable: true, validators: [Validators.required] }) + newListDescription = new FormControl('', { nonNullable: true }) + + private listsChangedSub: any + + constructor( + private songListService: SongListService, + private ref: ChangeDetectorRef, + ) { } + + ngOnInit() { + this.lists = this.songListService.getLists() + this.listsChangedSub = this.songListService.listsChanged.subscribe(lists => { + this.lists = lists + this.ref.detectChanges() + }) + } + + ngOnDestroy() { + if (this.listsChangedSub) { + this.listsChangedSub.unsubscribe() + } + } + + /** + * Opens the modal to add charts to lists. + * @param charts The charts to add + */ + open(charts: ChartData[]) { + this.charts = charts + this.selectedListIds.clear() + + // Pre-select lists that already contain all the charts + for (const list of this.lists) { + const allChartsInList = charts.every(chart => + list.entries.some(e => e.md5 === chart.md5) + ) + if (allChartsInList) { + this.selectedListIds.add(list.id) + } + } + + this.modal.nativeElement.showModal() + } + + toggleList(listId: string) { + if (this.selectedListIds.has(listId)) { + this.selectedListIds.delete(listId) + } else { + this.selectedListIds.add(listId) + } + } + + isSelected(listId: string): boolean { + return this.selectedListIds.has(listId) + } + + isPartiallyInList(list: SongList): boolean { + const inListCount = this.charts.filter(chart => + list.entries.some(e => e.md5 === chart.md5) + ).length + return inListCount > 0 && inListCount < this.charts.length + } + + openCreateNewList() { + this.newListName.reset() + this.newListDescription.reset() + this.createNewListModal.nativeElement.showModal() + } + + async createNewList() { + if (this.newListName.valid) { + const newList = await this.songListService.createList( + this.newListName.value.trim(), + this.newListDescription.value.trim() + ) + this.createNewListModal.nativeElement.close() + + // Auto-select the newly created list + this.selectedListIds.add(newList.id) + this.ref.detectChanges() + } + } + + save() { + // Add charts to newly selected lists + for (const listId of this.selectedListIds) { + const list = this.lists.find(l => l.id === listId) + if (list) { + // Filter out charts already in this list + const chartsToAdd = this.charts.filter(chart => + !list.entries.some(e => e.md5 === chart.md5) + ) + if (chartsToAdd.length > 0) { + this.songListService.addCharts(listId, chartsToAdd) + } + } + } + + // Remove charts from deselected lists + for (const list of this.lists) { + if (!this.selectedListIds.has(list.id)) { + const chartsToRemove = this.charts.filter(chart => + list.entries.some(e => e.md5 === chart.md5) + ) + if (chartsToRemove.length > 0) { + this.songListService.removeCharts(list.id, chartsToRemove.map(c => c.md5)) + } + } + } + + this.modal.nativeElement.close() + } + + get chartDescription(): string { + if (this.charts.length === 1) { + return `"${this.charts[0].name}" by ${this.charts[0].artist}` + } + return `${this.charts.length} songs` + } +} diff --git a/src-angular/app/components/songlists/songlist-detail/songlist-detail.component.html b/src-angular/app/components/songlists/songlist-detail/songlist-detail.component.html new file mode 100644 index 0000000..48d68e3 --- /dev/null +++ b/src-angular/app/components/songlists/songlist-detail/songlist-detail.component.html @@ -0,0 +1,196 @@ +
+ @if (!list) { +
+ +

Song list not found

+ +
+ } @else { + +
+
+ +
+

{{ list.name }}

+

{{ list.description || 'No description' }}

+

+ {{ list.entries.length }} songs | Created {{ formatDate(list.createdAt) }} +

+
+
+
+ + +
+
+ + + @if (list.entries.length > 0) { +
+
+ + @if (selectedEntries.size > 0) { + {{ selectedEntries.size }} selected + } +
+
+ @if (selectedEntries.size > 0) { + + + } @else { + + } +
+
+ } + + + @if (list.entries.length === 0) { +
+ +

This list is empty

+

Add songs from the Browse tab to get started

+
+ } @else { +
+ + + + + + + + + + + + + + @for (entry of list.entries; track entry.md5) { + + + + + + + + + + } + +
NameArtistAlbumCharterAddedActions
+ + {{ entry.cache.name || 'Unknown' }}{{ entry.cache.artist || 'Unknown' }}{{ entry.cache.album || 'Unknown' }}{{ entry.cache.charter || 'Unknown' }}{{ formatDate(entry.addedAt) }} +
+ + +
+
+
+ } + } + + + + + + + + + + + + +
diff --git a/src-angular/app/components/songlists/songlist-detail/songlist-detail.component.ts b/src-angular/app/components/songlists/songlist-detail/songlist-detail.component.ts new file mode 100644 index 0000000..a16311f --- /dev/null +++ b/src-angular/app/components/songlists/songlist-detail/songlist-detail.component.ts @@ -0,0 +1,174 @@ +import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { FormControl, Validators } from '@angular/forms' +import { ActivatedRoute, Router } from '@angular/router' + +import { SongList, SongListEntry } from '../../../../../src-shared/interfaces/songlist.interface.js' +import { DownloadService } from '../../../core/services/download.service' +import { SongListService } from '../../../core/services/songlist.service' + +@Component({ + selector: 'app-songlist-detail', + templateUrl: './songlist-detail.component.html', + standalone: false, +}) +export class SongListDetailComponent implements OnInit, OnDestroy { + @ViewChild('editListModal') editListModal: ElementRef + @ViewChild('removeConfirmModal') removeConfirmModal: ElementRef + + list: SongList | null = null + selectedEntries = new Set() + + editName = new FormControl('', { nonNullable: true, validators: [Validators.required] }) + editDescription = new FormControl('', { nonNullable: true }) + + entryToRemove: SongListEntry | null = null + + private listsChangedSub: any + + constructor( + private route: ActivatedRoute, + private router: Router, + private songListService: SongListService, + private downloadService: DownloadService, + private ref: ChangeDetectorRef, + ) { } + + ngOnInit() { + const listId = this.route.snapshot.paramMap.get('id') + if (listId) { + this.list = this.songListService.getList(listId) || null + } + + this.listsChangedSub = this.songListService.listsChanged.subscribe(() => { + if (this.list) { + this.list = this.songListService.getList(this.list.id) || null + this.ref.detectChanges() + } + }) + } + + ngOnDestroy() { + if (this.listsChangedSub) { + this.listsChangedSub.unsubscribe() + } + } + + goBack() { + this.router.navigate(['/lists']) + } + + openEditModal() { + if (this.list) { + this.editName.setValue(this.list.name) + this.editDescription.setValue(this.list.description) + this.editListModal.nativeElement.showModal() + } + } + + saveEdit() { + if (this.list && this.editName.valid) { + const updatedList: SongList = { + ...this.list, + name: this.editName.value.trim(), + description: this.editDescription.value.trim(), + } + this.songListService.updateList(updatedList) + this.editListModal.nativeElement.close() + } + } + + toggleSelection(md5: string) { + if (this.selectedEntries.has(md5)) { + this.selectedEntries.delete(md5) + } else { + this.selectedEntries.add(md5) + } + } + + toggleSelectAll() { + if (!this.list) return + + if (this.selectedEntries.size === this.list.entries.length) { + this.selectedEntries.clear() + } else { + this.selectedEntries = new Set(this.list.entries.map(e => e.md5)) + } + } + + isSelected(md5: string): boolean { + return this.selectedEntries.has(md5) + } + + get allSelected(): boolean { + return this.list ? this.selectedEntries.size === this.list.entries.length && this.list.entries.length > 0 : false + } + + get someSelected(): boolean { + return this.selectedEntries.size > 0 && !this.allSelected + } + + downloadEntry(entry: SongListEntry) { + this.downloadService.addDownload({ + md5: entry.md5, + chartId: entry.chartId, + hasVideoBackground: true, // Default to true since we don't cache this + name: entry.cache.name || 'Unknown', + artist: entry.cache.artist || 'Unknown', + album: entry.cache.album || 'Unknown', + genre: 'Unknown', + year: 'Unknown', + charter: entry.cache.charter || 'Unknown', + } as any) + } + + downloadSelected() { + if (!this.list) return + + for (const entry of this.list.entries) { + if (this.selectedEntries.has(entry.md5)) { + this.downloadEntry(entry) + } + } + this.selectedEntries.clear() + } + + downloadAll() { + if (!this.list) return + + for (const entry of this.list.entries) { + this.downloadEntry(entry) + } + } + + confirmRemove(entry: SongListEntry, event: Event) { + event.stopPropagation() + this.entryToRemove = entry + this.removeConfirmModal.nativeElement.showModal() + } + + removeEntry() { + if (this.list && this.entryToRemove) { + this.songListService.removeCharts(this.list.id, [this.entryToRemove.md5]) + this.selectedEntries.delete(this.entryToRemove.md5) + this.entryToRemove = null + this.removeConfirmModal.nativeElement.close() + } + } + + removeSelected() { + if (this.list && this.selectedEntries.size > 0) { + this.songListService.removeCharts(this.list.id, Array.from(this.selectedEntries)) + this.selectedEntries.clear() + } + } + + async exportList() { + if (this.list) { + await this.songListService.exportList(this.list.id) + } + } + + formatDate(isoDate: string): string { + return new Date(isoDate).toLocaleDateString() + } +} diff --git a/src-angular/app/components/songlists/songlists.component.html b/src-angular/app/components/songlists/songlists.component.html new file mode 100644 index 0000000..5d6d5ee --- /dev/null +++ b/src-angular/app/components/songlists/songlists.component.html @@ -0,0 +1,183 @@ +
+
+
Song Lists
+
+ + +
+
+ + @if (lists.length === 0) { +
+ +

No song lists yet

+

Create a new list or import one to get started

+
+ } @else { +
+ + + + + + + + + + + + + @for (list of lists; track list.id) { + + + + + + + + + } + +
NameDescriptionSongsCreatedModifiedActions
{{ list.name }}{{ list.description || '-' }}{{ list.entries.length }}{{ formatDate(list.createdAt) }}{{ formatDate(list.modifiedAt) }} +
+ + +
+
+
+ } + + + + + + + + + + + + + + + + + + +
diff --git a/src-angular/app/components/songlists/songlists.component.ts b/src-angular/app/components/songlists/songlists.component.ts new file mode 100644 index 0000000..26e9778 --- /dev/null +++ b/src-angular/app/components/songlists/songlists.component.ts @@ -0,0 +1,108 @@ +import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { FormControl, Validators } from '@angular/forms' +import { Router } from '@angular/router' + +import { SongList } from '../../../../src-shared/interfaces/songlist.interface.js' +import { SongListService } from '../../core/services/songlist.service' + +@Component({ + selector: 'app-songlists', + templateUrl: './songlists.component.html', + standalone: false, +}) +export class SongListsComponent implements OnInit, OnDestroy { + @ViewChild('createListModal') createListModal: ElementRef + @ViewChild('importPreviewModal') importPreviewModal: ElementRef + @ViewChild('deleteConfirmModal') deleteConfirmModal: ElementRef + + lists: SongList[] = [] + newListName = new FormControl('', { nonNullable: true, validators: [Validators.required] }) + newListDescription = new FormControl('', { nonNullable: true }) + + importedList: SongList | null = null + listToDelete: SongList | null = null + + private listsChangedSub: any + + constructor( + public songListService: SongListService, + private ref: ChangeDetectorRef, + private router: Router, + ) { } + + ngOnInit() { + this.lists = this.songListService.getLists() + this.listsChangedSub = this.songListService.listsChanged.subscribe(lists => { + this.lists = lists + this.ref.detectChanges() + }) + } + + ngOnDestroy() { + if (this.listsChangedSub) { + this.listsChangedSub.unsubscribe() + } + } + + openCreateModal() { + this.newListName.reset() + this.newListDescription.reset() + this.createListModal.nativeElement.showModal() + } + + async createList() { + if (this.newListName.valid) { + await this.songListService.createList( + this.newListName.value.trim(), + this.newListDescription.value.trim() + ) + this.createListModal.nativeElement.close() + } + } + + viewList(list: SongList) { + this.router.navigate(['/lists', list.id]) + } + + async exportList(list: SongList, event: Event) { + event.stopPropagation() + const success = await this.songListService.exportList(list.id) + if (success) { + // Could show success toast + } + } + + confirmDelete(list: SongList, event: Event) { + event.stopPropagation() + this.listToDelete = list + this.deleteConfirmModal.nativeElement.showModal() + } + + deleteList() { + if (this.listToDelete) { + this.songListService.deleteList(this.listToDelete.id) + this.listToDelete = null + this.deleteConfirmModal.nativeElement.close() + } + } + + async importList() { + const list = await this.songListService.importList() + if (list) { + this.importedList = list + this.importPreviewModal.nativeElement.showModal() + } + } + + saveImportedList() { + if (this.importedList) { + this.songListService.saveImportedList(this.importedList) + this.importedList = null + this.importPreviewModal.nativeElement.close() + } + } + + formatDate(isoDate: string): string { + return new Date(isoDate).toLocaleDateString() + } +} diff --git a/src-angular/app/components/toolbar/toolbar.component.html b/src-angular/app/components/toolbar/toolbar.component.html index b70ab3f..1909d7a 100644 --- a/src-angular/app/components/toolbar/toolbar.component.html +++ b/src-angular/app/components/toolbar/toolbar.component.html @@ -1,6 +1,7 @@