Files
arne-drums/src/ui/Root/Root.vue
Daniel Ledda f4689489e4 test hook
2024-05-31 17:48:47 +02:00

453 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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,
};
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>