websockets for tracks and ui improvement

This commit is contained in:
raiper34
2024-07-10 23:08:05 +02:00
parent 3455db8d71
commit 2f0e4626f8
22 changed files with 389 additions and 101 deletions
+1
View File
@@ -29,6 +29,7 @@
"@ngneat/elf-requests": "^1.9.2",
"@ngneat/elf-state-history": "^1.4.0",
"bulma": "^1.0.1",
"ngx-socket-io": "^4.7.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
+5
View File
@@ -6,5 +6,10 @@
"pathRewrite": {
"^/api": ""
}
},
"/socket.io/*": {
"target": "http://localhost:3000/socket.io/",
"ws": true,
"logLevel": "debug"
}
}
+2 -23
View File
@@ -22,8 +22,7 @@
<hr>
<div class="box">
<div class="is-flex is-align-items-center">
<p class="subtitle">List</p>&nbsp;
<i class="fa-solid fa-arrows-rotate is-clickable" [class.loading-animation]="(loading$ | async)?.isLoading" (click)="fetchPlaylists()"></i>
<p class="subtitle">Playlists</p>&nbsp;
</div>
<article class="panel is-primary" *ngFor="let playlist of playlists$ | async">
@@ -38,27 +37,7 @@
<a [href]="playlist.spotifyUrl" target="_blank" class="is-color-black"><i class="fa-brands fa-spotify"></i></a>
</p>
<ng-container *ngIf="playlist.collapsed">
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of playlist.tracks">
<div>
<a href="api/track/download/{{track.id}}" class="panel-icon is-color-info" download>
<i class="fa-solid fa-download"></i>
</a>
<span>{{track.artist}} - {{track.song}}</span>&nbsp;
<a [href]="track.spotifyUrl" target="_blank" class="is-color-primary margin-left">
<i class="fa-brands fa-spotify"></i>
</a>&nbsp;
<a [href]="track.youtubeUrl" target="_blank" class="is-color-danger is-color-black">
<i class="fa-brands fa-youtube"></i>
</a>
</div>
<div>
<ng-container [ngSwitch]="track.status">
<span *ngSwitchCase="0" class="tag is-info">New</span>
<span *ngSwitchCase="1" class="tag is-warning">Queued</span>
<span *ngSwitchCase="2" class="tag is-success">Completed</span>
</ng-container>
</div>
</a>
<app-track-list [playlistId]="playlist.id"></app-track-list>
</ng-container>
</article>
</div>
-4
View File
@@ -3,10 +3,6 @@
border-radius: 10px;
}
.panel-block {
padding: 5px 10px;
}
.panel {
margin: 5px 0;
}
+2 -1
View File
@@ -3,11 +3,12 @@ import { RouterOutlet } from '@angular/router';
import {FormsModule} from "@angular/forms";
import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
import {PlaylistService} from "./services/playlist.service";
import {TrackListComponent} from "./components/track-list/track-list.component";
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase],
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase, TrackListComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
+4 -2
View File
@@ -1,13 +1,15 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import {provideHttpClient} from "@angular/common/http";
import {SocketIoModule} from "ngx-socket-io";
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
]
importProvidersFrom(SocketIoModule.forRoot({url: ''}))
],
};
@@ -0,0 +1,24 @@
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of tracks$ | async">
<div>
<span>{{ track.artist }} - {{ track.song }}</span>&nbsp;
<a [href]="track.spotifyUrl" target="_blank" class="is-color-primary margin-left">
<i class="fa-brands fa-spotify"></i>
</a>&nbsp;
<a [href]="track.youtubeUrl" target="_blank" class="is-color-danger is-color-black">
<i class="fa-brands fa-youtube"></i>
</a>&nbsp;
<a *ngIf="track.status === trackStatuses.Completed" href="api/track/download/{{track.id}}" class="is-color-info" download>
<i class="fa-solid fa-download"></i>
</a>
</div>
<div>
<ng-container [ngSwitch]="track.status">
<span *ngSwitchCase="trackStatuses.New" class="tag is-info">New</span>
<span *ngSwitchCase="trackStatuses.Searching" class="tag is-warning">Searching</span>
<span *ngSwitchCase="trackStatuses.Queued" class="tag is-warning">Queued</span>
<span *ngSwitchCase="trackStatuses.Downloading" class="tag is-warning">Downloading</span>
<span *ngSwitchCase="trackStatuses.Completed" class="tag is-success">Completed</span>
<span *ngSwitchCase="trackStatuses.Error" class="tag is-danger" [title]="track.error">Error</span>
</ng-container>
</div>
</a>
@@ -0,0 +1,3 @@
.panel-block {
padding: 5px 10px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TrackListComponent } from './track-list.component';
describe('TrackListComponent', () => {
let component: TrackListComponent;
let fixture: ComponentFixture<TrackListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TrackListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TrackListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,26 @@
import {Component, Input} from '@angular/core';
import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
import {Track, TrackService, TrackStatusEnum} from "../../services/track.service";
import {Observable} from "rxjs";
@Component({
selector: 'app-track-list',
standalone: true,
imports: [CommonModule, NgFor, NgSwitch, NgSwitchCase],
templateUrl: './track-list.component.html',
styleUrl: './track-list.component.scss'
})
export class TrackListComponent {
@Input() set playlistId(value: number) {
console.log(value);
this.tracks$ = this.service.getAllByPlaylist(value);
}
tracks$!: Observable<Track[]>;
trackStatuses = TrackStatusEnum;
constructor(
private readonly service: TrackService,
) {
}
}
+17 -7
View File
@@ -2,16 +2,17 @@ import { Injectable } from '@angular/core';
import {createStore} from "@ngneat/elf";
import {HttpClient} from "@angular/common/http";
import {
addEntities,
selectAllEntities, selectEntities,
setEntities,
UIEntitiesRef,
unionEntities, updateEntities,
unionEntities, updateEntities, upsertEntities,
withEntities,
withUIEntities
} from "@ngneat/elf-entities";
import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests";
import {tap} from "rxjs";
import {map, tap} from "rxjs";
import {TrackService} from "./track.service";
import {PlaylistEntity} from "spooty-be/dist/playlist/playlist.entity";
const STORE_NAME = 'playlist';
const ENDPOINT = '/api/playlist';
@@ -21,7 +22,7 @@ export interface Playlist {
id: number;
name: string;
spotifyUrl: string;
tracks?: any[]; //todo fix it
createdAt: number;
}
export interface PlaylistUi {
@@ -42,12 +43,14 @@ export class PlaylistService {
all$ = this.store.combine({
entities: this.store.pipe(selectAllEntities()),
UIEntities: this.store.pipe(selectEntities({ ref: UIEntitiesRef })),
}).pipe(unionEntities());
}).pipe(unionEntities(), map(data => data.sort((a, b) => b.createdAt - a.createdAt)));
loading$ = this.store.pipe(joinRequestResult([STORE_NAME]));
createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' }));
constructor(private readonly http: HttpClient) {
constructor(private readonly http: HttpClient,
private readonly trackService: TrackService,
) {
}
fetch(): void {
@@ -56,6 +59,7 @@ export class PlaylistService {
setEntities(data),
setEntities(data.map(item => ({id: item.id, collapsed: false})), {ref: UIEntitiesRef})
)),
tap((data: Playlist[]) => data.forEach(playlist => this.trackService.fetch(playlist.id))),
trackRequestResult([STORE_NAME], { skipCache: true }),
).subscribe();
}
@@ -63,7 +67,13 @@ export class PlaylistService {
create(spotifyUrl: string): void {
this.http.post(ENDPOINT, {spotifyUrl}).pipe(
trackRequestResult([CREATE_LOADING], { skipCache: true })
).subscribe();
).subscribe((playlist: Partial<Playlist>) => {
this.store.update(upsertEntities(playlist));
this.store.update(
upsertEntities(playlist),
upsertEntities({id: playlist.id, collapsed: false}, {ref: UIEntitiesRef})
)
});
}
toggleCollapsed(id: number): void {
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { TrackService } from './track.service';
describe('TrackService', () => {
let service: TrackService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(TrackService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import {createStore} from "@ngneat/elf";
import {selectManyByPredicate, upsertEntities, withEntities} from "@ngneat/elf-entities";
import {Socket} from "ngx-socket-io";
import {tap} from "rxjs";
import {trackRequestResult} from "@ngneat/elf-requests";
import {HttpClient} from "@angular/common/http";
const STORE_NAME = 'track';
const ENDPOINT = '/api/track';
export interface Track {
id: number;
artist: string;
song: string;
spotifyUrl: string;
youtubeUrl: string;
status: TrackStatusEnum;
playlistId?: number;
error?: string;
}
export enum TrackStatusEnum {
New,
Searching,
Queued,
Downloading,
Completed,
Error,
}
@Injectable({
providedIn: 'root'
})
export class TrackService {
private store = createStore(
{ name: STORE_NAME },
withEntities<Track>(),
);
getAllByPlaylist(id: number) {
return this.store.pipe(selectManyByPredicate(({playlistId}) => playlistId === id))
}
constructor(
private readonly http: HttpClient,
private readonly socket: Socket,
) {
this.socket.on('track', (track: Track) => this.store.update(upsertEntities(track)));
this.socket.on('trackNew', ({track, playlistId}: {track: Track, playlistId: number}) =>
this.store.update(upsertEntities([{...track, playlistId}]))
);
}
fetch(playlistId: number): void {
this.http.get<Track[]>(`${ENDPOINT}/playlist/${playlistId}`).pipe(
tap((data: Track[]) => this.store.update(upsertEntities(data.map(track => ({...track, playlistId}))))),
trackRequestResult([STORE_NAME], { skipCache: true }),
).subscribe();
}
}