Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add volume control #264

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions domain/player/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"classnames": "^2.3.1",
"fp-ts": "^2.11.9",
"fp-ts-local-storage": "^1.0.3",
"io-ts": "^2.2.16",
"lodash.throttle": "^4.1.1",
"retry-ts": "^0.1.3",
"zustand": "^3.7.1"
Expand Down
11 changes: 8 additions & 3 deletions domain/player/src/entities/Tracks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import * as NumberFP from "fp-ts/number";
import { identity, pipe } from "fp-ts/function";

import * as Track from "./Track";
import * as Volume from "./Volume";

type $Empty = {
volume: Volume.Volume;
autoplayEnabled: boolean;
};

type $Loaded = {
autoplayEnabled: boolean;
type $Loaded = $Empty & {
selected: string;
alreadyPlayed: boolean;
allTracks: ReadonlyNonEmptyArrayFP.ReadonlyNonEmptyArray<{
Expand All @@ -32,20 +33,24 @@ export type Tracks = Union.Type<typeof TracksAPI>;
export type Empty = ReturnType<typeof TracksAPI.of.Empty>;
export type Loaded = ReturnType<typeof TracksAPI.of.Loaded>;

export const volume = TracksAPI.lensFromProp("volume").get;
export const setVolume = TracksAPI.lensFromProp("volume").set;
export const autoplayEnabled = TracksAPI.lensFromProp("autoplayEnabled").get;
export const setAutoplay = TracksAPI.lensFromProp("autoplayEnabled").set;

export const isEmpty = TracksAPI.is.Empty;

export const { fold } = TracksAPI;

export const create = (data: { autoplayEnabled: boolean }) =>
export const create = (data: { autoplayEnabled: boolean; volume: number }) =>
TracksAPI.of.Empty({
volume: Volume.create(data.volume),
autoplayEnabled: data.autoplayEnabled,
});

const toLoaded = (track: Track.Track, weight: number) => (tracks: Empty) =>
TracksAPI.of.Loaded({
volume: volume(tracks),
autoplayEnabled: autoplayEnabled(tracks),
selected: Track.id(track),
alreadyPlayed: false,
Expand Down
15 changes: 15 additions & 0 deletions domain/player/src/entities/Volume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Union from "@fp51/opaque-union";

type $Volume = {
value: number; // [0-1]
};

const VolumeAPI = Union.of({
Volume: Union.type<$Volume>(),
});

export type Volume = Union.Type<typeof VolumeAPI>;

export const create = (value: number) => VolumeAPI.of.Volume({ value });

export const value = VolumeAPI.lensFromProp("value").get;
4 changes: 4 additions & 0 deletions domain/player/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as TrackSource from "./entities/TrackSource";
import * as Track from "./entities/Track";
import * as Tracks from "./entities/Tracks";
import * as Position from "./entities/Position";
import * as Volume from "./entities/Volume";

import { usePlayer, shallowEqual } from "./store";
import { playOrPause, play } from "./repositories/playPause";
Expand All @@ -12,12 +13,14 @@ import {
loadSoundcloud,
} from "./repositories/track";
import { saveAutoplayChoice } from "./repositories/autoplay";
import { updateVolume } from "./repositories/volume";

export {
Track,
Tracks,
TrackSource,
Position,
Volume,
usePlayer,
shallowEqual,
playOrPause,
Expand All @@ -27,4 +30,5 @@ export {
loadBandcamp,
loadSoundcloud,
saveAutoplayChoice,
updateVolume,
};
82 changes: 47 additions & 35 deletions domain/player/src/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,58 @@ import * as IOEither from "fp-ts/IOEither";
import * as IO from "fp-ts/IO";
import * as Option from "fp-ts/Option";
import * as Either from "fp-ts/Either";
import * as D from "io-ts/Decoder";
import * as C from "io-ts/Codec";
import { pipe } from "fp-ts/function";

const autoplayEnabledItemName = "cmd-player-autoplayEnabled";

function parseValue(value: string): Option.Option<boolean> {
switch (value) {
case "true":
return Option.some(true);

case "false":
return Option.some(false);
}
type LocalStorageIO<T> = {
readOrElse: (defaultValue: () => T) => IO.IO<T>;
silentWrite: (value: T) => IO.IO<void>;
};

function buildLocalStorageIO<T>(
key: string,
codec: C.Codec<unknown, string, T>
): LocalStorageIO<T> {
const readOrElse = (defaultValue: () => T) =>
pipe(
IOEither.tryCatch(LocalStorageFP.getItem(key), Either.toError),
IOEither.map((value) =>
pipe(
value,
Option.map(codec.decode),
Option.map(Either.getOrElse(defaultValue)),
Option.getOrElse(defaultValue)
)
),
IO.map(Either.getOrElse(defaultValue))
);

const silentWrite = (value: T): IOEither.IOEither<Error, void> =>
IOEither.tryCatch(
pipe(value, codec.encode, (serializedValue) =>
LocalStorageFP.setItem(key, serializedValue)
),
Either.toError
);

return Option.none;
return {
readOrElse,
silentWrite,
};
}

function serializeValue(value: boolean): string {
if (value) {
return "true";
}
const BooleanCodec = C.make(D.boolean, {
encode: String,
});

return "false";
}

export const readLocalStorageAutoplay: IO.IO<boolean> = pipe(
IOEither.tryCatch(
LocalStorageFP.getItem(autoplayEnabledItemName),
Either.toError
),
IO.map(Either.getOrElse((): Option.Option<string> => Option.none)),
IO.map(Option.chain(parseValue)),
IO.map(Option.getOrElse((): boolean => true)) // default true
export const autoplayEnabled = buildLocalStorageIO(
"cmd-player-autoplayEnabled",
BooleanCodec
);

export const writeLocalStorageAutoplay = (
value: boolean
): IOEither.IOEither<Error, void> =>
pipe(
IOEither.tryCatch(
LocalStorageFP.setItem(autoplayEnabledItemName, serializeValue(value)),
Either.toError
)
);
const NumberCodec = C.make(D.number, {
encode: String,
});

export const volume = buildLocalStorageIO("cmd-player-volume", NumberCodec);
12 changes: 4 additions & 8 deletions domain/player/src/repositories/autoplay.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import * as IO from "fp-ts/IO";
import * as IOEither from "fp-ts/IOEither";
import { pipe } from "fp-ts/function";

import * as Tracks from "../entities/Tracks";

import * as Store from "../store";
import { writeLocalStorageAutoplay } from "../localStorage";
import { autoplayEnabled } from "../localStorage";

export function saveAutoplayChoice(autoplayEnabled: boolean): IO.IO<void> {
export function saveAutoplayChoice(enabled: boolean): IO.IO<void> {
return pipe(
writeLocalStorageAutoplay(autoplayEnabled),
IOEither.chainFirstIOK(() =>
Store.write(Tracks.setAutoplay(autoplayEnabled))
),
IOEither.getOrElse((): IO.IO<void> => IO.of(undefined)) // silence error
autoplayEnabled.silentWrite(enabled),
IO.chainFirst(() => Store.write(Tracks.setAutoplay(enabled)))
);
}
6 changes: 4 additions & 2 deletions domain/player/src/repositories/playPause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import * as Tracks from "../entities/Tracks";
import * as Store from "../store";

import * as TrackRepo from "./track";
import { setVolumeForCurrentTrack } from "./volume";

function selectAndPlayTrack(track: Track.Initialized) {
return (state: Tracks.Loaded): IO.IO<void> => {
return pipe(
Store.write(() => pipe(state, Tracks.selectTrack(track), Tracks.playing)),
IO.chain(() => TrackRepo.play(track))
IO.chain(() => TrackRepo.play(track)),
IO.chain(setVolumeForCurrentTrack)
);
};
}
Expand All @@ -25,7 +27,7 @@ export const playOrPause = pipe(
return IO.of(undefined);
}

const selectedTrack = pipe(state, Tracks.selectedTrack);
const selectedTrack = Tracks.selectedTrack(state);

// nothing to do here
if (!Track.isInteractive(selectedTrack)) {
Expand Down
33 changes: 33 additions & 0 deletions domain/player/src/repositories/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ function pauseYoutube(source: Source.Youtube): IO.IO<void> {
return () => (Source.player(source) as any).pauseVideo();
}

function setVolumeYoutube(volume: number) {
return (source: Source.Youtube) => (): IO.IO<void> =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Source.player(source) as any).setVolume(volume * 100);
}

function resetSoundcloud(source: Source.Soundcloud): IO.IO<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return () => (Source.widget(source) as any).seekTo(0);
Expand All @@ -88,6 +94,12 @@ function pauseSoundcloud(source: Source.Soundcloud): IO.IO<void> {
return () => (Source.widget(source) as any).pause();
}

function setVolumeSoundcloud(volume: number) {
return (source: Source.Soundcloud) => (): IO.IO<void> =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Source.widget(source) as any).setVolume(volume * 100);
}

function resetBandcamp(source: Source.Bandcamp): IO.IO<void> {
return () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -105,6 +117,14 @@ function playBandcamp(source: Source.Bandcamp): IO.IO<void> {
return () => (Source.audio(source) as any).play();
}

function setVolumeBandcamp(volume: number) {
return (source: Source.Bandcamp): IO.IO<void> =>
() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Source.audio(source) as any).volue = volume;
};
}

export function reset(track: Track.Initialized): IO.IO<void> {
return pipe(
track,
Expand Down Expand Up @@ -141,6 +161,19 @@ export function pause(track: Track.Initialized): IO.IO<void> {
);
}

export function setVolume(volume: number) {
return (track: Track.Initialized): IO.IO<void> =>
pipe(
track,
Track.source,
Source.fold({
Youtube: setVolumeYoutube(volume),
Soundcloud: setVolumeSoundcloud(volume),
Bandcamp: setVolumeBandcamp(volume),
})
);
}

const aborted = doIfSelectedTrack((track: Track.Initialized) =>
Store.write(
Tracks.modifyIfNotEmpty(
Expand Down
41 changes: 41 additions & 0 deletions domain/player/src/repositories/volume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as IO from "fp-ts/IO";
import { pipe } from "fp-ts/function";

import * as Volume from "../entities/Volume";
import * as Tracks from "../entities/Tracks";
import * as Track from "../entities/Track";

import * as Store from "../store";
import { volume } from "../localStorage";

import * as TrackRepo from "./track";

export function setVolumeForCurrentTrack(): IO.IO<void> {
return () => {
const state = Store.read();

// nothing to do here
if (Tracks.isEmpty(state)) {
return IO.of(undefined);
}

const selectedTrack = Tracks.selectedTrack(state);

// nothing to do here
if (!Track.isInteractive(selectedTrack)) {
return IO.of(undefined);
}

const value = pipe(state, Tracks.volume, Volume.value);

return TrackRepo.setVolume(value)(selectedTrack);
};
}

export function updateVolume(newVolume: Volume.Volume): IO.IO<void> {
return pipe(
volume.silentWrite(Volume.value(newVolume)),
IO.chainFirst(() => Store.write(Tracks.setVolume(newVolume))),
IO.chain(setVolumeForCurrentTrack)
);
}
10 changes: 7 additions & 3 deletions domain/player/src/store.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import * as IO from "fp-ts/IO";
import { sequenceS } from "fp-ts/Apply";
import { pipe } from "fp-ts/function";

import createHook from "zustand";
import createStore from "zustand/vanilla";

import * as Tracks from "./entities/Tracks";
import { readLocalStorageAutoplay } from "./localStorage";
import { autoplayEnabled, volume } from "./localStorage";

const initTracks: IO.IO<Tracks.Tracks> = pipe(
readLocalStorageAutoplay,
IO.map((autoplayEnabled) => Tracks.create({ autoplayEnabled }))
sequenceS(IO.io)({
autoplayEnabled: autoplayEnabled.readOrElse(() => true),
volume: volume.readOrElse(() => 0.8),
}),
IO.map(Tracks.create)
);

const store = createStore<Tracks.Tracks>(initTracks);
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.