diff --git a/package-lock.json b/package-lock.json index 1b06e8f..9d45304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,17 @@ "workspaces": [ "spooty-be", "spooty-fe" - ] + ], + "dependencies": { + "@ngneat/elf": "^2.5.1", + "@ngneat/elf-cli-ng": "^1.0.0", + "@ngneat/elf-devtools": "^1.3.0", + "@ngneat/elf-entities": "^5.0.2", + "@ngneat/elf-pagination": "^1.1.0", + "@ngneat/elf-persist-state": "^1.2.1", + "@ngneat/elf-requests": "^1.9.2", + "@ngneat/elf-state-history": "^1.4.0" + } }, "node_modules/@ampproject/remapping": { "version": "2.3.0", @@ -4603,6 +4613,62 @@ "typeorm": "^0.3.0" } }, + "node_modules/@ngneat/elf": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@ngneat/elf/-/elf-2.5.1.tgz", + "integrity": "sha512-13BItNZFgHglTiXuP9XhisNczwQ5QSzH+imAv9nAPsdbCq/3ortqkIYRnlxB8DGPVcuIjLujQ4OcZa+9QWgZtw==", + "peerDependencies": { + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ngneat/elf-cli-ng": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/elf-cli-ng/-/elf-cli-ng-1.0.0.tgz", + "integrity": "sha512-twAvWL5tEtBy/Q65kqoa5aaSE4kG1sFGLZH7QPJwLEBuFayDIzyhT/swJQVLZc+Dv6M4cdHpAyLAg3OfeuWd+Q==" + }, + "node_modules/@ngneat/elf-devtools": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ngneat/elf-devtools/-/elf-devtools-1.3.0.tgz", + "integrity": "sha512-J9+4Vk/S/nKFGnXGZUDYrIx/K8Jfv4TLpzR3voBSOtSeq+c8Q0hdmo8iW7oaK6y5FWFDk0VJktlEh9cjylYxFg==" + }, + "node_modules/@ngneat/elf-entities": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@ngneat/elf-entities/-/elf-entities-5.0.2.tgz", + "integrity": "sha512-G4ag51lvM3tOSgpxVVFYAgsh/bOL5BkNb4Z0VtosaM/CTWTHoNrf8UdvcaeJ3+sP1RS3bmEdZ9xUE8ifnVxssA==", + "peerDependencies": { + "@ngneat/elf": ">=2.5.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ngneat/elf-pagination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ngneat/elf-pagination/-/elf-pagination-1.1.0.tgz", + "integrity": "sha512-MhaeHxRXlLMDg4SIidWS/USr82l8tJ5/QKl8JbOTWDu6YAygVb2pwlXITPFG0Nk2dusRTt5/P2JcGdcxQK6//Q==", + "peerDependencies": { + "@ngneat/elf-entities": "*" + } + }, + "node_modules/@ngneat/elf-persist-state": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ngneat/elf-persist-state/-/elf-persist-state-1.2.1.tgz", + "integrity": "sha512-R+5IRLC35cDT403sSs37UeqqOpHNnCwHl3eicPv/Rc+JJ7Av1bcQClSF2mHC0jE4pkYmEuVghpzUntvmrKCCwA==", + "peerDependencies": { + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ngneat/elf-requests": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@ngneat/elf-requests/-/elf-requests-1.9.2.tgz", + "integrity": "sha512-DSlUVGlltkxcCjH0lWIjSJLz7SAR6ZdGtVwQumip7pXdxeSYmYFnqIwDObR4Cbd3Jbmx0jQs+C9VSbgTDhtdLA==" + }, + "node_modules/@ngneat/elf-state-history": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ngneat/elf-state-history/-/elf-state-history-1.4.0.tgz", + "integrity": "sha512-tjKGVfd96npOyxQUgMSzfeybtRxSlvCcIWCeMTeWOfwYyPO2Z3FjGIzDupDUZdV0MY/6jWOiCxP4aNhj0z6YRQ==", + "peerDependencies": { + "@ngneat/elf-entities": "*" + } + }, "node_modules/@ngtools/webpack": { "version": "18.0.5", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.5.tgz", diff --git a/package.json b/package.json index a7e94e7..621e84e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,16 @@ { "name": "spooty", - "workspaces": ["spooty-be", "spooty-fe"], + "workspaces": [ + "spooty-be", + "spooty-fe" + ], "scripts": { "start:be": "npm run start:dev -w spooty-be", "start:fe": "npm run start -w spooty-fe", "build:be": "npm run build -w spooty-be", "build:fe": "npm run build -w spooty-fe", "build": "npm run build:be npm run build:fe", + "gen:fe": "npm run gen -w spooty-fe", "start": "npm run start:prod -w spooty-be" } -} \ No newline at end of file +} diff --git a/spooty-be/src/playlist/playlist.controller.ts b/spooty-be/src/playlist/playlist.controller.ts index 3c6af0f..e730ca3 100644 --- a/spooty-be/src/playlist/playlist.controller.ts +++ b/spooty-be/src/playlist/playlist.controller.ts @@ -20,10 +20,9 @@ export class PlaylistController { } @Post() - create(@Body() playlist: PlaylistModel): any { - getDetails(playlist.spotifyUrl).then(async details => { + async create(@Body() playlist: PlaylistModel): Promise { + await getDetails(playlist.spotifyUrl).then(async details => { const savedPlaylist = await this.service.create({...playlist, name: details.preview.title}); - console.log(savedPlaylist); details.tracks.forEach(track => { this.trackService.create({ artist: track.artist, @@ -32,6 +31,5 @@ export class PlaylistController { }, savedPlaylist); }); }); - //return {url: url.url}; } } \ No newline at end of file diff --git a/spooty-fe/package.json b/spooty-fe/package.json index dc5bfab..e10577a 100644 --- a/spooty-fe/package.json +++ b/spooty-fe/package.json @@ -6,7 +6,8 @@ "start": "ng serve --proxy-config proxy.conf.json", "build": "ng build", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "gen": "ng generate" }, "private": true, "dependencies": { @@ -19,6 +20,14 @@ "@angular/platform-browser-dynamic": "^18.0.0", "@angular/router": "^18.0.0", "@fortawesome/fontawesome-free": "^6.5.2", + "@ngneat/elf": "^2.5.1", + "@ngneat/elf-cli-ng": "^1.0.0", + "@ngneat/elf-devtools": "^1.3.0", + "@ngneat/elf-entities": "^5.0.2", + "@ngneat/elf-pagination": "^1.1.0", + "@ngneat/elf-persist-state": "^1.2.1", + "@ngneat/elf-requests": "^1.9.2", + "@ngneat/elf-state-history": "^1.4.0", "bulma": "^1.0.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/spooty-fe/src/app/app.component.html b/spooty-fe/src/app/app.component.html index bdcc6a4..0b8ade7 100644 --- a/spooty-fe/src/app/app.component.html +++ b/spooty-fe/src/app/app.component.html @@ -1,9 +1,10 @@

- Spooty + + Spooty

-

Self-hosted spotify downloader

+

Self-hosted spotify downloader

@@ -13,7 +14,7 @@

Download

-
@@ -21,18 +22,25 @@
-

List

- +

List

  +
-
+

- {{playlist.name}} + +   + {{playlist.name}} +

+
- + {{track.artist}} - {{track.song}}  @@ -51,6 +59,7 @@
+
diff --git a/spooty-fe/src/app/app.component.scss b/spooty-fe/src/app/app.component.scss index 2caa453..2e6b8f9 100644 --- a/spooty-fe/src/app/app.component.scss +++ b/spooty-fe/src/app/app.component.scss @@ -1,7 +1,22 @@ .panel-heading { padding: 5px 10px; + border-radius: 10px; } .panel-block { padding: 5px 10px; } + +.panel { + margin: 5px 0; +} + +.loading-animation { + animation: spin 0.3s linear infinite; +} + +@keyframes spin { + 100% { + transform:rotate(360deg); + } +} diff --git a/spooty-fe/src/app/app.component.ts b/spooty-fe/src/app/app.component.ts index 5651f66..5fba698 100644 --- a/spooty-fe/src/app/app.component.ts +++ b/spooty-fe/src/app/app.component.ts @@ -1,34 +1,36 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import {HttpClient} from "@angular/common/http"; import {FormsModule} from "@angular/forms"; -import {NgFor, NgSwitch, NgSwitchCase} from "@angular/common"; +import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common"; +import {PlaylistService} from "./services/playlist.service"; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase], + imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent { url = '' - playlists: any[] = []; - tracks = [ - {code: 'CC', name: 'Aaaa'}, - {code: 'CrC', name: 'Aaaadfdfd'}, - ] + playlists$ = this.playlistService.all$; + loading$ = this.playlistService.loading$; + createLoading$ = this.playlistService.createLoading$; - constructor(private readonly http: HttpClient) { - this.get(); + constructor(private readonly playlistService: PlaylistService) { + this.fetchPlaylists(); + } + + fetchPlaylists(): void { + this.playlistService.fetch(); } download(): void { - this.http.post('/api/playlist', {spotifyUrl: this.url}).subscribe(() => this.get()); + this.playlistService.create(this.url); } - get(): void { - this.http.get('/api/playlist').subscribe(data => this.playlists = data as any); + toggleCollapse(playlistId: number): void { + this.playlistService.toggleCollapsed(playlistId); } } diff --git a/spooty-fe/src/app/services/playlist.service.spec.ts b/spooty-fe/src/app/services/playlist.service.spec.ts new file mode 100644 index 0000000..d8b33c6 --- /dev/null +++ b/spooty-fe/src/app/services/playlist.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PlaylistService } from './playlist.service'; + +describe('PlaylistService', () => { + let service: PlaylistService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PlaylistService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/spooty-fe/src/app/services/playlist.service.ts b/spooty-fe/src/app/services/playlist.service.ts new file mode 100644 index 0000000..df2426b --- /dev/null +++ b/spooty-fe/src/app/services/playlist.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core'; +import {createStore} from "@ngneat/elf"; +import {HttpClient} from "@angular/common/http"; +import { + addEntities, + selectAllEntities, selectEntities, + setEntities, + UIEntitiesRef, + unionEntities, updateEntities, + withEntities, + withUIEntities +} from "@ngneat/elf-entities"; +import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests"; +import {tap} from "rxjs"; + +const STORE_NAME = 'playlist'; +const ENDPOINT = '/api/playlist'; +const CREATE_LOADING = 'CREATE_LOADING'; + +export interface Playlist { + id: number; + name: string; + spotifyUrl: string; + tracks?: any[]; //todo fix it +} + +export interface PlaylistUi { + id: number, + collapsed: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class PlaylistService { + + private store = createStore( + { name: STORE_NAME }, + withEntities(), + withUIEntities() + ); + all$ = this.store.combine({ + entities: this.store.pipe(selectAllEntities()), + UIEntities: this.store.pipe(selectEntities({ ref: UIEntitiesRef })), + }).pipe(unionEntities()); + + loading$ = this.store.pipe(joinRequestResult([STORE_NAME])); + createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' })); + + constructor(private readonly http: HttpClient) { + } + + fetch(): void { + this.http.get(ENDPOINT).pipe( + tap((data: Playlist[]) => this.store.update( + setEntities(data), + setEntities(data.map(item => ({id: item.id, collapsed: false})), {ref: UIEntitiesRef}) + )), + trackRequestResult([STORE_NAME], { skipCache: true }), + ).subscribe(); + } + + create(spotifyUrl: string): void { + this.http.post(ENDPOINT, {spotifyUrl}).pipe( + trackRequestResult([CREATE_LOADING], { skipCache: true }) + ).subscribe(); + } + + toggleCollapsed(id: number): void { + this.store.update(updateEntities(id, old => ({...old, collapsed: !old.collapsed}), { ref: UIEntitiesRef })) + } +} diff --git a/spooty-fe/src/main.ts b/spooty-fe/src/main.ts index 35b00f3..6823534 100644 --- a/spooty-fe/src/main.ts +++ b/spooty-fe/src/main.ts @@ -1,6 +1,8 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; +import { devTools } from '@ngneat/elf-devtools'; bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); +devTools();