import { Beat, deserialise as deserialiseBeat } from "@/Beat"; import { inject, computed, type InjectionKey, onScopeDispose, ref, shallowRef, triggerRef, watch, nextTick, getCurrentInstance } from "vue"; import { z } from "zod"; import { Bound } from "./utils"; export const DrumSlayerSaveSchema = z.object({ orientation: z.union([z.literal("horizontal"), z.literal("vertical")]), activeBeatIndex: z.number(), beats: z.array(z.unknown()), }); export type DrumSlayerSave = z.infer; function createDefaultMainBeatGroup(): Beat { const defaultSettings = { barCount: 2, isLooping: false, timeSigUp: 8, }; const mainBeatGroup = Beat.asScoped(defaultSettings); mainBeatGroup.addTrack({ name: "Crash" }); mainBeatGroup.addTrack({ name: "Hi-Hat" }); mainBeatGroup.addTrack({ name: "Snare" }); mainBeatGroup.addTrack({ name: "Kick" }); return mainBeatGroup; } export class BeatStore extends Bound { private saveDirtyGlobal = ref(false); beats = shallowRef([ createDefaultMainBeatGroup() ]); saveDirty = computed(() => this.saveDirtyGlobal.value || this.beats.value.reduce((last, beat) => beat.saveDirty.value || last, false)); activeBeatIndex = ref(0); activeBeat = computed(() => this.beats.value[this.activeBeatIndex.value] ?? null); orientation = ref<"horizontal" | "vertical">("horizontal"); constructor() { super(); watch([this.activeBeatIndex, this.orientation, this.beats], () => { this.saveDirtyGlobal.value = true; }); const saveInterval = setInterval(() => this.saveDirtyGlobal.value && this.save("localStorage"), 5 * 60 * 1000); onScopeDispose(() => clearInterval(saveInterval)); window.addEventListener("beforeunload", (e) => { if (this.saveDirty.value) { e.preventDefault(); e.returnValue = true; } }); const savedItem = localStorage.getItem("drum-slayer-save"); if (savedItem) { const serial = JSON.parse(savedItem); this.beats.value = [createDefaultMainBeatGroup()]; this.loadFromSave(serial); } } resetActiveBeat(): void { const current = this.activeBeat.value; this.beats.value[this.activeBeatIndex.value] = createDefaultMainBeatGroup(); current?.destroy(); triggerRef(this.beats); } removeBeat(index: number): void { const beat = this.beats.value[index]; this.beats.value.splice(index, 1); if (this.activeBeatIndex.value === index) { this.activeBeatIndex.value = 0; } if (this.beats.value.length === 0) { this.addNewBeat(); this.activeBeatIndex.value = 0; } triggerRef(this.beats); nextTick(() => { beat?.destroy(); }); } addNewBeat(config?: unknown): number { let newBeat: Beat | null = null; if (config) { newBeat = deserialiseBeat(config); if (!newBeat) { return -1; } } else { newBeat = createDefaultMainBeatGroup(); } const newIndex = this.beats.value.push(newBeat); triggerRef(this.beats); this.activeBeatIndex.value = newIndex - 1; return newIndex - 1; } save(destination: "localStorage"): void { if (destination === "localStorage") { const serials = this.beats.value.map(beat => beat.serialise()); localStorage.setItem("drum-slayer-save", JSON.stringify({ beats: serials, activeBeatIndex: this.activeBeatIndex.value, orientation: this.orientation.value ?? "horizontal", } satisfies DrumSlayerSave)); for (const beat of this.beats.value) { beat.saveDirty.value = false; } this.saveDirtyGlobal.value = false; } } loadFromSave(source: unknown): void { this.beats.value.length = 0; const parse = DrumSlayerSaveSchema.safeParse(source); if (parse.success) { parse.data.beats.forEach((beat: unknown) => { const deserialisedBeat = deserialiseBeat(beat); if (deserialisedBeat) { this.beats.value.push(deserialisedBeat); } }); this.activeBeatIndex.value = parse.data.activeBeatIndex; this.orientation.value = parse.data.orientation; } if (this.beats.value.length === 0) { this.resetActiveBeat(); } nextTick(() => this.saveDirtyGlobal.value = false); } bakeAll(): void { this.activeBeat.value?.bakeLoops(); } } const BeatStoreKey = Symbol("BeatStore") as InjectionKey; export function useBeatStore(): BeatStore { return inject(BeatStoreKey, () => { const store = new BeatStore(); getCurrentInstance()?.appContext?.app.provide(BeatStoreKey, store); return store; }, true); }