454 lines
14 KiB
Vue
454 lines
14 KiB
Vue
<template>
|
||
<div
|
||
class="root"
|
||
:class="{ 'sidebar-visible': sidebarActive, 'vertical-mode': currentOrientation === 'vertical' }">
|
||
<div class="sidebar">
|
||
<div class="sidebar-left-strip">
|
||
<draggable animation="150"
|
||
@start="onStartDragBeatTab"
|
||
@end="onEndDragBeatTab"
|
||
v-model="beats"
|
||
ghost-class="ghost"
|
||
itemKey="name.value">
|
||
<template #item="{ element, index }">
|
||
<div
|
||
:key="element.name.value"
|
||
class="sidebar-left-tab"
|
||
:class="{ 'active': index === activeBeatIndex }"
|
||
@click="activeBeatIndex = index">
|
||
<span v-if="element.saveDirty.value" class="unsaved">•</span>
|
||
<span class="name">{{ element.name.value }}</span>
|
||
<div
|
||
class="delete"
|
||
@click="onDeleteBeat(index)"
|
||
title="delete">
|
||
<div class="x">×</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
<div
|
||
class="tab-add"
|
||
@click="onAddNewBeat">
|
||
+
|
||
</div>
|
||
</div>
|
||
<div class="settings">
|
||
<h1 class="title">{{ title }}</h1>
|
||
<beat-settings :beat-index="activeBeatIndex" />
|
||
</div>
|
||
<div class="sidebar-toggle">
|
||
<div class="buttons">
|
||
<div
|
||
class="quick-access-button"
|
||
:title="`${ sidebarActive ? 'Hide' : 'Show' } sidebar`"
|
||
@click="sidebarActive = !sidebarActive">
|
||
<icon icon-name="list" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
<div
|
||
class="quick-access-button"
|
||
title="Change orientation"
|
||
@click="toggleOrientation">
|
||
<icon icon-name="arrowClockwise" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
<div
|
||
class="quick-access-button"
|
||
title="Bake all tracks"
|
||
@click="bakeAll">
|
||
<icon icon-name="snowflake" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
<div
|
||
class="quick-access-button"
|
||
title="Reset all"
|
||
@click="resetActiveBeat">
|
||
<icon icon-name="trash" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
<div
|
||
class="quick-access-button"
|
||
:class="{ 'unclickable': !saveDirty }"
|
||
:title="saveDirty ? 'Save all changes' : 'No unsaved changes'"
|
||
@click="save('localStorage')">
|
||
<icon icon-name="save" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
<div
|
||
class="quick-access-button"
|
||
title="Save current beat to file"
|
||
@click="saveCurrentBeatToFile">
|
||
<icon icon-name="download" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
<div
|
||
class="quick-access-button"
|
||
title="Save current beat to file"
|
||
@click="uploadDialog?.showModal()">
|
||
<icon icon-name="upload" color="var(--color-ui-neutral-dark)" />
|
||
</div>
|
||
</div>
|
||
<toolbox class="toolbox" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="beat-stage-container">
|
||
<div class="beat-stage">
|
||
<beat-view
|
||
:beat-index="activeBeatIndex"
|
||
:orientation="currentOrientation" />
|
||
</div>
|
||
</div>
|
||
<dialog ref="uploadDialog" class="upload-dialog">
|
||
<h2>Upload a Drum Slayer file</h2>
|
||
<input id="upload-file" type="file" @change="onFileInputChange">
|
||
<button @click="onUpload" :disabled="!canUpload">Upload</button>
|
||
<button @click="uploadDialog?.close()">Cancel</button>
|
||
</dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||
import BeatSettings from "@/ui/BeatSettings/BeatSettings.vue";
|
||
import BeatView from "@/ui/Beat/Beat.vue";
|
||
import Icon from "@/ui/Widgets/Icon/Icon.vue";
|
||
import Toolbox from "@/ui/Root/Toolbox.vue";
|
||
import { useBeatStore } from "@/BeatStore";
|
||
import { useAppStateStore } from "@/AppState";
|
||
import Draggable from "vuedraggable";
|
||
|
||
const TITLE = 'Drum Slayer';
|
||
|
||
defineProps<{
|
||
title: string,
|
||
}>();
|
||
|
||
const currentOrientation = ref<'horizontal' | 'vertical'>('horizontal');
|
||
const sidebarActive = ref(false);
|
||
|
||
const appStateStore = useAppStateStore();
|
||
const beatStore = useBeatStore();
|
||
|
||
window.drumslayer = {
|
||
appState: appStateStore,
|
||
beatStore,
|
||
test: "HELLO",
|
||
};
|
||
|
||
const {
|
||
save,
|
||
resetActiveBeat,
|
||
activeBeatIndex,
|
||
activeBeat,
|
||
beats,
|
||
addNewBeat,
|
||
bakeAll,
|
||
saveDirty,
|
||
removeBeat,
|
||
} = beatStore;
|
||
|
||
function onAddNewBeat() {
|
||
activeBeatIndex.value = addNewBeat();
|
||
}
|
||
|
||
let lastActiveBeatIndex: number | null = null;
|
||
function onStartDragBeatTab() {
|
||
lastActiveBeatIndex = activeBeat.value?.id ?? null;
|
||
}
|
||
function onEndDragBeatTab() {
|
||
if (lastActiveBeatIndex !== null) {
|
||
activeBeatIndex.value = beats.value.findIndex(beat => beat.id === lastActiveBeatIndex) ?? lastActiveBeatIndex;
|
||
}
|
||
}
|
||
|
||
function onDeleteBeat(index: number) {
|
||
const beatToDelete = beats.value[index];
|
||
if (beatToDelete) {
|
||
if (confirm('ARE YOU SURE?')) {
|
||
removeBeat(index);
|
||
}
|
||
}
|
||
}
|
||
|
||
const uploadErr = ref(false);
|
||
const canUpload = ref(false);
|
||
const uploadDialog = ref<HTMLDialogElement | null>(null);
|
||
|
||
function onFileInputChange(event: Event) {
|
||
const inputEl = event.currentTarget as HTMLInputElement;
|
||
canUpload.value = !!inputEl?.files?.length;
|
||
}
|
||
|
||
async function onUpload() {
|
||
const input = document.getElementById('upload-file') as HTMLInputElement;
|
||
if (input.files) {
|
||
const file = input.files.item(0);
|
||
const text = await file?.text() ?? null;
|
||
if (text) {
|
||
try {
|
||
const parsed = JSON.parse(text);
|
||
const newBeatIndex = addNewBeat(parsed);
|
||
if (newBeatIndex === -1) {
|
||
uploadErr.value = true;
|
||
}
|
||
} catch (e) {
|
||
|
||
}
|
||
}
|
||
}
|
||
uploadDialog.value?.close();
|
||
}
|
||
|
||
function saveCurrentBeatToFile() {
|
||
if (activeBeat.value) {
|
||
const serial = activeBeat.value.serialise();
|
||
const a = document.createElement("a");
|
||
const file = new Blob([JSON.stringify(serial)], { type: 'text' });
|
||
a.href = URL.createObjectURL(file);
|
||
a.download = `${ activeBeat.value.name.value }.drms`;
|
||
a.click();
|
||
}
|
||
}
|
||
|
||
watch(saveDirty, (dirty) => {
|
||
if (dirty) {
|
||
document.title = `${ TITLE } (unsaved changes)`;
|
||
} else {
|
||
document.title = TITLE;
|
||
}
|
||
});
|
||
|
||
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
||
function onMediaChange(event: MediaQueryListEvent | MediaQueryList) {
|
||
sidebarActive.value = event.matches;
|
||
}
|
||
mediaQueryList.addEventListener('change', onMediaChange);
|
||
onMediaChange(mediaQueryList);
|
||
|
||
function windowMouseUp() {
|
||
appStateStore.selectingUnits.value = false;
|
||
appStateStore.deselectingUnits.value = false;
|
||
appStateStore.unitMouseStart.value = null;
|
||
}
|
||
|
||
onMounted(() => {
|
||
window.addEventListener('mouseup', windowMouseUp);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('mouseup', windowMouseUp);
|
||
});
|
||
|
||
function toggleOrientation(): void {
|
||
if (currentOrientation.value === "vertical") {
|
||
currentOrientation.value = "horizontal";
|
||
} else {
|
||
currentOrientation.value = "vertical";
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.root {
|
||
position: relative;
|
||
overflow: hidden;
|
||
color: var(--color-p-light);
|
||
background-color: var(--color-bg-dark);
|
||
height: 100vh;
|
||
align-content: center;
|
||
|
||
.sidebar {
|
||
position: absolute;
|
||
left: -28em;
|
||
width: 30em;
|
||
height: 100vh;
|
||
display: flex;
|
||
transition: left 400ms;
|
||
top: 0;
|
||
}
|
||
|
||
&.sidebar-visible {
|
||
.beat-stage {
|
||
max-width: calc(100vw - 30em);
|
||
}
|
||
|
||
.beat-stage-container {
|
||
left: 30em;
|
||
width: calc(100vw - 30em);
|
||
}
|
||
|
||
.sidebar {
|
||
left: 0;
|
||
}
|
||
}
|
||
|
||
.settings {
|
||
z-index: 1;
|
||
width: 28em;
|
||
background-color: var(--color-bg-light);
|
||
overflow: scroll;
|
||
display: inline-block;
|
||
|
||
.title {
|
||
color: var(--color-title-light);
|
||
text-align: center;
|
||
}
|
||
}
|
||
|
||
.sidebar-toggle {
|
||
z-index: 1;
|
||
height: 100vh;
|
||
min-width: 2em;
|
||
background-color: var(--color-bg-light);
|
||
left: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.buttons {
|
||
flex: 1;
|
||
|
||
.quick-access-button {
|
||
flex: 1;
|
||
right: 0;
|
||
width: 2em;
|
||
height: 2em;
|
||
cursor: pointer;
|
||
margin-bottom: 0.5em;
|
||
|
||
&.unclickable {
|
||
opacity: 50%;
|
||
cursor: auto;
|
||
}
|
||
}
|
||
}
|
||
|
||
.toolbox {
|
||
width: 2em;
|
||
}
|
||
}
|
||
|
||
.beat-stage-container {
|
||
position: absolute;
|
||
height: 100%;
|
||
left: 0;
|
||
top: 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;
|
||
transition: max-width 400ms;
|
||
padding-left: 3em;
|
||
}
|
||
|
||
&.vertical-mode {
|
||
.beat-stage {
|
||
margin: auto auto;
|
||
height: 100vh;
|
||
}
|
||
|
||
.beat-stage-container {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.sidebar-left-strip {
|
||
writing-mode: vertical-rl;
|
||
background-color: var(--color-bg-medium);
|
||
> * {
|
||
display: inline-block;
|
||
}
|
||
}
|
||
|
||
.sidebar-left-tab, .tab-add {
|
||
display: inline-block;
|
||
width: 100%;
|
||
padding: 8px 3px 24px 3px;
|
||
position: relative;
|
||
|
||
.unsaved {
|
||
margin-bottom: 5px;
|
||
opacity: 50%;
|
||
}
|
||
|
||
.delete {
|
||
position: absolute;
|
||
bottom: 5px;
|
||
left: 6px;
|
||
width: 15px;
|
||
height: 15px;
|
||
display: inline-block;
|
||
color: var(--color-ui-neutral-dark);
|
||
border-radius: 2px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
filter: brightness(1.5);
|
||
}
|
||
|
||
.x {
|
||
position: absolute;
|
||
right: -3px;
|
||
bottom: -3px;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background-color: var(--color-bg-light);
|
||
display: inline-block;
|
||
}
|
||
|
||
&:hover:not(.active) {
|
||
cursor: pointer;
|
||
background-color: var(--color-ui-neutral-dark);
|
||
transition: background-color 200ms;
|
||
}
|
||
|
||
&:hover {
|
||
.delete {
|
||
filter: brightness(1.2);
|
||
}
|
||
}
|
||
}
|
||
|
||
.tab-name {
|
||
display: inline-block;
|
||
}
|
||
|
||
@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;
|
||
}
|
||
}
|
||
|
||
.upload-dialog {
|
||
z-index: 20;
|
||
}
|
||
</style>
|
||
|