From 728c73388031f3c1814567d5ae43e3f5dc6b4c00 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sun, 23 Nov 2025 21:58:35 +0100 Subject: [PATCH] Add volume control to player --- src/API/API.ts | 12 +++++ src/API/IAPI.ts | 1 + src/constants.ts | 1 + src/main.ts | 22 +++++++++ src/store/index.ts | 1 + src/types/IPodNotesSettings.ts | 1 + src/ui/PodcastView/EpisodePlayer.svelte | 61 +++++++++++++++++++------ src/ui/obsidian/Slider.svelte | 2 +- src/ui/settings/PodNotesSettingsTab.ts | 17 +++++++ 9 files changed, 103 insertions(+), 15 deletions(-) diff --git a/src/API/API.ts b/src/API/API.ts index ef05535..4f4bbc4 100644 --- a/src/API/API.ts +++ b/src/API/API.ts @@ -8,11 +8,15 @@ import { duration, isPaused, plugin, + volume as volumeStore, } from "src/store"; import { get } from "svelte/store"; import encodePodnotesURI from "src/utility/encodePodnotesURI"; import { isLocalFile } from "src/utility/isLocalFile"; +const clampVolume = (value: number): number => + Math.min(1, Math.max(0, value)); + export class API implements IAPI { public get podcast(): Episode { return get(currentEpisode); @@ -34,6 +38,14 @@ export class API implements IAPI { return !get(isPaused); } + public get volume(): number { + return get(volumeStore); + } + + public set volume(value: number) { + volumeStore.set(clampVolume(value)); + } + /** * Gets the current time in the given moment format. * @param format Moment format. diff --git a/src/API/IAPI.ts b/src/API/IAPI.ts index db76d96..09760fe 100644 --- a/src/API/IAPI.ts +++ b/src/API/IAPI.ts @@ -5,6 +5,7 @@ export interface IAPI { readonly isPlaying: boolean; readonly length: number; currentTime: number; + volume: number; getPodcastTimeFormatted(format: string, linkify?: boolean): string; diff --git a/src/constants.ts b/src/constants.ts index b0532e7..d98b49f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,6 +33,7 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = { savedFeeds: {}, podNotes: {}, defaultPlaybackRate: 1, + defaultVolume: 1, playedEpisodes: {}, favorites: { ...FAVORITES_SETTINGS, diff --git a/src/main.ts b/src/main.ts index 924bd10..c121f19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { playlists, queue, savedFeeds, + volume, } from "src/store"; import { Plugin, type WorkspaceLeaf } from "obsidian"; import { API } from "src/API/API"; @@ -40,6 +41,7 @@ import getContextMenuHandler from "./getContextMenuHandler"; import getUniversalPodcastLink from "./getUniversalPodcastLink"; import type { IconType } from "./types/IconType"; import { TranscriptionService } from "./services/TranscriptionService"; +import type { Unsubscriber } from "svelte/store"; export default class PodNotes extends Plugin implements IPodNotes { public api!: IAPI; @@ -65,6 +67,7 @@ export default class PodNotes extends Plugin implements IPodNotes { [podcastName: string]: DownloadedEpisode[]; }>; private transcriptionService?: TranscriptionService; + private volumeUnsubscribe?: Unsubscriber; private maxLayoutReadyAttempts = 10; private layoutReadyAttempts = 0; @@ -84,6 +87,9 @@ export default class PodNotes extends Plugin implements IPodNotes { if (this.settings.currentEpisode) { currentEpisode.set(this.settings.currentEpisode); } + volume.set( + Math.min(1, Math.max(0, this.settings.defaultVolume ?? 1)), + ); this.playedEpisodeController = new EpisodeStatusController( playedEpisodes, @@ -104,6 +110,21 @@ export default class PodNotes extends Plugin implements IPodNotes { ).on(); this.api = new API(); + this.volumeUnsubscribe = volume.subscribe((value) => { + const clamped = Math.min(1, Math.max(0, value)); + + if (clamped !== value) { + volume.set(clamped); + return; + } + + if (clamped === this.settings.defaultVolume) { + return; + } + + this.settings.defaultVolume = clamped; + void this.saveSettings(); + }); this.addCommand({ id: "podnotes-show-leaf", @@ -337,6 +358,7 @@ export default class PodNotes extends Plugin implements IPodNotes { this.localFilesController?.off(); this.downloadedEpisodesController?.off(); this.currentEpisodeController?.off(); + this.volumeUnsubscribe?.(); } async loadSettings() { diff --git a/src/store/index.ts b/src/store/index.ts index 2acee09..05281a2 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -12,6 +12,7 @@ import type { LocalEpisode } from "src/types/LocalEpisode"; export const plugin = writable(); export const currentTime = writable(0); export const duration = writable(0); +export const volume = writable(1); export const currentEpisode = (() => { const store = writable(); diff --git a/src/types/IPodNotesSettings.ts b/src/types/IPodNotesSettings.ts index 611556b..db107b1 100644 --- a/src/types/IPodNotesSettings.ts +++ b/src/types/IPodNotesSettings.ts @@ -9,6 +9,7 @@ export interface IPodNotesSettings { savedFeeds: { [podcastName: string]: PodcastFeed }; podNotes: { [episodeName: string]: PodNote }; defaultPlaybackRate: number; + defaultVolume: number; playedEpisodes: { [episodeName: string]: PlayedEpisode }; skipBackwardLength: number; skipForwardLength: number; diff --git a/src/ui/PodcastView/EpisodePlayer.svelte b/src/ui/PodcastView/EpisodePlayer.svelte index 63b021a..82432b7 100644 --- a/src/ui/PodcastView/EpisodePlayer.svelte +++ b/src/ui/PodcastView/EpisodePlayer.svelte @@ -5,6 +5,7 @@ currentEpisode, isPaused, plugin, + volume, playedEpisodes, queue, playlists, @@ -36,9 +37,11 @@ const offBinding = new CircumentForcedTwoWayBinding(); //#endregion + const clampVolume = (value: number): number => Math.min(1, Math.max(0, value)); let isHoveringArtwork: boolean = false; let isLoading: boolean = true; + let playerVolume: number = 1; function togglePlayback() { isPaused.update((value) => !value); @@ -84,6 +87,12 @@ offBinding.playbackRate = event.detail.value; } + function onVolumeChange(event: CustomEvent<{ value: number }>) { + const newVolume = clampVolume(event.detail.value); + + volume.set(newVolume); + } + function onMetadataLoaded() { isLoading = false; @@ -125,10 +134,15 @@ srcPromise = getSrc($currentEpisode); }); + const unsubVolume = volume.subscribe((value) => { + playerVolume = clampVolume(value); + }); + return () => { unsub(); unsubDownloadedSource(); unsubCurrentEpisode(); + unsubVolume(); }; }); @@ -223,6 +237,7 @@ bind:currentTime={playerTime} bind:paused={$isPaused} bind:playbackRate={offBinding._playbackRate} + bind:volume={playerVolume} on:ended={onEpisodeEnded} on:loadedmetadata={onMetadataLoaded} on:play|preventDefault @@ -259,18 +274,29 @@ on:click={$plugin.api.skipForward.bind($plugin.api)} style={{ margin: "0", - cursor: "pointer", - }} - /> - + cursor: "pointer", + }} + /> + -
- {offBinding.playbackRate}x - +
+
+ Volume: {Math.round(playerVolume * 100)}% + +
+ +
+ {offBinding.playbackRate}x + +
diff --git a/src/ui/obsidian/Slider.svelte b/src/ui/obsidian/Slider.svelte index 55aa9e2..d485242 100644 --- a/src/ui/obsidian/Slider.svelte +++ b/src/ui/obsidian/Slider.svelte @@ -28,7 +28,7 @@ }); function updateSliderAttributes(sldr: SliderComponent) { - if (value) sldr.setValue(value); + if (value !== undefined) sldr.setValue(value); if (limits) { if (limits.length === 2) { sldr.setLimits(limits[0], limits[1], 1); diff --git a/src/ui/settings/PodNotesSettingsTab.ts b/src/ui/settings/PodNotesSettingsTab.ts index 4ef96c7..9faa5ed 100644 --- a/src/ui/settings/PodNotesSettingsTab.ts +++ b/src/ui/settings/PodNotesSettingsTab.ts @@ -66,6 +66,7 @@ export class PodNotesSettingsTab extends PluginSettingTab { }); this.addDefaultPlaybackRateSetting(settingsContainer); + this.addDefaultVolumeSetting(settingsContainer); this.addSkipLengthSettings(settingsContainer); this.addNoteSettings(settingsContainer); this.addDownloadSettings(settingsContainer); @@ -94,6 +95,22 @@ export class PodNotesSettingsTab extends PluginSettingTab { ); } + private addDefaultVolumeSetting(container: HTMLElement): void { + new Setting(container) + .setName("Default Volume") + .setDesc("Set the default playback volume.") + .addSlider((slider) => + slider + .setLimits(0, 1, 0.05) + .setValue(this.plugin.settings.defaultVolume) + .onChange((value) => { + this.plugin.settings.defaultVolume = value; + this.plugin.saveSettings(); + }) + .setDynamicTooltip(), + ); + } + private addSkipLengthSettings(container: HTMLElement): void { new Setting(container) .setName("Skip backward length (s)")