From cffe9ad8fef96b53c5636f6312bbee872cdfbaa0 Mon Sep 17 00:00:00 2001 From: officialMECH Date: Tue, 14 Jul 2026 18:59:19 -0500 Subject: [PATCH 01/16] move audio context to singleton --- src/helpers/audio.helpers.ts | 13 +++++++++---- src/index.tsx | 3 ++- src/services/audio.service.ts | 4 +++- src/services/packaging.service.ts | 6 ++---- src/setup.ts | 4 ++++ src/store/middleware/file.middleware.ts | 5 ++--- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/helpers/audio.helpers.ts b/src/helpers/audio.helpers.ts index 3f0f25be..cfb365b8 100644 --- a/src/helpers/audio.helpers.ts +++ b/src/helpers/audio.helpers.ts @@ -2,6 +2,7 @@ import { distinct } from "@std/collections/distinct"; import { createAudioData, createBPMEvent, type IWrapAudioData, type IWrapAudioDataBPM, type IWrapBPMEvent, type IWrapDifficulty, sortObjectFn } from "bsmap"; import { default as WaveformData } from "waveform-data"; +import { getAudioContext } from "$/setup"; import { roundToNearest } from "$/utils"; import { convertFileToArrayBuffer } from "./file.helpers"; @@ -18,14 +19,18 @@ export function convertBeatsToMilliseconds(beats: number, bpm: number) { return (beats / bps) * 1000; } -export async function deriveAudioDataFromFile(file: Blob | MediaSource, audioContext: AudioContext) { +export async function deriveAudioDataFromFile(file: Blob | MediaSource) { + const audioContext = getAudioContext(); + const arrayBuffer = await convertFileToArrayBuffer(file); return await audioContext.decodeAudioData(arrayBuffer).then((audioBuffer) => { return { duration: audioBuffer.duration, frequency: audioBuffer.sampleRate, sampleCount: audioBuffer.length }; }); } -export async function deriveWaveformDataFromFile(file: Blob | MediaSource, audioContext: AudioContext) { +export async function deriveWaveformDataFromFile(file: Blob | MediaSource) { + const audioContext = getAudioContext(); + const arrayBuffer = await convertFileToArrayBuffer(file); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer.slice(0)); @@ -41,8 +46,8 @@ export async function deriveWaveformDataFromFile(file: Blob | MediaSource, audio ); } -export async function createAudioDataContentsFromFile(songFile: File, audioContext: AudioContext, options: { bpm: number; version?: number }): Promise { - const { duration, frequency, sampleCount } = await deriveAudioDataFromFile(songFile, audioContext); +export async function createAudioDataContentsFromFile(songFile: File, options: { bpm: number; version?: number }): Promise { + const { duration, frequency, sampleCount } = await deriveAudioDataFromFile(songFile); // map will not load properly in-game if there isn't at least one bpm change defined. we call this peak stupid. const region: IWrapAudioDataBPM = { diff --git a/src/index.tsx b/src/index.tsx index c1d716c2..4c2e95e1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,11 +6,12 @@ import { Provider } from "react-redux"; import { SetupProvider } from "./components/context"; import { getRouter, setupRouter } from "./router"; -import { getAppBeatmapFilestore, getAppStore, getAppToaster, setupAppStore } from "./setup"; +import { getAppBeatmapFilestore, getAppStore, getAppToaster, setupAppStore, setupAudioContext } from "./setup"; import "./index.css"; setupRouter(); +setupAudioContext(); setupAppStore(); const root = document.getElementById("root"); diff --git a/src/services/audio.service.ts b/src/services/audio.service.ts index b4876891..c1cf51aa 100644 --- a/src/services/audio.service.ts +++ b/src/services/audio.service.ts @@ -1,3 +1,5 @@ +import { getAudioContext } from "$/setup"; + export interface AudioSampleOptions { url?: string; volume?: number; @@ -23,7 +25,7 @@ export class AudioSample { this.gain = volume; this.playbackRate = playbackRate; - this.context = new AudioContext(); + this.context = getAudioContext(); // Audio contexts have an always-incrementing `currentTime` ticker. // When we start the file, we might be 20 seconds into that process, so we'll store the currentTime position that the audio started playing. diff --git a/src/services/packaging.service.ts b/src/services/packaging.service.ts index 5e7907d8..1933ff14 100644 --- a/src/services/packaging.service.ts +++ b/src/services/packaging.service.ts @@ -32,8 +32,6 @@ import { getAppBeatmapFilestore } from "$/setup"; import type { App, SongId } from "$/types"; import { deepAssign, ensureArray, yieldValue } from "$/utils"; -const audioContext = new AudioContext(); - const decoder = new TextDecoder("utf-8"); const encoder = new TextEncoder(); @@ -75,7 +73,7 @@ export interface ImportMapArchiveOptions { } export async function importMapArchive(archive: Uint8Array, { loadOptions }: ImportMapArchiveOptions): Promise { - const unzipped = await new Promise>((resolve, reject) => + const unzipped = await new Promise((resolve, reject) => unzip(archive, (err, data) => { if (err) return reject(err); resolve(data); @@ -134,7 +132,7 @@ export async function importMapArchive(archive: Uint8Array, { loadOptions }: Imp return loadAudioData(JSON.parse(decoder.decode(data)), null, loadOptions); }) .catch(async () => { - const { frequency, bpmData, ...rest } = await createAudioDataContentsFromFile(songFile, audioContext, { version: info.version, bpm: info.audio.bpm }); + const { frequency, bpmData, ...rest } = await createAudioDataContentsFromFile(songFile, { version: info.version, bpm: info.audio.bpm }); return createAudioData({ ...rest, frequency, bpmData: createBpmDataFromDifficulty(beatmaps[0].difficulty, frequency, bpmData[0].endBeat) }); }); diff --git a/src/setup.ts b/src/setup.ts index a26ad10b..80c08254 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -75,6 +75,10 @@ const appFileDriver = createDriver { + return new AudioContext({ latencyHint: "playback" }); +}); + export const { get: getAppBeatmapFilestore, setup: setupAppBeatmapFilestore } = createLazySingleton((driver?: Driver) => { return new BeatmapFilestore({ storage: createStorage({ diff --git a/src/store/middleware/file.middleware.ts b/src/store/middleware/file.middleware.ts index 0f675699..f7b378e7 100644 --- a/src/store/middleware/file.middleware.ts +++ b/src/store/middleware/file.middleware.ts @@ -15,7 +15,6 @@ import { deepAssign } from "$/utils"; export default function createFileMiddleware() { const instance = createListenerMiddleware(); const filestore = getAppBeatmapFilestore(); - const audioContext = new AudioContext(); instance.startListening({ actionCreator: rehydrate, @@ -56,7 +55,7 @@ export default function createFileMiddleware() { if (finishLoadingMap.match(action) || songFile) { const activeSongFile = songFile ?? (await filestore.loadSongFile(songId)); - await deriveWaveformDataFromFile(activeSongFile, audioContext).then((waveformData) => { + await deriveWaveformDataFromFile(activeSongFile).then((waveformData) => { return api.dispatch(reloadVisualizer({ duration: waveformData.duration, waveformData: waveformData.toJSON() })); }); } @@ -71,7 +70,7 @@ export default function createFileMiddleware() { const infoContents = serializeInfoContents(selectSongById(state, songId), { songDuration: selectDuration(state), }); - const audioDataContents = await createAudioDataContentsFromFile(songFile, audioContext, { bpm: selectBpm(state, songId) }); + const audioDataContents = await createAudioDataContentsFromFile(songFile, { bpm: selectBpm(state, songId) }); await Promise.all([ filestore.saveSongFile(songId, songFile), From 0c67238058fa934006fae90f658cb6a2a78a034b Mon Sep 17 00:00:00 2001 From: officialMECH Date: Wed, 15 Jul 2026 14:51:02 -0500 Subject: [PATCH 02/16] fix dependency resolution for mdx types --- package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index 2d0709e3..22ab06f8 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@tanstack/router-plugin": "^1.167.4", "@tanstack/virtual-file-routes": "^1.161.7", "@types/file-saver": "^2.0.7", + "@types/mdx": "^2.0.14", "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e590901c..2b268b9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 + '@types/mdx': + specifier: ^2.0.14 + version: 2.0.14 '@types/node': specifier: ^24.12.0 version: 24.13.1 From f5769eae09f760ce93da718888a636b1c4cda225 Mon Sep 17 00:00:00 2001 From: officialMECH Date: Tue, 11 Aug 2026 12:42:45 -0500 Subject: [PATCH 03/16] derive all editor settings from custom info data --- src/helpers/packaging.helpers.ts | 91 ++++++++++++++++++++------------ src/helpers/song.helpers.ts | 4 +- src/types/beatmap/custom.ts | 5 ++ src/utils/object.utils.ts | 2 +- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/helpers/packaging.helpers.ts b/src/helpers/packaging.helpers.ts index bb2d915e..a3ff596b 100644 --- a/src/helpers/packaging.helpers.ts +++ b/src/helpers/packaging.helpers.ts @@ -11,6 +11,7 @@ import { type EnvironmentV3Name, type IV2CustomDataDifficulty, type IV2CustomDataInfoDifficulty, + type IV2EditorInfo, type IWrapBeatmap, type IWrapInfo, type IWrapInfoColorScheme, @@ -21,12 +22,12 @@ import { } from "bsmap"; import { DEFAULT_GRID } from "$/constants"; -import { type App, ColorSchemeKey } from "$/types"; +import { type App, ColorSchemeKey, type IEditorData } from "$/types"; import { deepAssign, ensureArray, ensureObject, hasKeys } from "$/utils"; import { deserializeCustomBookmark, resolveBookmarkId, serializeCustomBookmark } from "./bookmarks.helpers"; import { deriveColorSchemeFromEnvironment } from "./colors.helpers"; import { createDataFactory } from "./factory.helpers"; -import { createAppBeatmap, createAppSong } from "./song.helpers"; +import { createAppBeatmap, createAppSong, createSongId } from "./song.helpers"; export function resolveBeatmapIdFromFilename(filename: string): string { let fn = filename; @@ -42,6 +43,47 @@ export function patchEnvironmentName(environment: str return environment as T; } +export function getEditorSettings(data: IWrapInfo): IEditorData { + const { editorSettings } = { + ...(data.customData._editors?.Beatmapper as IV2EditorInfo & { editorSettings?: IEditorData }), + }; + + const activeCustomColors = Object.values(ColorSchemeKey).reduce( + (acc, key) => { + const color = data.difficulties.find((x) => x.customData[`_${key}`])?.customData[`_${key}`]; + acc[key as ColorSchemeKey] = color ? colorToHex(color).slice(0, 7) : null; + return acc; + }, + {} as { [key in ColorSchemeKey]: string | null }, + ); + + const isCustomColorsEnabled = data.difficulties.some((beatmap) => { + return hasKeys(beatmap.customData, "_colorLeft", "_colorRight", "_envColorLeft", "_envColorRight", "_envColorWhite", "_envColorLeftBoost", "_envColorRightBoost", "_envColorWhiteBoost", "_obstacleColor"); + }); + + const customColors = ensureObject({ + isEnabled: isCustomColorsEnabled, + colorLeft: activeCustomColors.colorLeft ?? undefined, + colorRight: activeCustomColors.colorRight ?? undefined, + envColorLeft: activeCustomColors.envColorLeft ?? undefined, + envColorRight: activeCustomColors.envColorRight ?? undefined, + envColorWhite: activeCustomColors.envColorWhite ?? undefined, + envColorLeftBoost: activeCustomColors.envColorLeftBoost ?? undefined, + envColorRightBoost: activeCustomColors.envColorRightBoost ?? undefined, + envColorWhiteBoost: activeCustomColors.envColorWhiteBoost ?? undefined, + obstacleColor: activeCustomColors.obstacleColor ?? undefined, + }); + + return deepAssign( + { ...editorSettings }, + { + modSettings: { + customColors: isCustomColorsEnabled ? customColors : undefined, + }, + }, + ); +} + export const { serialize: serializeInfoContents, deserialize: deserializeInfoContents } = createDataFactory({ container: { serialize: function serializeInfoContents(data: Omit, options: { songDuration?: number | null }) { @@ -79,7 +121,7 @@ export const { serialize: serializeInfoContents, deserialize: deserializeInfoCon filename: data.songFilename, audioDataFilename: "AudioData.dat", bpm: data.bpm, - duration: options.songDuration ? options.songDuration / 1000 : undefined, + duration: options.songDuration ?? undefined, previewStartTime: data.previewStartTime, previewDuration: data.previewDuration, }, @@ -125,6 +167,11 @@ export const { serialize: serializeInfoContents, deserialize: deserializeInfoCon Beatmapper: { version: version, editorSettings: { + id: createSongId(data), + readonly: data.demo, + createdAt: data.createdAt, + lastOpenedAt: data.lastOpenedAt, + lastOpenedBeatmapId: data.selectedDifficulty, modSettings: { mappingExtensions: ensureObject({ isEnabled: !!mappingExtensions?.isEnabled, @@ -178,37 +225,10 @@ export const { serialize: serializeInfoContents, deserialize: deserializeInfoCon return acc; }, {}); - const activeCustomColors = Object.values(ColorSchemeKey).reduce( - (acc, key) => { - const color = data.difficulties.find((x) => x.customData[`_${key}`])?.customData[`_${key}`]; - acc[key as ColorSchemeKey] = color ? colorToHex(color).slice(0, 7) : null; - return acc; - }, - {} as { [key in ColorSchemeKey]: string | null }, - ); - - const isCustomColorsEnabled = data.difficulties.some((beatmap) => { - return hasKeys(beatmap.customData, "_colorLeft", "_colorRight", "_envColorLeft", "_envColorRight", "_envColorWhite", "_envColorLeftBoost", "_envColorRightBoost", "_envColorWhiteBoost", "_obstacleColor"); - }); - - const customColors = ensureObject({ - isEnabled: isCustomColorsEnabled, - colorLeft: activeCustomColors.colorLeft ?? undefined, - colorRight: activeCustomColors.colorRight ?? undefined, - envColorLeft: activeCustomColors.envColorLeft ?? undefined, - envColorRight: activeCustomColors.envColorRight ?? undefined, - envColorWhite: activeCustomColors.envColorWhite ?? undefined, - envColorLeftBoost: activeCustomColors.envColorLeftBoost ?? undefined, - envColorRightBoost: activeCustomColors.envColorRightBoost ?? undefined, - envColorWhiteBoost: activeCustomColors.envColorWhiteBoost ?? undefined, - obstacleColor: activeCustomColors.obstacleColor ?? undefined, - }); - - const baseModSettings = { - customColors: isCustomColorsEnabled ? customColors : undefined, - }; + const { id, readonly, createdAt, lastOpenedAt, lastOpenedBeatmapId, modSettings } = getEditorSettings(data); return createAppSong({ + id: id, name: data.song.title, subName: data.song.subTitle, artistName: data.song.author, @@ -221,8 +241,11 @@ export const { serialize: serializeInfoContents, deserialize: deserializeInfoCon coverArtFilename: data.coverImageFilename, difficultiesById: beatmapsById, colorSchemesById: colorSchemesById, - demo: options.readonly, - modSettings: deepAssign(baseModSettings, { ...data.customData.editors?.Beatmapper?.editorSettings?.modSettings }), + demo: readonly ?? options.readonly, + createdAt: createdAt, + lastOpenedAt: lastOpenedAt, + selectedDifficulty: lastOpenedBeatmapId, + modSettings: modSettings, }); }, }, diff --git a/src/helpers/song.helpers.ts b/src/helpers/song.helpers.ts index 47a5cd56..1ccf3600 100644 --- a/src/helpers/song.helpers.ts +++ b/src/helpers/song.helpers.ts @@ -33,10 +33,8 @@ export function resolveBeatmapId(x: Pick, "name" | "bpm" | "songFilename" | "coverArtFilename">): App.ISong { - const songId = createSongId(data); - return { - id: data.id ?? songId, + id: data.id ?? createSongId(data), name: data.name, subName: data.subName ?? "", artistName: data.artistName ?? "", diff --git a/src/types/beatmap/custom.ts b/src/types/beatmap/custom.ts index 431c07d4..4631c5db 100644 --- a/src/types/beatmap/custom.ts +++ b/src/types/beatmap/custom.ts @@ -1,5 +1,10 @@ import type { IModSettings } from "./app"; export interface IEditorData { + id?: string; + readonly?: boolean; + createdAt?: number; + lastOpenedAt?: number; + lastOpenedBeatmapId?: string; modSettings?: Partial; } diff --git a/src/utils/object.utils.ts b/src/utils/object.utils.ts index d1bbc254..cf4170a0 100644 --- a/src/utils/object.utils.ts +++ b/src/utils/object.utils.ts @@ -13,7 +13,7 @@ export function ensureObject(object: T) { export function hasKeys>(obj: T, ...keys: K[]): boolean { for (const key of keys) { - if (Object.hasOwn(obj, key)) return true; + if (Object.hasOwn(obj, key) && obj[key]) return true; } return false; } From 5c99365e40db280b257b22eafefed8f20eac6de8 Mon Sep 17 00:00:00 2001 From: officialMECH Date: Thu, 27 Aug 2026 15:53:27 -0500 Subject: [PATCH 04/16] Improvements for Redux (#53) * refactor storage middleware to strategy-based enhancers * normalize real time calculation to seconds * compute audio latency directly from audio context * add shallow apis (and explicit types) for thunk action creators * add dedicated apis for matched route selectors * simplify payloads for atomic update actions/hydrators * refactor incremental thunk action payload creators * add declarative types and implicit route accessors for listener middleware * untangle circular references for storage enhancers * consolidate beatmap entity slices with colocated history adapters * normalize reducer paths for slices * rework file structure for store modules and setup methods * improve type inference for thunk utils * move route selectors into store helpers * improve dependency injection patterns for middleware/thunks * rework singleton apis to decouple setup modules * improve type inference for thunk arguments * add shared utility for meta property action matching * make filestore source of truth for songs hydration step * fix regression in property resolution for object placement logic * move cross-slice selectors out of slice definitions * reconcile stateful thunk payloads using listener middleware * consolidate editor lifecycle logic into imperative actions * revert back to original songs hydrator (for now) * consolidate audio sample effects into imperative actions * migrate remaining cross-slice extra reducers into listener middleware effects * untangle circular references for same-site actions and selectors --- package.json | 1 + pnpm-lock.yaml | 23 + src/{setup.ts => _setup.ts} | 31 +- src/components/app/forms/create-beatmap.tsx | 7 +- src/components/app/forms/create-map.tsx | 6 +- src/components/app/forms/export-map.tsx | 6 +- src/components/app/forms/import-map.tsx | 6 +- src/components/app/forms/settings/audio.tsx | 12 +- .../app/forms/settings/controls.tsx | 2 +- .../app/forms/settings/graphics.tsx | 4 +- src/components/app/forms/settings/user.tsx | 2 +- src/components/app/forms/update-beatmap.tsx | 10 +- src/components/app/forms/update-song.tsx | 30 +- src/components/app/hooks/local-file.hooks.ts | 7 +- .../app/layouts/event-grid/machine.ts | 45 +- .../app/layouts/visualizer/markers.tsx | 5 +- .../app/templates/download/index.tsx | 2 +- .../editor/action-panel-groups/default.tsx | 19 +- .../editor/action-panel-groups/direction.tsx | 18 +- .../action-panel-groups/grid-presets.tsx | 2 +- .../editor/action-panel-groups/grid.tsx | 2 +- .../editor/action-panel-groups/selection.tsx | 24 +- .../editor/action-panel-groups/tool.tsx | 8 +- .../templates/editor/navigation-controls.tsx | 14 +- .../app/templates/editor/status-bar.tsx | 22 +- .../app/templates/editor/visualizer.tsx | 10 +- .../app/templates/events/basic-track.tsx | 37 +- .../app/templates/events/boost-track.tsx | 17 +- .../app/templates/events/controls.tsx | 16 +- src/components/app/templates/events/grid.tsx | 10 +- .../app/templates/home/first-time.tsx | 17 +- .../app/templates/shortcuts/default.tsx | 71 +-- .../app/templates/shortcuts/events.tsx | 32 +- .../app/templates/shortcuts/notes.tsx | 71 ++- .../app/templates/tables/songs/actions.tsx | 10 +- src/components/context.ts | 10 +- .../scene/hooks/use-object-placement.ts | 92 ++-- .../templates/environment/side-lasers.tsx | 2 +- .../scene/templates/placement-grid.tsx | 24 +- .../scene/templates/visualization.tsx | 65 +-- src/components/ui/compositions/audio.tsx | 7 +- .../ui/compositions/file-upload.tsx | 8 +- src/constants/editor.constants.ts | 2 + src/helpers/audio.helpers.test.ts | 24 - src/helpers/audio.helpers.ts | 74 +-- src/helpers/events.helpers.ts | 4 +- src/helpers/item.helpers.ts | 31 +- src/index.tsx | 66 ++- src/router.ts | 20 - src/routes/converter.tsx | 11 +- src/routes/editor/layout.tsx | 32 +- src/routes/editor/scene/preview.tsx | 5 +- src/routes/root.tsx | 15 +- src/services/audio.service.ts | 185 ++++--- src/services/backup.service.ts | 60 --- src/services/file.service.ts | 6 + src/services/packaging.service.ts | 16 +- src/store/_setup.ts | 38 ++ src/store/actions.ts | 437 ++++------------ src/store/enhancers/_setup.ts | 287 +++++++++++ src/store/enhancers/index.ts | 0 src/store/enhancers/storage.enhancer.ts | 164 ++++++ src/store/features/_setup.ts | 16 + src/store/features/actions.ts | 58 +++ src/store/features/beatmap.slice.ts | 83 +++ src/store/features/bookmarks.slice.ts | 46 ++ src/store/features/clipboard.slice.ts | 53 +- src/store/features/editor.thunks.ts | 17 + src/store/features/editor/beatmap.slice.ts | 96 ---- src/store/features/editor/index.ts | 13 - src/store/features/editor/lightshow.slice.ts | 127 ----- src/store/features/entities.thunks.ts | 112 +++++ .../features/entities/beatmap/bombs.slice.ts | 134 ----- src/store/features/entities/beatmap/index.ts | 43 -- .../features/entities/beatmap/notes.slice.ts | 138 ----- .../entities/beatmap/obstacles.slice.ts | 134 ----- .../entities/editor/bookmarks.slice.ts | 55 -- src/store/features/entities/editor/index.ts | 9 - src/store/features/entities/index.ts | 13 - .../entities/lightshow/basic.slice.ts | 128 ----- .../entities/lightshow/boost.slice.ts | 128 ----- .../features/entities/lightshow/index.ts | 40 -- src/store/features/events.slice.ts | 265 ++++++++++ src/store/features/global.slice.ts | 25 +- src/store/features/index.ts | 27 - src/store/features/lightshow.slice.ts | 125 +++++ src/store/features/navigation.slice.ts | 164 +++--- src/store/features/objects.slice.ts | 276 ++++++++++ src/store/features/playback.thunks.ts | 103 ++++ src/store/features/selectors.ts | 122 +++++ src/store/features/songs.slice.ts | 117 +++-- src/store/features/timeline.slice.ts | 19 +- src/store/features/user.slice.ts | 70 +-- src/store/features/visualizer.slice.ts | 13 +- src/store/helpers.ts | 142 ------ src/store/helpers/editor.helpers.ts | 47 ++ src/store/helpers/route.helpers.ts | 25 + src/store/helpers/selectors.ts | 14 + src/store/hooks.ts | 2 +- src/store/middleware/_setup.ts | 43 ++ src/store/middleware/audio.middleware.ts | 141 +++--- src/store/middleware/backup.middleware.ts | 98 ++-- src/store/middleware/demo.middleware.ts | 31 -- src/store/middleware/file.middleware.ts | 105 ++-- src/store/middleware/history.middleware.ts | 58 +-- src/store/middleware/index.ts | 49 -- src/store/middleware/packaging.middleware.ts | 52 +- src/store/middleware/playback.middleware.ts | 201 ++------ src/store/middleware/shared.middleware.ts | 53 ++ src/store/middleware/storage.middleware.ts | 257 ---------- src/store/selectors.ts | 472 +++--------------- src/store/setup.ts | 256 ---------- src/store/types.ts | 22 + src/store/utils/guards.utils.ts | 8 + src/store/utils/thunk.utils.ts | 85 ++++ src/utils/array.utils.ts | 19 +- src/utils/function.utils.ts | 39 +- 117 files changed, 3370 insertions(+), 3902 deletions(-) rename src/{setup.ts => _setup.ts} (70%) delete mode 100644 src/helpers/audio.helpers.test.ts delete mode 100644 src/router.ts delete mode 100644 src/services/backup.service.ts create mode 100644 src/store/_setup.ts create mode 100644 src/store/enhancers/_setup.ts delete mode 100644 src/store/enhancers/index.ts create mode 100644 src/store/enhancers/storage.enhancer.ts create mode 100644 src/store/features/_setup.ts create mode 100644 src/store/features/actions.ts create mode 100644 src/store/features/beatmap.slice.ts create mode 100644 src/store/features/bookmarks.slice.ts create mode 100644 src/store/features/editor.thunks.ts delete mode 100644 src/store/features/editor/beatmap.slice.ts delete mode 100644 src/store/features/editor/index.ts delete mode 100644 src/store/features/editor/lightshow.slice.ts create mode 100644 src/store/features/entities.thunks.ts delete mode 100644 src/store/features/entities/beatmap/bombs.slice.ts delete mode 100644 src/store/features/entities/beatmap/index.ts delete mode 100644 src/store/features/entities/beatmap/notes.slice.ts delete mode 100644 src/store/features/entities/beatmap/obstacles.slice.ts delete mode 100644 src/store/features/entities/editor/bookmarks.slice.ts delete mode 100644 src/store/features/entities/editor/index.ts delete mode 100644 src/store/features/entities/index.ts delete mode 100644 src/store/features/entities/lightshow/basic.slice.ts delete mode 100644 src/store/features/entities/lightshow/boost.slice.ts delete mode 100644 src/store/features/entities/lightshow/index.ts create mode 100644 src/store/features/events.slice.ts delete mode 100644 src/store/features/index.ts create mode 100644 src/store/features/lightshow.slice.ts create mode 100644 src/store/features/objects.slice.ts create mode 100644 src/store/features/playback.thunks.ts create mode 100644 src/store/features/selectors.ts delete mode 100644 src/store/helpers.ts create mode 100644 src/store/helpers/editor.helpers.ts create mode 100644 src/store/helpers/route.helpers.ts create mode 100644 src/store/helpers/selectors.ts create mode 100644 src/store/middleware/_setup.ts delete mode 100644 src/store/middleware/demo.middleware.ts delete mode 100644 src/store/middleware/index.ts create mode 100644 src/store/middleware/shared.middleware.ts delete mode 100644 src/store/middleware/storage.middleware.ts delete mode 100644 src/store/setup.ts create mode 100644 src/store/types.ts create mode 100644 src/store/utils/guards.utils.ts create mode 100644 src/store/utils/thunk.utils.ts diff --git a/package.json b/package.json index 22ab06f8..672d9eb5 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "date-fns": "^4.1.0", "fflate": "^0.8.2", "file-saver": "^2.0.5", + "history-adapter": "^2.1.1", "idb": "^8.0.3", "lucide-react": "^1.0.1", "postprocessing": "^6.39.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b268b9a..1e8929be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: file-saver: specifier: ^2.0.5 version: 2.0.5 + history-adapter: + specifier: ^2.1.1 + version: 2.1.1(@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)) idb: specifier: ^8.0.3 version: 8.0.3 @@ -3359,6 +3362,15 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + history-adapter@2.1.1: + resolution: {integrity: sha512-a1JcX59psNFMWGeMW9qei6SImcgl+RTmGCNoX1n88gwdcFpBBkEzciHxiM/m4qeqFkZPl1fafXIu5FKK90Uv8w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@reduxjs/toolkit': ^1.9.7 || ^2 + peerDependenciesMeta: + '@reduxjs/toolkit': + optional: true + hls.js@1.6.16: resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} @@ -3395,6 +3407,9 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + immer@11.1.8: resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} @@ -9386,6 +9401,12 @@ snapshots: property-information: 7.2.0 space-separated-tokens: 2.0.2 + history-adapter@2.1.1(@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)): + dependencies: + immer: 10.2.0 + optionalDependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) + hls.js@1.6.16: {} hono@4.12.24: {} @@ -9416,6 +9437,8 @@ snapshots: immediate@3.0.6: {} + immer@10.2.0: {} + immer@11.1.8: {} indent-string@2.1.0: diff --git a/src/setup.ts b/src/_setup.ts similarity index 70% rename from src/setup.ts rename to src/_setup.ts index 80c08254..101e56ba 100644 --- a/src/setup.ts +++ b/src/_setup.ts @@ -2,20 +2,16 @@ import { createToaster } from "@ark-ui/react/toast"; import { typeByExtension } from "@std/media-types/type-by-extension"; import { extname } from "@std/path/extname"; import { toPascalCase } from "@std/text/to-pascal-case"; -import { createBeatmap, loadDifficulty, loadInfo, setupLogger } from "bsmap"; -import { createStorage, type Driver, type StorageValue } from "unstorage"; +import { createBeatmap, loadDifficulty, loadInfo } from "bsmap"; +import { type CreateStorageOptions, createStorage, type StorageValue } from "unstorage"; import { BeatmapFilestore } from "./services/file.service"; import { createDriver, type LegacyStorageSchema } from "./services/storage.service"; -import { createAppStore } from "./store/setup"; +import { createAppStore } from "./store/_setup"; import type { App } from "./types"; -import { createLazySingleton } from "./utils"; +import { createSingleton } from "./utils"; -if (import.meta.env.DEV) { - setupLogger(); -} - -const appFileDriver = createDriver({ +export const createAppFileStorageDriver = createDriver({ name: "beat-mapper-files", version: 4, async upgrade(idb, _current, next, tx) { @@ -75,21 +71,16 @@ const appFileDriver = createDriver { - return new AudioContext({ latencyHint: "playback" }); +export const AppAudioContext = createSingleton((options: Omit) => { + return new AudioContext({ ...options, latencyHint: "playback" }); }); -export const { get: getAppBeatmapFilestore, setup: setupAppBeatmapFilestore } = createLazySingleton((driver?: Driver) => { +export const AppFilestore = createSingleton((options: CreateStorageOptions) => { return new BeatmapFilestore({ - storage: createStorage({ - driver: driver ?? appFileDriver({ name: "entries" }), - }), + storage: createStorage(options), }); }); -export const { get: getAppToaster, setup: setupAppToaster } = createLazySingleton((toaster?: ReturnType): NonNullable | null => { - if (toaster !== undefined) return toaster ?? null; - return createToaster({ placement: "bottom-end", overlap: true, max: 8 }); -}); +export const AppToaster = createSingleton(createToaster); -export const { get: getAppStore, setup: setupAppStore } = createLazySingleton(createAppStore); +export const AppStore = createSingleton(createAppStore); diff --git a/src/components/app/forms/create-beatmap.tsx b/src/components/app/forms/create-beatmap.tsx index 83d97634..7f172a55 100644 --- a/src/components/app/forms/create-beatmap.tsx +++ b/src/components/app/forms/create-beatmap.tsx @@ -8,7 +8,7 @@ import type { PropsWithChildren, ReactNode } from "react"; import { object } from "valibot"; import { createBeatmapCharacteristicListCollection, createBeatmapDifficultyListCollection } from "$/components/app/constants"; -import { useSetupContext } from "$/components/context"; +import { useToaster } from "$/components/context"; import { useAppForm } from "$/components/ui/compositions"; import { type createAppBeatmap, resolveBeatmapId } from "$/helpers/song.helpers"; import { useAppSelector } from "$/store/hooks"; @@ -30,8 +30,7 @@ function CreateBeatmapForm({ dialog, onSubmit, children }: Assign selectAllBeatmaps(state, sid)); @@ -54,7 +53,7 @@ function CreateBeatmapForm({ dialog, onSubmit, children }: Assign void; } function ExportMapForm({ onSubmit, ...rest }: Assign, Props>) { - const { toaster } = useSetupContext(); + const toaster = useToaster(); const { collection: serialVersionCollection } = useListCollection({ initialItems: ["4", "3", "2", "1"], @@ -46,7 +46,7 @@ function ExportMapForm({ onSubmit, ...rest }: Assign void; } function ImportMapForm({ dialog, onAccept, ...rest }: Assign, Props>) { - const { toaster } = useSetupContext(); + const toaster = useToaster(); const handleFileChange = useCallback( (details: FileUploadFileChangeDetails) => { @@ -23,7 +23,7 @@ function ImportMapForm({ dialog, onAccept, ...rest }: Assign - dispatch(updateTickType({ value: TICK_MAP.indexOf(details.valueAsString) }))} /> - - - dispatch(updateProcessingDelay({ value: details.valueAsNumber }))} /> + dispatch(updateTickType(TICK_MAP.indexOf(details.valueAsString)))} /> diff --git a/src/components/app/forms/settings/controls.tsx b/src/components/app/forms/settings/controls.tsx index c1a11ba0..0f654d06 100644 --- a/src/components/app/forms/settings/controls.tsx +++ b/src/components/app/forms/settings/controls.tsx @@ -21,7 +21,7 @@ function AppControlsSettings() { - dispatch(updateObstaclePlacementMode({ value: x.value as ObstaclePlacementMode }))} /> + dispatch(updateObstaclePlacementMode(x.value as ObstaclePlacementMode))} /> diff --git a/src/components/app/forms/settings/graphics.tsx b/src/components/app/forms/settings/graphics.tsx index 2c2534d3..3a077933 100644 --- a/src/components/app/forms/settings/graphics.tsx +++ b/src/components/app/forms/settings/graphics.tsx @@ -13,10 +13,10 @@ function AppGraphicsSettings() { - dispatch(updateRenderScale({ value: details.value[0] }))} /> + dispatch(updateRenderScale(details.value[0]))} /> - dispatch(updateBloomEnabled({ checked: details.checked }))} /> + dispatch(updateBloomEnabled(details.checked))} /> diff --git a/src/components/app/forms/settings/user.tsx b/src/components/app/forms/settings/user.tsx index 8c34a739..97546a4b 100644 --- a/src/components/app/forms/settings/user.tsx +++ b/src/components/app/forms/settings/user.tsx @@ -12,7 +12,7 @@ function AppUserSettings() { - dispatch(updateUsername({ value: details.valueAsString }))} /> + dispatch(updateUsername(details.valueAsString))} /> diff --git a/src/components/app/forms/update-beatmap.tsx b/src/components/app/forms/update-beatmap.tsx index acce4b1f..eb80540f 100644 --- a/src/components/app/forms/update-beatmap.tsx +++ b/src/components/app/forms/update-beatmap.tsx @@ -9,7 +9,7 @@ import { array, endsWith, type GenericSchema, gtValue, null_, number, object, pi import { ENVIRONMENT_OVERRIDE_COLLECTION } from "$/components/app/constants"; import { CreateBeatmapForm } from "$/components/app/forms"; -import { useSetupContext } from "$/components/context"; +import { useToaster } from "$/components/context"; import { Interleave } from "$/components/ui/atoms"; import { AlertDialogProvider, Button, Collapsible, Dialog, Heading, RouterLink, Stat, useAppForm } from "$/components/ui/compositions"; import { addColorScheme, copyBeatmap, removeBeatmap, updateBeatmap } from "$/store/actions"; @@ -40,10 +40,10 @@ function UpdateBeatmapForm({ bid }: Props) { const { sid } = useParams({ from: "/_/edit/$sid/$bid/_" }); const { view } = useRouteContext({ from: "/_/edit/$sid/$bid/_" }); - const { toaster } = useSetupContext(); + const navigate = useNavigate(); + const toaster = useToaster(); const dispatch = useAppDispatch(); - const navigate = useNavigate(); const bpm = useAppSelector((state) => selectBpm(state, sid)); const beatmaps = useAppSelector((state) => selectBeatmaps(state, sid)); const lightshowIds = useAppSelector((state) => selectLightshowIds(state, sid)); @@ -86,7 +86,7 @@ function UpdateBeatmapForm({ bid }: Props) { formApi.reset(value); } catch (error) { - toaster?.error({ description: `Could not update beatmap: ${error instanceof Error ? error.message : "See console for more information."}` }); + toaster.error({ description: `Could not update beatmap: ${error instanceof Error ? error.message : "See console for more information."}` }); return console.error(error); } }, @@ -109,7 +109,7 @@ function UpdateBeatmapForm({ bid }: Props) { // Don't let the user delete the last difficulty! const remainingDifficultyIds = Object.keys(mutableDifficultiesCopy); if (remainingDifficultyIds.length === 0) { - return toaster?.error({ + return toaster.error({ id: "last-difficulty", description: "Sorry, you cannot delete the only remaining difficulty! Please create another difficulty first.", }); diff --git a/src/components/app/forms/update-song.tsx b/src/components/app/forms/update-song.tsx index fd4b6a31..6a8a3cfb 100644 --- a/src/components/app/forms/update-song.tsx +++ b/src/components/app/forms/update-song.tsx @@ -5,11 +5,11 @@ import { endsWith, type GenericSchema, gtValue, minValue, number, object, pipe, import { COVER_ART_FILE_ACCEPT_TYPE, ENVIRONMENT_COLLECTION, SONG_FILE_ACCEPT_TYPE } from "$/components/app/constants"; import { useLocalFileMutation, useLocalFileQuery } from "$/components/app/hooks/local-file.hooks"; -import { useSetupContext } from "$/components/context"; +import { useToaster } from "$/components/context"; import { Audio, Field, FileUpload, useAppForm } from "$/components/ui/compositions"; import { remuxImageToSquare } from "$/helpers/file.helpers"; import { BeatmapFilestore } from "$/services/file.service"; -import { updateSong } from "$/store/actions"; +import { loadSongFile, updateSong } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; import { selectSongById, selectSongVolume } from "$/store/selectors"; @@ -39,7 +39,7 @@ const SCHEMA = object({ function UpdateSongForm() { const { sid } = useParams({ from: "/_/edit/$sid/$bid/_" }); - const { filestore, toaster } = useSetupContext(); + const toaster = useToaster(); const dispatch = useAppDispatch(); const song = useAppSelector((state) => selectSongById(state, sid)); @@ -56,12 +56,14 @@ function UpdateSongForm() { const { mutate: handleAcceptSongFile } = useLocalFileMutation(BeatmapFilestore.resolveFilename(sid, "song", {}), { onSuccess: () => { - toaster?.success({ id: "song-file-accepted", description: "Successfully updated song file!" }); + toaster.success({ id: "song-file-accepted", description: "Successfully updated song file!" }); + // reload the song file to reconcile the redux store + dispatch(loadSongFile({ songId: sid })); }, }); const { mutate: handleAcceptCoverArtFile } = useLocalFileMutation(BeatmapFilestore.resolveFilename(sid, "cover", {}), { onSuccess: () => { - toaster?.success({ id: "cover-art-file-accepted", description: "Successfully updated cover art file!" }); + toaster.success({ id: "cover-art-file-accepted", description: "Successfully updated cover art file!" }); }, }); @@ -85,24 +87,10 @@ function UpdateSongForm() { }, onSubmit: async ({ value, formApi }) => { try { - const newSongObject = { ...song, ...value }; - - if (acceptedCoverArtFile) { - const { filename: coverArtFilename } = await filestore.saveCoverArtFile(sid, acceptedCoverArtFile[0]); - newSongObject.coverArtFilename = coverArtFilename; - } - - if (acceptedSongFile) { - const { filename: songFilename } = await filestore.saveSongFile(sid, acceptedSongFile[0]); - newSongObject.songFilename = songFilename; - } - - // Update our redux state - dispatch(updateSong({ songId: sid, songFile: acceptedSongFile?.[0], changes: newSongObject })); - + dispatch(updateSong({ songId: sid, changes: value })); formApi.reset(value); } catch (error) { - toaster?.error({ description: `Could not update song: ${error instanceof Error ? error.message : "See console for more information."}` }); + toaster.error({ description: `Could not update song: ${error instanceof Error ? error.message : "See console for more information."}` }); return console.error(error); } }, diff --git a/src/components/app/hooks/local-file.hooks.ts b/src/components/app/hooks/local-file.hooks.ts index 731e3dcb..96688d0e 100644 --- a/src/components/app/hooks/local-file.hooks.ts +++ b/src/components/app/hooks/local-file.hooks.ts @@ -1,12 +1,12 @@ import { type UseQueryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useSetupContext } from "$/components/context"; +import { useBeatmapFilestore } from "$/components/context"; interface UseLocalFileQueryOptions extends Omit, "queryFn"> { transformFile?: (file: File) => Promise | T; } export function useLocalFileQuery(filename: string, { ...rest }: UseLocalFileQueryOptions) { - const { filestore } = useSetupContext(); + const filestore = useBeatmapFilestore(); return useQuery({ ...rest, @@ -21,9 +21,8 @@ export function useLocalFileQuery(filename: string, { ...rest }: UseLo } export function useLocalFileMutation(filename: string, options: { onSuccess: () => void }) { - const { filestore } = useSetupContext(); - const client = useQueryClient(); + const filestore = useBeatmapFilestore(); return useMutation({ mutationFn: async (file: File) => { diff --git a/src/components/app/layouts/event-grid/machine.ts b/src/components/app/layouts/event-grid/machine.ts index 82cedc23..5c6c9cdb 100644 --- a/src/components/app/layouts/event-grid/machine.ts +++ b/src/components/app/layouts/event-grid/machine.ts @@ -1,3 +1,4 @@ +import type { EntityId } from "@reduxjs/toolkit"; import { shuffle } from "@std/random/shuffle"; import { createMachine, type MachineSchema, type Params, type Service } from "@zag-js/core"; import { normalizeProps } from "@zag-js/react"; @@ -39,6 +40,7 @@ export interface EventGridSchema extends MachineSchema { refs: { mouseDownAt: { x: number; y: number; button: number } | null; hoveredTrackId: number | null; + lastPlacedId: string | null; norm: number; }; computed: { @@ -84,6 +86,7 @@ export const machine = createMachine({ return { mouseDownAt: null, hoveredTrackId: null, + lastPlacedId: null, norm: 0, }; }, @@ -195,13 +198,14 @@ export const machine = createMachine({ }); export interface IEventPlacementActions { - onCreate: (time: number, norm: number) => App.IWrapEditorObject; - onPlace: (data: App.IWrapEditorObject, isBulk?: boolean) => void; - onDelete: (data: App.IWrapEditorObject, isBulk?: boolean) => void; - onSelect: (data: App.IWrapEditorObject) => void; - onDeselect: (data: App.IWrapEditorObject) => void; - onPick?: (data: App.IWrapEditorObject) => void; - onWheel?: (data: App.IWrapEditorObject, delta: number) => void; + selectId: (data: T) => EntityId; + onCreate: (time: number, norm: number) => T; + onPlace: (data: T, id: EntityId) => void; + onDelete: (data: T, id: EntityId) => void; + onSelect: (data: T, id: EntityId) => void; + onDeselect: (data: T, id: EntityId) => void; + onPick?: (data: T, id: EntityId) => void; + onWheel?: (data: T, id: EntityId, delta: number) => void; } export function connect({ scope, send, prop, context, refs, computed }: Service, normalize = normalizeProps) { @@ -300,17 +304,24 @@ export function connect({ scope, send, prop, context, refs, computed }: Service< if (isPlaceMode && event.button === 0) { const data = actions.onCreate(context.get("pointer"), refs.get("norm")); - return actions.onPlace?.(data, false); + const id = actions.selectId(data); + refs.set("lastPlacedId", id.toString()); + return actions.onPlace?.(data, id); } }, onPointerUp: () => { refs.set("norm", 0); refs.set("hoveredTrackId", null); + refs.set("lastPlacedId", null); }, onPointerMove: () => { if (isPlaceMode && activeButton === 0 && isHoveringTrack(trackId)) { const data = actions.onCreate(context.get("pointer"), refs.get("norm")); - return actions.onPlace?.(data, true); + const id = actions.selectId(data); + if (id !== refs.get("lastPlacedId")) { + refs.set("lastPlacedId", id.toString()); + return actions.onPlace?.(data, id); + } } }, onPointerOver: () => { @@ -331,28 +342,34 @@ export function connect({ scope, send, prop, context, refs, computed }: Service< style: { ...style, transform: `translateX(${centeredOffset}px)` }, onContextMenu: (e) => e.preventDefault(), onPointerDown: (ev) => { + const id = actions.selectId(data); + switch (ev.button) { case 0: { - return data.selected ? actions.onDeselect?.(data) : actions.onSelect?.(data); + return data.selected ? actions.onDeselect?.(data, id) : actions.onSelect?.(data, id); } case 1: { ev.preventDefault(); - return actions.onPick?.(data); + return actions.onPick?.(data, id); } case 2: { - return actions.onDelete?.(data, false); + return actions.onDelete?.(data, id); } } }, onPointerOver: () => { + const id = actions.selectId(data); + if (activeButton === 2) { - actions.onDelete?.(data, true); + actions.onDelete?.(data, id); } }, onWheel: (event: React.WheelEvent) => { + const id = actions.selectId(data); + if (event.altKey) { const delta = event.deltaY > 0 ? -1 : 1; - actions.onWheel?.(data, delta); + actions.onWheel?.(data, id, delta); } }, }); diff --git a/src/components/app/layouts/visualizer/markers.tsx b/src/components/app/layouts/visualizer/markers.tsx index 5304960b..8568ebd0 100644 --- a/src/components/app/layouts/visualizer/markers.tsx +++ b/src/components/app/layouts/visualizer/markers.tsx @@ -19,9 +19,8 @@ function AudioVisualizerMarkers({ markers, duration, offset, timeProcessor, onMa const reversed = markers.sort((a, b) => b.time - a.time); return reversed.map((marker) => { - const realTimeInSeconds = timeProcessor.toRealTime(marker.time); - const realTimeMs = realTimeInSeconds * 1000 + offset; - const offsetPercentage = (realTimeMs / duration) * 100; + const realTimeInSeconds = timeProcessor.toRealTime(marker.time) + offset; + const offsetPercentage = (realTimeInSeconds / duration) * 100; return { ...marker, offset: offsetPercentage }; }); }, [timeProcessor, markers, duration, offset]); diff --git a/src/components/app/templates/download/index.tsx b/src/components/app/templates/download/index.tsx index c8f7747f..b4a2b994 100644 --- a/src/components/app/templates/download/index.tsx +++ b/src/components/app/templates/download/index.tsx @@ -21,7 +21,7 @@ function Download() { Download Map Unfortunately, the demo map is not available for download.}> - dispatch(downloadMapFiles({ songId: sid, ...options }))} /> + dispatch(downloadMapFiles({ songId: sid, options }))} /> ); diff --git a/src/components/app/templates/editor/action-panel-groups/default.tsx b/src/components/app/templates/editor/action-panel-groups/default.tsx index e8b7289f..f57eebd3 100644 --- a/src/components/app/templates/editor/action-panel-groups/default.tsx +++ b/src/components/app/templates/editor/action-panel-groups/default.tsx @@ -1,4 +1,4 @@ -import { useParams, useRouteContext } from "@tanstack/react-router"; +import { useParams } from "@tanstack/react-router"; import type { MouseEventHandler } from "react"; import { createJumpToBeatPrompt, createQuickSelectPrompt } from "$/components/app/constants"; @@ -14,7 +14,6 @@ interface Props { } function DefaultActionPanelGroup({ handleGridConfigClick }: Props) { const { sid } = useParams({ from: "/_/edit/$sid/$bid/_" }); - const { view } = useRouteContext({ from: "/_/edit/$sid/$bid/_" }); const dispatch = useAppDispatch(); const canUndo = useAppSelector(selectObjectsCanUndo); @@ -34,36 +33,36 @@ function DefaultActionPanelGroup({ handleGridConfigClick }: Props) { if (typeof endBeat !== "number") { endBeat = Number.POSITIVE_INFINITY; } - dispatch(selectAllEntitiesInRange({ songId: sid, view: view, startBeat, endBeat })); - dispatch(jumpToBeat({ songId: sid, value: startBeat, pauseTrack: true })); + dispatch(selectAllEntitiesInRange({ startBeat, endBeat })); + dispatch(jumpToBeat({ value: startBeat })); }, }), ); const { trigger: triggerJumpToBeat } = usePrompt( createJumpToBeatPrompt({ render: ({ form }) => {(ctx) => }, - onSubmit: ({ value: { beatNum } }) => dispatch(jumpToBeat({ songId: sid, pauseTrack: true, value: beatNum })), + onSubmit: ({ value: { beatNum } }) => dispatch(jumpToBeat({ value: beatNum })), }), ); return ( - - - - - diff --git a/src/components/app/templates/editor/action-panel-groups/direction.tsx b/src/components/app/templates/editor/action-panel-groups/direction.tsx index 2775cf04..c22d1faa 100644 --- a/src/components/app/templates/editor/action-panel-groups/direction.tsx +++ b/src/components/app/templates/editor/action-panel-groups/direction.tsx @@ -20,31 +20,31 @@ function NoteDirectionActionPanelGroup() { return ( - - - - - - - - - diff --git a/src/components/app/templates/editor/action-panel-groups/grid-presets.tsx b/src/components/app/templates/editor/action-panel-groups/grid-presets.tsx index 65549731..c46e8e69 100644 --- a/src/components/app/templates/editor/action-panel-groups/grid-presets.tsx +++ b/src/components/app/templates/editor/action-panel-groups/grid-presets.tsx @@ -27,7 +27,7 @@ function GridPresetsActionPanelGroup() { "Load Grid Preset"}> - diff --git a/src/components/app/templates/editor/action-panel-groups/grid.tsx b/src/components/app/templates/editor/action-panel-groups/grid.tsx index 6d253ed1..dda134f3 100644 --- a/src/components/app/templates/editor/action-panel-groups/grid.tsx +++ b/src/components/app/templates/editor/action-panel-groups/grid.tsx @@ -25,7 +25,7 @@ function GridActionPanelGroup({ finishTweakingGrid }: Props) { validate: object({ slot: pipe(string(), nonEmpty()) }), render: ({ form }) => {(ctx) => }, onSubmit: ({ value }) => { - return dispatch(saveGridPreset({ songId: sid, presetSlot: value.slot })); + return dispatch(saveGridPreset({ slot: value.slot })); }, }); diff --git a/src/components/app/templates/editor/action-panel-groups/selection.tsx b/src/components/app/templates/editor/action-panel-groups/selection.tsx index 3b2eef1f..e35022f1 100644 --- a/src/components/app/templates/editor/action-panel-groups/selection.tsx +++ b/src/components/app/templates/editor/action-panel-groups/selection.tsx @@ -1,13 +1,13 @@ -import { useParams, useRouteContext } from "@tanstack/react-router"; +import { useRouteContext } from "@tanstack/react-router"; import { ArrowDownToLineIcon, ArrowUpToLineIcon, FlipHorizontal2Icon, FlipVertical2Icon } from "lucide-react"; import { type MouseEventHandler, useMemo } from "react"; import { ActionPanelGroup } from "$/components/app/layouts"; import { Show } from "$/components/ui/atoms"; import { Button, Tooltip } from "$/components/ui/compositions"; -import { deselectAllEntities, deselectAllEntitiesOfType, mirrorSelection, nudgeSelection } from "$/store/actions"; +import { deselectAllEntities, deselectAllObjectsOfType, mirrorSelection, nudgeSelection } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; -import { selectGridSize, selectSelectedBeatmapEntities } from "$/store/selectors"; +import { selectSelectedBeatmapEntities } from "$/store/selectors"; import { ObjectType } from "$/types"; import { StrikethroughOnHover, Text } from "$:styled-system/jsx"; @@ -30,44 +30,42 @@ function SelectionCount({ num, label, onClick }: CountProps) { } function SelectionActionPanelGroup() { - const { sid } = useParams({ from: "/_/edit/$sid/$bid/_" }); const { view } = useRouteContext({ from: "/_/edit/$sid/$bid/_" }); const dispatch = useAppDispatch(); const selectedEntities = useAppSelector((state) => selectSelectedBeatmapEntities(state, view)); - const grid = useAppSelector((state) => selectGridSize(state, sid)); return ( - {(items) => dispatch(deselectAllEntitiesOfType({ itemType: ObjectType.NOTE }))} />} - {(items) => dispatch(deselectAllEntitiesOfType({ itemType: ObjectType.BOMB }))} />} - {(items) => dispatch(deselectAllEntitiesOfType({ itemType: ObjectType.OBSTACLE }))} />} + {(items) => dispatch(deselectAllObjectsOfType({ itemType: ObjectType.NOTE }))} />} + {(items) => dispatch(deselectAllObjectsOfType({ itemType: ObjectType.BOMB }))} />} + {(items) => dispatch(deselectAllObjectsOfType({ itemType: ObjectType.OBSTACLE }))} />} - "Mirror selection horizontally"}> - "Mirror selection vertically"}> - "Nudge selection forwards"}> - "Nudge selection backwards"}> - diff --git a/src/components/app/templates/editor/action-panel-groups/tool.tsx b/src/components/app/templates/editor/action-panel-groups/tool.tsx index 8ccd1bd3..fd914415 100644 --- a/src/components/app/templates/editor/action-panel-groups/tool.tsx +++ b/src/components/app/templates/editor/action-panel-groups/tool.tsx @@ -20,22 +20,22 @@ function NoteToolActionPanelGroup() { "Left Color Note"}> - "Right Color Note"}> - "Bomb Note"}> - "Obstacle"}> - diff --git a/src/components/app/templates/editor/navigation-controls.tsx b/src/components/app/templates/editor/navigation-controls.tsx index 8f0bacb1..8602aa48 100644 --- a/src/components/app/templates/editor/navigation-controls.tsx +++ b/src/components/app/templates/editor/navigation-controls.tsx @@ -6,7 +6,7 @@ import { NavigationPanel } from "$/components/app/layouts"; import { Button, Select, Stat } from "$/components/ui/compositions"; import { SNAPPING_INCREMENTS } from "$/constants"; import { formatCursorPosition, formatCursorPositionInBeats } from "$/helpers/audio.helpers"; -import { jumpToEnd, jumpToStart, seekBackwards, seekForwards, togglePlayback, updateSnap } from "$/store/actions"; +import { jumpBackwards, jumpForwards, jumpToEnd, jumpToStart, togglePlayback, updateSnap } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; import { selectCursorPosition, selectCursorPositionInBeats, selectLoading, selectPlaying, selectSnap } from "$/store/selectors"; import { getMetaKeyLabel, roundToNearest } from "$/utils"; @@ -45,22 +45,22 @@ function EditorNavigationControls() { return ( - dispatch(updateSnap(Number.parseFloat(ev.value[0])))} /> - - - - - diff --git a/src/components/app/templates/editor/status-bar.tsx b/src/components/app/templates/editor/status-bar.tsx index 3619e14e..96a61902 100644 --- a/src/components/app/templates/editor/status-bar.tsx +++ b/src/components/app/templates/editor/status-bar.tsx @@ -3,9 +3,9 @@ import { BellIcon, BellOffIcon, BoxIcon, CuboidIcon, EyeClosedIcon, EyeIcon, Fas import { StatusBar } from "$/components/app/layouts"; import { Show } from "$/components/ui/atoms"; -import { updateBeatDepth, updateEventsEditorPreview, updateEventsEditorTrackHeight, updateEventsEditorTrackOpacity, updatePlaybackRate, updateSongVolume, updateTickVolume } from "$/store/actions"; +import { updateEventsEditorPreview, updateEventsEditorTrackHeight, updateEventsEditorTrackOpacity, updatePlaybackRate, updateSongVolume, updateTickVolume, updateTrackScale } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; -import { selectBeatDepth, selectEventsEditorPreview, selectEventsEditorTrackHeight, selectEventsEditorTrackOpacity, selectedTotalBombNotes, selectLoading, selectNoteDensity, selectPlaybackRate, selectSongVolume, selectTickVolume, selectTotalColorNotes, selectTotalObstacles } from "$/store/selectors"; +import { selectBeatDepth, selectEventsEditorPreview, selectEventsEditorTrackHeight, selectEventsEditorTrackOpacity, selectLoading, selectNoteDensity, selectPlaybackRate, selectSongVolume, selectTickVolume, selectTotalBombNotes, selectTotalColorNotes, selectTotalObstacles } from "$/store/selectors"; import { View } from "$/types"; import { pluralize } from "$/utils"; import { styled } from "$:styled-system/jsx"; @@ -20,7 +20,7 @@ function EditorStatusBar() { const songVolume = useAppSelector(selectSongVolume); const tickVolume = useAppSelector(selectTickVolume); const numOfBlocks = useAppSelector(selectTotalColorNotes); - const numOfMines = useAppSelector(selectedTotalBombNotes); + const numOfMines = useAppSelector(selectTotalBombNotes); const numOfObstacles = useAppSelector(selectTotalObstacles); const showLightingPreview = useAppSelector(selectEventsEditorPreview); const rowHeight = useAppSelector(selectEventsEditorTrackHeight); @@ -45,21 +45,21 @@ function EditorStatusBar() { {noteDensity.toFixed(2)} - dispatch(updateBeatDepth({ value: details.value[0] }))} /> - dispatch(updateTickVolume({ value: details.value[0] }))} /> + dispatch(updateTrackScale(details.value[0]))} /> + dispatch(updateTickVolume(details.value[0]))} /> - dispatch(updateEventsEditorPreview())} /> - dispatch(updateEventsEditorTrackHeight({ newHeight: details.value[0] }))} /> - dispatch(updateEventsEditorTrackOpacity({ newOpacity: details.value[0] }))} /> + dispatch(updateEventsEditorPreview(details.checked))} /> + dispatch(updateEventsEditorTrackHeight(details.value[0]))} /> + dispatch(updateEventsEditorTrackOpacity(details.value[0]))} /> - dispatch(updateTickVolume({ value: details.value[0] }))} /> + dispatch(updateTickVolume(details.value[0]))} /> - dispatch(updatePlaybackRate({ value: details.value[0] }))} /> - dispatch(updateSongVolume({ value: details.value[0] }))} /> + dispatch(updatePlaybackRate(details.value[0]))} /> + dispatch(updateSongVolume(details.value[0]))} /> ); diff --git a/src/components/app/templates/editor/visualizer.tsx b/src/components/app/templates/editor/visualizer.tsx index 01075cfa..eb24ebaa 100644 --- a/src/components/app/templates/editor/visualizer.tsx +++ b/src/components/app/templates/editor/visualizer.tsx @@ -27,9 +27,9 @@ function EditorAudioVisualizer() { const handleVisualizerClick = useCallback( (_: MouseEvent, offset: number) => { - dispatch(jumpToTime({ songId: sid, value: offset })); + dispatch(jumpToTime({ value: offset })); }, - [dispatch, sid], + [dispatch], ); const handleMarkerClick = useCallback( @@ -40,16 +40,16 @@ function EditorAudioVisualizer() { return dispatch(removeBookmark({ beatNum: time })); } default: { - return dispatch(jumpToBeat({ songId: sid, value: time })); + return dispatch(jumpToBeat({ value: time })); } } }, - [dispatch, sid], + [dispatch], ); return ( - + {(ref) => } {duration !== null && ( diff --git a/src/components/app/templates/events/basic-track.tsx b/src/components/app/templates/events/basic-track.tsx index 3f1ebb41..cfa2d311 100644 --- a/src/components/app/templates/events/basic-track.tsx +++ b/src/components/app/templates/events/basic-track.tsx @@ -7,21 +7,21 @@ import { EventGrid } from "$/components/app/layouts"; import { For } from "$/components/ui/atoms"; import { resolveColorForItem } from "$/helpers/colors.helpers"; import { deserializeBasicEventValue, isBasicLightEvent, isBasicValueEvent, resolveBasicEventColor, resolveBasicEventEffect, resolveEventId, serializeBasicEventValue } from "$/helpers/events.helpers"; -import { addBasicEvent, bulkAddBasicEvent, bulkRemoveEvent, deselectEvent, removeEvent, selectEvent, updateBasicEvent } from "$/store/actions"; +import { addBasicEvent, deselectBasicEvent, removeBasicEvent, selectBasicEvent, updateBasicEvent } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; import { selectAllBasicEventsForTrack, selectAllBoostEvents, + selectColorBoostAtBeat, selectColorScheme, selectCurrentLightStateForTrack, selectEditorOffsetInBeats, selectEnvironment, - selectEventEditorStartAndEndBeat, selectEventsEditorColor, selectEventsEditorMirrorLock, + selectEventsEditorStartAndEndBeat, selectEventsEditorTool, selectEventTracksForEnvironment, - selectToggleAtBeat, } from "$/store/selectors"; import { type App, BasicEventEffect, EventColor } from "$/types"; import { clamp, cycle, isColorDark, normalize, roundToNearest } from "$/utils"; @@ -63,7 +63,7 @@ const BasicEvent = memo(function BasicEvent({ data, actions }: { data: App.IBasi const api = EventGrid.useContext(); - const isEventBoosted = useAppSelector((state) => selectToggleAtBeat(state, { trackId: 5, beforeBeat: data.time + 0.001 })); + const isEventBoosted = useAppSelector((state) => selectColorBoostAtBeat(state, { beforeBeat: data.time + 0.001 })); const resolveEventStyle = useCallback( (data: IWrapBasicEvent) => { @@ -91,7 +91,7 @@ function BasicEventTrack({ trackId, ...rest }: Assign selectEventEditorStartAndEndBeat(state, sid)); + const { startBeat, endBeat } = useAppSelector((state) => selectEventsEditorStartAndEndBeat(state, sid)); const tracks = useAppSelector((state) => selectEventTracksForEnvironment(state, sid, bid)); const environment = useAppSelector((state) => selectEnvironment(state, sid, bid)); const basicEvents = useAppSelector((state) => selectAllBasicEventsForTrack(state, trackId)); @@ -147,12 +147,13 @@ function BasicEventTrack({ trackId, ...rest }: Assign>(() => { return { + selectId: resolveEventId, onCreate: resolveEventData, - onPlace: (data, isBulk) => dispatch((isBulk ? bulkAddBasicEvent : addBasicEvent)({ query: data, data: data, environment, areLasersLocked })), - onDelete: (data, isBulk) => dispatch((isBulk ? bulkRemoveEvent : removeEvent)({ query: data, environment, areLasersLocked })), - onSelect: (data) => dispatch(selectEvent({ query: data, environment, areLasersLocked })), - onDeselect: (data) => dispatch(deselectEvent({ query: data, environment, areLasersLocked })), - onPick: (data) => { + onPlace: (data, _) => dispatch(addBasicEvent({ data: data, environment, areLasersLocked })), + onDelete: (_, id) => dispatch(removeBasicEvent({ id, environment, areLasersLocked })), + onSelect: (_, id) => dispatch(selectBasicEvent({ id, environment, areLasersLocked })), + onDeselect: (_, id) => dispatch(deselectBasicEvent({ id, environment, areLasersLocked })), + onPick: (data, id) => { const MIRRORABLE_COLORS = Object.values(EventColor).slice(0, -1); switch (tracks[trackId].type) { @@ -160,14 +161,14 @@ function BasicEventTrack({ trackId, ...rest }: Assign { + onWheel: (data, id, delta) => { const step = (x: number, step: number) => { return x + step / delta; }; @@ -175,27 +176,27 @@ function BasicEventTrack({ trackId, ...rest }: Assign selectEventEditorStartAndEndBeat(state, sid)); + const { startBeat, endBeat } = useAppSelector((state) => selectEventsEditorStartAndEndBeat(state, sid)); const environment = useAppSelector((state) => selectEnvironment(state, sid, bid)); const boostEvents = useAppSelector((state) => selectAllBoostEvents(state)); @@ -57,12 +57,13 @@ function BoostEventTrack({ trackId, ...rest }: Assign>(() => { return { + selectId: resolveEventId, onCreate: resolveEventData, - onPlace: (data, isBulk) => dispatch((isBulk ? bulkAddBoostEvent : addBoostEvent)({ query: data, data: data, environment, areLasersLocked })), - onDelete: (data, isBulk) => dispatch((isBulk ? bulkRemoveEvent : removeEvent)({ query: data, environment, areLasersLocked })), - onSelect: (data) => dispatch(selectEvent({ query: data, environment, areLasersLocked })), - onDeselect: (data) => dispatch(deselectEvent({ query: data, environment, areLasersLocked })), - onWheel: (data, delta) => dispatch(updateBoostEvent({ query: data, environment, areLasersLocked, changes: { toggle: delta > 0 } })), + onPlace: (data) => dispatch(addBoostEvent({ data: data, environment, areLasersLocked })), + onDelete: (_, id) => dispatch(removeBoostEvent({ id, environment, areLasersLocked })), + onSelect: (_, id) => dispatch(selectBoostEvent({ id, environment, areLasersLocked })), + onDeselect: (_, id) => dispatch(deselectBoostEvent({ id, environment, areLasersLocked })), + onWheel: (_, id, delta) => dispatch(updateBoostEvent({ id, environment, areLasersLocked, changes: { toggle: delta > 0 } })), }; }, [dispatch, resolveEventData, environment, areLasersLocked]); diff --git a/src/components/app/templates/events/controls.tsx b/src/components/app/templates/events/controls.tsx index 6d41b0c3..65b2350a 100644 --- a/src/components/app/templates/events/controls.tsx +++ b/src/components/app/templates/events/controls.tsx @@ -7,7 +7,7 @@ import { EventEffectIcon } from "$/components/icons"; import { Button, Field, Toggle, ToggleGroup, Tooltip } from "$/components/ui/compositions"; import { ZOOM_LEVEL_MAX, ZOOM_LEVEL_MIN } from "$/constants"; import type { ColorResolverOptions } from "$/helpers/colors.helpers"; -import { decrementEventsEditorZoom, incrementEventsEditorZoom, updateEventsEditorColor, updateEventsEditorEditMode, updateEventsEditorMirrorLock, updateEventsEditorTool, updateEventsEditorWindowLock } from "$/store/actions"; +import { decrementEventsEditorZoomLevel, incrementEventsEditorZoomLevel, updateEventsEditorColor, updateEventsEditorEditMode, updateEventsEditorMirrorLock, updateEventsEditorTool, updateEventsEditorWindowLock } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; import { selectColorScheme, selectEventsEditorColor, selectEventsEditorEditMode, selectEventsEditorMirrorLock, selectEventsEditorTool, selectEventsEditorWindowLock, selectEventsEditorZoomLevel } from "$/store/selectors"; import { EventColor, EventEditMode, EventTool } from "$/types"; @@ -62,23 +62,23 @@ function EventGridControls({ ...rest }: ComponentProps) { - details.value.length > 0 && dispatch(updateEventsEditorEditMode({ editMode: details.value[0] as EventEditMode }))} /> + details.value.length > 0 && dispatch(updateEventsEditorEditMode(details.value[0] as EventEditMode))} /> - details.value.length > 0 && dispatch(updateEventsEditorColor({ color: details.value[0] as EventColor }))} /> + details.value.length > 0 && dispatch(updateEventsEditorColor(details.value[0] as EventColor))} /> - details.value.length > 0 && dispatch(updateEventsEditorTool({ tool: details.value[0] as EventTool }))} /> + details.value.length > 0 && dispatch(updateEventsEditorTool(details.value[0] as EventTool))} /> "Loop playback within the current event window"}> - dispatch(updateEventsEditorWindowLock())}> + dispatch(updateEventsEditorWindowLock(pressed))}> "Clone event placements for symmetrical tracks"}> - dispatch(updateEventsEditorMirrorLock())}> + dispatch(updateEventsEditorMirrorLock(pressed))}> @@ -88,10 +88,10 @@ function EventGridControls({ ...rest }: ComponentProps) { - - diff --git a/src/components/app/templates/events/grid.tsx b/src/components/app/templates/events/grid.tsx index 07bdf95f..79ef5541 100644 --- a/src/components/app/templates/events/grid.tsx +++ b/src/components/app/templates/events/grid.tsx @@ -18,10 +18,10 @@ import { selectDurationInBeats, selectEditorOffsetInBeats, selectEnvironment, - selectEventEditorStartAndEndBeat, selectEventsEditorCursor, selectEventsEditorEditMode, selectEventsEditorMirrorLock, + selectEventsEditorStartAndEndBeat, selectEventsEditorTrackHeight, selectEventTracksForEnvironment, selectLoading, @@ -43,7 +43,7 @@ function EventGridEditor({ ...rest }: Omit const snapTo = useAppSelector(selectSnap); const rowHeight = useAppSelector(selectEventsEditorTrackHeight); const areLasersLocked = useAppSelector(selectEventsEditorMirrorLock); - const { startBeat, numOfBeatsToShow } = useAppSelector((state) => selectEventEditorStartAndEndBeat(state, sid)); + const { startBeat, endBeat, numOfBeatsToShow } = useAppSelector((state) => selectEventsEditorStartAndEndBeat(state, sid)); const tracks = useAppSelector((state) => selectEventTracksForEnvironment(state, sid, bid)); const environment = useAppSelector((state) => selectEnvironment(state, sid, bid)); const cursorPositionInBeats = useAppSelector((state) => selectCursorPositionInBeats(state, sid)); @@ -78,14 +78,14 @@ function EventGridEditor({ ...rest }: Omit return dispatch(updateEventsEditorCursor({ selectedBeat })); }, onTrackHeightChange: ({ height: newHeight }) => { - return dispatch(updateEventsEditorTrackHeight({ newHeight })); + return dispatch(updateEventsEditorTrackHeight(newHeight)); }, onSelectionCommit: ({ selectionBoxInBeats }) => { const filteredTracks = allFilteredTracks.reduce((acc: ITrackDefinitions<{ id: number }>, track) => { acc[track.id] = track; return acc; }, {}); - return dispatch(drawEventSelectionBox({ songId: sid, tracks: filteredTracks, selectionBoxInBeats: selectionBoxInBeats })); + return dispatch(drawEventSelectionBox({ window: { startBeat, endBeat }, tracks: filteredTracks, selectionBoxInBeats: selectionBoxInBeats })); }, }); @@ -177,7 +177,7 @@ function EventGridEditor({ ...rest }: Omit Track Visibility - dispatch(jumpToBeat({ songId: sid, value: beat }))} /> + dispatch(jumpToBeat({ value: beat }))} /> ev.stopPropagation()}> diff --git a/src/components/app/templates/home/first-time.tsx b/src/components/app/templates/home/first-time.tsx index f74e460f..082b075c 100644 --- a/src/components/app/templates/home/first-time.tsx +++ b/src/components/app/templates/home/first-time.tsx @@ -1,10 +1,11 @@ +import { useNavigate } from "@tanstack/react-router"; import { CirclePlusIcon, DownloadIcon, PackageOpenIcon } from "lucide-react"; import { useCallback, useState } from "react"; -import { heroVideo } from "$/assets"; +import { demoFileUrl, heroVideo } from "$/assets"; import { CreateMapForm, ImportMapForm } from "$/components/app/forms"; import { Button, Dialog, Heading } from "$/components/ui/compositions"; -import { addSongFromFile, loadDemoMap } from "$/store/actions"; +import { addSongFromFile } from "$/store/actions"; import { useAppDispatch, useAppSelector } from "$/store/hooks"; import { selectSongIds } from "$/store/selectors"; import { styled, VStack, Wrap } from "$:styled-system/jsx"; @@ -14,12 +15,17 @@ function FirstTimeHome() { const dispatch = useAppDispatch(); const songIds = useAppSelector(selectSongIds); + const navigate = useNavigate(); + const [isLoadingDemo, setIsLoadingDemo] = useState(false); - const handleDemoClick = useCallback(() => { + const handleDemoClick = useCallback(async () => { setIsLoadingDemo(true); - dispatch(loadDemoMap()); - }, [dispatch]); + const file = await fetch(demoFileUrl).then((response) => response.blob()); + const { songId, beatmapId } = await dispatch(addSongFromFile({ file, options: { readonly: true } })).unwrap(); + navigate({ to: "/edit/$sid/$bid/notes", params: { sid: songId.toString(), bid: beatmapId.toString() } }); + setIsLoadingDemo(false); + }, [dispatch, navigate]); return ( @@ -43,7 +49,6 @@ function FirstTimeHome() { - {/** biome-ignore lint/suspicious/useIterableCallbackReturn: doesn't matter */} files.forEach((file) => void dispatch(addSongFromFile({ file, options: { currentSongIds: songIds } })))} />}>