websockets for tracks and ui improvement
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
},
|
||||
"/socket.io/*": {
|
||||
"target": "http://localhost:3000/socket.io/",
|
||||
"ws": true,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
<hr>
|
||||
<div class="box">
|
||||
<div class="is-flex is-align-items-center">
|
||||
<p class="subtitle">List</p>
|
||||
<i class="fa-solid fa-arrows-rotate is-clickable" [class.loading-animation]="(loading$ | async)?.isLoading" (click)="fetchPlaylists()"></i>
|
||||
<p class="subtitle">Playlists</p>
|
||||
</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>
|
||||
<a [href]="track.spotifyUrl" target="_blank" class="is-color-primary margin-left">
|
||||
<i class="fa-brands fa-spotify"></i>
|
||||
</a>
|
||||
<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>
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
<a [href]="track.spotifyUrl" target="_blank" class="is-color-primary margin-left">
|
||||
<i class="fa-brands fa-spotify"></i>
|
||||
</a>
|
||||
<a [href]="track.youtubeUrl" target="_blank" class="is-color-danger is-color-black">
|
||||
<i class="fa-brands fa-youtube"></i>
|
||||
</a>
|
||||
<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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user