feat: stuff i'm tired

This commit is contained in:
Daniel Ledda
2022-03-27 00:28:51 +01:00
parent 8056a5dc89
commit fcec98d7e5
18 changed files with 400 additions and 154 deletions

View File

@@ -1,7 +1,6 @@
import Beat, {BeatEvents, BeatInitOptions} from "@/Beat"; import Beat, {BeatEvents, BeatInitOptions} from "@/Beat";
import {IPublisher, Publisher} from "@/Publisher"; import {IPublisher, Publisher} from "@/Publisher";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import BeatLike from "@/BeatLike";
import {greatestCommonDivisor, isPosInt} from "@/utils"; import {greatestCommonDivisor, isPosInt} from "@/utils";
type BeatGroupInitOptions = { type BeatGroupInitOptions = {
@@ -11,6 +10,7 @@ type BeatGroupInitOptions = {
beats?: BeatInitOptions[], beats?: BeatInitOptions[],
loopLength?: number, loopLength?: number,
useAutoBeatLength?: boolean, useAutoBeatLength?: boolean,
name?: string,
}; };
export const enum BeatGroupEvents { export const enum BeatGroupEvents {
@@ -20,6 +20,9 @@ export const enum BeatGroupEvents {
TimeSigUpChanged="bge-3", TimeSigUpChanged="bge-3",
AutoBeatSettingsChanged="bge-4", AutoBeatSettingsChanged="bge-4",
LockingChanged="bge-5", LockingChanged="bge-5",
GlobalLoopLengthChanged="bge-5",
GlobalDisplayTypeChanged="bge-6",
NameChanged="bge-7",
} }
type EventTypeSubscriptions = type EventTypeSubscriptions =
@@ -28,19 +31,25 @@ type EventTypeSubscriptions =
| BeatEvents.WantsRemoval | BeatEvents.WantsRemoval
| BeatEvents.Baked; | BeatEvents.Baked;
type EventTypePublications = BeatGroupEvents | BeatEvents; export default class BeatGroup implements IPublisher<BeatGroupEvents>, ISubscriber<EventTypeSubscriptions> {
private static globalCounter = 0;
export default class BeatGroup implements IPublisher<EventTypePublications>, BeatLike, ISubscriber<EventTypeSubscriptions> {
private beats: Beat[] = []; private beats: Beat[] = [];
private publisher: Publisher<EventTypePublications, BeatGroup> = new Publisher<EventTypePublications, BeatGroup>(this); private publisher: Publisher<BeatGroupEvents, BeatGroup> = new Publisher<BeatGroupEvents, BeatGroup>(this);
private barCount: number; private barCount: number;
private timeSigUp: number; private timeSigUp: number;
private globalLoopLength: number; private globalLoopLength: number;
private globalIsLooping: boolean; private globalIsLooping: boolean;
private useAutoBeatLength: boolean; private useAutoBeatLength: boolean;
private barSettingsLocked = false; private barSettingsLocked = false;
private name: string;
constructor(options?: BeatGroupInitOptions) { constructor(options?: BeatGroupInitOptions) {
BeatGroup.globalCounter++;
if (options?.name) {
this.name = options.name;
} else {
this.name = `Pattern ${BeatGroup.globalCounter}`;
}
if (options?.beats) { if (options?.beats) {
for (const beatOptions of options.beats) { for (const beatOptions of options.beats) {
this.addBeat(beatOptions); this.addBeat(beatOptions);
@@ -68,7 +77,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
} }
} }
addSubscriber(subscriber: ISubscriber<EventTypePublications>, eventType: SubscriptionEvent<EventTypePublications>): { unbind: () => void } { addSubscriber(subscriber: ISubscriber<BeatGroupEvents>, eventType: BeatGroupEvents | BeatGroupEvents[]): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType); return this.publisher.addSubscriber(subscriber, eventType);
} }
@@ -103,7 +112,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
for (const beat of this.beats) { for (const beat of this.beats) {
beat.setLoopLength(loopLength); beat.setLoopLength(loopLength);
} }
this.publisher.notifySubs(BeatEvents.LoopLengthChanged); this.publisher.notifySubs(BeatGroupEvents.GlobalLoopLengthChanged);
} }
getLoopLength(): number { getLoopLength(): number {
@@ -115,7 +124,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
for (const beat of this.beats) { for (const beat of this.beats) {
beat.setLooping(isLooping); beat.setLooping(isLooping);
} }
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged); this.publisher.notifySubs(BeatGroupEvents.GlobalDisplayTypeChanged);
} }
isLooping(): boolean { isLooping(): boolean {
@@ -238,6 +247,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
const beat = this.getBeatByKey(beatKey); const beat = this.getBeatByKey(beatKey);
this.beats.splice(this.beats.indexOf(beat), 1); this.beats.splice(this.beats.indexOf(beat), 1);
this.autoBeatLength(); this.autoBeatLength();
console.log("removing");
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged); this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
} }
@@ -284,4 +294,13 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
bakeLoops(): void { bakeLoops(): void {
this.beats.forEach(beat => beat.bakeLoops()); this.beats.forEach(beat => beat.bakeLoops());
} }
setName(newName: string): void {
this.name = newName;
this.publisher.notifySubs(BeatGroupEvents.NameChanged);
}
getName(): string {
return this.name;
}
} }

View File

@@ -6,6 +6,7 @@ const appNode = document.querySelector("#app");
if (appNode) { if (appNode) {
try { try {
const appRoot = new RootView({ const appRoot = new RootView({
orientation: "vertical",
title: "Drum Slayer", title: "Drum Slayer",
}); });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -3,10 +3,12 @@ import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import BeatView from "@/ui/Beat/BeatView"; import BeatView from "@/ui/Beat/BeatView";
import "./BeatGroup.css"; import "./BeatGroup.css";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import {ISubscription} from "@/Publisher";
export type BeatGroupUINodeOptions = UINodeOptions & { export type BeatGroupUINodeOptions = UINodeOptions & {
title: string, title: string,
beatGroup: BeatGroup, beatGroup: BeatGroup,
orientation?: "horizontal" | "vertical",
}; };
const EventTypeSubscriptions = [ const EventTypeSubscriptions = [
@@ -18,31 +20,56 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
private title: string; private title: string;
private beatGroup: BeatGroup; private beatGroup: BeatGroup;
private beatViews: BeatView[] = []; private beatViews: BeatView[] = [];
private currentOrientation: "vertical" | "horizontal";
private subscription: ISubscription;
constructor(options: BeatGroupUINodeOptions) { constructor(options: BeatGroupUINodeOptions) {
super(options); super(options);
this.beatGroup = options.beatGroup; this.beatGroup = options.beatGroup;
this.title = options.title; this.title = options.title;
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged); this.currentOrientation = options.orientation ?? "horizontal";
this.subscription = this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
this.setupBeatViews();
} }
notify(publisher: unknown, event: EventTypeSubscriptions): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
if (event === BeatGroupEvents.BeatListChanged) { if (event === BeatGroupEvents.BeatListChanged) {
this.setupBeatViews();
this.redraw(); this.redraw();
} }
} }
setBeatGroup(newBeatGroup: BeatGroup): void { private setupBeatViews(): void {
this.beatGroup = newBeatGroup;
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.redraw();
}
build(): HTMLDivElement {
this.beatViews = []; this.beatViews = [];
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) { for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)})); this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
} }
if (this.currentOrientation === "vertical") {
this.reverseDisplayOrder();
}
}
setOrientation(orientation: "vertical" | "horizontal"): void {
if (this.currentOrientation !== orientation) {
this.reverseDisplayOrder();
this.currentOrientation = orientation;
}
}
private reverseDisplayOrder(): void {
this.beatViews = this.beatViews.reverse();
this.getNode().classList.toggle("vertical");
this.redraw();
}
setBeatGroup(newBeatGroup: BeatGroup): void {
this.subscription.unbind();
this.beatGroup = newBeatGroup;
this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.redraw();
}
build(): HTMLDivElement {
return UINode.make("div", { return UINode.make("div", {
classes: ["beat-group"], classes: ["beat-group"],
},[ },[

View File

@@ -1,9 +1,8 @@
import "./BeatGroupSettings.css"; import "./BeatGroupSettings.css";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {UINodeOptions} from "@/ui/UINode";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import ISubscriber, {SubscriptionEvent} from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup"; import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import {BeatEvents} from "@/Beat";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView"; import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView"; import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView"; import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
@@ -15,13 +14,14 @@ export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
const EventTypeSubscriptions = [ const EventTypeSubscriptions = [
BeatGroupEvents.BarCountChanged, BeatGroupEvents.BarCountChanged,
BeatGroupEvents.TimeSigUpChanged, BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged, BeatGroupEvents.GlobalDisplayTypeChanged,
BeatGroupEvents.BeatListChanged, BeatGroupEvents.BeatListChanged,
BeatGroupEvents.LockingChanged, BeatGroupEvents.LockingChanged,
BeatGroupEvents.AutoBeatSettingsChanged, BeatGroupEvents.AutoBeatSettingsChanged,
]; ];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatGroupSettingsView extends UINode implements ISubscriber<typeof EventTypeSubscriptions> { export default class BeatGroupSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beatGroup: BeatGroup; private beatGroup: BeatGroup;
private barCountInput!: NumberInputView; private barCountInput!: NumberInputView;
private timeSigUpInput!: NumberInputView; private timeSigUpInput!: NumberInputView;
@@ -45,7 +45,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
this.beatGroup.addSubscriber(this, EventTypeSubscriptions); this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
} }
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch(event) { switch(event) {
case BeatGroupEvents.BarCountChanged: case BeatGroupEvents.BarCountChanged:
this.barCountInput.setValue(this.beatGroup.getBarCount()); this.barCountInput.setValue(this.beatGroup.getBarCount());
@@ -66,7 +66,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
case BeatGroupEvents.AutoBeatSettingsChanged: case BeatGroupEvents.AutoBeatSettingsChanged:
this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn()); this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn());
break; break;
case BeatEvents.DisplayTypeChanged: case BeatGroupEvents.GlobalDisplayTypeChanged:
break; break;
} }
} }
@@ -82,9 +82,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
} }
} }
if (!this.beatSettingsContainer) { if (!this.beatSettingsContainer) {
this.beatSettingsContainer = UINode.make("div", { this.beatSettingsContainer = UINode.make("div", {}, this.beatSettingsViews.map(view => view.render()));
subs: this.beatSettingsViews.map(view => view.render())
});
} else { } else {
this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render())); this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render()));
} }
@@ -118,18 +116,18 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
UINode.make("div", { UINode.make("div", {
classes: ["beat-group-settings-boxes", "beat-group-settings-option"], classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
}, [ }, [
this.timeSigUpInput.render(), this.timeSigUpInput,
]), ]),
UINode.make("div", { UINode.make("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"] classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
, ,
}, [ }, [
this.barCountInput.render(), this.barCountInput,
]), ]),
UINode.make("div", { UINode.make("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"], classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
}, [ }, [
this.autoBeatLengthCheckbox.render(), this.autoBeatLengthCheckbox,
]), ]),
new ActionButtonView({ new ActionButtonView({
label: "New Track", label: "New Track",

View File

@@ -2,12 +2,16 @@
} }
.beat-settings-title-input { .beat-settings-title-container {
width: 100%;
}
.beat-settings-title-container input {
min-width: 100%;
height: 2em; height: 2em;
} }
.beat-settings-title { .beat-settings-title-container > div {
width: 100%; width: 100%;
font-weight: bold; font-weight: bold;
padding: 0.5em; padding: 0.5em;
@@ -15,7 +19,7 @@
cursor: pointer; cursor: pointer;
} }
.beat-settings-title:hover { .beat-settings-title-container > div:hover {
background-color: var(--color-ui-neutral-dark-hover); background-color: var(--color-ui-neutral-dark-hover);
} }

View File

@@ -1,11 +1,12 @@
import "./BeatSettings.css"; import "./BeatSettings.css";
import Beat, {BeatEvents} from "@/Beat"; import Beat, {BeatEvents} from "@/Beat";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {UINodeOptions} from "@/ui/UINode";
import ISubscriber, {SubscriptionEvent} from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import {ISubscription} from "@/Publisher"; import {ISubscription} from "@/Publisher";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView"; import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView"; import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
export type BeatSettingsViewUINodeOptions = UINodeOptions & { export type BeatSettingsViewUINodeOptions = UINodeOptions & {
beat: Beat, beat: Beat,
@@ -16,16 +17,16 @@ const EventTypeSubscriptions = [
BeatEvents.LoopLengthChanged, BeatEvents.LoopLengthChanged,
BeatEvents.DisplayTypeChanged, BeatEvents.DisplayTypeChanged,
]; ];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatSettingsView extends UINode implements ISubscriber<typeof EventTypeSubscriptions> { export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beat: Beat; private beat: Beat;
private loopLengthInput!: NumberInputView; private loopLengthInput!: NumberInputView;
private bakeButton!: ActionButtonView; private bakeButton!: ActionButtonView;
private loopCheckbox!: BoolBoxView; private loopCheckbox!: BoolBoxView;
private loopLengthSection!: HTMLDivElement; private loopLengthSection!: HTMLDivElement;
private sub!: ISubscription; private sub!: ISubscription;
private titleInput!: HTMLInputElement; private title!: EditableTextFieldView;
private titleDisplay!: HTMLSpanElement;
private editingTitle: boolean; private editingTitle: boolean;
constructor(options: BeatSettingsViewUINodeOptions) { constructor(options: BeatSettingsViewUINodeOptions) {
@@ -36,7 +37,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
} }
private setupBindings() { private setupBindings() {
this.sub = this.beat.addSubscriber(this, "all"); this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
} }
setBeat(beat: Beat): void { setBeat(beat: Beat): void {
@@ -46,11 +47,10 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType)); EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
} }
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch(event) { switch(event) {
case BeatEvents.NewName: case BeatEvents.NewName:
this.titleInput.value = this.beat.getName(); this.title.setText(this.beat.getName());
this.titleDisplay.innerText = this.beat.getName();
break; break;
case BeatEvents.LoopLengthChanged: case BeatEvents.LoopLengthChanged:
this.loopLengthInput.setValue(this.beat.getLoopLength()); this.loopLengthInput.setValue(this.beat.getLoopLength());
@@ -68,27 +68,9 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
} }
build(): HTMLElement { build(): HTMLElement {
this.titleInput = UINode.make("input", { this.title = new EditableTextFieldView({
value: this.beat.getName(), initialText: this.beat.getName(),
classes: ["beat-settings-title-input"], setter: (newText) => this.beat.setName(newText),
type: "text",
oninput: (event: Event) => {
this.beat.setName((event.target as HTMLInputElement).value);
},
onblur: () => this.titleInput.replaceWith(this.titleDisplay),
onkeyup: (event: KeyboardEvent) => {
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
}
}
});
this.titleDisplay = UINode.make("div", {
innerText: this.beat.getName(),
classes: ["beat-settings-title"],
onclick: () => {
this.titleDisplay.replaceWith(this.titleInput);
this.titleInput.focus();
}
}); });
this.bakeButton = new ActionButtonView({ this.bakeButton = new ActionButtonView({
icon: "snowflake", icon: "snowflake",
@@ -121,11 +103,15 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
return UINode.make("div", { return UINode.make("div", {
classes: ["beat-settings"], classes: ["beat-settings"],
}, [ }, [
this.titleDisplay, UINode.make("div", {
classes: ["beat-settings-title-container"]
}, [
this.title,
]),
UINode.make("div", { UINode.make("div", {
classes: ["beat-settings-lower"], classes: ["beat-settings-lower"],
}, [ }, [
this.bakeButton.render(), this.bakeButton,
new ActionButtonView({ new ActionButtonView({
icon: "trash", icon: "trash",
type: "secondary", type: "secondary",
@@ -135,7 +121,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
UINode.make("div", { UINode.make("div", {
classes: ["loop-settings"], classes: ["loop-settings"],
}, [ }, [
this.loopCheckbox.render(), this.loopCheckbox,
]), ]),
this.loopLengthSection, this.loopLengthSection,
]), ]),

View File

@@ -33,23 +33,23 @@
background-color: var(--color-ui-accent-hover); background-color: var(--color-ui-accent-hover);
} }
.beat-unit.beat-unit-on.beat-unit-ghost { .beat-unit.beat-unit-on.beat-unit-accent {
border-color: var(--color-ui-neutral-light); border-color: var(--color-ui-neutral-light);
background-color: var(--color-ui-accent); background-color: var(--color-ui-accent);
} }
.beat-unit.beat-unit-on.beat-unit-ghost:hover { .beat-unit.beat-unit-on.beat-unit-accent:hover {
border-color: var(--color-ui-neutral-light); border-color: var(--color-ui-neutral-light);
background-color: var(--color-ui-accent-hover); background-color: var(--color-ui-accent-hover);
} }
.beat-unit.beat-unit-on.beat-unit-accent { .beat-unit.beat-unit-on.beat-unit-ghost {
border-color: var(--color-ui-accent); border-color: var(--color-ui-accent);
background-color: var(--color-ui-accent); background-color: var(--color-ui-accent);
opacity: 60%; opacity: 60%;
} }
.beat-unit.beat-unit-on.beat-unit-accent:hover { .beat-unit.beat-unit-on.beat-unit-ghost: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);
} }

View File

@@ -20,7 +20,8 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
private subscription: ISubscription | null = null; private subscription: ISubscription | null = null;
private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, BeatUnitView>(this); private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, BeatUnitView>(this);
private touchTimeout: ReturnType<typeof setTimeout> | null = null; private touchTimeout: ReturnType<typeof setTimeout> | null = null;
private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
constructor(options: BeatUnitUINodeOptions) { constructor(options: BeatUnitUINodeOptions) {
super(options); super(options);
@@ -35,26 +36,37 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
this.notify(this.publisher, BeatUnitEvent.TypeChange); this.notify(this.publisher, BeatUnitEvent.TypeChange);
} }
private handleMouseDown(ev: MouseEvent): void {
if (ev.button === 1) {
this.beatUnit.rotateType();
}
}
private handleTouchStart(ev: TouchEvent): void {
this.touchTimeout = this.touchTimeout || setTimeout(() => {
this.beatUnit.rotateType();
this.touchTimeout = null;
}, 400);
}
private handleTouchEnd(ev: TouchEvent): void {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
this.touchTimeout = null;
}
}
private setupBindings() { private setupBindings() {
this.subscription?.unbind(); this.subscription?.unbind();
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions); this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
this.onMouseDown((ev: MouseEvent) => { this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
if (ev.button === 1) { this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
this.beatUnit.rotateType(); this.redraw();
} this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener));
}); this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener));
this.getNode().addEventListener("touchstart", () => { this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
this.touchTimeout = setTimeout(() => { this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
this.beatUnit.rotateType(); this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
this.touchTimeout = null;
}, 400);
});
this.getNode().addEventListener("touchend", () => {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
this.touchTimeout = null;
}
});
} }
toggle(): void { toggle(): void {
@@ -108,14 +120,12 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
} }
onHover(cb: () => void): void { onHover(cb: () => void): void {
this.getNode().addEventListener("mouseover", cb); this.hoverListeners.push(cb);
this.setupBindings();
} }
onMouseDown(cb: (ev: MouseEvent) => void): void { onMouseDown(cb: (ev: MouseEvent) => void): void {
this.getNode().addEventListener("mousedown", cb); this.mouseDownListeners.push(cb);
} this.setupBindings();
onMouseUp(cb: (ev: MouseEvent) => void): void {
this.getNode().addEventListener("mouseup", cb);
} }
} }

View File

@@ -88,6 +88,7 @@
} }
.root-beat-stage { .root-beat-stage {
position: relative;
padding: 2em; padding: 2em;
max-height: 100vh; max-height: 100vh;
margin: auto; margin: auto;
@@ -96,6 +97,8 @@
} }
.vertical-mode .root-beat-stage { .vertical-mode .root-beat-stage {
margin: 5em auto auto;
padding-left: 3em;
height: 100vh; height: 100vh;
} }

View File

@@ -4,25 +4,43 @@ import BeatGroup from "@/BeatGroup";
import "./Root.css"; import "./Root.css";
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView"; import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
import IconView from "@/ui/Widgets/Icon/IconView"; import IconView from "@/ui/Widgets/Icon/IconView";
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
export type RootUINodeOptions = UINodeOptions & { export type RootUINodeOptions = UINodeOptions & {
title: string, title: string,
mainBeatGroup?: BeatGroup, mainBeatGroup?: BeatGroup,
orientation?: "horizontal" | "vertical",
}; };
export default class RootView extends UINode { export default class RootView extends UINode {
private title: string; private title: string;
private beatGroupView: BeatGroupView; private beatGroupView: BeatGroupView;
private mainBeatGroup: BeatGroup; private focusedBeatGroup: BeatGroup;
private beatGroupSettingsView!: BeatGroupSettingsView; private beatGroupSettingsView!: BeatGroupSettingsView;
private currentOrientation: "horizontal" | "vertical";
private stageTitleBarView: StageTitleBarView;
constructor(options: RootUINodeOptions) { constructor(options: RootUINodeOptions) {
super(options); super(options);
this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup(); this.currentOrientation = options.orientation ?? "horizontal";
this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup}); this.focusedBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.mainBeatGroup}); this.beatGroupView = new BeatGroupView({
title: options.title,
beatGroup: this.focusedBeatGroup,
orientation: this.currentOrientation,
});
this.stageTitleBarView = new StageTitleBarView({beatGroup: this.focusedBeatGroup});
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.focusedBeatGroup});
this.title = options.title; this.title = options.title;
this.setOrientation(this.currentOrientation);
this.openSidebarForDesktop();
}
private openSidebarForDesktop() {
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
if (mediaQueryList.matches) {
this.toggleSidebar();
}
} }
static defaultMainBeatGroup(): BeatGroup { static defaultMainBeatGroup(): BeatGroup {
@@ -39,12 +57,33 @@ export default class RootView extends UINode {
return mainBeatGroup; return mainBeatGroup;
} }
setMainBeatGroup(beatGroup: BeatGroup): void {
this.focusedBeatGroup = beatGroup;
this.beatGroupSettingsView.setBeatGroup(this.focusedBeatGroup);
this.beatGroupView.setBeatGroup(this.focusedBeatGroup);
this.stageTitleBarView.setBeatGroup(this.focusedBeatGroup);
}
toggleSidebar(): void { toggleSidebar(): void {
this.getNode().classList.toggle("sidebar-visible"); this.getNode().classList.toggle("sidebar-visible");
} }
toggleOrientation(): void { toggleOrientation(): void {
this.getNode().classList.toggle("vertical-mode"); if (this.currentOrientation === "vertical") {
this.setOrientation("horizontal");
} else {
this.setOrientation("vertical");
}
}
setOrientation(orientation: "horizontal" | "vertical"): void {
this.currentOrientation = orientation;
if (orientation === "vertical") {
this.getNode().classList.add("vertical-mode");
} else {
this.getNode().classList.remove("vertical-mode");
}
this.beatGroupView.setOrientation(orientation);
} }
private buildSidebarStrip(): HTMLElement { private buildSidebarStrip(): HTMLElement {
@@ -71,7 +110,7 @@ export default class RootView extends UINode {
]), ]),
UINode.make("div", { UINode.make("div", {
classes: ["root-quick-access-button"], classes: ["root-quick-access-button"],
onclick: () => this.mainBeatGroup.bakeLoops(), onclick: () => this.focusedBeatGroup.bakeLoops(),
}, [ }, [
new IconView({ new IconView({
iconName: "snowflake", iconName: "snowflake",
@@ -81,11 +120,7 @@ export default class RootView extends UINode {
UINode.make("div", { UINode.make("div", {
classes: ["root-quick-access-button"], classes: ["root-quick-access-button"],
title: "Reset all", title: "Reset all",
onclick: () => { onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()),
this.mainBeatGroup = RootView.defaultMainBeatGroup();
this.beatGroupSettingsView.setBeatGroup(this.mainBeatGroup);
this.beatGroupView.setBeatGroup(this.mainBeatGroup);
},
}, [ }, [
new IconView({ new IconView({
iconName: "trash", iconName: "trash",
@@ -99,11 +134,10 @@ export default class RootView extends UINode {
return ( return (
UINode.make("div", {classes: ["root-sidebar"]}, [ UINode.make("div", {classes: ["root-sidebar"]}, [
UINode.make("div", {classes: ["root-settings"]}, [ UINode.make("div", {classes: ["root-settings"]}, [
UINode.make("h1", {classes: ["root-title"], innerText: this.title}, [ UINode.make("h1", {classes: ["root-title"], innerText: this.title}),
this.beatGroupSettingsView.render(), this.beatGroupSettingsView.render(),
]),
this.buildSidebarStrip(),
]), ]),
this.buildSidebarStrip(),
]) ])
); );
} }
@@ -113,6 +147,7 @@ export default class RootView extends UINode {
UINode.make("div", {classes: ["root", "sidebar-visible"]}, [ UINode.make("div", {classes: ["root", "sidebar-visible"]}, [
this.buildSidebar(), this.buildSidebar(),
UINode.make("div", {classes: ["root-beat-stage-container"]}, [ UINode.make("div", {classes: ["root-beat-stage-container"]}, [
this.stageTitleBarView.render(),
UINode.make("div", {classes: ["root-beat-stage"]}, [ UINode.make("div", {classes: ["root-beat-stage"]}, [
this.beatGroupView.render(), this.beatGroupView.render(),
]) ])

View File

@@ -0,0 +1,25 @@
.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-preamble {
margin-bottom: 4px;
font-size: 12px;
}
.stage-title-bar * {
flex: 1;
}
.stage-title-bar h2 {
margin: 0;
}

View File

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

View File

@@ -13,7 +13,7 @@ type IRenderAttributes<
export default abstract class UINode { export default abstract class UINode {
protected node: HTMLElement | null = null; protected node: HTMLElement | null = null;
constructor(options: UINodeOptions) {} constructor(options: UINodeOptions) { /* dummy */ }
render(): HTMLElement { render(): HTMLElement {
if (!this.node) { if (!this.node) {
@@ -51,7 +51,7 @@ export default abstract class UINode {
K extends keyof HTMLElementTagNameMap[T]>( K extends keyof HTMLElementTagNameMap[T]>(
type: T, type: T,
attributes: IRenderAttributes<T, K>, attributes: IRenderAttributes<T, K>,
subElements?: HTMLElement[], subNodes?: (Node | UINode)[],
): HTMLElementTagNameMap[T] { ): HTMLElementTagNameMap[T] {
const element = document.createElement(type); const element = document.createElement(type);
if (attributes) { if (attributes) {
@@ -63,8 +63,14 @@ export default abstract class UINode {
} }
} }
} }
if (subElements) { if (subNodes) {
element.append(...subElements); for (const subElement of subNodes) {
if (subElement instanceof UINode) {
element.append(subElement.render());
} else {
element.append(subElement);
}
}
} }
return element; return element;
} }

View File

@@ -50,16 +50,15 @@ export default class ActionButtonView extends UINode {
protected build(): HTMLButtonElement { protected build(): HTMLButtonElement {
this.buttonElement = UINode.make("button", { this.buttonElement = UINode.make("button", {
classes: ["action-button", `action-button-${this.type}`], classes: ["action-button", `action-button-${this.type}`],
onclick: (event: MouseEvent) => this.disabled || this.onClick(event), onclick: (event: MouseEvent) => this.disabled || this.onClick(event)
subs: [ }, [
this.icon !== null ? new IconView({ this.icon !== null ? new IconView({
iconName: this.icon, iconName: this.icon,
color: "var(--color-p-light)", color: "var(--color-p-light)",
}).render() : UINode.make("span", { }).render() : UINode.make("span", {
innerText: this.label ?? "" innerText: this.label ?? ""
}), }),
], ]);
});
if (this.alt) { if (this.alt) {
this.buttonElement.title = this.alt; this.buttonElement.title = this.alt;
} }

View File

@@ -55,10 +55,9 @@ export default class BoolBoxView extends UINode {
}); });
return UINode.make("div", { return UINode.make("div", {
classes: ["bool-box"], classes: ["bool-box"],
subs: [ },[
this.labelElement, this.labelElement,
this.checkboxElement, this.checkboxElement,
], ]);
});
} }
} }

View File

@@ -0,0 +1,14 @@
input.editable-text-field-view {
width: fit-content;
max-width: 200px;
}
div.editable-text-field-view {
width: 100%;
transition: background-color 200ms;
cursor: pointer;
}
div.editable-text-field-view:hover {
background-color: var(--color-ui-neutral-dark-hover);
}

View File

@@ -0,0 +1,71 @@
import UINode, {UINodeOptions} from "@/ui/UINode";
import "./EditableTextFieldView.css";
export type EditableTextFieldViewOptions = UINodeOptions & {
initialText?: string,
setter?: (newString: string) => void,
noEmpty?: boolean,
};
export default class EditableTextFieldView extends UINode {
private text: string;
private titleInput!: HTMLInputElement;
private setter: (newString: string) => void;
private titleDisplay!: HTMLElement;
private noEmpty: boolean;
private lastNonEmptyInput = "";
constructor(options: EditableTextFieldViewOptions) {
super(options);
this.setter = options.setter ?? (() => {/* dummy */});
this.text = options.initialText ?? "";
this.noEmpty = options.noEmpty ?? false;
}
setText(newText: string): void {
if (newText !== "" || !this.noEmpty) {
this.text = newText;
this.titleInput.value = this.text;
this.titleDisplay.innerText = this.text;
}
}
build(): HTMLSpanElement {
this.titleInput = UINode.make("input", {
value: this.text,
classes: ["editable-text-field-view"],
type: "text",
oninput: (event: Event) => {
const input = (event.target as HTMLInputElement).value;
if (input === "") {
if (!this.noEmpty) {
this.setter(input);
}
} else {
this.setter(input);
this.lastNonEmptyInput = input;
}
},
onblur: (event: FocusEvent) => {
if ((event.target as HTMLInputElement).value === "") {
this.setText(this.lastNonEmptyInput);
}
this.titleInput.replaceWith(this.titleDisplay);
},
onkeyup: (event: KeyboardEvent) => {
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
}
},
});
this.titleDisplay = UINode.make("div", {
innerText: this.text,
classes: ["editable-text-field-view"],
onclick: () => {
this.titleDisplay.replaceWith(this.titleInput);
this.titleInput.focus();
},
});
return this.titleDisplay;
}
}

View File

@@ -101,32 +101,31 @@ export default class NumberInputView extends UINode {
}); });
return UINode.make("div", { return UINode.make("div", {
classes: ["number-input"], classes: ["number-input"],
subs: [ }, [
this.labelElement, this.labelElement,
UINode.make("button", { UINode.make("button", {
innerText: "-", innerText: "-",
classes: ["number-input-button", "number-input-dec"], classes: ["number-input-button", "number-input-dec"],
onclick: () => { onclick: () => {
if (this.onDecrement) { if (this.onDecrement) {
this.onDecrement(); this.onDecrement();
} else if (this.setter && this.getter) { } else if (this.setter && this.getter) {
this.setter(this.getter() - 1); this.setter(this.getter() - 1);
} }
}, },
}), }),
this.inputElement, this.inputElement,
UINode.make("button", { UINode.make("button", {
innerText: "+", innerText: "+",
classes: ["number-input-button", "number-input-inc"], classes: ["number-input-button", "number-input-inc"],
onclick: () => { onclick: () => {
if (this.onIncrement) { if (this.onIncrement) {
this.onIncrement(); this.onIncrement();
} else if (this.setter && this.getter) { } else if (this.setter && this.getter) {
this.setter(this.getter() + 1); this.setter(this.getter() + 1);
} }
}, },
}), }),
], ]);
});
} }
} }