feat: auto-save and multiple tracks

This commit is contained in:
Daniel Ledda
2022-04-17 14:08:38 +02:00
parent 77b5e25e64
commit 1861403f24
31 changed files with 498 additions and 201 deletions

View File

@@ -7,9 +7,28 @@
flex-direction: column;
}
.vertical-mode .beat {
align-items: center;
}
.vertical-mode .beat {
height: inherit;
overflow-x: hidden;
overflow-y: scroll;
display: block;
}
.beat-title {
color: var(--color-title-light);
text-align: center;
width: fit-content;
padding-left: 16px;
}
.vertical-mode .beat-title {
color: var(--color-title-light);
text-align: center;
padding-left: 0;
}
.beat-track-container {
}

View File

@@ -4,21 +4,21 @@ import TrackView from "@/ui/Track/TrackView";
import "./Beat.css";
import ISubscriber from "@/Subscriber";
import {ISubscription} from "@/Publisher";
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
export type BeatUINodeOptions = UINodeOptions & {
title: string,
beat: Beat,
orientation?: "horizontal" | "vertical",
};
const EventTypeSubscriptions = [
BeatEvents.TrackListChanged
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
BeatEvents.TrackListChanged,
] as const;
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private title: string;
private beat: Beat;
private title: EditableTextFieldView;
private trackViews: TrackView[] = [];
private currentOrientation: "vertical" | "horizontal";
private subscription: ISubscription;
@@ -26,9 +26,13 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
constructor(options: BeatUINodeOptions) {
super(options);
this.beat = options.beat;
this.title = options.title;
this.currentOrientation = options.orientation ?? "horizontal";
this.subscription = this.beat.addSubscriber(this, EventTypeSubscriptions);
this.title = new EditableTextFieldView({
setter: (text: string) => this.beat.setName(text),
noEmpty: true,
initialText: this.beat.getName().val,
});
this.setupTrackViews();
}
@@ -69,19 +73,37 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
this.redraw();
}
setBeat(newBeat: Beat): void {
this.beat = newBeat;
this.subscription.unbind();
this.subscription = this.beat.addSubscriber(this, BeatEvents.TrackListChanged);
private onNewBeat(): void {
this.beat.getName().watch((newVal) => {
this.title.setText(newVal);
});
this.title.setText(this.beat.getName().val);
EventTypeSubscriptions.forEach(event => this.notify(this, event));
this.setupTrackViews();
this.redraw();
}
setBeat(newBeat: Beat): void {
this.beat = newBeat;
this.subscription.unbind();
this.subscription = this.beat.addSubscriber(this, BeatEvents.TrackListChanged);
this.onNewBeat();
}
build(): HTMLDivElement {
return h("div", {
classes: ["beat"],
className: "beat",
},[
...this.trackViews
h("h2", {
className: "beat-title",
}, [
this.title,
]),
h("div", {
className: "beat-track-container",
}, [
...this.trackViews,
]),
]);
}
}

View File

@@ -19,7 +19,7 @@ const EventTypeSubscriptions = [
BeatEvents.LockingChanged,
BeatEvents.AutoBeatSettingsChanged,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beat: Beat;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -9,6 +9,7 @@
--color-ui-neutral-dark-hover: #a1a1a1;
--color-ui-neutral-dark-active: #c1c1c1;
--color-bg-light: #464646;
--color-bg-medium: #323232;
--color-bg-dark: #282828;
--color-p-light: #fafafa;
--color-p-light-hover: #fafafa;
@@ -45,7 +46,6 @@
.root-settings {
z-index: 1;
width: 28em;
padding: 0 0 0 2em;
background-color: var(--color-bg-light);
overflow: scroll;
display: inline-block;
@@ -97,7 +97,7 @@
}
.vertical-mode .root-beat-stage {
margin: 5em auto auto;
margin: auto auto;
padding-left: 3em;
height: 100vh;
}
@@ -106,6 +106,39 @@
max-width: calc(100vw - 30em);
}
.root-sidebar-left-strip {
text-align: right;
writing-mode: sideways-lr;
background-color: var(--color-bg-light);
}
.root-sidebar-left-strip > * {
display: inline-block;
}
.root-sidebar-left-tab {
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;

View File

@@ -1,11 +1,11 @@
import UINode, {h, UINodeOptions} from "@/ui/UINode";
import UINode, {h, q, UINodeOptions} from "@/ui/UINode";
import BeatView from "@/ui/Beat/BeatView";
import Beat from "@/Beat";
import "./Root.css";
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
import IconView from "@/ui/Widgets/Icon/IconView";
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
import Ref from "@/Ref";
import BeatStore from "@/BeatStore";
export type RootUINodeOptions = UINodeOptions & {
title: string,
@@ -16,24 +16,34 @@ export type RootUINodeOptions = UINodeOptions & {
export default class RootView extends UINode {
private title: string;
private beatView: BeatView;
private focusedBeat: Beat;
private beatStore: BeatStore;
private activeBeat: Ref<Beat>;
private beatSettingsView: BeatSettingsView;
private currentOrientation: "horizontal" | "vertical";
private stageTitleBarView: StageTitleBarView;
private showHideSidebarButton: Ref<HTMLDivElement | null> = Ref.new<HTMLDivElement | null>(null);
private sidebarActive = true;
private sidebarLeftTabs: Ref<HTMLDivElement | null> = Ref.new<HTMLDivElement | null>(null);
constructor(options: RootUINodeOptions) {
super(options);
this.beatStore = new BeatStore({
loadFromLocalStorage: true,
autoSave: true,
});
this.currentOrientation = options.orientation ?? "horizontal";
this.focusedBeat = options.mainBeat ?? RootView.defaultMainBeatGroup();
this.activeBeat = this.beatStore.getActiveBeat();
this.activeBeat.watch((newVal) => {
this.beatSettingsView.setBeat(newVal);
this.beatView.setBeat(newVal);
});
this.beatView = new BeatView({
title: options.title,
beat: this.focusedBeat,
beat: this.activeBeat.val,
orientation: this.currentOrientation,
});
this.stageTitleBarView = new StageTitleBarView({beat: this.focusedBeat});
this.beatSettingsView = new BeatSettingsView({beat: this.focusedBeat});
this.beatStore.onBeatChanges(() => {
this.sidebarLeftTabs.val?.replaceChildren(...this.buildTabs());
});
this.beatSettingsView = new BeatSettingsView({beat: this.activeBeat.val});
this.title = options.title;
this.setOrientation(this.currentOrientation);
this.openSidebarForDesktop();
@@ -46,27 +56,6 @@ export default class RootView extends UINode {
}
}
static defaultMainBeatGroup(): Beat {
const defaultSettings = {
barCount: 2,
isLooping: false,
timeSigUp: 8,
};
const mainBeatGroup = new Beat(defaultSettings);
mainBeatGroup.addTrack({name: "LF"});
mainBeatGroup.addTrack({name: "LH"});
mainBeatGroup.addTrack({name: "RH"});
mainBeatGroup.addTrack({name: "RF"});
return mainBeatGroup;
}
setMainBeatGroup(beat: Beat): void {
this.focusedBeat = beat;
this.beatSettingsView.setBeat(this.focusedBeat);
this.beatView.setBeat(this.focusedBeat);
this.stageTitleBarView.setBeat(this.focusedBeat);
}
toggleSidebar(): void {
this.sidebarActive = !this.sidebarActive;
this.showHideSidebarButton.val!.title = this.sidebarText();
@@ -95,8 +84,40 @@ export default class RootView extends UINode {
return `${this.sidebarActive ? "Hide" : "Show"} sidebar`;
}
private buildSidebarStripLeft(): HTMLElement {
return h("div", {
className: "root-sidebar-left-strip",
}, [
h("div", {
className: "root-sidebar-add-beat",
onclick: () => this.beatStore.addNewBeat(),
innerText: "+",
}),
h("div", {
saveTo: this.sidebarLeftTabs
}, this.buildTabs()),
]);
}
private buildSidebarStrip(): HTMLElement {
private buildTabs(): HTMLElement[] {
return this.beatStore.getBeats().map((beat) => {
const node = h("div", {
className: "root-sidebar-left-tab" + (beat === this.activeBeat.val ? " active" : ""),
onclick: () => this.beatStore.setActiveBeat(beat),
innerText: beat.getName(),
});
this.activeBeat.watch((newVal) => {
if (beat === newVal) {
node.classList.add("active");
} else {
node.classList.remove("active");
}
});
return node;
}).reverse();
}
private buildSidebarQuickButtons(): HTMLElement {
return h("div", {
classes: ["root-sidebar-toggle"],
}, [
@@ -124,7 +145,7 @@ export default class RootView extends UINode {
h("div", {
classes: ["root-quick-access-button"],
title: "Bake all tracks",
onclick: () => this.focusedBeat.bakeLoops(),
onclick: () => this.activeBeat.val.bakeLoops(),
}, [
new IconView({
iconName: "snowflake",
@@ -134,7 +155,7 @@ export default class RootView extends UINode {
h("div", {
classes: ["root-quick-access-button"],
title: "Reset all",
onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()),
onclick: () => this.beatStore.resetActiveBeat(),
}, [
new IconView({
iconName: "trash",
@@ -147,11 +168,12 @@ export default class RootView extends UINode {
private buildSidebar(): HTMLElement {
return (
h("div", {classes: ["root-sidebar"]}, [
this.buildSidebarStripLeft(),
h("div", {classes: ["root-settings"]}, [
h("h1", {classes: ["root-title"], innerText: this.title}),
this.beatSettingsView,
]),
this.buildSidebarStrip(),
this.buildSidebarQuickButtons(),
])
);
}
@@ -161,7 +183,6 @@ export default class RootView extends UINode {
h("div", {classes: ["root", "sidebar-visible"]}, [
this.buildSidebar(),
h("div", {classes: ["root-beat-stage-container"]}, [
this.stageTitleBarView,
h("div", {classes: ["root-beat-stage"]}, [
this.beatView,
])

View File

@@ -1,20 +0,0 @@
.stage-title-bar {
position: absolute;
background-color: var(--color-bg-light);
padding: 15px;
border-radius: 0 0 5px 5px;
color: var(--color-title-light);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
}
.stage-title-bar * {
flex: 1;
}
.stage-title-bar h2 {
margin: 0;
}

View File

@@ -1,54 +0,0 @@
import "./StageTitleBar.css";
import UINode, {h, UINodeOptions} from "@/ui/UINode";
import {ISubscription} from "@/Publisher";
import Beat, {BeatEvents} from "@/Beat";
import ISubscriber from "@/Subscriber";
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
import DropdownView, {DropdownViewOption} from "@/ui/Widgets/Dropdown/DropdownView";
import Ref from "@/Ref";
export type StageTitleBarViewOptions = UINodeOptions & {
beat: Beat,
};
const EventTypeSubscription = [BeatEvents.NameChanged];
type EventTypeSubscription = FlatArray<typeof EventTypeSubscription, 1>;
export default class StageTitleBarView extends UINode implements ISubscriber<EventTypeSubscription> {
private sub: ISubscription;
private beat: Beat;
private title: EditableTextFieldView;
private options: Ref<DropdownViewOption[]>;
constructor(options: StageTitleBarViewOptions) {
super(options);
this.beat = options.beat;
this.sub = options.beat.addSubscriber(this, EventTypeSubscription);
this.title = new EditableTextFieldView({
initialText: this.beat.getName(),
setter: (text) => this.beat.setName(text),
noEmpty: true,
});
this.options = Ref.new<DropdownViewOption[]>([]);
}
notify(publisher: unknown, event: EventTypeSubscription): void {
if (event === BeatEvents.NameChanged) {
this.title.setText(this.beat.getName());
}
}
setBeat(beat: Beat): void {
this.sub.unbind();
this.beat = beat;
this.sub = beat.addSubscriber(this, EventTypeSubscription);
this.notify(this, BeatEvents.NameChanged);
}
protected build(): HTMLElement {
return h("div", {classes: ["stage-title-bar"]}, [
h("h2", {}, [this.title]),
new DropdownView({options: this.options})
]);
}
}

View File

@@ -18,7 +18,7 @@ const EventTypeSubscriptions = [
TrackEvents.LoopLengthChanged,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
export default class TrackView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private track!: Track;

View File

@@ -17,7 +17,7 @@ const EventTypeSubscriptions = [
TrackEvents.LoopLengthChanged,
TrackEvents.DisplayTypeChanged,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
export default class TrackSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private track: Track;

View File

@@ -13,7 +13,7 @@ const EventTypeSubscriptions = [
TrackUnitEvent.Off,
TrackUnitEvent.TypeChange,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
export default class TrackUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private trackUnit: TrackUnit;

View File

@@ -1,10 +1,9 @@
import UINode, {h, UINodeOptions} from "@/ui/UINode";
import "./Icon.css";
import List from "./svgs/list.svg";
import ArrowClockwise from "./svgs/arrow-clockwise.svg";
import Trash from "./svgs/trash.svg";
import Snowflake from "./svgs/snowflake.svg";
import Ref from "@/Ref";
import List from "assets/svgs/list.svg";
import ArrowClockwise from "assets/svgs/arrow-clockwise.svg";
import Trash from "assets/svgs/trash.svg";
import Snowflake from "assets/svgs/snowflake.svg";
const IconUrlMap = {
arrowClockwise: ArrowClockwise,

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>

Before

Width:  |  Height:  |  Size: 352 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 344 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-snow2" viewBox="0 0 16 16">
<path d="M8 16a.5.5 0 0 1-.5-.5v-1.293l-.646.647a.5.5 0 0 1-.707-.708L7.5 12.793v-1.086l-.646.647a.5.5 0 0 1-.707-.708L7.5 10.293V8.866l-1.236.713-.495 1.85a.5.5 0 1 1-.966-.26l.237-.882-.94.542-.496 1.85a.5.5 0 1 1-.966-.26l.237-.882-1.12.646a.5.5 0 0 1-.5-.866l1.12-.646-.884-.237a.5.5 0 1 1 .26-.966l1.848.495.94-.542-.882-.237a.5.5 0 1 1 .258-.966l1.85.495L7 8l-1.236-.713-1.849.495a.5.5 0 1 1-.258-.966l.883-.237-.94-.542-1.85.495a.5.5 0 0 1-.258-.966l.883-.237-1.12-.646a.5.5 0 1 1 .5-.866l1.12.646-.237-.883a.5.5 0 0 1 .966-.258l.495 1.849.94.542-.236-.883a.5.5 0 0 1 .966-.258l.495 1.849 1.236.713V5.707L6.147 4.354a.5.5 0 1 1 .707-.708l.646.647V3.207L6.147 1.854a.5.5 0 1 1 .707-.708l.646.647V.5a.5.5 0 0 1 1 0v1.293l.647-.647a.5.5 0 1 1 .707.708L8.5 3.207v1.086l.647-.647a.5.5 0 1 1 .707.708L8.5 5.707v1.427l1.236-.713.495-1.85a.5.5 0 1 1 .966.26l-.236.882.94-.542.495-1.85a.5.5 0 1 1 .966.26l-.236.882 1.12-.646a.5.5 0 0 1 .5.866l-1.12.646.883.237a.5.5 0 1 1-.26.966l-1.848-.495-.94.542.883.237a.5.5 0 1 1-.26.966l-1.848-.495L9 8l1.236.713 1.849-.495a.5.5 0 0 1 .259.966l-.883.237.94.542 1.849-.495a.5.5 0 0 1 .259.966l-.883.237 1.12.646a.5.5 0 0 1-.5.866l-1.12-.646.236.883a.5.5 0 1 1-.966.258l-.495-1.849-.94-.542.236.883a.5.5 0 0 1-.966.258L9.736 9.58 8.5 8.866v1.427l1.354 1.353a.5.5 0 0 1-.707.708l-.647-.647v1.086l1.354 1.353a.5.5 0 0 1-.707.708l-.647-.647V15.5a.5.5 0 0 1-.5.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash3-fill" viewBox="0 0 16 16">
<path d="M11 1.5v1h3.5a.5.5 0 0 1 0 1h-.538l-.853 10.66A2 2 0 0 1 11.115 16h-6.23a2 2 0 0 1-1.994-1.84L2.038 3.5H1.5a.5.5 0 0 1 0-1H5v-1A1.5 1.5 0 0 1 6.5 0h3A1.5 1.5 0 0 1 11 1.5Zm-5 0v1h4v-1a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5ZM4.5 5.029l.5 8.5a.5.5 0 1 0 .998-.06l-.5-8.5a.5.5 0 1 0-.998.06Zm6.53-.528a.5.5 0 0 0-.528.47l-.5 8.5a.5.5 0 0 0 .998.058l.5-8.5a.5.5 0 0 0-.47-.528ZM8 4.5a.5.5 0 0 0-.5.5v8.5a.5.5 0 0 0 1 0V5a.5.5 0 0 0-.5-.5Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 582 B

View File

@@ -76,26 +76,26 @@ body {
font-family: 'DMSans';
font-style: normal;
font-weight: 400;
src: url(./DMSans-Regular.ttf) format('woff2');
src: url(assets/fonts/DMSans-Regular.ttf) format('woff2');
}
@font-face {
font-family: 'DMSans';
font-style: normal;
font-weight: 600;
src: url(./DMSans-Bold.ttf) format('woff2');
src: url(assets/fonts/DMSans-Bold.ttf) format('woff2');
}
@font-face {
font-family: 'DMSans';
font-style: italic;
font-weight: 400;
src: url(./DMSans-Italic.ttf) format('woff2');
src: url(assets/fonts/DMSans-Italic.ttf) format('woff2');
}
@font-face {
font-family: 'DMSans';
font-style: italic;
font-weight: 600;
src: url(./DMSans-BoldItalic.ttf) format('woff2');
src: url(assets/fonts/DMSans-BoldItalic.ttf) format('woff2');
}