feat: cleaned up mouse behaviour and saving
This commit is contained in:
4
assets/svgs/download.svg
Normal file
4
assets/svgs/download.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
@@ -1,4 +1,4 @@
|
|||||||
import { inject, type InjectionKey, ref } from 'vue';
|
import { inject, type InjectionKey, ref } from "vue";
|
||||||
|
|
||||||
export type UITool =
|
export type UITool =
|
||||||
| "track-unit-type"
|
| "track-unit-type"
|
||||||
@@ -6,9 +6,10 @@ export type UITool =
|
|||||||
| "sticking";
|
| "sticking";
|
||||||
|
|
||||||
export function createAppStateStore() {
|
export function createAppStateStore() {
|
||||||
const selectedTool = ref<UITool>('track-unit-type');
|
const selectedTool = ref<UITool>("track-unit-type");
|
||||||
const activeTrackUnitType = ref(0);
|
const activeTrackUnitType = ref(0);
|
||||||
const activeStickingType = ref(1);
|
const activeStickingType = ref(1);
|
||||||
|
const unitMouseStart = ref<string | null>(null);
|
||||||
const selectingUnits = ref(false);
|
const selectingUnits = ref(false);
|
||||||
const deselectingUnits = ref(false);
|
const deselectingUnits = ref(false);
|
||||||
|
|
||||||
@@ -18,13 +19,14 @@ export function createAppStateStore() {
|
|||||||
selectedTool,
|
selectedTool,
|
||||||
activeTrackUnitType,
|
activeTrackUnitType,
|
||||||
activeStickingType,
|
activeStickingType,
|
||||||
|
unitMouseStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type AppStateStore = ReturnType<typeof createAppStateStore>;
|
export type AppStateStore = ReturnType<typeof createAppStateStore>;
|
||||||
|
|
||||||
export const AppStateStoreKey = Symbol('AppStateStore') as InjectionKey<AppStateStore>;
|
export const AppStateStoreKey = Symbol("AppStateStore") as InjectionKey<AppStateStore>;
|
||||||
|
|
||||||
export function useAppStateStore(): AppStateStore {
|
export function useAppStateStore(): AppStateStore {
|
||||||
return inject(AppStateStoreKey, createAppStateStore, true);
|
return inject(AppStateStoreKey, createAppStateStore, true);
|
||||||
|
|||||||
24
src/Beat.ts
24
src/Beat.ts
@@ -2,7 +2,12 @@ import { deserialise as deserialiseTrack, type TrackInitOptions, createTrack, is
|
|||||||
import { greatestCommonDivisor, isPosInt } from "@/utils";
|
import { greatestCommonDivisor, isPosInt } from "@/utils";
|
||||||
import { watch, computed, effectScope, ref, shallowRef, triggerRef } from "vue";
|
import { watch, computed, effectScope, ref, shallowRef, triggerRef } from "vue";
|
||||||
|
|
||||||
|
export interface BeatManager {
|
||||||
|
notifyChange(): void;
|
||||||
|
}
|
||||||
|
|
||||||
type BeatGroupInitOptions = {
|
type BeatGroupInitOptions = {
|
||||||
|
manager: BeatManager,
|
||||||
barCount: number;
|
barCount: number;
|
||||||
isLooping: boolean;
|
isLooping: boolean;
|
||||||
timeSigUp: number;
|
timeSigUp: number;
|
||||||
@@ -33,11 +38,12 @@ function isBeatSerial(serial: Record<string, unknown>): serial is BeatSerial {
|
|||||||
typeof serial.barSettingsLocked === "boolean";
|
typeof serial.barSettingsLocked === "boolean";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserialise(serial: {}) {
|
export function deserialise(serial: {}, manager: BeatManager) {
|
||||||
if (!isBeatSerial(serial)) {
|
if (!isBeatSerial(serial)) {
|
||||||
throw new Error("Not a valid beat serial");
|
throw new Error("Not a valid beat serial");
|
||||||
}
|
}
|
||||||
const newBeat = createBeat({
|
const newBeat = createBeat({
|
||||||
|
manager,
|
||||||
loopLength: serial.globalLoopLength,
|
loopLength: serial.globalLoopLength,
|
||||||
barCount: serial.barCount,
|
barCount: serial.barCount,
|
||||||
isLooping: serial.globalIsLooping,
|
isLooping: serial.globalIsLooping,
|
||||||
@@ -46,7 +52,7 @@ export function deserialise(serial: {}) {
|
|||||||
useAutoBeatLength: serial.useAutoBeatLength,
|
useAutoBeatLength: serial.useAutoBeatLength,
|
||||||
});
|
});
|
||||||
serial.tracks.forEach(trackSerial => {
|
serial.tracks.forEach(trackSerial => {
|
||||||
const track = deserialiseTrack(trackSerial);
|
const track = deserialiseTrack(trackSerial, manager);
|
||||||
if (track) newBeat.tracks.value.push(track);
|
if (track) newBeat.tracks.value.push(track);
|
||||||
});
|
});
|
||||||
return newBeat;
|
return newBeat;
|
||||||
@@ -55,6 +61,7 @@ export function deserialise(serial: {}) {
|
|||||||
export function createBeat(opts: BeatGroupInitOptions) {
|
export function createBeat(opts: BeatGroupInitOptions) {
|
||||||
const scope = effectScope();
|
const scope = effectScope();
|
||||||
return scope.run(() => {
|
return scope.run(() => {
|
||||||
|
const manager = opts.manager;
|
||||||
const tracks = shallowRef<Track[]>([]);
|
const tracks = shallowRef<Track[]>([]);
|
||||||
const barCountInternal = ref<number>(opts.barCount ?? 4);
|
const barCountInternal = ref<number>(opts.barCount ?? 4);
|
||||||
const barCount = computed({
|
const barCount = computed({
|
||||||
@@ -124,17 +131,18 @@ export function createBeat(opts: BeatGroupInitOptions) {
|
|||||||
tracks.value[trackIndex2] = track1;
|
tracks.value[trackIndex2] = track1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTrack(options?: TrackInitOptions): Track | null {
|
function addTrack(opts?: Omit<TrackInitOptions, 'manager'>): Track | null {
|
||||||
let newTrack: Track | null;
|
let newTrack: Track | null;
|
||||||
options = {
|
const options: TrackInitOptions = {
|
||||||
|
manager,
|
||||||
bars: barCount.value,
|
bars: barCount.value,
|
||||||
isLooping: globalIsLooping.value,
|
isLooping: globalIsLooping.value,
|
||||||
loopLength: globalLoopLength.value,
|
loopLength: globalLoopLength.value,
|
||||||
...options,
|
...opts,
|
||||||
timeSig: {
|
timeSig: {
|
||||||
up: timeSigUp.value,
|
up: timeSigUp.value,
|
||||||
down: 4,
|
down: 4,
|
||||||
...options?.timeSig ?? {},
|
...opts?.timeSig ?? {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
newTrack = createTrack(options) ?? null;
|
newTrack = createTrack(options) ?? null;
|
||||||
@@ -172,6 +180,10 @@ export function createBeat(opts: BeatGroupInitOptions) {
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(tracks, () => {
|
||||||
|
manager.notifyChange();
|
||||||
|
});
|
||||||
|
|
||||||
watch([barCount, timeSigUp], ([newBarCount, newTimeSigUp]) => {
|
watch([barCount, timeSigUp], ([newBarCount, newTimeSigUp]) => {
|
||||||
for (const track of tracks.value) {
|
for (const track of tracks.value) {
|
||||||
track.barCount.value = newBarCount;
|
track.barCount.value = newBarCount;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { type Beat, createBeat, deserialise as deserialiseBeat } from "@/Beat";
|
import { type Beat, createBeat, deserialise as deserialiseBeat, type BeatManager } from "@/Beat";
|
||||||
import { inject, computed, type InjectionKey, ref, shallowRef, triggerRef, watch } from "vue";
|
import { inject, computed, type InjectionKey, ref, shallowRef, triggerRef, watch, onScopeDispose } from "vue";
|
||||||
|
|
||||||
function defaultMainBeatGroup(): Beat {
|
function defaultMainBeatGroup(manager: BeatManager): Beat {
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
|
manager,
|
||||||
barCount: 2,
|
barCount: 2,
|
||||||
isLooping: false,
|
isLooping: false,
|
||||||
timeSigUp: 8,
|
timeSigUp: 8,
|
||||||
@@ -16,15 +17,23 @@ function defaultMainBeatGroup(): Beat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBeatStore() {
|
export function createBeatStore() {
|
||||||
const beats = shallowRef<Beat[]>([ defaultMainBeatGroup() ]);
|
const saveDirty = ref(false);
|
||||||
|
|
||||||
|
const manager = {
|
||||||
|
notifyChange() {
|
||||||
|
saveDirty.value = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const beats = shallowRef<Beat[]>([ defaultMainBeatGroup(manager) ]);
|
||||||
const activeBeatIndex = ref(0);
|
const activeBeatIndex = ref(0);
|
||||||
const activeBeat = computed<Beat | null>(() => beats.value[activeBeatIndex.value] ?? null);
|
const activeBeat = computed<Beat | null>(() => beats.value[activeBeatIndex.value] ?? null);
|
||||||
const autoSave = ref(true);
|
const autoSave = ref(true);
|
||||||
const orientation = ref<'horizontal' | 'vertical'>('horizontal');
|
const orientation = ref<"horizontal" | "vertical">("horizontal");
|
||||||
|
|
||||||
function resetActiveBeat(): void {
|
function resetActiveBeat(): void {
|
||||||
const current = activeBeat.value;
|
const current = activeBeat.value;
|
||||||
beats.value[activeBeatIndex.value] = defaultMainBeatGroup();
|
beats.value[activeBeatIndex.value] = defaultMainBeatGroup(manager);
|
||||||
current?.destroy();
|
current?.destroy();
|
||||||
triggerRef(beats);
|
triggerRef(beats);
|
||||||
}
|
}
|
||||||
@@ -37,7 +46,7 @@ export function createBeatStore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addNewBeat(): void {
|
function addNewBeat(): void {
|
||||||
const newBeat = defaultMainBeatGroup();
|
const newBeat = defaultMainBeatGroup(manager);
|
||||||
beats.value.push(newBeat);
|
beats.value.push(newBeat);
|
||||||
if (autoSave.value) {
|
if (autoSave.value) {
|
||||||
save("localStorage");
|
save("localStorage");
|
||||||
@@ -51,8 +60,9 @@ export function createBeatStore() {
|
|||||||
localStorage.setItem("drum-slayer-save", JSON.stringify({
|
localStorage.setItem("drum-slayer-save", JSON.stringify({
|
||||||
beats: serials,
|
beats: serials,
|
||||||
activeBeatIndex: activeBeatIndex.value,
|
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.activeBeatIndex === "number" || typeof source.activeBeatIndex === "undefined")
|
||||||
&& typeof source.orientation === "string") {
|
&& typeof source.orientation === "string") {
|
||||||
try {
|
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") {
|
if (typeof source.activeBeatIndex === "number") {
|
||||||
activeBeatIndex.value = source.activeBeatIndex;
|
activeBeatIndex.value = source.activeBeatIndex;
|
||||||
}
|
}
|
||||||
@@ -73,6 +83,7 @@ export function createBeatStore() {
|
|||||||
} else {
|
} else {
|
||||||
resetActiveBeat();
|
resetActiveBeat();
|
||||||
}
|
}
|
||||||
|
saveDirty.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bakeAll(): void {
|
function bakeAll(): void {
|
||||||
@@ -80,21 +91,35 @@ export function createBeatStore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch([activeBeatIndex, orientation, beats], () => {
|
watch([activeBeatIndex, orientation, beats], () => {
|
||||||
save('localStorage');
|
save("localStorage");
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedItem = localStorage.getItem('drum-slayer-save');
|
const savedItem = localStorage.getItem("drum-slayer-save");
|
||||||
if (savedItem) {
|
if (savedItem) {
|
||||||
const serial = JSON.parse(savedItem);
|
const serial = JSON.parse(savedItem);
|
||||||
beats.value = [defaultMainBeatGroup()];
|
beats.value = [defaultMainBeatGroup(manager)];
|
||||||
loadFromSave(serial);
|
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 {
|
return {
|
||||||
beats,
|
beats,
|
||||||
activeBeatIndex,
|
activeBeatIndex,
|
||||||
activeBeat,
|
activeBeat,
|
||||||
autoSave,
|
autoSave,
|
||||||
|
saveDirty,
|
||||||
orientation,
|
orientation,
|
||||||
|
|
||||||
save,
|
save,
|
||||||
@@ -107,7 +132,7 @@ export function createBeatStore() {
|
|||||||
|
|
||||||
export type BeatStore = ReturnType<typeof createBeatStore>;
|
export type BeatStore = ReturnType<typeof createBeatStore>;
|
||||||
|
|
||||||
export const BeatStoreKey = Symbol('BeatStore') as InjectionKey<BeatStore>;
|
export const BeatStoreKey = Symbol("BeatStore") as InjectionKey<BeatStore>;
|
||||||
|
|
||||||
export function useBeatStore(): BeatStore {
|
export function useBeatStore(): BeatStore {
|
||||||
return inject(BeatStoreKey, createBeatStore, true);
|
return inject(BeatStoreKey, createBeatStore, true);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { isPosInt } from "@/utils";
|
import { isPosInt } from "@/utils";
|
||||||
import { ref, shallowRef, computed, watch, reactive, triggerRef, effectScope } from "vue";
|
import { ref, shallowRef, computed, watch, reactive, triggerRef, effectScope } from "vue";
|
||||||
|
import type { BeatManager } from "./Beat";
|
||||||
|
|
||||||
export type TrackInitOptions = {
|
export type TrackInitOptions = {
|
||||||
|
manager: BeatManager,
|
||||||
timeSig?: {
|
timeSig?: {
|
||||||
up: number,
|
up: number,
|
||||||
down: number,
|
down: number,
|
||||||
@@ -52,7 +54,7 @@ function isTrackSerial(serial: any): serial is TrackSerial {
|
|||||||
return correctTypes && serial.units.isOn.length === serial.units.type.length;
|
return correctTypes && serial.units.isOn.length === serial.units.type.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserialise(serial: Record<string, unknown>) {
|
export function deserialise(serial: Record<string, unknown>, manager: BeatManager) {
|
||||||
if (!isTrackSerial(serial)) {
|
if (!isTrackSerial(serial)) {
|
||||||
throw new Error("Invalid track serial.");
|
throw new Error("Invalid track serial.");
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,7 @@ export function deserialise(serial: Record<string, unknown>) {
|
|||||||
stickingType: serial.units.stickingType[i] ?? 0,
|
stickingType: serial.units.stickingType[i] ?? 0,
|
||||||
}));
|
}));
|
||||||
return createTrack({
|
return createTrack({
|
||||||
|
manager,
|
||||||
bars: serial.barCount,
|
bars: serial.barCount,
|
||||||
isLooping: serial.looping,
|
isLooping: serial.looping,
|
||||||
loopLength: serial.loopLength,
|
loopLength: serial.loopLength,
|
||||||
@@ -236,6 +239,8 @@ export function createTrack(options: TrackInitOptions) {
|
|||||||
triggerRef(unitRecord);
|
triggerRef(unitRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const manager = options.manager;
|
||||||
|
watch(unitRecord, () => manager.notifyChange());
|
||||||
watch([barCount, timeSigDown, timeSigUp], () => updateTrackUnitLength(), { immediate: true });
|
watch([barCount, timeSigDown, timeSigUp], () => updateTrackUnitLength(), { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,62 +2,63 @@
|
|||||||
<div
|
<div
|
||||||
class="root"
|
class="root"
|
||||||
:class="{ 'sidebar-visible': sidebarActive, 'vertical-mode': currentOrientation === 'vertical' }">
|
:class="{ 'sidebar-visible': sidebarActive, 'vertical-mode': currentOrientation === 'vertical' }">
|
||||||
<div class="root-sidebar">
|
<div class="sidebar">
|
||||||
<div class="root-sidebar-left-strip">
|
<div class="sidebar-left-strip">
|
||||||
<div v-for="(beat, i) in beats"
|
<div v-for="(beat, i) in beats"
|
||||||
:key="beat.name.value"
|
:key="beat.name.value"
|
||||||
class="root-sidebar-left-tab"
|
class="sidebar-left-tab"
|
||||||
:class="{ 'active': i === activeBeatIndex }"
|
:class="{ 'active': i === activeBeatIndex }"
|
||||||
@click="activeBeatIndex = i">
|
@click="activeBeatIndex = i">
|
||||||
{{ beat.name.value }}
|
{{ beat.name.value }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="root-sidebar-add-beat"
|
class="sidebar-add-beat"
|
||||||
@click="addNewBeat()">
|
@click="addNewBeat()">
|
||||||
+
|
+
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="root-settings">
|
<div class="settings">
|
||||||
<h1 class="root-title">{{ title }}</h1>
|
<h1 class="title">{{ title }}</h1>
|
||||||
<beat-settings :beat-index="activeBeatIndex" />
|
<beat-settings :beat-index="activeBeatIndex" />
|
||||||
</div>
|
</div>
|
||||||
<div class="root-sidebar-toggle">
|
<div class="sidebar-toggle">
|
||||||
<div
|
<div
|
||||||
class="root-quick-access-button"
|
class="quick-access-button"
|
||||||
:title="`${ sidebarActive ? 'Hide' : 'Show' } sidebar`"
|
:title="`${ sidebarActive ? 'Hide' : 'Show' } sidebar`"
|
||||||
@click="sidebarActive = !sidebarActive">
|
@click="sidebarActive = !sidebarActive">
|
||||||
<icon icon-name="list" color="var(--color-ui-neutral-dark)" />
|
<icon icon-name="list" color="var(--color-ui-neutral-dark)" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="root-quick-access-button"
|
class="quick-access-button"
|
||||||
title="Change orientation"
|
title="Change orientation"
|
||||||
@click="toggleOrientation">
|
@click="toggleOrientation">
|
||||||
<icon icon-name="arrowClockwise" color="var(--color-ui-neutral-dark)" />
|
<icon icon-name="arrowClockwise" color="var(--color-ui-neutral-dark)" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="root-quick-access-button"
|
class="quick-access-button"
|
||||||
title="Bake all tracks"
|
title="Bake all tracks"
|
||||||
@click="bakeAll">
|
@click="bakeAll">
|
||||||
<icon icon-name="snowflake" color="var(--color-ui-neutral-dark)" />
|
<icon icon-name="snowflake" color="var(--color-ui-neutral-dark)" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="root-quick-access-button"
|
class="quick-access-button"
|
||||||
title="Reset all"
|
title="Reset all"
|
||||||
@click="resetActiveBeat">
|
@click="resetActiveBeat">
|
||||||
<icon icon-name="trash" color="var(--color-ui-neutral-dark)" />
|
<icon icon-name="trash" color="var(--color-ui-neutral-dark)" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="root-quick-access-button"
|
class="quick-access-button"
|
||||||
title="Reset all"
|
:class="{ 'unclickable': !saveDirty }"
|
||||||
|
:title="saveDirty ? 'Save changes' : 'No unsaved changes'"
|
||||||
@click="save('localStorage')">
|
@click="save('localStorage')">
|
||||||
Save
|
<icon icon-name="download" color="var(--color-ui-neutral-dark)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="root-beat-stage-container">
|
<div class="beat-stage-container">
|
||||||
<toolbox />
|
<toolbox />
|
||||||
<div class="root-beat-stage">
|
<div class="beat-stage">
|
||||||
<beat-view
|
<beat-view
|
||||||
:beat-index="activeBeatIndex"
|
:beat-index="activeBeatIndex"
|
||||||
:orientation="currentOrientation" />
|
:orientation="currentOrientation" />
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, provide, ref } from "vue";
|
import { onBeforeUnmount, onMounted, provide, ref, watch } from "vue";
|
||||||
import BeatSettings from "@/ui/BeatSettings/BeatSettings.vue";
|
import BeatSettings from "@/ui/BeatSettings/BeatSettings.vue";
|
||||||
import BeatView from "@/ui/Beat/Beat.vue";
|
import BeatView from "@/ui/Beat/Beat.vue";
|
||||||
import Icon from "@/ui/Widgets/Icon/Icon.vue";
|
import Icon from "@/ui/Widgets/Icon/Icon.vue";
|
||||||
@@ -95,8 +96,19 @@
|
|||||||
beats,
|
beats,
|
||||||
addNewBeat,
|
addNewBeat,
|
||||||
bakeAll,
|
bakeAll,
|
||||||
|
saveDirty,
|
||||||
} = beatStore;
|
} = beatStore;
|
||||||
|
|
||||||
|
const TITLE = 'Drum Slayer';
|
||||||
|
|
||||||
|
watch(saveDirty, (dirty) => {
|
||||||
|
if (dirty) {
|
||||||
|
document.title = `${ TITLE } (unsaved changes)`;
|
||||||
|
} else {
|
||||||
|
document.title = TITLE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
||||||
const onMediaChange = (event: MediaQueryListEvent | MediaQueryList) => {
|
const onMediaChange = (event: MediaQueryListEvent | MediaQueryList) => {
|
||||||
sidebarActive.value = event.matches;
|
sidebarActive.value = event.matches;
|
||||||
@@ -107,6 +119,7 @@
|
|||||||
function windowMouseUp() {
|
function windowMouseUp() {
|
||||||
appStateStore.selectingUnits.value = false;
|
appStateStore.selectingUnits.value = false;
|
||||||
appStateStore.deselectingUnits.value = false;
|
appStateStore.deselectingUnits.value = false;
|
||||||
|
appStateStore.unitMouseStart.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -134,142 +147,149 @@
|
|||||||
background-color: var(--color-bg-dark);
|
background-color: var(--color-bg-dark);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar {
|
.sidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -28em;
|
left: -28em;
|
||||||
width: 30em;
|
width: 30em;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: left 400ms;
|
transition: left 400ms;
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-visible .root-sidebar {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-settings {
|
|
||||||
z-index: 1;
|
|
||||||
width: 28em;
|
|
||||||
background-color: var(--color-bg-light);
|
|
||||||
overflow: scroll;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-settings .root-title {
|
|
||||||
color: var(--color-title-light);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-toggle {
|
|
||||||
z-index: 1;
|
|
||||||
height: 100vh;
|
|
||||||
min-width: 2em;
|
|
||||||
background-color: var(--color-bg-light);
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-quick-access-button {
|
|
||||||
right: 0;
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-beat-stage-container {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
transition: left 400ms, width 400ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-visible .root-beat-stage-container {
|
|
||||||
left: 30em;
|
|
||||||
width: calc(100vw - 30em);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-beat-stage {
|
|
||||||
position: relative;
|
|
||||||
max-height: 100vh;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 100vw;
|
|
||||||
transition: max-width 400ms;
|
|
||||||
padding-left: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .root-beat-stage {
|
|
||||||
margin: auto auto;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-visible .root-beat-stage {
|
|
||||||
max-width: calc(100vw - 30em);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-left-strip {
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
background-color: var(--color-bg-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-left-strip > * {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-left-tab {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 3px 8px 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-left-tab.active {
|
|
||||||
background-color: var(--color-bg-medium);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-add-beat {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 3px 8px 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-sidebar-add-beat:hover,
|
|
||||||
.root-sidebar-left-tab:hover:not(.active) {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--color-ui-neutral-dark);
|
|
||||||
transition: background-color 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
.sidebar-visible .root-sidebar {
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
}
|
||||||
.root-sidebar {
|
|
||||||
left: calc(-100vw + 2em);
|
&.sidebar-visible {
|
||||||
width: 100vw;
|
.beat-stage {
|
||||||
|
max-width: calc(100vw - 30em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat-stage-container {
|
||||||
|
left: 30em;
|
||||||
|
width: calc(100vw - 30em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.root-settings {
|
|
||||||
width: calc(100vw - 2em);
|
.settings {
|
||||||
|
z-index: 1;
|
||||||
|
width: 28em;
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
overflow: scroll;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.sidebar-visible .root-beat-stage-container {
|
|
||||||
left: 100vw;
|
.settings .title {
|
||||||
|
color: var(--color-title-light);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.root-beat-stage-container {
|
|
||||||
|
.sidebar-toggle {
|
||||||
|
z-index: 1;
|
||||||
|
height: 100vh;
|
||||||
|
min-width: 2em;
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
.sidebar-visible .root-beat-stage {
|
|
||||||
|
.quick-access-button {
|
||||||
|
right: 0;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
&.unclickable {
|
||||||
|
opacity: 50%;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat-stage-container {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: left 400ms, width 400ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat-stage {
|
||||||
|
position: relative;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: auto;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
|
transition: max-width 400ms;
|
||||||
|
padding-left: 3em;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
&.vertical-mode .beat-stage {
|
||||||
user-drag: none;
|
margin: auto auto;
|
||||||
user-select: none;
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-left-strip {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-left-strip > * {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-left-tab {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 3px 8px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-left-tab.active {
|
||||||
|
background-color: var(--color-bg-medium);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-add-beat {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 3px 8px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-add-beat:hover,
|
||||||
|
.sidebar-left-tab:hover:not(.active) {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-ui-neutral-dark);
|
||||||
|
transition: background-color 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
&.sidebar-visible .sidebar {
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
left: calc(-100vw + 2em);
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
.settings {
|
||||||
|
width: calc(100vw - 2em);
|
||||||
|
}
|
||||||
|
.sidebar-visible .beat-stage-container {
|
||||||
|
left: 100vw;
|
||||||
|
}
|
||||||
|
.beat-stage-container {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.sidebar-visible .beat-stage {
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
user-drag: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,17 @@
|
|||||||
<div class="track-main">
|
<div class="track-main">
|
||||||
<div class="track-unit-block">
|
<div class="track-unit-block">
|
||||||
<track-unit v-for="(trackUnit, i) in trackUnits"
|
<track-unit v-for="(trackUnit, i) in trackUnits"
|
||||||
|
:key="`tu${ trackIndex }${ i }`"
|
||||||
|
:id="`tu${ trackIndex }${ i }`"
|
||||||
class="track-unit"
|
class="track-unit"
|
||||||
:class="{ spaced: (i + 1) % beat!.timeSigUp.value === 0 }"
|
:class="{ spaced: (i + 1) % beat!.timeSigUp.value === 0 }"
|
||||||
:sticking-type="trackUnit.stickingType"
|
:sticking-type="trackUnit.stickingType"
|
||||||
:type="trackUnit.type"
|
:type="trackUnit.type"
|
||||||
:on="trackUnit.on"
|
:on="trackUnit.on"
|
||||||
@rotate-type="rotateTrackUnit(i)"
|
@rotate-type="rotateTrackUnit(i)"
|
||||||
@mouseup="applyCurrentToolToTrackUnit(i)"
|
@deactivate="deactivateUnit(i)"
|
||||||
@mousedown="applyCurrentToolToTrackUnit(i)"
|
@toggle="toggle(i)"
|
||||||
@mouseover="applyCurrentToolToTrackUnit(i)" />
|
@apply-tool="applyCurrentToolToTrackUnit(i)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,27 +55,34 @@
|
|||||||
return units;
|
return units;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggle(index: number) {
|
||||||
|
if (!track.value) return;
|
||||||
|
track.value.toggleUnit(index);
|
||||||
|
if (track.value.getUnitByIndex(index).on) {
|
||||||
|
applyCurrentToolToTrackUnit(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function rotateTrackUnit(index: number) {
|
function rotateTrackUnit(index: number) {
|
||||||
track.value?.rotateUnit(index);
|
track.value?.rotateUnit(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deactivateUnit(index: number) {
|
||||||
|
track.value?.setStickingType(index, 0);
|
||||||
|
track.value?.setUnitOn(index, false);
|
||||||
|
}
|
||||||
|
|
||||||
function applyCurrentToolToTrackUnit(index: number) {
|
function applyCurrentToolToTrackUnit(index: number) {
|
||||||
if (selectedTool.value === "sticking") {
|
switch (selectedTool.value) {
|
||||||
if (selectingUnits.value) {
|
case "sticking":
|
||||||
track.value?.setStickingType(index, activeStickingType.value);
|
track.value?.setStickingType(index, activeStickingType.value);
|
||||||
} else if (deselectingUnits.value) {
|
break;
|
||||||
track.value?.setStickingType(index, 0);
|
case "track-unit-type":
|
||||||
}
|
|
||||||
} else if (selectedTool.value === "track-unit-type") {
|
|
||||||
if (selectingUnits.value) {
|
|
||||||
track.value?.updateUnit(index, { on: true, type: activeTrackUnitType.value });
|
track.value?.updateUnit(index, { on: true, type: activeTrackUnitType.value });
|
||||||
} else if (deselectingUnits.value) {
|
break;
|
||||||
|
case "eraser":
|
||||||
track.value?.setUnitOn(index, false);
|
track.value?.setUnitOn(index, false);
|
||||||
}
|
break;
|
||||||
} else if (selectedTool.value === "eraser") {
|
|
||||||
if (selectingUnits.value || deselectingUnits.value) {
|
|
||||||
track.value?.setUnitOn(index, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
<div :class="classes"
|
<div :class="classes"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@mouseup="handleMouseUp"
|
@mouseup="handleMouseUp"
|
||||||
|
@mouseover="handleMouseOver"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
@mouseout="handleMouseOut"
|
@mouseout="handleMouseOut"
|
||||||
@touchstart="handleTouchStart"
|
@touchstart="handleTouchStart"
|
||||||
@touchend="handleTouchEnd"
|
@touchend="handleTouchEnd"
|
||||||
@contextmenu="() => false">
|
@contextmenu.prevent.stop="() => false">
|
||||||
<icon :icon-name="iconName" />
|
<icon :icon-name="iconName" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
import { useAppStateStore } from "@/AppState";
|
import { useAppStateStore } from "@/AppState";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
id: string,
|
||||||
stickingType: number,
|
stickingType: number,
|
||||||
type: number,
|
type: number,
|
||||||
on: boolean,
|
on: boolean,
|
||||||
@@ -25,17 +28,22 @@
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'rotateType'): true,
|
(e: 'rotateType'): true,
|
||||||
(e: 'mousedown'): true,
|
(e: 'toggle'): true,
|
||||||
(e: 'mouseup'): true,
|
(e: 'deactivate'): true,
|
||||||
|
(e: 'applyTool'): true,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectingUnits,
|
selectingUnits,
|
||||||
|
lastTrackUnit,
|
||||||
deselectingUnits,
|
deselectingUnits,
|
||||||
|
unitMouseStart,
|
||||||
} = useAppStateStore();
|
} = useAppStateStore();
|
||||||
|
|
||||||
let rotationTimeout: ReturnType<typeof setTimeout> | null = null;
|
let rotationTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let blockNextMouseUp = false;
|
let blockNextMouseUp = false;
|
||||||
|
let mouseHeld = false;
|
||||||
|
let movement = 0;
|
||||||
|
|
||||||
const classes = computed(() => getClasses({
|
const classes = computed(() => getClasses({
|
||||||
on: props.on,
|
on: props.on,
|
||||||
@@ -47,33 +55,56 @@
|
|||||||
const iconName = computed(() => StickingTypeIconMap[TrackUnitStickingTypeList[props.stickingType] ?? 'none']);
|
const iconName = computed(() => StickingTypeIconMap[TrackUnitStickingTypeList[props.stickingType] ?? 'none']);
|
||||||
|
|
||||||
function handleMouseDown(ev: MouseEvent): void {
|
function handleMouseDown(ev: MouseEvent): void {
|
||||||
|
blockNextMouseUp = false;
|
||||||
if (ev.button === 0) {
|
if (ev.button === 0) {
|
||||||
|
unitMouseStart.value = props.id;
|
||||||
selectingUnits.value = true;
|
selectingUnits.value = true;
|
||||||
emit('mousedown');
|
|
||||||
} else if (ev.button === 2) {
|
} else if (ev.button === 2) {
|
||||||
|
unitMouseStart.value = props.id;
|
||||||
deselectingUnits.value = true;
|
deselectingUnits.value = true;
|
||||||
emit('mousedown');
|
|
||||||
} else if (ev.button === 1) {
|
} else if (ev.button === 1) {
|
||||||
emit('rotateType');
|
emit('rotateType');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(ev: MouseEvent): void {
|
||||||
|
if (!blockNextMouseUp && unitMouseStart.value === props.id) {
|
||||||
|
if (ev.button === 0) {
|
||||||
|
emit('toggle');
|
||||||
|
} else if (ev.button === 2) {
|
||||||
|
emit('deactivate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(ev: MouseEvent): void {
|
||||||
|
if (selectingUnits.value || deselectingUnits.value) {
|
||||||
|
movement += ev.movementX**2 + ev.movementY**2;
|
||||||
|
}
|
||||||
|
if (unitMouseStart.value === props.id && movement > 2**2) {
|
||||||
|
handleMouseOver();
|
||||||
|
blockNextMouseUp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseOver(ev?: MouseEvent): void {
|
||||||
|
if (selectingUnits.value) {
|
||||||
|
emit('applyTool');
|
||||||
|
} else if (deselectingUnits.value) {
|
||||||
|
emit('deactivate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMouseOut(ev: MouseEvent): void {
|
function handleMouseOut(ev: MouseEvent): void {
|
||||||
|
movement = 0;
|
||||||
if (rotationTimeout) {
|
if (rotationTimeout) {
|
||||||
clearTimeout(rotationTimeout);
|
clearTimeout(rotationTimeout);
|
||||||
rotationTimeout = null;
|
rotationTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseUp(ev: MouseEvent): void {
|
|
||||||
if (!blockNextMouseUp) {
|
|
||||||
emit('mouseup');
|
|
||||||
}
|
|
||||||
blockNextMouseUp = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTouchStart(ev: TouchEvent): void {
|
function handleTouchStart(ev: TouchEvent): void {
|
||||||
rotationTimeout = rotationTimeout || setTimeout(() => {
|
rotationTimeout ??= setTimeout(() => {
|
||||||
emit('rotateType');
|
emit('rotateType');
|
||||||
rotationTimeout = null;
|
rotationTimeout = null;
|
||||||
}, 400);
|
}, 400);
|
||||||
@@ -118,15 +149,15 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-unit.on.Accent, .track-unit.on.Accent.highlightable:hover {
|
||||||
|
border-color: var(--color-ui-neutral-light);
|
||||||
|
}
|
||||||
|
|
||||||
.track-unit.on.highlightable:hover {
|
.track-unit.on.highlightable:hover {
|
||||||
border-color: var(--color-ui-accent-hover);
|
border-color: var(--color-ui-accent-hover);
|
||||||
background-color: var(--color-ui-accent-hover);
|
background-color: var(--color-ui-accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-unit.on.Accent {
|
|
||||||
border-color: var(--color-ui-neutral-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.track-unit.on.Ghost {
|
.track-unit.on.Ghost {
|
||||||
opacity: 60%;
|
opacity: 60%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import ArrowClockwise from "assets/svgs/arrow-clockwise.svg";
|
|||||||
import Trash from "assets/svgs/trash.svg";
|
import Trash from "assets/svgs/trash.svg";
|
||||||
import Snowflake from "assets/svgs/snowflake.svg";
|
import Snowflake from "assets/svgs/snowflake.svg";
|
||||||
import LeftHand from "assets/svgs/LH.png";
|
import LeftHand from "assets/svgs/LH.png";
|
||||||
|
import Download from "assets/svgs/download.svg";
|
||||||
import RightHand from "assets/svgs/RH.png";
|
import RightHand from "assets/svgs/RH.png";
|
||||||
import LeftFoot from "assets/svgs/LF.png";
|
import LeftFoot from "assets/svgs/LF.png";
|
||||||
import RightFoot from "assets/svgs/RF.png";
|
import RightFoot from "assets/svgs/RF.png";
|
||||||
|
|
||||||
export const IconUrlMap = {
|
export const IconUrlMap = {
|
||||||
arrowClockwise: ArrowClockwise,
|
arrowClockwise: ArrowClockwise,
|
||||||
|
download: Download,
|
||||||
list: List,
|
list: List,
|
||||||
trash: Trash,
|
trash: Trash,
|
||||||
snowflake: Snowflake,
|
snowflake: Snowflake,
|
||||||
|
|||||||
Reference in New Issue
Block a user