Files
arne-drums/src/BeatStore.ts
2024-06-01 20:31:06 +02:00

147 lines
5.0 KiB
TypeScript

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<typeof DrumSlayerSaveSchema>;
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<Beat[]>([ createDefaultMainBeatGroup() ]);
saveDirty = computed(() => this.saveDirtyGlobal.value || this.beats.value.reduce((last, beat) => beat.saveDirty.value || last, false));
activeBeatIndex = ref(0);
activeBeat = computed<Beat | null>(() => 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<BeatStore>;
export function useBeatStore(): BeatStore {
return inject(BeatStoreKey, () => {
const store = new BeatStore();
getCurrentInstance()?.appContext?.app.provide(BeatStoreKey, store);
return store;
}, true);
}