mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
Add playlists
This commit is contained in:
@@ -5,12 +5,14 @@ import { BrowseComponent } from './components/browse/browse.component'
|
||||
import { SettingsComponent } from './components/settings/settings.component'
|
||||
import { ToolsComponent } from './components/tools/tools.component'
|
||||
import { TabPersistStrategy } from './core/tab-persist.strategy'
|
||||
import { PlaylistComponent } from './components/playlist/playlist.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'browse', component: BrowseComponent, data: { shouldReuse: true } },
|
||||
{ path: 'library', redirectTo: '/browse' },
|
||||
{ path: 'tools', component: ToolsComponent, data: { shouldReuse: true } },
|
||||
{ path: 'settings', component: SettingsComponent, data: { shouldReuse: true } },
|
||||
{ path: 'playlist', component: PlaylistComponent, data: { shouldReuse: true } },
|
||||
{ path: 'about', redirectTo: '/browse' },
|
||||
{ path: '**', redirectTo: '/browse' },
|
||||
]
|
||||
|
||||
@@ -18,6 +18,9 @@ import { StatusBarComponent } from './components/browse/status-bar/status-bar.co
|
||||
import { SettingsComponent } from './components/settings/settings.component'
|
||||
import { ToolbarComponent } from './components/toolbar/toolbar.component'
|
||||
import { RemoveStyleTagsPipe } from './core/pipes/remove-style-tags.pipe'
|
||||
import { PlaylistTableComponent } from './components/playlist/playlist-table/playlist-table.component'
|
||||
import { PlaylistComponent } from './components/playlist/playlist.component'
|
||||
import { PlaylistBarComponent } from './components/playlist/playlist-bar/playlist-bar.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -35,6 +38,9 @@ import { RemoveStyleTagsPipe } from './core/pipes/remove-style-tags.pipe'
|
||||
DownloadsModalComponent,
|
||||
RemoveStyleTagsPipe,
|
||||
SettingsComponent,
|
||||
PlaylistTableComponent,
|
||||
PlaylistComponent,
|
||||
PlaylistBarComponent,
|
||||
],
|
||||
bootstrap: [AppComponent], imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ChartSidebarComponent implements OnInit {
|
||||
private renderer: Renderer2,
|
||||
private searchService: SearchService,
|
||||
private downloadService: DownloadService,
|
||||
public settingsService: SettingsService
|
||||
public settingsService: SettingsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="border-t border-t-neutral p-2 flex gap-2 items-center max-w-full justify-between">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
{{ (this.playlistService.tracks$ | async)?.length ?? 0 }} Songs
|
||||
|
||||
@if ((this.playlistService.selectedSongs$ | async)!.length > 0) {
|
||||
<button class="btn btn-sm btn-primary" (click)="exportSelected()">Export Selected</button>
|
||||
} @else {
|
||||
<button class="btn btn-sm btn-primary" (click)="exportPlaylist()">Export</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-primary" (click)="importPlaylist()">Import</button>
|
||||
<input type="file" #fileInput accept=".setlist" class="hidden" (change)="onFileSelected($event)" />
|
||||
|
||||
@if ((this.playlistService.selectedSongs$ | async)!.length === 0) {
|
||||
<button type="button" class="btn btn-sm min-w-[108px] hover:btn-error" (click)="this.playlistService.clearPlaylist()">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete all
|
||||
</button>
|
||||
} @else {
|
||||
<button type="button" class="btn btn-sm min-w-[108px] hover:btn-error" (click)="this.playlistService.removeFromPlaylist()">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete selected
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div *ngIf="downloadService.downloadCount > 0" class="max-w-100 text-ellipsis text-nowrap overflow-hidden whitespace-nowrap flex items-center">
|
||||
{{ downloadService.currentDownloadText }}
|
||||
</div>
|
||||
<div *ngIf="downloadService.downloadCount > 0" class="flex gap-2 items-center">
|
||||
<button (click)="downloadsModal.showModal()" class="btn btn-sm btn-ghost w-[30vw] flex gap-2">
|
||||
<div class="flex-1">
|
||||
<progress
|
||||
[attr.value]="downloadService.totalDownloadingPercent"
|
||||
max="100"
|
||||
class="progress h-3 rounded-md"
|
||||
[class.progress-error]="downloadService.anyErrorsExist"></progress>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-info-circle text-xs"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog #downloadsModal class="modal whitespace-normal">
|
||||
<div class="modal-box bg-base-100 text-base-content flex flex-col gap-2 w-9/12 max-w-7xl min-h-0 overflow-y-clip">
|
||||
<app-downloads-modal />
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component, ElementRef, ViewChild } from '@angular/core'
|
||||
import { PlaylistService } from 'src-angular/app/core/services/playlist.service'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-bar',
|
||||
templateUrl: './playlist-bar.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class PlaylistBarComponent {
|
||||
@ViewChild('fileInput', { static: false }) fileInput: ElementRef<HTMLInputElement>
|
||||
|
||||
constructor(public playlistService: PlaylistService, public downloadService: DownloadService) { }
|
||||
|
||||
exportPlaylist() {
|
||||
this.playlistService.storePlaylist()
|
||||
}
|
||||
|
||||
exportSelected() {
|
||||
this.playlistService.storeSelectedSongs()
|
||||
}
|
||||
|
||||
importPlaylist() {
|
||||
this.fileInput.nativeElement.click()
|
||||
}
|
||||
|
||||
onFileSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
|
||||
if (input.files && input.files.length > 0) {
|
||||
const file = input.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const importedTracks = JSON.parse(reader.result as string)
|
||||
if (Array.isArray(importedTracks)) {
|
||||
this.playlistService.downloadPlaylist(importedTracks)
|
||||
} else {
|
||||
console.error('Invalid file format')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing file:', error)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<div class="collapse navbar grid bg-base-100 overflow-visible rounded-none border-t-base-200 border-t">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="input input-bordered pr-14 ng-pristine ng-valid ng-touched"
|
||||
[(ngModel)]="searchTerm"
|
||||
(keyup)="filterSongs()" />
|
||||
</div>
|
||||
<div
|
||||
*ngIf="filteredSongs.length > 0"
|
||||
class="basis-2/3 flex-1 overflow-y-auto scrollbar scrollbar-w-2 scrollbar-h-2 scrollbar-track-base-300 scrollbar-thumb-neutral scrollbar-thumb-rounded-full h-[calc(100vh-164px)]">
|
||||
<table id="resultTable" class="table table-zebra table-pin-rows" [class.table-xs]="settingsService.isCompactTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="collapsing" id="checkboxColumn">
|
||||
<button type="button" class="btn btn-sm min-w-[108px] hover:btn-primary" (click)="toggleSelectAll()">
|
||||
{{ allRowsSelected ? 'Deselect All' : 'Select All' }}
|
||||
</button>
|
||||
</th>
|
||||
<th [ngClass]="sortDirection" class="cursor-pointer" (click)="onColClicked('name')">
|
||||
Name <i *ngIf="sortColumn === 'name'" class="bi bi-caret-{{ sortDirection === 'asc' ? 'down' : 'up' }}-fill"></i>
|
||||
</th>
|
||||
<th [ngClass]="sortDirection" class="cursor-pointer" (click)="onColClicked('artist')">
|
||||
Artist
|
||||
<i *ngIf="sortColumn === 'artist'" class="bi bi-caret-{{ sortDirection === 'asc' ? 'down' : 'up' }}-fill"></i>
|
||||
</th>
|
||||
<th [ngClass]="sortDirection" class="cursor-pointer" (click)="onColClicked('album')">
|
||||
Album <i *ngIf="sortColumn === 'album'" class="bi bi-caret-{{ sortDirection === 'asc' ? 'down' : 'up' }}-fill"></i>
|
||||
</th>
|
||||
<th [ngClass]="sortDirection" class="cursor-pointer" (click)="onColClicked('genre')">
|
||||
Genre <i *ngIf="sortColumn === 'genre'" class="bi bi-caret-{{ sortDirection === 'asc' ? 'down' : 'up' }}-fill"></i>
|
||||
</th>
|
||||
<th [ngClass]="sortDirection" class="cursor-pointer" (click)="onColClicked('year')">
|
||||
Year <i *ngIf="sortColumn === 'year'" class="bi bi-caret-{{ sortDirection === 'asc' ? 'down' : 'up' }}-fill"></i>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let song of filteredSongs; trackBy: trackByFn">
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
#inputBox
|
||||
(change)="onCheckboxChange(song, $event.target!)"
|
||||
[checked]="selectedSongs.includes(song)" />
|
||||
</td>
|
||||
<td>{{ song.name }}</td>
|
||||
<td>{{ song.artist }}</td>
|
||||
<td>{{ song.album || 'Various' }}</td>
|
||||
<td>{{ song.genre || 'Various' }}</td>
|
||||
<td>{{ song.year || 'Various' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="align-center h-[calc(100vh-169.5px)]"
|
||||
*ngIf="songs.length > 1 && filteredSongs.length < 1"
|
||||
style="display: flex; justify-content: center; align-items: center">
|
||||
<p class="text-center" style="font-size: 1.5rem">No songs found!</p>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center justify-center items-center h-[calc(100vh-169.5px)]" *ngIf="songs.length < 1 && filteredSongs.length === 0">
|
||||
<p class="text-center" style="font-size: 1.5rem">No songs added!</p>
|
||||
</div>
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { PlaylistService } from 'src-angular/app/core/services/playlist.service'
|
||||
import { SelectionService } from 'src-angular/app/core/services/selection.service'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
import { SettingsService } from '../../../core/services/settings.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
type SortColumn = 'name' | 'artist' | 'album' | 'genre' | 'year' | 'charter' | 'length' | 'modifiedTime' | null
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-table',
|
||||
templateUrl: './playlist-table.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class PlaylistTableComponent implements OnInit, OnDestroy {
|
||||
songs: ChartData[] = []
|
||||
sortDirection: 'asc' | 'desc' = 'asc'
|
||||
sortColumn: SortColumn = null
|
||||
filteredSongs: ChartData[] = []
|
||||
searchTerm: string = ''
|
||||
allRowsSelected: boolean = false
|
||||
subscriptions: Subscription[] = []
|
||||
selectedSongs: ChartData[] = []
|
||||
|
||||
constructor(
|
||||
public playlistService: PlaylistService,
|
||||
public settingsService: SettingsService,
|
||||
private selectionService: SelectionService
|
||||
) { }
|
||||
|
||||
get allSelected() {
|
||||
return this.selectionService.isAllSelected()
|
||||
}
|
||||
|
||||
set allSelected(value: boolean) {
|
||||
if (value) {
|
||||
this.selectionService.selectAll()
|
||||
} else {
|
||||
this.selectionService.deselectAll()
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscriptions.push(
|
||||
this.playlistService.tracks$
|
||||
.subscribe(tracks => {
|
||||
this.songs = tracks
|
||||
this.filterSongs()
|
||||
})
|
||||
)
|
||||
this.filteredSongs = [...this.songs]
|
||||
this.subscriptions.push(
|
||||
this.playlistService.selectedSongs$.subscribe(songs =>
|
||||
this.selectedSongs = songs
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
filterSongs(): void {
|
||||
const term = this.searchTerm.toLowerCase()
|
||||
this.filteredSongs = this.songs.filter(
|
||||
song =>
|
||||
song.name?.toLowerCase().includes(term) ||
|
||||
song.artist?.toLowerCase().includes(term) ||
|
||||
song.album?.toLowerCase().includes(term) ||
|
||||
song.genre?.toLowerCase().includes(term) ||
|
||||
song.year?.toLowerCase().includes(term) ||
|
||||
song.charter?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
|
||||
trackByFn(index: number): number {
|
||||
return index
|
||||
}
|
||||
|
||||
onColClicked(column: SortColumn) {
|
||||
if (this.filteredSongs.length === 0) { return }
|
||||
|
||||
if (this.sortColumn !== column) {
|
||||
this.sortColumn = column
|
||||
this.sortDirection = 'asc'
|
||||
} else if (this.sortDirection === 'asc') {
|
||||
this.sortDirection = 'desc'
|
||||
} else {
|
||||
this.sortDirection = 'asc'
|
||||
this.sortColumn = null
|
||||
}
|
||||
|
||||
if (this.sortColumn) {
|
||||
this.filteredSongs.sort((a, b) => {
|
||||
const valueA = a[this.sortColumn! as keyof ChartData]
|
||||
const valueB = b[this.sortColumn! as keyof ChartData]
|
||||
|
||||
if (valueA == null && valueB == null) return 0
|
||||
if (valueA == null) return this.sortDirection === 'asc' ? -1 : 1
|
||||
if (valueB == null) return this.sortDirection === 'asc' ? 1 : -1
|
||||
|
||||
if (valueA < valueB) return this.sortDirection === 'asc' ? -1 : 1
|
||||
if (valueA > valueB) return this.sortDirection === 'asc' ? 1 : -1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onCheckboxChange(song: ChartData, target?: EventTarget): void {
|
||||
const input = target as HTMLInputElement
|
||||
if (input.checked) {
|
||||
this.playlistService.addToSelectedSongs(song)
|
||||
} else {
|
||||
this.playlistService.removeFromSelectedSongs(song)
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll(): void {
|
||||
this.allRowsSelected = !this.allRowsSelected
|
||||
|
||||
if (this.allRowsSelected) {
|
||||
this.filteredSongs.forEach(song => this.playlistService.addToSelectedSongs(song))
|
||||
} else {
|
||||
this.playlistService.clearSelectedSongs()
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="overflow-hidden">
|
||||
<app-playlist-table></app-playlist-table>
|
||||
</div>
|
||||
<app-playlist-bar></app-playlist-bar>
|
||||
10
src-angular/app/components/playlist/playlist.component.ts
Normal file
10
src-angular/app/components/playlist/playlist.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist',
|
||||
templateUrl: './playlist.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class PlaylistComponent {
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<div style="-webkit-app-region: no-drag">
|
||||
<button class="btn btn-ghost rounded-none" routerLinkActive="btn-active" routerLink="/browse">Browse</button>
|
||||
<button class="btn btn-ghost rounded-none" routerLinkActive="btn-active" routerLink="/tools">Tools</button>
|
||||
<button class="btn btn-ghost rounded-none" routerLinkActive="btn-active" routerLink="/playlist">Playlist</button>
|
||||
<button class="btn btn-ghost rounded-none flex flex-nowrap" routerLinkActive="btn-active" routerLink="/settings">
|
||||
<i *ngIf="updateAvailable === 'error'" class="bi bi-exclamation-triangle-fill text-warning"></i>
|
||||
Settings
|
||||
|
||||
@@ -6,6 +6,7 @@ import { resolveChartFolderName } from 'src-shared/UtilFunctions'
|
||||
|
||||
import { DownloadProgress } from '../../../../src-shared/interfaces/download.interface'
|
||||
import { SettingsService } from './settings.service'
|
||||
import { PlaylistService } from './playlist.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -15,7 +16,7 @@ export class DownloadService {
|
||||
public downloadCountChanges = new EventEmitter<number>()
|
||||
public downloads: DownloadProgress[] = []
|
||||
|
||||
constructor(zone: NgZone, private settingsService: SettingsService) {
|
||||
constructor(zone: NgZone, private settingsService: SettingsService, private playlistService: PlaylistService) {
|
||||
window.electron.on.downloadQueueUpdate(download => zone.run(() => {
|
||||
const downloadIndex = this.downloads.findIndex(d => d.md5 === download.md5)
|
||||
if (download.type === 'cancel') {
|
||||
@@ -68,6 +69,9 @@ export class DownloadService {
|
||||
if (this.downloads.every(d => d.type === 'done')) { // Reset overall progress bar if it finished
|
||||
this.downloads.forEach(d => d.stale = true)
|
||||
}
|
||||
|
||||
this.playlistService.playlistAdd(chart)
|
||||
|
||||
const newChart = {
|
||||
name: chart.name ?? 'Unknown Name',
|
||||
artist: chart.artist ?? 'Unknown Artist',
|
||||
|
||||
92
src-angular/app/core/services/playlist.service.ts
Normal file
92
src-angular/app/core/services/playlist.service.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Injectable, Injector } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
import { DownloadService } from './download.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PlaylistService {
|
||||
private _tracks = new BehaviorSubject<ChartData[]>([])
|
||||
tracks$ = this._tracks.asObservable()
|
||||
|
||||
private _selectedSongs = new BehaviorSubject<ChartData[]>([])
|
||||
selectedSongs$ = this._selectedSongs.asObservable()
|
||||
|
||||
private _downloadService: DownloadService | null = null
|
||||
|
||||
constructor(private injector: Injector) {
|
||||
const playlist = localStorage.getItem('playlist')
|
||||
if (playlist) {
|
||||
this._tracks.next(JSON.parse(playlist))
|
||||
}
|
||||
}
|
||||
|
||||
private get downloadService(): DownloadService {
|
||||
if (!this._downloadService) {
|
||||
this._downloadService = this.injector.get(DownloadService)
|
||||
}
|
||||
return this._downloadService
|
||||
}
|
||||
|
||||
getPlaylist() {
|
||||
return this._tracks.value
|
||||
}
|
||||
|
||||
playlistAdd(chart: ChartData) {
|
||||
const updatedTracks = [...this._tracks.value, chart]
|
||||
this._tracks.next(updatedTracks)
|
||||
localStorage.setItem('playlist', JSON.stringify(updatedTracks))
|
||||
}
|
||||
|
||||
downloadPlaylist(songs: ChartData[]) {
|
||||
songs.forEach(track => {
|
||||
if (!this._tracks.value.includes(track)) {
|
||||
this.downloadService.addDownload(track)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
storePlaylist() {
|
||||
const fakeLink = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(this._tracks.value)], { type: 'application/json' })
|
||||
fakeLink.href = URL.createObjectURL(file)
|
||||
fakeLink.download = 'songs.setlist'
|
||||
fakeLink.click()
|
||||
}
|
||||
|
||||
storeSelectedSongs() {
|
||||
const fakeLink = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(this._selectedSongs.value)], { type: 'application/json' })
|
||||
fakeLink.href = URL.createObjectURL(file)
|
||||
fakeLink.download = 'selected.setlist'
|
||||
fakeLink.click()
|
||||
}
|
||||
|
||||
addToSelectedSongs(song: ChartData) {
|
||||
const updatedSelectedSongs = [...this._selectedSongs.value, song]
|
||||
this._selectedSongs.next(updatedSelectedSongs)
|
||||
}
|
||||
|
||||
removeFromSelectedSongs(song: ChartData) {
|
||||
const updatedSelectedSongs = this._selectedSongs.value.filter(selectedSong => selectedSong !== song)
|
||||
this._selectedSongs.next(updatedSelectedSongs)
|
||||
}
|
||||
|
||||
clearSelectedSongs() {
|
||||
this._selectedSongs.next([])
|
||||
}
|
||||
|
||||
removeFromPlaylist() {
|
||||
this._selectedSongs.value.forEach(selectedSong => {
|
||||
const updatedTracks = this._tracks.value.filter(track => track !== selectedSong)
|
||||
this._tracks.next(updatedTracks)
|
||||
localStorage.setItem('playlist', JSON.stringify(updatedTracks))
|
||||
})
|
||||
}
|
||||
|
||||
clearPlaylist() {
|
||||
this._tracks.next([])
|
||||
localStorage.removeItem('playlist')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user