From ecb7799611b459750210e7b235233d604440601b Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 00:35:44 -0800 Subject: [PATCH 1/7] fix time interval for wavesurfer no zoom mode --- src/components/Wavesurfer/WavesurferWaveform.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Wavesurfer/WavesurferWaveform.tsx b/src/components/Wavesurfer/WavesurferWaveform.tsx index 565c8285..c8a97fd2 100644 --- a/src/components/Wavesurfer/WavesurferWaveform.tsx +++ b/src/components/Wavesurfer/WavesurferWaveform.tsx @@ -44,7 +44,6 @@ const DEFAULT_WAVESURFER_OPTIONS: Partial = { const DEFAULT_TIMELINE_OPTIONS: TimelinePluginOptions = { insertPosition: "beforebegin", - timeInterval: 0.25, style: { fontSize: "14px", color: "#000000", @@ -106,6 +105,7 @@ export const WavesurferWaveform = observer(function WavesurferWaveform() { height: uiStore.canTimelineZoom ? 60 : 80, primaryLabelInterval: uiStore.canTimelineZoom ? 5 : 30, secondaryLabelInterval: uiStore.canTimelineZoom ? 1 : 0, + timeInterval: uiStore.canTimelineZoom ? 0.25 : 5, }; // initialize wavesurfer From f2e7693a670618b6ae915a4ff7a34d241e060df1 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 01:58:35 -0800 Subject: [PATCH 2/7] better timer controls --- .../PlaylistEditor/PlaylistEditor.tsx | 8 +- src/components/Timeline/TimerAndWaveform.tsx | 2 +- src/components/Timeline/TimerControls.tsx | 109 ++++++++++++------ src/components/Timeline/TimerReadout.tsx | 2 +- src/types/AudioStore.ts | 13 ++- src/types/PlaylistStore.ts | 60 +++++++--- 6 files changed, 137 insertions(+), 57 deletions(-) diff --git a/src/components/PlaylistEditor/PlaylistEditor.tsx b/src/components/PlaylistEditor/PlaylistEditor.tsx index ef1cd21f..c343bcee 100644 --- a/src/components/PlaylistEditor/PlaylistEditor.tsx +++ b/src/components/PlaylistEditor/PlaylistEditor.tsx @@ -16,7 +16,7 @@ import { useStore } from "@/src/types/StoreContext"; import { observer } from "mobx-react-lite"; import { PlaylistItem } from "@/src/components/PlaylistEditor/PlaylistItem"; import { MdOutlinePlaylistAdd } from "react-icons/md"; -import { action } from "mobx"; +import { action, runInAction } from "mobx"; import { AddExperienceModal } from "@/src/components/PlaylistEditor/AddExperienceModal"; import { BiShuffle } from "react-icons/bi"; import { ImLoop } from "react-icons/im"; @@ -46,6 +46,12 @@ export const PlaylistEditor = observer(function PlaylistEditor() { } ); + useEffect(() => { + runInAction(() => { + playlistStore.selectedPlaylist = data?.playlist ?? null; + }); + }, [data?.playlist]); + useEffect(() => { if (!data?.experiencesAndUsers.length || store.experienceName) return; // once experiences are fetched, load the first experience in the playlist diff --git a/src/components/Timeline/TimerAndWaveform.tsx b/src/components/Timeline/TimerAndWaveform.tsx index b9c8f73f..2de7e9cc 100644 --- a/src/components/Timeline/TimerAndWaveform.tsx +++ b/src/components/Timeline/TimerAndWaveform.tsx @@ -36,8 +36,8 @@ export const TimerAndWaveform = observer(function TimerAndWaveform() { zIndex={18} bgColor="gray.500" > - {!embeddedViewer && } + diff --git a/src/components/Timeline/TimerControls.tsx b/src/components/Timeline/TimerControls.tsx index 310adf46..fd11f999 100644 --- a/src/components/Timeline/TimerControls.tsx +++ b/src/components/Timeline/TimerControls.tsx @@ -1,45 +1,88 @@ import { observer } from "mobx-react-lite"; -import { HStack, IconButton } from "@chakra-ui/react"; +import { ButtonGroup, HStack, IconButton, VStack } from "@chakra-ui/react"; import { FaPlay, FaPause, FaStepForward, FaStepBackward } from "react-icons/fa"; -import { MAX_TIME } from "@/src/utils/time"; import { useStore } from "@/src/types/StoreContext"; import { action } from "mobx"; +import { MdForward10, MdReplay10 } from "react-icons/md"; export const TimerControls = observer(function TimerControls() { const store = useStore(); - const { audioStore } = store; + const { audioStore, playlistStore } = store; const playing = audioStore.audioState === "playing"; return ( - - } - onClick={action(() => audioStore.setTimeWithCursor(0))} - /> - : } - onClick={action(store.togglePlaying)} - /> - } - onClick={action(() => { - audioStore.setTimeWithCursor(MAX_TIME); - store.pause(); - })} - /> - + + + + } + onClick={() => playlistStore.playPreviousExperience()} + /> + : } + onClick={action(store.togglePlaying)} + /> + } + onClick={() => playlistStore.playNextExperience()} + /> + + + + + } + onClick={action(() => audioStore.skip(-10))} + /> + } + onClick={action(() => audioStore.skip(10))} + /> + + + ); }); diff --git a/src/components/Timeline/TimerReadout.tsx b/src/components/Timeline/TimerReadout.tsx index 8a7f478b..ea5cfef3 100644 --- a/src/components/Timeline/TimerReadout.tsx +++ b/src/components/Timeline/TimerReadout.tsx @@ -15,7 +15,7 @@ export const TimerReadout = observer(function TimerReadout() { const { audioStore } = useStore(); return ( - + { if (!this.wavesurfer) return; - this.lastCursorPosition = time; - this.globalTime = time; + + const validTime = Math.max(0, time); + this.lastCursorPosition = validTime; + this.globalTime = validTime; const duration = this.wavesurfer.getDuration(); - if (this.wavesurfer.getCurrentTime() === time || duration === 0) return; - this.wavesurfer.seekTo(time / duration); + if (this.wavesurfer.getCurrentTime() === validTime || duration === 0) + return; + this.wavesurfer.seekTo(validTime / duration); }; skipForward = () => this.setTimeWithCursor(this.globalTime + 0.01); skipBackward = () => this.setTimeWithCursor(this.globalTime - 0.01); + skip = (delta: number) => this.setTimeWithCursor(this.globalTime + delta); + // called by wavesurfer, which defaults to 60fps onTick = (time: number) => { this.globalTime = time; diff --git a/src/types/PlaylistStore.ts b/src/types/PlaylistStore.ts index 13fe17df..d20e2ec9 100644 --- a/src/types/PlaylistStore.ts +++ b/src/types/PlaylistStore.ts @@ -3,6 +3,7 @@ import { ExperienceStore } from "@/src/types/ExperienceStore"; import { AudioStore } from "@/src/types/AudioStore"; import { Context } from "@/src/types/context"; import { Playlist } from "@/src/types/Playlist"; +import { MAX_TIME } from "@/src/utils/time"; // Define a new RootStore interface here so that we avoid circular dependencies interface RootStore { @@ -21,6 +22,10 @@ export class PlaylistStore { selectedPlaylist: Playlist | null = null; + get playlistExperienceIds() { + return this.selectedPlaylist?.orderedExperienceIds; + } + constructor( readonly rootStore: RootStore, readonly audioStore: AudioStore, @@ -51,29 +56,50 @@ export class PlaylistStore { }); }); - playNextExperience = async () => { - if (!this.selectedPlaylist?.orderedExperienceIds.length) return; + playPreviousExperience = async () => { + if (this.rootStore.context === "experienceEditor") { + this.audioStore.setTimeWithCursor(0); + return; + } - if (!this.rootStore.experienceId) { - await this.loadAndPlayExperience( - this.selectedPlaylist.orderedExperienceIds[0] - ); + const previousExperienceId = this.getDeltaExperienceId(-1); + if (previousExperienceId === null) return; + + this.rootStore.pause(); + await this.loadAndPlayExperience(previousExperienceId); + }; + + playNextExperience = async () => { + if (this.rootStore.context === "experienceEditor") { + this.audioStore.setTimeWithCursor(MAX_TIME); return; } - const currentIndex = this.selectedPlaylist?.orderedExperienceIds.indexOf( - this.rootStore.experienceId - ); - let nextIndex = currentIndex + 1; + const nextExperienceId = this.getDeltaExperienceId(1); + if (nextExperienceId === null) return; + + this.rootStore.pause(); + await this.loadAndPlayExperience(nextExperienceId); + }; + + // e.g. if delta is -1, get the experience ID of the previous experience + getDeltaExperienceId = (delta: number) => { + if (!this.playlistExperienceIds?.length) return null; + + let desiredIndex; + if (!this.rootStore.experienceId) desiredIndex = 0; + else { + const currentIndex = this.playlistExperienceIds.indexOf( + this.rootStore.experienceId + ); + desiredIndex = currentIndex + delta; - if (currentIndex < 0) return; - if (nextIndex > this.selectedPlaylist.orderedExperienceIds.length - 1) { - if (this.loopingPlaylist) nextIndex = 0; - else return; + if (desiredIndex < 0) + desiredIndex = this.playlistExperienceIds.length - 1; + if (desiredIndex > this.playlistExperienceIds.length - 1) + desiredIndex = 0; } - await this.loadAndPlayExperience( - this.selectedPlaylist.orderedExperienceIds[nextIndex] - ); + return this.playlistExperienceIds[desiredIndex]; }; } From 90ed3c0079c559535f6c4b36477f8a3a544c780a Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 02:41:17 -0800 Subject: [PATCH 3/7] shuffling logic is hard --- src/types/PlaylistStore.ts | 46 ++++++++++++++++++++++++++++++-------- src/utils/array.ts | 4 ++++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/types/PlaylistStore.ts b/src/types/PlaylistStore.ts index d20e2ec9..5ab5aa15 100644 --- a/src/types/PlaylistStore.ts +++ b/src/types/PlaylistStore.ts @@ -4,6 +4,7 @@ import { AudioStore } from "@/src/types/AudioStore"; import { Context } from "@/src/types/context"; import { Playlist } from "@/src/types/Playlist"; import { MAX_TIME } from "@/src/utils/time"; +import { areEqual } from "@/src/utils/array"; // Define a new RootStore interface here so that we avoid circular dependencies interface RootStore { @@ -16,14 +17,38 @@ interface RootStore { export class PlaylistStore { autoplay = ["playlistEditor", "viewer"].includes(this.rootStore.context); - // TODO: implement shufflingPlaylist = false; loopingPlaylist = false; selectedPlaylist: Playlist | null = null; - get playlistExperienceIds() { - return this.selectedPlaylist?.orderedExperienceIds; + _cachedPlaylistOrderedExperienceIds: number[] = []; + _cachedExperienceIdPlayOrder: number[] = []; + get experienceIdPlayOrder() { + if (!this.selectedPlaylist) return []; + if (!this.shufflingPlaylist) + return this.selectedPlaylist?.orderedExperienceIds; + + // check if the current playlist order is the same as the cached order + if ( + areEqual( + this.selectedPlaylist.orderedExperienceIds, + this._cachedPlaylistOrderedExperienceIds + ) + ) { + return this._cachedExperienceIdPlayOrder; + } + + // current playlist order and cached order are different, so update the cache + this._cachedPlaylistOrderedExperienceIds = + this.selectedPlaylist.orderedExperienceIds.slice(); + this._cachedExperienceIdPlayOrder = + this.selectedPlaylist.orderedExperienceIds.slice(); + + console.log("shuffling"); + // shuffle the play order + this._cachedExperienceIdPlayOrder.sort(() => Math.random() - 0.5); + return this._cachedExperienceIdPlayOrder; } constructor( @@ -31,7 +56,10 @@ export class PlaylistStore { readonly audioStore: AudioStore, readonly experienceStore: ExperienceStore ) { - makeAutoObservable(this); + makeAutoObservable(this, { + _cachedPlaylistOrderedExperienceIds: false, + _cachedExperienceIdPlayOrder: false, + }); } loadAndPlayExperience = async (experienceId: number) => { @@ -84,22 +112,22 @@ export class PlaylistStore { // e.g. if delta is -1, get the experience ID of the previous experience getDeltaExperienceId = (delta: number) => { - if (!this.playlistExperienceIds?.length) return null; + if (!this.experienceIdPlayOrder?.length) return null; let desiredIndex; if (!this.rootStore.experienceId) desiredIndex = 0; else { - const currentIndex = this.playlistExperienceIds.indexOf( + const currentIndex = this.experienceIdPlayOrder.indexOf( this.rootStore.experienceId ); desiredIndex = currentIndex + delta; if (desiredIndex < 0) - desiredIndex = this.playlistExperienceIds.length - 1; - if (desiredIndex > this.playlistExperienceIds.length - 1) + desiredIndex = this.experienceIdPlayOrder.length - 1; + if (desiredIndex > this.experienceIdPlayOrder.length - 1) desiredIndex = 0; } - return this.playlistExperienceIds[desiredIndex]; + return this.experienceIdPlayOrder[desiredIndex]; }; } diff --git a/src/utils/array.ts b/src/utils/array.ts index a9379a32..07908495 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -8,3 +8,7 @@ export const reorder: ( result.splice(endIndex, 0, removed); return result; }; + +export const areEqual: (a: T[], b: T[]) => boolean = (a, b) => { + return JSON.stringify(a) === JSON.stringify(b); +}; From 822b54a390d6a48fe155dd6ad78792369fc313a1 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 02:45:15 -0800 Subject: [PATCH 4/7] implement drag to seek on wave surfer waveform --- src/components/Wavesurfer/WavesurferWaveform.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Wavesurfer/WavesurferWaveform.tsx b/src/components/Wavesurfer/WavesurferWaveform.tsx index c8a97fd2..a5469783 100644 --- a/src/components/Wavesurfer/WavesurferWaveform.tsx +++ b/src/components/Wavesurfer/WavesurferWaveform.tsx @@ -40,6 +40,7 @@ const DEFAULT_WAVESURFER_OPTIONS: Partial = { autoScroll: false, autoCenter: false, interact: true, + dragToSeek: { debounceTime: 50 }, }; const DEFAULT_TIMELINE_OPTIONS: TimelinePluginOptions = { From 5394ce0345107b0d48d8f2215cd3713a41fe65b4 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 03:05:48 -0800 Subject: [PATCH 5/7] fix playlist select bug --- src/components/PlaylistEditor/PlaylistEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/PlaylistEditor/PlaylistEditor.tsx b/src/components/PlaylistEditor/PlaylistEditor.tsx index c343bcee..0f912e59 100644 --- a/src/components/PlaylistEditor/PlaylistEditor.tsx +++ b/src/components/PlaylistEditor/PlaylistEditor.tsx @@ -48,7 +48,8 @@ export const PlaylistEditor = observer(function PlaylistEditor() { useEffect(() => { runInAction(() => { - playlistStore.selectedPlaylist = data?.playlist ?? null; + // this is very hacky and I hate it but it works + if (data?.playlist) playlistStore.selectedPlaylist = data.playlist; }); }, [data?.playlist]); From 4fc7a0f6ef23b4873250f4b76f6a95585030346e Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 03:11:44 -0800 Subject: [PATCH 6/7] do not allow adding repeat experiences to a playlist --- .../ExperiencesTable/ExperiencesTable.tsx | 90 ++++++++++--------- .../PlaylistEditor/AddExperienceModal.tsx | 1 + 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/components/ExperiencesTable/ExperiencesTable.tsx b/src/components/ExperiencesTable/ExperiencesTable.tsx index 5d19802e..59b80bbd 100644 --- a/src/components/ExperiencesTable/ExperiencesTable.tsx +++ b/src/components/ExperiencesTable/ExperiencesTable.tsx @@ -18,12 +18,14 @@ import { Experience } from "@/src/types/Experience"; export const ExperiencesTable = observer(function ExperiencesTable({ experiencesAndUsers, onLoadExperience, + omitIds, selectable, selectedExperiences, setSelectedExperiences, }: { experiencesAndUsers: { user: { username: string }; experience: Experience }[]; onLoadExperience: (experience: Experience) => void; + omitIds?: number[]; // TODO: implement selectable experiences in the table selectable?: boolean; selectedExperiences?: string[]; @@ -44,51 +46,53 @@ export const ExperiencesTable = observer(function ExperiencesTable({ - {experiencesAndUsers.map(({ user, experience }) => ( - - - - - {user.username} - - {experience.song?.artist} - {experience.song?.name} - - - {user.username === username && ( - } - disabled={true} - onClick={action(() => { - if ( - !confirm( - "Are you sure you want to delete this experience? This will permanently cast the experience into the fires of Mount Doom. (jk doesn't work yet)" + {experiencesAndUsers + .filter(({ experience }) => !omitIds?.includes(experience.id!)) + .map(({ user, experience }) => ( + + + + + {user.username} + + {experience.song?.artist} - {experience.song?.name} + + + {user.username === username && ( + } + disabled={true} + onClick={action(() => { + if ( + !confirm( + "Are you sure you want to delete this experience? This will permanently cast the experience into the fires of Mount Doom. (jk doesn't work yet)" + ) ) - ) - return; + return; - // TODO: - // trpc.experience.deleteExperience.mutate({ - // name: experience.name, - // usingLocalData, - // }); - })} - /> - )} - - - ))} + // TODO: + // trpc.experience.deleteExperience.mutate({ + // name: experience.name, + // usingLocalData, + // }); + })} + /> + )} + + + ))} diff --git a/src/components/PlaylistEditor/AddExperienceModal.tsx b/src/components/PlaylistEditor/AddExperienceModal.tsx index 79874e10..b0d2e378 100644 --- a/src/components/PlaylistEditor/AddExperienceModal.tsx +++ b/src/components/PlaylistEditor/AddExperienceModal.tsx @@ -79,6 +79,7 @@ export const AddExperienceModal = observer(function AddExperienceModal({ }); onClose(); })} + omitIds={playlist.orderedExperienceIds} /> )} From e4b2aff9502bd0a313c197a3da04681f5a67f63f Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Wed, 6 Nov 2024 03:42:24 -0800 Subject: [PATCH 7/7] remove playlist description --- src/components/PlaylistEditor/PlaylistLibrary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PlaylistEditor/PlaylistLibrary.tsx b/src/components/PlaylistEditor/PlaylistLibrary.tsx index fc149230..8768f982 100644 --- a/src/components/PlaylistEditor/PlaylistLibrary.tsx +++ b/src/components/PlaylistEditor/PlaylistLibrary.tsx @@ -64,7 +64,7 @@ export const PlaylistLibrary = observer(function PlaylistLibrary() { username, usingLocalData, name: "New Playlist", - description: "New Playlist", + description: "", orderedExperienceIds: [], }); runInAction(() => {