From 1b33bb16486344ff7f62df8a737581e79d51cb04 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 15 Dec 2024 01:51:53 -0800 Subject: [PATCH] chore: lift UI state to providers for styling reasons --- src/app/app.component.ts | 9 +- src/app/injectables/metadata.ts | 30 ++++++ src/app/services/fs.ts | 9 ++ src/app/services/music.ts | 32 +++++++ src/app/store/tauri.ts | 7 +- src/app/views/home.component.ts | 162 ++++++++++++-------------------- 6 files changed, 141 insertions(+), 108 deletions(-) create mode 100644 src/app/injectables/metadata.ts create mode 100644 src/app/services/fs.ts create mode 100644 src/app/services/music.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2f97951..db87ee2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, effect } from '@angular/core'; +import {Component, computed, effect} from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { injectDispatch, injectSelector } from "@reduxjs/angular-redux"; @@ -6,10 +6,15 @@ import {Event, listen} from '@tauri-apps/api/event'; import {setPosition, musicSync, Duration} from './store/tauri'; import { AppDispatch, RootState } from './store'; import { UnderTitlebar } from './components/under-titlebar.component'; +import {injectQuery} from "@tanstack/angular-query-experimental"; +import {invoke} from "@tauri-apps/api/core"; +import {SongMetadata} from "./types/song-metadata"; +import {Metadata} from "./injectables/metadata"; @Component({ selector: 'app-root', imports: [CommonModule, RouterOutlet, UnderTitlebar], + providers: [Metadata], template: ` @@ -40,7 +45,7 @@ export class App { // Allows us to get the position of the music player. Probably not the best way to do this. _getPos = effect((onCleanup) => { if (this.tauri().paused) return; - if (this.tauri().currentlyPlaying === null) return; + if (this.tauri().currentlyPlayingPath === null) return; let unlisten = () => { }; diff --git a/src/app/injectables/metadata.ts b/src/app/injectables/metadata.ts new file mode 100644 index 0000000..2cea39b --- /dev/null +++ b/src/app/injectables/metadata.ts @@ -0,0 +1,30 @@ +import {computed, Injectable} from "@angular/core"; +import {injectSelector} from "@reduxjs/angular-redux"; +import {RootState} from "../store"; +import {injectQuery} from "@tanstack/angular-query-experimental"; +import {invoke} from "@tauri-apps/api/core"; +import {SongMetadata} from "../types/song-metadata"; + +// TODO: Remove this in favor of using the `@reduxjs/toolkit` package and RTK Query (not out yet for Angular) +@Injectable({providedIn: null}) +export class Metadata { + path = injectSelector((state: RootState) => state.tauri.currentlyPlayingPath); + + mp3Metadata = injectQuery(() => ({ + queryKey: ['mp3Metadata', this.path()], + queryFn: async () => { + const path = this.path(); + if (!path) return null; + return await invoke("read_metadata", { path }); + } + })); + + mp3CoverImage = computed(() => { + const metadata = this.mp3Metadata.data(); + if (!metadata) return null; + const cover = metadata.visuals[Object.keys(metadata.visuals)[0] as keyof typeof metadata.visuals | never]?.data; + if (!cover) return null; + // url(base64) + return cover; + }); +} diff --git a/src/app/services/fs.ts b/src/app/services/fs.ts new file mode 100644 index 0000000..f4d5879 --- /dev/null +++ b/src/app/services/fs.ts @@ -0,0 +1,9 @@ +import {open} from "@tauri-apps/plugin-dialog"; + +export function pickSong() { + return open({ + multiple: false, + directory: false, + filters: [{ name: 'MP3', extensions: ['mp3'] }, { name: 'FLAC', extensions: ['flac'] }] + }); +} diff --git a/src/app/services/music.ts b/src/app/services/music.ts new file mode 100644 index 0000000..84138e3 --- /dev/null +++ b/src/app/services/music.ts @@ -0,0 +1,32 @@ +import {invoke} from "@tauri-apps/api/core"; + +export function play(fsPath: string) { + return invoke("play", { + path: fsPath + }); +} + +// TODO: Implement this function +export function importSong(fsPath: string) { +} + +export function stop() { + return invoke("stop"); +} + +export function resume() { + return invoke("resume"); +} + +export function pause() { + return invoke("pause"); +} + +export function seek(seconds: number) { + return invoke("seek_to", { + position: { + secs: Math.floor(seconds), + nanos: 0 + } + }); +} diff --git a/src/app/store/tauri.ts b/src/app/store/tauri.ts index 8bc0b72..274dcb4 100644 --- a/src/app/store/tauri.ts +++ b/src/app/store/tauri.ts @@ -27,10 +27,11 @@ const initialState = { speed: 1, paused: true, position: null as Duration | null, - currentlyPlaying: null as string | null, + currentlyPlayingPath: null as string | null, duration: null as Duration | null } +// TODO: Rename to `playbackStateSlice` export const tauriSlice = createSlice({ name: "tauri", initialState, @@ -44,7 +45,7 @@ export const tauriSlice = createSlice({ state.volume = action.payload.volume; state.speed = action.payload.speed; state.paused = action.payload.paused; - state.currentlyPlaying = action.payload["currently_playing_file_path"]; + state.currentlyPlayingPath = action.payload["currently_playing_file_path"]; state.duration = action.payload["currently_playing_duration"]; }); // TODO: Add error handling @@ -53,4 +54,4 @@ export const tauriSlice = createSlice({ export const {setPosition} = tauriSlice.actions; -export default tauriSlice.reducer; \ No newline at end of file +export default tauriSlice.reducer; diff --git a/src/app/views/home.component.ts b/src/app/views/home.component.ts index 2e5bded..2d4177e 100644 --- a/src/app/views/home.component.ts +++ b/src/app/views/home.component.ts @@ -1,130 +1,86 @@ -import { Component, computed } from '@angular/core'; -import { invoke } from "@tauri-apps/api/core"; -import { open } from '@tauri-apps/plugin-dialog'; -import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental'; -import { JsonPipe } from '@angular/common'; -import { injectSelector } from '@reduxjs/angular-redux'; -import { RootState } from '../store'; -import { SongMetadata } from '../types/song-metadata'; +import {Component, computed, inject} from '@angular/core'; +import {JsonPipe} from '@angular/common'; +import {injectSelector} from '@reduxjs/angular-redux'; +import {RootState} from '../store'; +import {pause, play, resume, seek} from "../services/music"; +import {pickSong} from "../services/fs"; +import {Metadata} from "../injectables/metadata"; +import {injectMutation} from "@tanstack/angular-query-experimental"; @Component({ // TODO: Rename from "home" to "now-playing" - selector: 'app-home', - imports: [JsonPipe], template: ` - @if (!openFileMutation.data()) { - - } - @if (openFileMutation.isError()) { -
Error: {{ openFileMutation.error | json }}
- } - @if (mp3Metadata.isError()) { -
Error: {{ mp3Metadata.error | json }}
- } - @if (openFileMutation.isPending()) { -
Loading...
- } - @if (mp3Metadata.isLoading()) { -
Loading song metadata...
- } - - @if (openFileMutation.data() && openFileMutation.isSuccess() && mp3Metadata.isSuccess()) { - - @if (mp3CoverImage()) { -
+ @if (!openFileMutation.data()) { + + } + @if (openFileMutation.isError()) { +
Error: {{ openFileMutation.error | json }}
+ } + @if (stateMetadata.mp3Metadata.isError()) { +
Error: {{ stateMetadata.mp3Metadata.error | json }}
+ } + @if (openFileMutation.isPending()) { +
Loading...
+ } + @if (stateMetadata.mp3Metadata.isLoading()) { +
Loading song metadata...
} -
{{ mp3Metadata.data()?.tags?.TrackTitle }}
-
{{ mp3Metadata.data()?.tags?.AlbumArtist }}
-
{{ mp3Metadata.data()?.tags?.Album }}
- -
- @if (stateMetadata().paused) { - - } @else { - - } - - @if (stateMetadata().currentlyPlaying === null) { - - } @else { - - } -
- -
-
- } + @if (openFileMutation.data() && stateMetadata.mp3Metadata.isSuccess() && openFileMutation.isSuccess()) { + + @if (stateMetadata.mp3CoverImage()) { +
+ } +
{{ stateMetadata.mp3Metadata.data()?.tags?.TrackTitle }}
+
{{ stateMetadata.mp3Metadata.data()?.tags?.AlbumArtist }}
+
{{ stateMetadata.mp3Metadata.data()?.tags?.Album }}
+ +
+ @if (playingMetadata().paused) { + + } @else { + + } + +
+ +
+
+ } `, + selector: 'app-home', + imports: [JsonPipe], }) export class Home { - stateMetadata = injectSelector((state: RootState) => state.tauri); + playingMetadata = injectSelector((state: RootState) => state.tauri); + stateMetadata = inject(Metadata); openFileMutation = injectMutation(() => ({ mutationKey: ['openFile'], mutationFn: async () => { - const result = await open({ - multiple: false, - directory: false, - filters: [{ name: 'MP3', extensions: ['mp3'] }, { name: 'FLAC', extensions: ['flac'] }] - }); - return result; + const path = await pickSong(); + if (!path) { + return; + } + await play(path); } - })); + })) currentTime = computed(() => { - return this.stateMetadata().position?.secs; + return this.playingMetadata().position?.secs; }) totalTime = computed(() => { - return this.stateMetadata().duration?.secs; + return this.playingMetadata().duration?.secs; }) - mp3Metadata = injectQuery(() => ({ - queryKey: ['mp3Metadata', this.openFileMutation.data()], - queryFn: async () => { - const path = this.openFileMutation.data(); - if (!path) return null; - // TODO: Move invoke to a service - return await invoke("read_metadata", { path }); - } - })); - - mp3CoverImage = computed(() => { - const metadata = this.mp3Metadata.data(); - if (!metadata) return null; - // uint8array to base64 - const cover = metadata.visuals[Object.keys(metadata.visuals)[0] as keyof typeof metadata.visuals | never]?.data; - if (!cover) return null; - return cover; - }); - - play() { - invoke("play", { - path: this.openFileMutation.data() - }); - } - - stop() { - invoke("stop"); - } - - resume() { - invoke("resume"); - } - - pause() { - invoke("pause"); - } + resume = resume; + pause = pause; seekFromProgressBar(event: MouseEvent) { const progressBar = event.target as HTMLProgressElement; const value = event.offsetX / progressBar.offsetWidth * parseInt(progressBar.getAttribute('max') || '0'); - invoke("seek_to", { - position: { - secs: Math.floor(value), - nanos: 0 - } - }); + seek(value); } } -- 2.51.2