diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index ce8cb80..d535bec 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,7 +1,7 @@ import { Routes } from "@angular/router"; -import { CurrentlyPlaying } from "./views/currently-playing.component"; -import { Settings } from "./views/settings.component"; import { AppLayout } from "./layouts/app-layout"; +import { CurrentlyPlaying } from "./views/currently-playing/currently-playing"; +import { Settings } from "./views/settings/settings"; export const routes: Routes = [ { diff --git a/src/app/components/album-art/album-art.html b/src/app/components/album-art/album-art.html new file mode 100644 index 0000000..728115d --- /dev/null +++ b/src/app/components/album-art/album-art.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/src/app/components/album-art/album-art.scss b/src/app/components/album-art/album-art.scss new file mode 100644 index 0000000..1ffbb88 --- /dev/null +++ b/src/app/components/album-art/album-art.scss @@ -0,0 +1,56 @@ + +.album-art-container { + position: relative; + width: fit-content; + --border-size: 10px; + --art-size: 212px; + --border-radius: 16px; + height: var(--art-size); + width: var(--art-size); + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: + 0px -0.5px 1px 0px rgba(255, 255, 255, 0.3) inset, + 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, + 0px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, + 0px 1.5px 4px 0px rgba(0, 0, 0, 0.1) inset; +} + +.album-art-bg { + background-size: var(--art-size); + background-repeat: no-repeat; + background-position: center; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.album-art-border { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 41%, + rgba(255, 255, 255, 0) 57%, + rgba(255, 255, 255, 0.1) 100% + ); + /*background-color: lightblue;*/ +} + +.album-art-fg { + background-size: var(--art-size); + background-repeat: no-repeat; + background-position: center; + position: absolute; + top: var(--border-size); + left: var(--border-size); + right: var(--border-size); + bottom: var(--border-size); + border-radius: calc(var(--border-radius) - var(--border-size)); +} diff --git a/src/app/components/album-art/album-art.ts b/src/app/components/album-art/album-art.ts new file mode 100644 index 0000000..a018da1 --- /dev/null +++ b/src/app/components/album-art/album-art.ts @@ -0,0 +1,10 @@ +import { Component, input } from "@angular/core"; + +@Component({ + selector: "album-art", + templateUrl: "./album-art.html", + styleUrl: "./album-art.scss", +}) +export class AlbumArt { + src = input(); +} diff --git a/src/app/components/controls/play-pause.ts b/src/app/components/controls/play-pause.ts deleted file mode 100644 index 1c60f46..0000000 --- a/src/app/components/controls/play-pause.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Component, inject, input, output } from "@angular/core"; -import { Metadata } from "../../injectables/metadata"; - -@Component({ - selector: "app-play-pause-btn", - template: ` -
- @if (isPaused()) { - - Play - } @else { - - Pause - } -
- `, - styles: [ - ` - :host { - display: inline-flex; - height: 1px; - overflow: visible; - justify-content: center; - align-items: center; - } - - .playPauseBtnContainer { - position: relative; - width: fit-content; - margin: -12px 0; - } - - .playPauseBtn { - height: 72px; - width: 72px; - border-radius: 2.125rem; - border: 1px solid var(--Angled-Stroke, rgba(255, 255, 255, 0.2)); - aspect-ratio: 1; - - background-color: var(--bg-color); - color: var(--color); - - /* Blur + Shadow Big */ - box-shadow: - 0px 7px 10px 0px rgba(0, 0, 0, 0.05), - 0px 40px 80px 0px rgba(0, 0, 0, 0.08); - } - - .playPauseIcon { - pointer-events: none; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 32px; - height: 32px; - } - `, - ], -}) -export class PlayPauseBtn { - metadata = inject(Metadata); - resumeit = output(); - pauseit = output(); - isPaused = input.required(); -} - -@Component({ - selector: "app-play-pause", - imports: [PlayPauseBtn], - template: ` -
- - -
- `, - styles: [ - ` - .buttonsRow { - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - padding-left: 1rem; - padding-right: 1rem; - border-radius: 14px; - } - - .invisibleButton { - background: none; - border: none; - padding: 12px; - } - `, - ], -}) -export class PlayPause { - metadata = inject(Metadata); - resumeit = output(); - pauseit = output(); - volumeClick = output(); - isPaused = input.required(); -} diff --git a/src/app/components/controls/play-pause/play-pause-btn.html b/src/app/components/controls/play-pause/play-pause-btn.html new file mode 100644 index 0000000..2c72be2 --- /dev/null +++ b/src/app/components/controls/play-pause/play-pause-btn.html @@ -0,0 +1,21 @@ +
+ @if (isPaused()) { + + Play + } @else { + + Pause + } +
diff --git a/src/app/components/controls/play-pause/play-pause-btn.scss b/src/app/components/controls/play-pause/play-pause-btn.scss new file mode 100644 index 0000000..db402f9 --- /dev/null +++ b/src/app/components/controls/play-pause/play-pause-btn.scss @@ -0,0 +1,40 @@ + +:host { + display: inline-flex; + height: 1px; + overflow: visible; + justify-content: center; + align-items: center; +} + +.playPauseBtnContainer { + position: relative; + width: fit-content; + margin: -12px 0; +} + +.playPauseBtn { + height: 72px; + width: 72px; + border-radius: 2.125rem; + border: 1px solid var(--Angled-Stroke, rgba(255, 255, 255, 0.2)); + aspect-ratio: 1; + + background-color: var(--bg-color); + color: var(--color); + + /* Blur + Shadow Big */ + box-shadow: + 0px 7px 10px 0px rgba(0, 0, 0, 0.05), + 0px 40px 80px 0px rgba(0, 0, 0, 0.08); +} + +.playPauseIcon { + pointer-events: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 32px; + height: 32px; +} diff --git a/src/app/components/controls/play-pause/play-pause-btn.ts b/src/app/components/controls/play-pause/play-pause-btn.ts new file mode 100644 index 0000000..30a651c --- /dev/null +++ b/src/app/components/controls/play-pause/play-pause-btn.ts @@ -0,0 +1,14 @@ +import { Component, inject, input, output } from "@angular/core"; +import { Metadata } from "../../../injectables/metadata"; + +@Component({ + selector: "play-pause-btn", + templateUrl: "./play-pause-btn.html", + styleUrl: "./play-pause-btn.scss", +}) +export class PlayPauseBtn { + metadata = inject(Metadata); + resumeit = output(); + pauseit = output(); + isPaused = input.required(); +} diff --git a/src/app/components/controls/play-pause/play-pause.html b/src/app/components/controls/play-pause/play-pause.html new file mode 100644 index 0000000..aab59cd --- /dev/null +++ b/src/app/components/controls/play-pause/play-pause.html @@ -0,0 +1,20 @@ +
+ + +
diff --git a/src/app/components/controls/play-pause/play-pause.scss b/src/app/components/controls/play-pause/play-pause.scss new file mode 100644 index 0000000..7ff44b0 --- /dev/null +++ b/src/app/components/controls/play-pause/play-pause.scss @@ -0,0 +1,15 @@ +.buttonsRow { + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + padding-left: 1rem; + padding-right: 1rem; + border-radius: 14px; +} + +.invisibleButton { + background: none; + border: none; + padding: 12px; +} diff --git a/src/app/components/controls/play-pause/play-pause.ts b/src/app/components/controls/play-pause/play-pause.ts new file mode 100644 index 0000000..038bf35 --- /dev/null +++ b/src/app/components/controls/play-pause/play-pause.ts @@ -0,0 +1,17 @@ +import { Component, inject, input, output } from "@angular/core"; +import { Metadata } from "../../../injectables/metadata"; +import { PlayPauseBtn } from "./play-pause-btn"; + +@Component({ + selector: "play-pause", + imports: [PlayPauseBtn], + templateUrl: "./play-pause.html", + styleUrl: "./play-pause.scss", +}) +export class PlayPause { + metadata = inject(Metadata); + resumeit = output(); + pauseit = output(); + volumeClick = output(); + isPaused = input.required(); +} diff --git a/src/app/components/controls/volume-slider.ts b/src/app/components/controls/volume-slider.ts deleted file mode 100644 index 684577a..0000000 --- a/src/app/components/controls/volume-slider.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Component, inject, input, output } from "@angular/core"; -import { Metadata } from "../../injectables/metadata"; -import { setVolume } from "../../services/music"; - -@Component({ - selector: "app-volume-slider", - template: ` -
- - - -
- `, - styles: [ - ` - :host { - display: block; - width: 100%; - } - - .volumeRow { - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - padding: 12px 1rem; - border-radius: 14px; - } - - .iconButton { - background: none; - border: none; - padding: 12px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - } - - .backArrow { - font-size: 28px; - color: rgba(255, 255, 255, 0.8); - line-height: 1; - } - - .volumeSlider { - flex: 1; - height: 8px; - border-radius: 1.5rem; - appearance: none; - background: rgba(0, 0, 0, 0.12); - box-shadow: - 0px -0.5px 1px 0px rgba(255, 255, 255, 0.3) inset, - 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, - 0px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, - 0px 1.5px 4px 0px rgba(0, 0, 0, 0.1) inset; - } - - .volumeSlider::-webkit-slider-thumb { - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.9); - cursor: pointer; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); - margin-top: -4px; - } - - .volumeSlider::-webkit-slider-runnable-track { - height: 8px; - border-radius: 1.5rem; - } - `, - ], -}) -export class VolumeSlider { - metadata = inject(Metadata); - volume = input.required(); - backClick = output(); - - onVolumeChange(event: Event) { - const target = event.target as HTMLInputElement; - const value = parseFloat(target.value); - setVolume(value); - } -} diff --git a/src/app/components/controls/volume-slider/volume-slider.html b/src/app/components/controls/volume-slider/volume-slider.html new file mode 100644 index 0000000..5a7c2ce --- /dev/null +++ b/src/app/components/controls/volume-slider/volume-slider.html @@ -0,0 +1,20 @@ +
+ + + +
diff --git a/src/app/components/controls/volume-slider/volume-slider.scss b/src/app/components/controls/volume-slider/volume-slider.scss new file mode 100644 index 0000000..bf41daf --- /dev/null +++ b/src/app/components/controls/volume-slider/volume-slider.scss @@ -0,0 +1,59 @@ + +:host { + display: block; + width: 100%; +} + +.volumeRow { + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + padding: 12px 1rem; + border-radius: 14px; +} + +.iconButton { + background: none; + border: none; + padding: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.backArrow { + font-size: 28px; + color: rgba(255, 255, 255, 0.8); + line-height: 1; +} + +.volumeSlider { + flex: 1; + height: 8px; + border-radius: 1.5rem; + appearance: none; + background: rgba(0, 0, 0, 0.12); + box-shadow: + 0px -0.5px 1px 0px rgba(255, 255, 255, 0.3) inset, + 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, + 0px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, + 0px 1.5px 4px 0px rgba(0, 0, 0, 0.1) inset; +} + +.volumeSlider::-webkit-slider-thumb { + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.9); + cursor: pointer; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); + margin-top: -4px; +} + +.volumeSlider::-webkit-slider-runnable-track { + height: 8px; + border-radius: 1.5rem; +} diff --git a/src/app/components/controls/volume-slider/volume-slider.ts b/src/app/components/controls/volume-slider/volume-slider.ts new file mode 100644 index 0000000..b0bcb58 --- /dev/null +++ b/src/app/components/controls/volume-slider/volume-slider.ts @@ -0,0 +1,20 @@ +import { Component, inject, input, output } from "@angular/core"; +import { Metadata } from "../../../injectables/metadata"; +import { setVolume } from "../../../services/music"; + +@Component({ + selector: "volume-slider", + templateUrl: "./volume-slider.html", + styleUrl: "./volume-slider.scss", +}) +export class VolumeSlider { + metadata = inject(Metadata); + volume = input.required(); + backClick = output(); + + onVolumeChange(event: Event) { + const target = event.target as HTMLInputElement; + const value = parseFloat(target.value); + void setVolume(value); + } +} diff --git a/src/app/components/currently-playing-material/currently-playing-material.html b/src/app/components/currently-playing-material/currently-playing-material.html new file mode 100644 index 0000000..0a31123 --- /dev/null +++ b/src/app/components/currently-playing-material/currently-playing-material.html @@ -0,0 +1,6 @@ +
+
+ diff --git a/src/app/components/currently-playing-material/currently-playing-material.scss b/src/app/components/currently-playing-material/currently-playing-material.scss new file mode 100644 index 0000000..4d2687f --- /dev/null +++ b/src/app/components/currently-playing-material/currently-playing-material.scss @@ -0,0 +1,25 @@ +.coverImgBg { + position: fixed; + z-index: -2; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + filter: blur(90px); +} + +.scrim { + position: fixed; + z-index: -1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: 50px 50px; + background-repeat: repeat; + background-position: 0% 0%; + background-color: #222222; + mix-blend-mode: overlay; + backdrop-filter: blur(27px); +} diff --git a/src/app/components/currently-playing-material/currently-playing-material.ts b/src/app/components/currently-playing-material/currently-playing-material.ts new file mode 100644 index 0000000..f7fd785 --- /dev/null +++ b/src/app/components/currently-playing-material/currently-playing-material.ts @@ -0,0 +1,11 @@ +import { Component, inject } from "@angular/core"; +import { Metadata } from "../../injectables/metadata"; + +@Component({ + selector: "currently-playing-material", + templateUrl: "./currently-playing-material.html", + styleUrl: "./currently-playing-material.scss", +}) +export class CurrentlyPlayingMaterial { + stateMetadata = inject(Metadata); +} diff --git a/src/app/components/light-glass-outlined.ts b/src/app/components/light-glass-outlined.ts deleted file mode 100644 index af2b987..0000000 --- a/src/app/components/light-glass-outlined.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "light-glass-outlined", - template: ` -
- -
- `, - styles: [ - ` - :host { - box-sizing: border-box; - display: flex; - flex-direction: column; - padding: 2px; - border-radius: 48px; - background: linear-gradient( - 160deg, - rgba(255, 255, 255, 0.2) 0%, - rgba(255, 255, 255, 0) 41%, - rgba(255, 255, 255, 0) 57%, - rgba(255, 255, 255, 0.1) 100% - ); - } - - .innerContentsContainer { - flex-grow: 1; - height: 1px; - /* rgba(255,255,255,0.03) on top of #060606 */ - background: #131316; - border-radius: calc(48px - 2px); - } - `, - ], -}) -export class LightGlassOutlined {} diff --git a/src/app/components/light-glass-outlined/light-glass-outlined.html b/src/app/components/light-glass-outlined/light-glass-outlined.html new file mode 100644 index 0000000..0b807c5 --- /dev/null +++ b/src/app/components/light-glass-outlined/light-glass-outlined.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/components/light-glass-outlined/light-glass-outlined.scss b/src/app/components/light-glass-outlined/light-glass-outlined.scss new file mode 100644 index 0000000..d3b28d8 --- /dev/null +++ b/src/app/components/light-glass-outlined/light-glass-outlined.scss @@ -0,0 +1,22 @@ +:host { + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: 2px; + border-radius: 48px; + background: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 41%, + rgba(255, 255, 255, 0) 57%, + rgba(255, 255, 255, 0.1) 100% + ); +} + +.innerContentsContainer { + flex-grow: 1; + height: 1px; + /* rgba(255,255,255,0.03) on top of #060606 */ + background: #131316; + border-radius: calc(48px - 2px); +} diff --git a/src/app/components/light-glass-outlined/light-glass-outlined.ts b/src/app/components/light-glass-outlined/light-glass-outlined.ts new file mode 100644 index 0000000..3a4791c --- /dev/null +++ b/src/app/components/light-glass-outlined/light-glass-outlined.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "light-glass-outlined", + templateUrl: "./light-glass-outlined.html", + styleUrl: "./light-glass-outlined.scss", +}) +export class LightGlassOutlined {} diff --git a/src/app/components/under-titlebar.component.ts b/src/app/components/under-titlebar.component.ts deleted file mode 100644 index 1016c4f..0000000 --- a/src/app/components/under-titlebar.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component, effect, input } from "@angular/core"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { injectQuery } from "@tanstack/angular-query-experimental"; -import { listen, TauriEvent } from "@tauri-apps/api/event"; -import { getPlatform } from "../utils/platform"; - -@Component({ - selector: "app-under-titlebar-macos", - template: ` -
- -
- `, - styles: [ - ` - .under-titlebar-macos { - display: flex; - flex-direction: row; - justify-content: flex-end; - } - - .logo { - opacity: 0.5; - mix-blend-mode: plus-lighter; - } - `, - ], -}) -export class UnderTitlebarMacos { - theme = input<"light" | "dark">(); -} - -@Component({ - selector: "app-under-titlebar", - imports: [UnderTitlebarMacos], - template: ` - @if (currentPlatform === "macos") { - - } @else { -
- } - `, -}) -export class UnderTitlebar { - currentPlatform = getPlatform(); - - theme = injectQuery(() => ({ - queryKey: ["theme"], - queryFn: async () => (await getCurrentWindow().theme()) ?? "dark", - })); - - _themeChange = effect((onCleanup) => { - let cleanup = () => {}; - listen(TauriEvent.WINDOW_THEME_CHANGED, () => { - this.theme.refetch(); - }).then((c) => (cleanup = c)); - onCleanup(() => cleanup()); - }); -} diff --git a/src/app/components/under-titlebar/under-titlebar-macos.html b/src/app/components/under-titlebar/under-titlebar-macos.html new file mode 100644 index 0000000..2aa9ee4 --- /dev/null +++ b/src/app/components/under-titlebar/under-titlebar-macos.html @@ -0,0 +1,13 @@ +
+ +
diff --git a/src/app/components/under-titlebar/under-titlebar-macos.scss b/src/app/components/under-titlebar/under-titlebar-macos.scss new file mode 100644 index 0000000..5fcff6f --- /dev/null +++ b/src/app/components/under-titlebar/under-titlebar-macos.scss @@ -0,0 +1,10 @@ +.under-titlebar-macos { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.logo { + opacity: 0.5; + mix-blend-mode: plus-lighter; +} diff --git a/src/app/components/under-titlebar/under-titlebar-macos.ts b/src/app/components/under-titlebar/under-titlebar-macos.ts new file mode 100644 index 0000000..f0f82ad --- /dev/null +++ b/src/app/components/under-titlebar/under-titlebar-macos.ts @@ -0,0 +1,10 @@ +import { Component, input } from "@angular/core"; + +@Component({ + selector: "under-titlebar-macos", + templateUrl: "./under-titlebar-macos.html", + styleUrl: "./under-titlebar-macos.scss", +}) +export class UnderTitlebarMacos { + theme = input<"light" | "dark">(); +} diff --git a/src/app/components/under-titlebar/under-titlebar.html b/src/app/components/under-titlebar/under-titlebar.html new file mode 100644 index 0000000..e590ad3 --- /dev/null +++ b/src/app/components/under-titlebar/under-titlebar.html @@ -0,0 +1,5 @@ +@if (currentPlatform === "macos") { + +} @else { +
+} diff --git a/src/app/components/under-titlebar/under-titlebar.ts b/src/app/components/under-titlebar/under-titlebar.ts new file mode 100644 index 0000000..d91cbad --- /dev/null +++ b/src/app/components/under-titlebar/under-titlebar.ts @@ -0,0 +1,28 @@ +import { Component, effect } from "@angular/core"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { injectQuery } from "@tanstack/angular-query-experimental"; +import { listen, TauriEvent } from "@tauri-apps/api/event"; +import { getPlatform } from "../../utils/platform"; +import { UnderTitlebarMacos } from "./under-titlebar-macos"; + +@Component({ + selector: "under-titlebar", + imports: [UnderTitlebarMacos], + templateUrl: "./under-titlebar.html", +}) +export class UnderTitlebar { + currentPlatform = getPlatform(); + + theme = injectQuery(() => ({ + queryKey: ["theme"], + queryFn: async () => (await getCurrentWindow().theme()) ?? "dark", + })); + + _themeChange = effect((onCleanup) => { + let cleanup = () => {}; + listen(TauriEvent.WINDOW_THEME_CHANGED, () => { + this.theme.refetch(); + }).then((c) => (cleanup = c)); + onCleanup(() => cleanup()); + }); +} diff --git a/src/app/layouts/app-layout.ts b/src/app/layouts/app-layout.ts index 8d865f0..0c6de6e 100644 --- a/src/app/layouts/app-layout.ts +++ b/src/app/layouts/app-layout.ts @@ -1,12 +1,12 @@ import { Component, ViewEncapsulation } from "@angular/core"; -import { UnderTitlebar } from "../components/under-titlebar.component"; import { RouterOutlet } from "@angular/router"; +import { UnderTitlebar } from "../components/under-titlebar/under-titlebar"; @Component({ imports: [UnderTitlebar, RouterOutlet], selector: "app-layout", template: ` - +
diff --git a/src/app/views/currently-playing.component.ts b/src/app/views/currently-playing.component.ts deleted file mode 100644 index 2bcf8d2..0000000 --- a/src/app/views/currently-playing.component.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { Component, computed, inject, input, signal } from "@angular/core"; -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"; -import { PlayPause } from "../components/controls/play-pause"; -import { VolumeSlider } from "../components/controls/volume-slider"; -import { zeroPad } from "../utils/strings"; -import { ControlType } from "../types/currently-playing"; - -@Component({ - selector: "currently-playing-material", - template: ` -
-
- - `, - styles: [ - ` - .coverImgBg { - position: fixed; - z-index: -2; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - filter: blur(90px); - } - - .scrim { - position: fixed; - z-index: -1; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: 50px 50px; - background-repeat: repeat; - background-position: 0% 0%; - background-color: #222222; - mix-blend-mode: overlay; - backdrop-filter: blur(27px); - } - `, - ], -}) -class CurrentlyPlayingMaterial { - stateMetadata = inject(Metadata); -} - -@Component({ - selector: "album-art", - template: ` -
-
-
-
-
- `, - styles: [ - ` - .album-art-container { - position: relative; - width: fit-content; - --border-size: 10px; - --art-size: 212px; - --border-radius: 16px; - height: var(--art-size); - width: var(--art-size); - border-radius: var(--border-radius); - overflow: hidden; - box-shadow: - 0px -0.5px 1px 0px rgba(255, 255, 255, 0.3) inset, - 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, - 0px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, - 0px 1.5px 4px 0px rgba(0, 0, 0, 0.1) inset; - } - - .album-art-bg { - background-size: var(--art-size); - background-repeat: no-repeat; - background-position: center; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - } - - .album-art-border { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: linear-gradient( - 180deg, - rgba(255, 255, 255, 0.2) 0%, - rgba(255, 255, 255, 0) 41%, - rgba(255, 255, 255, 0) 57%, - rgba(255, 255, 255, 0.1) 100% - ); - /*background-color: lightblue;*/ - } - - .album-art-fg { - background-size: var(--art-size); - background-repeat: no-repeat; - background-position: center; - position: absolute; - top: var(--border-size); - left: var(--border-size); - right: var(--border-size); - bottom: var(--border-size); - border-radius: calc(var(--border-radius) - var(--border-size)); - } - `, - ], -}) -export class AlbumArt { - src = input(); -} - -@Component({ - selector: "app-now-playing", - imports: [CurrentlyPlayingMaterial, AlbumArt, PlayPause, VolumeSlider], - template: ` - -
- @if (!openFileMutation.data()) { - - } - - @if ( - openFileMutation.data() && - stateMetadata.metadata.isSuccess() && - openFileMutation.isSuccess() - ) { - -
-
-

- {{ - stateMetadata.metadata.data()?.tags?.TrackTitle ?? - "Unknown Title" - }} -

-

- {{ - stateMetadata.metadata.data()?.tags?.Artist ?? - "Unknown Artist" - }} -

-

- {{ - stateMetadata.metadata.data()?.tags?.Album ?? "Unknown Album" - }} -

-
- -
-
-

{{ currentTimeFormatted() }}

-

{{ totalTimeFormatted() }}

-
- - -
- -
- @if (controls() === 'volume-change') { - - } @else { - - } -
-
- } -
-
- `, - styles: [ - ` - .container { - padding: 24px; - display: flex; - flex-direction: row; - overflow: hidden; - justify-content: center; - align-items: center; - /* 32 because of top area */ - height: calc(100vh - 32px); - box-sizing: border-box; - } - - .infoContainer { - display: flex; - flex-direction: column; - gap: 20px; - justify-content: center; - align-items: center; - padding: 0 32px; - text-align: center; - width: 1px; - flex-grow: 1; - } - - .textContainer { - display: flex; - flex-direction: column; - gap: 8px; - justify-content: center; - align-items: center; - width: 100%; - } - - .title { - font-size: 24px; - font-weight: bold; - margin: 0; - color: white; - width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .artist { - font-size: 16px; - margin: 0; - color: white; - opacity: 0.8; - } - - .album { - font-size: 16px; - margin: 0; - color: white; - opacity: 0.8; - } - - .progressContainer { - width: 100%; - padding: 8px 0; - flex-direction: column; - gap: 8px; - } - - .progressTextContainer { - display: flex; - justify-content: space-between; - } - - .progressText { - font-size: 14px; - color: white; - opacity: 0.5; - margin: 0; - } - - .progressBar { - border-radius: 1.5rem; - width: 100%; - box-shadow: - 0px -0.5px 1px 0px rgba(255, 255, 255, 0.3) inset, - 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, - 0px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, - 0px 1.5px 4px 0px rgba(0, 0, 0, 0.1) inset; - background-blend-mode: plus-darker; - height: 0.5rem; - appearance: none; - background: rgba(0, 0, 0, 0.12); - cursor: pointer; - } - - .progressBar::-webkit-slider-thumb { - appearance: none; - width: 0; - height: 0; - } - - .progressBar::-webkit-slider-runnable-track { - height: 0.5rem; - border-radius: 1.5rem; - background: linear-gradient( - to right, - rgba(255, 255, 255, 0.8) 0%, - rgba(255, 255, 255, 0.8) var(--progress, 0%), - transparent var(--progress, 0%), - transparent 100% - ); - } - `, - ], -}) -export class CurrentlyPlaying { - playingMetadata = injectSelector((state: RootState) => state.tauri); - stateMetadata = inject(Metadata); - - controls = signal("play-pause"); - - openFileMutation = injectMutation(() => ({ - mutationKey: ["openFile"], - mutationFn: async () => { - const path = await pickSong(); - if (!path) { - return; - } - await play(path); - return true; - }, - })); - - totalSecs = computed(() => { - return this.playingMetadata().duration?.secs; - }); - - currentSecs = computed(() => { - return this.playingMetadata().position?.secs; - }); - - progressPercent = computed(() => { - const current = this.currentSecs() ?? 0; - const total = this.totalSecs() ?? 0; - if (total === 0) return 0; - return (current / total) * 100; - }); - - totalTime = computed(() => { - // Format as `hh:mm:ss` but only show minutes and hours if they are non-zero - const duration = this.playingMetadata().duration?.secs; - if (!duration) { - return { - hours: 0, - minutes: 0, - seconds: 0, - }; - } - // Format - const hours = Math.floor(duration / 3600); - const minutes = Math.floor(duration / 60) % 60; - const seconds = duration % 60; - return { - hours, - minutes, - seconds, - }; - }); - - totalTimeFormatted = computed(() => { - const duration = this.totalTime(); - return `${duration.hours ? duration.hours + ":" : ""}${zeroPad(duration.minutes, 2)}:${zeroPad(duration.seconds, 2)}`; - }); - - currentTimeFormatted = computed(() => { - // Format as `hh:mm:ss` but only if `totalTime` has an hour or minute value - const totalDuration = this.totalTime(); - const duration = this.playingMetadata().position?.secs; - if (!duration) { - return "0:00"; - } - // Format - const hours = Math.floor(duration / 3600); - const minutes = Math.floor(duration / 60) % 60; - const seconds = duration % 60; - const showHours = totalDuration.hours > 0 || hours > 0; - const showMinutes = totalDuration.minutes > 0 || minutes > 0; - return `${showHours ? hours + ":" : ""}${showMinutes ? zeroPad(minutes, 2) : ""}:${zeroPad(seconds, 2)}`; - }); - - resume = resume; - pause = pause; - - seekFromInput(event: Event) { - const input = event.target as HTMLInputElement; - const value = parseFloat(input.value); - seek(value); - } -} diff --git a/src/app/views/currently-playing/currently-playing.html b/src/app/views/currently-playing/currently-playing.html new file mode 100644 index 0000000..2cc4300 --- /dev/null +++ b/src/app/views/currently-playing/currently-playing.html @@ -0,0 +1,70 @@ + +
+ @if (!openFileMutation.data()) { + + } + + @if ( + openFileMutation.data() && + stateMetadata.metadata.isSuccess() && + openFileMutation.isSuccess() + ) { + +
+
+

+ {{ + stateMetadata.metadata.data()?.tags?.TrackTitle ?? + "Unknown Title" + }} +

+

+ {{ + stateMetadata.metadata.data()?.tags?.Artist ?? + "Unknown Artist" + }} +

+

+ {{ + stateMetadata.metadata.data()?.tags?.Album ?? "Unknown Album" + }} +

+
+ +
+
+

{{ currentTimeFormatted() }}

+

{{ totalTimeFormatted() }}

+
+ + +
+ +
+ @if (controls() === "volume-change") { + + } @else { + + } +
+
+ } +
+
diff --git a/src/app/views/currently-playing/currently-playing.scss b/src/app/views/currently-playing/currently-playing.scss new file mode 100644 index 0000000..1b42e34 --- /dev/null +++ b/src/app/views/currently-playing/currently-playing.scss @@ -0,0 +1,110 @@ + +.container { + padding: 24px; + display: flex; + flex-direction: row; + overflow: hidden; + justify-content: center; + align-items: center; + /* 32 because of top area */ + height: calc(100vh - 32px); + box-sizing: border-box; +} + +.infoContainer { + display: flex; + flex-direction: column; + gap: 20px; + justify-content: center; + align-items: center; + padding: 0 32px; + text-align: center; + width: 1px; + flex-grow: 1; +} + +.textContainer { + display: flex; + flex-direction: column; + gap: 8px; + justify-content: center; + align-items: center; + width: 100%; +} + +.title { + font-size: 24px; + font-weight: bold; + margin: 0; + color: white; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.artist { + font-size: 16px; + margin: 0; + color: white; + opacity: 0.8; +} + +.album { + font-size: 16px; + margin: 0; + color: white; + opacity: 0.8; +} + +.progressContainer { + width: 100%; + padding: 8px 0; + flex-direction: column; + gap: 8px; +} + +.progressTextContainer { + display: flex; + justify-content: space-between; +} + +.progressText { + font-size: 14px; + color: white; + opacity: 0.5; + margin: 0; +} + +.progressBar { + border-radius: 1.5rem; + width: 100%; + box-shadow: + 0px -0.5px 1px 0px rgba(255, 255, 255, 0.3) inset, + 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, + 0px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, + 0px 1.5px 4px 0px rgba(0, 0, 0, 0.1) inset; + background-blend-mode: plus-darker; + height: 0.5rem; + appearance: none; + background: rgba(0, 0, 0, 0.12); + cursor: pointer; +} + +.progressBar::-webkit-slider-thumb { + appearance: none; + width: 0; + height: 0; +} + +.progressBar::-webkit-slider-runnable-track { + height: 0.5rem; + border-radius: 1.5rem; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0.8) 0%, + rgba(255, 255, 255, 0.8) var(--progress, 0%), + transparent var(--progress, 0%), + transparent 100% + ); +} diff --git a/src/app/views/currently-playing/currently-playing.ts b/src/app/views/currently-playing/currently-playing.ts new file mode 100644 index 0000000..c2fccc9 --- /dev/null +++ b/src/app/views/currently-playing/currently-playing.ts @@ -0,0 +1,104 @@ +import { Component, computed, inject, input, signal } from "@angular/core"; +import { injectSelector } from "@reduxjs/angular-redux"; +import { CurrentlyPlayingMaterial } from "../../components/currently-playing-material/currently-playing-material"; +import { AlbumArt } from "../../components/album-art/album-art"; +import { PlayPause } from "../../components/controls/play-pause/play-pause"; +import { VolumeSlider } from "../../components/controls/volume-slider/volume-slider"; +import { Metadata } from "../../injectables/metadata"; +import { ControlType } from "../../types/currently-playing"; +import { RootState } from "../../store"; +import { injectMutation } from "@tanstack/angular-query-experimental"; +import { pickSong } from "../../services/fs"; +import { pause, play, resume, seek } from "../../services/music"; +import { zeroPad } from "../../utils/strings"; + +@Component({ + selector: "now-playing", + imports: [CurrentlyPlayingMaterial, AlbumArt, PlayPause, VolumeSlider], + templateUrl: "./currently-playing.html", + styleUrl: "currently-playing.scss", +}) +export class CurrentlyPlaying { + playingMetadata = injectSelector((state: RootState) => state.tauri); + stateMetadata = inject(Metadata); + + controls = signal("play-pause"); + + openFileMutation = injectMutation(() => ({ + mutationKey: ["openFile"], + mutationFn: async () => { + const path = await pickSong(); + if (!path) { + return; + } + await play(path); + return true; + }, + })); + + totalSecs = computed(() => { + return this.playingMetadata().duration?.secs; + }); + + currentSecs = computed(() => { + return this.playingMetadata().position?.secs; + }); + + progressPercent = computed(() => { + const current = this.currentSecs() ?? 0; + const total = this.totalSecs() ?? 0; + if (total === 0) return 0; + return (current / total) * 100; + }); + + totalTime = computed(() => { + // Format as `hh:mm:ss` but only show minutes and hours if they are non-zero + const duration = this.playingMetadata().duration?.secs; + if (!duration) { + return { + hours: 0, + minutes: 0, + seconds: 0, + }; + } + // Format + const hours = Math.floor(duration / 3600); + const minutes = Math.floor(duration / 60) % 60; + const seconds = duration % 60; + return { + hours, + minutes, + seconds, + }; + }); + + totalTimeFormatted = computed(() => { + const duration = this.totalTime(); + return `${duration.hours ? duration.hours + ":" : ""}${zeroPad(duration.minutes, 2)}:${zeroPad(duration.seconds, 2)}`; + }); + + currentTimeFormatted = computed(() => { + // Format as `hh:mm:ss` but only if `totalTime` has an hour or minute value + const totalDuration = this.totalTime(); + const duration = this.playingMetadata().position?.secs; + if (!duration) { + return "0:00"; + } + // Format + const hours = Math.floor(duration / 3600); + const minutes = Math.floor(duration / 60) % 60; + const seconds = duration % 60; + const showHours = totalDuration.hours > 0 || hours > 0; + const showMinutes = totalDuration.minutes > 0 || minutes > 0; + return `${showHours ? hours + ":" : ""}${showMinutes ? zeroPad(minutes, 2) : ""}:${zeroPad(seconds, 2)}`; + }); + + resume = resume; + pause = pause; + + seekFromInput(event: Event) { + const input = event.target as HTMLInputElement; + const value = parseFloat(input.value); + void seek(value); + } +} diff --git a/src/app/views/settings.component.ts b/src/app/views/settings.component.ts deleted file mode 100644 index 6ab083e..0000000 --- a/src/app/views/settings.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Component, effect } from "@angular/core"; -import { LightGlassOutlined } from "../components/light-glass-outlined"; -import { setBodyBg } from "../utils/styling"; - -@Component({ - selector: "app-settings", - template: ` -
- -
-

Settings

-
-
-
-
- `, - styles: [ - ` - :host { - display: contents; - } - - .container { - height: 1px; - flex-grow: 1; - display: flex; - flex-direction: column; - box-sizing: border-box; - padding: 2rem; - } - - .container > light-glass-outlined { - flex-grow: 1; - height: 1px; - } - - .innerContents { - padding: 2rem; - } - - .title { - margin: 0; - font-family: "Roboto Flex", sans-serif; - font-variation-settings: - "wght" 500, - "wdth" 151, - "GRAD" 150, - "XOPQ" 96; - } - - .hr { - all: unset; - display: block; - height: 2px; - width: 100%; - background: rgba(255, 255, 255, 0.08); - margin-top: 20px; - margin-bottom: 20px; - } - - p { - margin: 0; - } - `, - ], - imports: [LightGlassOutlined], -}) -export class Settings { - _ = effect(() => { - setBodyBg("#060606"); - }); -} diff --git a/src/app/views/settings/settings.html b/src/app/views/settings/settings.html new file mode 100644 index 0000000..c0448c5 --- /dev/null +++ b/src/app/views/settings/settings.html @@ -0,0 +1,8 @@ +
+ +
+

Settings

+
+
+
+
diff --git a/src/app/views/settings/settings.scss b/src/app/views/settings/settings.scss new file mode 100644 index 0000000..d63fe92 --- /dev/null +++ b/src/app/views/settings/settings.scss @@ -0,0 +1,41 @@ +:host { + display: contents; +} + +.container { + height: 1px; + flex-grow: 1; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding: 2rem; +} + +.container > light-glass-outlined { + flex-grow: 1; + height: 1px; +} + +.innerContents { + padding: 2rem; +} + +.title { + margin: 0; + font-family: "Roboto Flex", sans-serif; + font-variation-settings: + "wght" 500, + "wdth" 151, + "GRAD" 150, + "XOPQ" 96; +} + +.hr { + all: unset; + display: block; + height: 2px; + width: 100%; + background: rgba(255, 255, 255, 0.08); + margin-top: 20px; + margin-bottom: 20px; +} diff --git a/src/app/views/settings/settings.ts b/src/app/views/settings/settings.ts new file mode 100644 index 0000000..0c0aa47 --- /dev/null +++ b/src/app/views/settings/settings.ts @@ -0,0 +1,15 @@ +import { Component, effect } from "@angular/core"; +import { LightGlassOutlined } from "../../components/light-glass-outlined/light-glass-outlined"; +import { setBodyBg } from "../../utils/styling"; + +@Component({ + selector: "settings-view", + templateUrl: "./settings.html", + styleUrl: "./settings.scss", + imports: [LightGlassOutlined], +}) +export class Settings { + _ = effect(() => { + setBodyBg("#060606"); + }); +}