diff --git a/assets/svgs/download.svg b/assets/svgs/download.svg new file mode 100644 index 0000000..80a5817 --- /dev/null +++ b/assets/svgs/download.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/AppState.ts b/src/AppState.ts index df3bc4d..936cbcd 100644 --- a/src/AppState.ts +++ b/src/AppState.ts @@ -1,4 +1,4 @@ -import { inject, type InjectionKey, ref } from 'vue'; +import { inject, type InjectionKey, ref } from "vue"; export type UITool = | "track-unit-type" @@ -6,9 +6,10 @@ export type UITool = | "sticking"; export function createAppStateStore() { - const selectedTool = ref('track-unit-type'); + const selectedTool = ref("track-unit-type"); const activeTrackUnitType = ref(0); const activeStickingType = ref(1); + const unitMouseStart = ref(null); const selectingUnits = ref(false); const deselectingUnits = ref(false); @@ -18,13 +19,14 @@ export function createAppStateStore() { selectedTool, activeTrackUnitType, activeStickingType, + unitMouseStart, }; } export type AppStateStore = ReturnType; -export const AppStateStoreKey = Symbol('AppStateStore') as InjectionKey; +export const AppStateStoreKey = Symbol("AppStateStore") as InjectionKey; export function useAppStateStore(): AppStateStore { return inject(AppStateStoreKey, createAppStateStore, true); diff --git a/src/Beat.ts b/src/Beat.ts index db1a38b..102619e 100644 --- a/src/Beat.ts +++ b/src/Beat.ts @@ -2,7 +2,12 @@ import { deserialise as deserialiseTrack, type TrackInitOptions, createTrack, is import { greatestCommonDivisor, isPosInt } from "@/utils"; import { watch, computed, effectScope, ref, shallowRef, triggerRef } from "vue"; +export interface BeatManager { + notifyChange(): void; +} + type BeatGroupInitOptions = { + manager: BeatManager, barCount: number; isLooping: boolean; timeSigUp: number; @@ -33,11 +38,12 @@ function isBeatSerial(serial: Record): serial is BeatSerial { typeof serial.barSettingsLocked === "boolean"; } -export function deserialise(serial: {}) { +export function deserialise(serial: {}, manager: BeatManager) { if (!isBeatSerial(serial)) { throw new Error("Not a valid beat serial"); } const newBeat = createBeat({ + manager, loopLength: serial.globalLoopLength, barCount: serial.barCount, isLooping: serial.globalIsLooping, @@ -46,7 +52,7 @@ export function deserialise(serial: {}) { useAutoBeatLength: serial.useAutoBeatLength, }); serial.tracks.forEach(trackSerial => { - const track = deserialiseTrack(trackSerial); + const track = deserialiseTrack(trackSerial, manager); if (track) newBeat.tracks.value.push(track); }); return newBeat; @@ -55,6 +61,7 @@ export function deserialise(serial: {}) { export function createBeat(opts: BeatGroupInitOptions) { const scope = effectScope(); return scope.run(() => { + const manager = opts.manager; const tracks = shallowRef([]); const barCountInternal = ref(opts.barCount ?? 4); const barCount = computed({ @@ -124,17 +131,18 @@ export function createBeat(opts: BeatGroupInitOptions) { tracks.value[trackIndex2] = track1; } - function addTrack(options?: TrackInitOptions): Track | null { + function addTrack(opts?: Omit): Track | null { let newTrack: Track | null; - options = { + const options: TrackInitOptions = { + manager, bars: barCount.value, isLooping: globalIsLooping.value, loopLength: globalLoopLength.value, - ...options, + ...opts, timeSig: { up: timeSigUp.value, down: 4, - ...options?.timeSig ?? {}, + ...opts?.timeSig ?? {}, }, }; newTrack = createTrack(options) ?? null; @@ -172,6 +180,10 @@ export function createBeat(opts: BeatGroupInitOptions) { } as const; } + watch(tracks, () => { + manager.notifyChange(); + }); + watch([barCount, timeSigUp], ([newBarCount, newTimeSigUp]) => { for (const track of tracks.value) { track.barCount.value = newBarCount; diff --git a/src/BeatStore.ts b/src/BeatStore.ts index 2ad1493..bf81f14 100644 --- a/src/BeatStore.ts +++ b/src/BeatStore.ts @@ -1,8 +1,9 @@ -import { type Beat, createBeat, deserialise as deserialiseBeat } from "@/Beat"; -import { inject, computed, type InjectionKey, ref, shallowRef, triggerRef, watch } from "vue"; +import { type Beat, createBeat, deserialise as deserialiseBeat, type BeatManager } from "@/Beat"; +import { inject, computed, type InjectionKey, ref, shallowRef, triggerRef, watch, onScopeDispose } from "vue"; -function defaultMainBeatGroup(): Beat { +function defaultMainBeatGroup(manager: BeatManager): Beat { const defaultSettings = { + manager, barCount: 2, isLooping: false, timeSigUp: 8, @@ -16,15 +17,23 @@ function defaultMainBeatGroup(): Beat { } export function createBeatStore() { - const beats = shallowRef([ defaultMainBeatGroup() ]); + const saveDirty = ref(false); + + const manager = { + notifyChange() { + saveDirty.value = true; + }, + }; + + const beats = shallowRef([ defaultMainBeatGroup(manager) ]); const activeBeatIndex = ref(0); const activeBeat = computed(() => beats.value[activeBeatIndex.value] ?? null); const autoSave = ref(true); - const orientation = ref<'horizontal' | 'vertical'>('horizontal'); + const orientation = ref<"horizontal" | "vertical">("horizontal"); function resetActiveBeat(): void { const current = activeBeat.value; - beats.value[activeBeatIndex.value] = defaultMainBeatGroup(); + beats.value[activeBeatIndex.value] = defaultMainBeatGroup(manager); current?.destroy(); triggerRef(beats); } @@ -37,7 +46,7 @@ export function createBeatStore() { } function addNewBeat(): void { - const newBeat = defaultMainBeatGroup(); + const newBeat = defaultMainBeatGroup(manager); beats.value.push(newBeat); if (autoSave.value) { save("localStorage"); @@ -51,8 +60,9 @@ export function createBeatStore() { localStorage.setItem("drum-slayer-save", JSON.stringify({ beats: serials, activeBeatIndex: activeBeatIndex.value, - orientation: orientation.value ?? 'horizontal', + orientation: orientation.value ?? "horizontal", })); + saveDirty.value = false; } } @@ -62,7 +72,7 @@ export function createBeatStore() { && (typeof source.activeBeatIndex === "number" || typeof source.activeBeatIndex === "undefined") && typeof source.orientation === "string") { try { - source.beats.forEach((beat: any) => beats.value.push(deserialiseBeat(beat))); + source.beats.forEach((beat: any) => beats.value.push(deserialiseBeat(beat, manager))); if (typeof source.activeBeatIndex === "number") { activeBeatIndex.value = source.activeBeatIndex; } @@ -73,6 +83,7 @@ export function createBeatStore() { } else { resetActiveBeat(); } + saveDirty.value = false; } function bakeAll(): void { @@ -80,21 +91,35 @@ export function createBeatStore() { } watch([activeBeatIndex, orientation, beats], () => { - save('localStorage'); + save("localStorage"); }); - const savedItem = localStorage.getItem('drum-slayer-save'); + const savedItem = localStorage.getItem("drum-slayer-save"); if (savedItem) { const serial = JSON.parse(savedItem); - beats.value = [defaultMainBeatGroup()]; + beats.value = [defaultMainBeatGroup(manager)]; loadFromSave(serial); } + const saveInterval = setInterval(() => saveDirty && save("localStorage"), 5 * 60 * 1000); + + watch(saveDirty, () => console.log(saveDirty.value)); + + onScopeDispose(() => clearInterval(saveInterval)); + + window.addEventListener("beforeunload", (e) => { + if (saveDirty) { + e.preventDefault(); + e.returnValue = true; + } + }); + return { beats, activeBeatIndex, activeBeat, autoSave, + saveDirty, orientation, save, @@ -107,7 +132,7 @@ export function createBeatStore() { export type BeatStore = ReturnType; -export const BeatStoreKey = Symbol('BeatStore') as InjectionKey; +export const BeatStoreKey = Symbol("BeatStore") as InjectionKey; export function useBeatStore(): BeatStore { return inject(BeatStoreKey, createBeatStore, true); diff --git a/src/Track.ts b/src/Track.ts index 1d0272a..0f59e99 100644 --- a/src/Track.ts +++ b/src/Track.ts @@ -1,7 +1,9 @@ import { isPosInt } from "@/utils"; import { ref, shallowRef, computed, watch, reactive, triggerRef, effectScope } from "vue"; +import type { BeatManager } from "./Beat"; export type TrackInitOptions = { + manager: BeatManager, timeSig?: { up: number, down: number, @@ -52,7 +54,7 @@ function isTrackSerial(serial: any): serial is TrackSerial { return correctTypes && serial.units.isOn.length === serial.units.type.length; } -export function deserialise(serial: Record) { +export function deserialise(serial: Record, manager: BeatManager) { if (!isTrackSerial(serial)) { throw new Error("Invalid track serial."); } @@ -62,6 +64,7 @@ export function deserialise(serial: Record) { stickingType: serial.units.stickingType[i] ?? 0, })); return createTrack({ + manager, bars: serial.barCount, isLooping: serial.looping, loopLength: serial.loopLength, @@ -236,6 +239,8 @@ export function createTrack(options: TrackInitOptions) { triggerRef(unitRecord); } + const manager = options.manager; + watch(unitRecord, () => manager.notifyChange()); watch([barCount, timeSigDown, timeSigUp], () => updateTrackUnitLength(), { immediate: true }); return { diff --git a/src/ui/Root/Root.vue b/src/ui/Root/Root.vue index d6849d5..2406974 100644 --- a/src/ui/Root/Root.vue +++ b/src/ui/Root/Root.vue @@ -2,62 +2,63 @@
-
-
+