feat: stuff i'm tired
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import Beat, {BeatEvents, BeatInitOptions} from "@/Beat";
|
||||
import {IPublisher, Publisher} from "@/Publisher";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import BeatLike from "@/BeatLike";
|
||||
import {greatestCommonDivisor, isPosInt} from "@/utils";
|
||||
|
||||
type BeatGroupInitOptions = {
|
||||
@@ -11,6 +10,7 @@ type BeatGroupInitOptions = {
|
||||
beats?: BeatInitOptions[],
|
||||
loopLength?: number,
|
||||
useAutoBeatLength?: boolean,
|
||||
name?: string,
|
||||
};
|
||||
|
||||
export const enum BeatGroupEvents {
|
||||
@@ -20,6 +20,9 @@ export const enum BeatGroupEvents {
|
||||
TimeSigUpChanged="bge-3",
|
||||
AutoBeatSettingsChanged="bge-4",
|
||||
LockingChanged="bge-5",
|
||||
GlobalLoopLengthChanged="bge-5",
|
||||
GlobalDisplayTypeChanged="bge-6",
|
||||
NameChanged="bge-7",
|
||||
}
|
||||
|
||||
type EventTypeSubscriptions =
|
||||
@@ -28,19 +31,25 @@ type EventTypeSubscriptions =
|
||||
| BeatEvents.WantsRemoval
|
||||
| BeatEvents.Baked;
|
||||
|
||||
type EventTypePublications = BeatGroupEvents | BeatEvents;
|
||||
|
||||
export default class BeatGroup implements IPublisher<EventTypePublications>, BeatLike, ISubscriber<EventTypeSubscriptions> {
|
||||
export default class BeatGroup implements IPublisher<BeatGroupEvents>, ISubscriber<EventTypeSubscriptions> {
|
||||
private static globalCounter = 0;
|
||||
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 timeSigUp: number;
|
||||
private globalLoopLength: number;
|
||||
private globalIsLooping: boolean;
|
||||
private useAutoBeatLength: boolean;
|
||||
private barSettingsLocked = false;
|
||||
private name: string;
|
||||
|
||||
constructor(options?: BeatGroupInitOptions) {
|
||||
BeatGroup.globalCounter++;
|
||||
if (options?.name) {
|
||||
this.name = options.name;
|
||||
} else {
|
||||
this.name = `Pattern ${BeatGroup.globalCounter}`;
|
||||
}
|
||||
if (options?.beats) {
|
||||
for (const beatOptions of options.beats) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -103,7 +112,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
||||
for (const beat of this.beats) {
|
||||
beat.setLoopLength(loopLength);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
|
||||
this.publisher.notifySubs(BeatGroupEvents.GlobalLoopLengthChanged);
|
||||
}
|
||||
|
||||
getLoopLength(): number {
|
||||
@@ -115,7 +124,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
||||
for (const beat of this.beats) {
|
||||
beat.setLooping(isLooping);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
|
||||
this.publisher.notifySubs(BeatGroupEvents.GlobalDisplayTypeChanged);
|
||||
}
|
||||
|
||||
isLooping(): boolean {
|
||||
@@ -238,6 +247,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
||||
const beat = this.getBeatByKey(beatKey);
|
||||
this.beats.splice(this.beats.indexOf(beat), 1);
|
||||
this.autoBeatLength();
|
||||
console.log("removing");
|
||||
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
||||
}
|
||||
|
||||
@@ -284,4 +294,13 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
||||
bakeLoops(): void {
|
||||
this.beats.forEach(beat => beat.bakeLoops());
|
||||
}
|
||||
|
||||
setName(newName: string): void {
|
||||
this.name = newName;
|
||||
this.publisher.notifySubs(BeatGroupEvents.NameChanged);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const appNode = document.querySelector("#app");
|
||||
if (appNode) {
|
||||
try {
|
||||
const appRoot = new RootView({
|
||||
orientation: "vertical",
|
||||
title: "Drum Slayer",
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
@@ -3,10 +3,12 @@ import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
||||
import BeatView from "@/ui/Beat/BeatView";
|
||||
import "./BeatGroup.css";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
|
||||
export type BeatGroupUINodeOptions = UINodeOptions & {
|
||||
title: string,
|
||||
beatGroup: BeatGroup,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
|
||||
const EventTypeSubscriptions = [
|
||||
@@ -18,31 +20,56 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
|
||||
private title: string;
|
||||
private beatGroup: BeatGroup;
|
||||
private beatViews: BeatView[] = [];
|
||||
private currentOrientation: "vertical" | "horizontal";
|
||||
private subscription: ISubscription;
|
||||
|
||||
constructor(options: BeatGroupUINodeOptions) {
|
||||
super(options);
|
||||
this.beatGroup = options.beatGroup;
|
||||
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 {
|
||||
if (event === BeatGroupEvents.BeatListChanged) {
|
||||
this.setupBeatViews();
|
||||
this.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
setBeatGroup(newBeatGroup: BeatGroup): void {
|
||||
this.beatGroup = newBeatGroup;
|
||||
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
build(): HTMLDivElement {
|
||||
private setupBeatViews(): void {
|
||||
this.beatViews = [];
|
||||
for (let i = 0; i < this.beatGroup.getBeatCount(); 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", {
|
||||
classes: ["beat-group"],
|
||||
},[
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "./BeatGroupSettings.css";
|
||||
import UINode, {UINodeOptions} from "@/ui/UINode";
|
||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
||||
import ISubscriber, {SubscriptionEvent} from "@/Subscriber";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
||||
import {BeatEvents} from "@/Beat";
|
||||
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
|
||||
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
|
||||
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
|
||||
@@ -15,13 +14,14 @@ export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
|
||||
const EventTypeSubscriptions = [
|
||||
BeatGroupEvents.BarCountChanged,
|
||||
BeatGroupEvents.TimeSigUpChanged,
|
||||
BeatEvents.DisplayTypeChanged,
|
||||
BeatGroupEvents.GlobalDisplayTypeChanged,
|
||||
BeatGroupEvents.BeatListChanged,
|
||||
BeatGroupEvents.LockingChanged,
|
||||
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 barCountInput!: NumberInputView;
|
||||
private timeSigUpInput!: NumberInputView;
|
||||
@@ -45,7 +45,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
||||
this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch(event) {
|
||||
case BeatGroupEvents.BarCountChanged:
|
||||
this.barCountInput.setValue(this.beatGroup.getBarCount());
|
||||
@@ -66,7 +66,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
||||
case BeatGroupEvents.AutoBeatSettingsChanged:
|
||||
this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn());
|
||||
break;
|
||||
case BeatEvents.DisplayTypeChanged:
|
||||
case BeatGroupEvents.GlobalDisplayTypeChanged:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -82,9 +82,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
||||
}
|
||||
}
|
||||
if (!this.beatSettingsContainer) {
|
||||
this.beatSettingsContainer = UINode.make("div", {
|
||||
subs: this.beatSettingsViews.map(view => view.render())
|
||||
});
|
||||
this.beatSettingsContainer = UINode.make("div", {}, this.beatSettingsViews.map(view => view.render()));
|
||||
} else {
|
||||
this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render()));
|
||||
}
|
||||
@@ -118,18 +116,18 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
||||
UINode.make("div", {
|
||||
classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
|
||||
}, [
|
||||
this.timeSigUpInput.render(),
|
||||
this.timeSigUpInput,
|
||||
]),
|
||||
UINode.make("div", {
|
||||
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
|
||||
,
|
||||
}, [
|
||||
this.barCountInput.render(),
|
||||
this.barCountInput,
|
||||
]),
|
||||
UINode.make("div", {
|
||||
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
|
||||
}, [
|
||||
this.autoBeatLengthCheckbox.render(),
|
||||
this.autoBeatLengthCheckbox,
|
||||
]),
|
||||
new ActionButtonView({
|
||||
label: "New Track",
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
}
|
||||
|
||||
.beat-settings-title-input {
|
||||
width: 100%;
|
||||
.beat-settings-title-container {
|
||||
|
||||
}
|
||||
|
||||
.beat-settings-title-container input {
|
||||
min-width: 100%;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.beat-settings-title {
|
||||
.beat-settings-title-container > div {
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
@@ -15,7 +19,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.beat-settings-title:hover {
|
||||
.beat-settings-title-container > div:hover {
|
||||
background-color: var(--color-ui-neutral-dark-hover);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import "./BeatSettings.css";
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
import UINode, {UINodeOptions} from "@/ui/UINode";
|
||||
import ISubscriber, {SubscriptionEvent} from "@/Subscriber";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
||||
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
|
||||
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
|
||||
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
||||
|
||||
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
|
||||
beat: Beat,
|
||||
@@ -16,16 +17,16 @@ const EventTypeSubscriptions = [
|
||||
BeatEvents.LoopLengthChanged,
|
||||
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 loopLengthInput!: NumberInputView;
|
||||
private bakeButton!: ActionButtonView;
|
||||
private loopCheckbox!: BoolBoxView;
|
||||
private loopLengthSection!: HTMLDivElement;
|
||||
private sub!: ISubscription;
|
||||
private titleInput!: HTMLInputElement;
|
||||
private titleDisplay!: HTMLSpanElement;
|
||||
private title!: EditableTextFieldView;
|
||||
private editingTitle: boolean;
|
||||
|
||||
constructor(options: BeatSettingsViewUINodeOptions) {
|
||||
@@ -36,7 +37,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
||||
}
|
||||
|
||||
private setupBindings() {
|
||||
this.sub = this.beat.addSubscriber(this, "all");
|
||||
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||
}
|
||||
|
||||
setBeat(beat: Beat): void {
|
||||
@@ -46,11 +47,10 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
||||
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch(event) {
|
||||
case BeatEvents.NewName:
|
||||
this.titleInput.value = this.beat.getName();
|
||||
this.titleDisplay.innerText = this.beat.getName();
|
||||
this.title.setText(this.beat.getName());
|
||||
break;
|
||||
case BeatEvents.LoopLengthChanged:
|
||||
this.loopLengthInput.setValue(this.beat.getLoopLength());
|
||||
@@ -68,27 +68,9 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
this.titleInput = UINode.make("input", {
|
||||
value: this.beat.getName(),
|
||||
classes: ["beat-settings-title-input"],
|
||||
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.title = new EditableTextFieldView({
|
||||
initialText: this.beat.getName(),
|
||||
setter: (newText) => this.beat.setName(newText),
|
||||
});
|
||||
this.bakeButton = new ActionButtonView({
|
||||
icon: "snowflake",
|
||||
@@ -121,11 +103,15 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
||||
return UINode.make("div", {
|
||||
classes: ["beat-settings"],
|
||||
}, [
|
||||
this.titleDisplay,
|
||||
UINode.make("div", {
|
||||
classes: ["beat-settings-title-container"]
|
||||
}, [
|
||||
this.title,
|
||||
]),
|
||||
UINode.make("div", {
|
||||
classes: ["beat-settings-lower"],
|
||||
}, [
|
||||
this.bakeButton.render(),
|
||||
this.bakeButton,
|
||||
new ActionButtonView({
|
||||
icon: "trash",
|
||||
type: "secondary",
|
||||
@@ -135,7 +121,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
||||
UINode.make("div", {
|
||||
classes: ["loop-settings"],
|
||||
}, [
|
||||
this.loopCheckbox.render(),
|
||||
this.loopCheckbox,
|
||||
]),
|
||||
this.loopLengthSection,
|
||||
]),
|
||||
|
||||
@@ -33,23 +33,23 @@
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
background-color: var(--color-ui-accent);
|
||||
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);
|
||||
background-color: var(--color-ui-accent-hover);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
private subscription: ISubscription | null = null;
|
||||
private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, BeatUnitView>(this);
|
||||
private touchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
|
||||
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
|
||||
|
||||
constructor(options: BeatUnitUINodeOptions) {
|
||||
super(options);
|
||||
@@ -35,26 +36,37 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
this.notify(this.publisher, BeatUnitEvent.TypeChange);
|
||||
}
|
||||
|
||||
private setupBindings() {
|
||||
this.subscription?.unbind();
|
||||
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
|
||||
this.onMouseDown((ev: MouseEvent) => {
|
||||
private handleMouseDown(ev: MouseEvent): void {
|
||||
if (ev.button === 1) {
|
||||
this.beatUnit.rotateType();
|
||||
}
|
||||
});
|
||||
this.getNode().addEventListener("touchstart", () => {
|
||||
this.touchTimeout = setTimeout(() => {
|
||||
}
|
||||
|
||||
private handleTouchStart(ev: TouchEvent): void {
|
||||
this.touchTimeout = this.touchTimeout || setTimeout(() => {
|
||||
this.beatUnit.rotateType();
|
||||
this.touchTimeout = null;
|
||||
}, 400);
|
||||
});
|
||||
this.getNode().addEventListener("touchend", () => {
|
||||
}
|
||||
|
||||
private handleTouchEnd(ev: TouchEvent): void {
|
||||
if (this.touchTimeout) {
|
||||
clearTimeout(this.touchTimeout);
|
||||
this.touchTimeout = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupBindings() {
|
||||
this.subscription?.unbind();
|
||||
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
|
||||
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
|
||||
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
|
||||
this.redraw();
|
||||
this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener));
|
||||
this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener));
|
||||
this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
|
||||
this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
|
||||
this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
@@ -108,14 +120,12 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
}
|
||||
|
||||
onHover(cb: () => void): void {
|
||||
this.getNode().addEventListener("mouseover", cb);
|
||||
this.hoverListeners.push(cb);
|
||||
this.setupBindings();
|
||||
}
|
||||
|
||||
onMouseDown(cb: (ev: MouseEvent) => void): void {
|
||||
this.getNode().addEventListener("mousedown", cb);
|
||||
}
|
||||
|
||||
onMouseUp(cb: (ev: MouseEvent) => void): void {
|
||||
this.getNode().addEventListener("mouseup", cb);
|
||||
this.mouseDownListeners.push(cb);
|
||||
this.setupBindings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
}
|
||||
|
||||
.root-beat-stage {
|
||||
position: relative;
|
||||
padding: 2em;
|
||||
max-height: 100vh;
|
||||
margin: auto;
|
||||
@@ -96,6 +97,8 @@
|
||||
}
|
||||
|
||||
.vertical-mode .root-beat-stage {
|
||||
margin: 5em auto auto;
|
||||
padding-left: 3em;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,43 @@ import BeatGroup from "@/BeatGroup";
|
||||
import "./Root.css";
|
||||
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
|
||||
import IconView from "@/ui/Widgets/Icon/IconView";
|
||||
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
|
||||
|
||||
export type RootUINodeOptions = UINodeOptions & {
|
||||
title: string,
|
||||
mainBeatGroup?: BeatGroup,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
|
||||
export default class RootView extends UINode {
|
||||
private title: string;
|
||||
private beatGroupView: BeatGroupView;
|
||||
private mainBeatGroup: BeatGroup;
|
||||
private focusedBeatGroup: BeatGroup;
|
||||
private beatGroupSettingsView!: BeatGroupSettingsView;
|
||||
|
||||
private currentOrientation: "horizontal" | "vertical";
|
||||
private stageTitleBarView: StageTitleBarView;
|
||||
|
||||
constructor(options: RootUINodeOptions) {
|
||||
super(options);
|
||||
this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
|
||||
this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup});
|
||||
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.mainBeatGroup});
|
||||
this.currentOrientation = options.orientation ?? "horizontal";
|
||||
this.focusedBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
|
||||
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.setOrientation(this.currentOrientation);
|
||||
this.openSidebarForDesktop();
|
||||
}
|
||||
|
||||
private openSidebarForDesktop() {
|
||||
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
||||
if (mediaQueryList.matches) {
|
||||
this.toggleSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
static defaultMainBeatGroup(): BeatGroup {
|
||||
@@ -39,12 +57,33 @@ export default class RootView extends UINode {
|
||||
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 {
|
||||
this.getNode().classList.toggle("sidebar-visible");
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -71,7 +110,7 @@ export default class RootView extends UINode {
|
||||
]),
|
||||
UINode.make("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
onclick: () => this.mainBeatGroup.bakeLoops(),
|
||||
onclick: () => this.focusedBeatGroup.bakeLoops(),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "snowflake",
|
||||
@@ -81,11 +120,7 @@ export default class RootView extends UINode {
|
||||
UINode.make("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
title: "Reset all",
|
||||
onclick: () => {
|
||||
this.mainBeatGroup = RootView.defaultMainBeatGroup();
|
||||
this.beatGroupSettingsView.setBeatGroup(this.mainBeatGroup);
|
||||
this.beatGroupView.setBeatGroup(this.mainBeatGroup);
|
||||
},
|
||||
onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "trash",
|
||||
@@ -99,11 +134,10 @@ export default class RootView extends UINode {
|
||||
return (
|
||||
UINode.make("div", {classes: ["root-sidebar"]}, [
|
||||
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.buildSidebarStrip(),
|
||||
]),
|
||||
])
|
||||
);
|
||||
}
|
||||
@@ -113,6 +147,7 @@ export default class RootView extends UINode {
|
||||
UINode.make("div", {classes: ["root", "sidebar-visible"]}, [
|
||||
this.buildSidebar(),
|
||||
UINode.make("div", {classes: ["root-beat-stage-container"]}, [
|
||||
this.stageTitleBarView.render(),
|
||||
UINode.make("div", {classes: ["root-beat-stage"]}, [
|
||||
this.beatGroupView.render(),
|
||||
])
|
||||
|
||||
25
src/ui/StageTitleBar/StageTitleBar.css
Normal file
25
src/ui/StageTitleBar/StageTitleBar.css
Normal 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;
|
||||
}
|
||||
50
src/ui/StageTitleBar/StageTitleBarView.ts
Normal file
50
src/ui/StageTitleBar/StageTitleBarView.ts
Normal 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]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ type IRenderAttributes<
|
||||
export default abstract class UINode {
|
||||
protected node: HTMLElement | null = null;
|
||||
|
||||
constructor(options: UINodeOptions) {}
|
||||
constructor(options: UINodeOptions) { /* dummy */ }
|
||||
|
||||
render(): HTMLElement {
|
||||
if (!this.node) {
|
||||
@@ -51,7 +51,7 @@ export default abstract class UINode {
|
||||
K extends keyof HTMLElementTagNameMap[T]>(
|
||||
type: T,
|
||||
attributes: IRenderAttributes<T, K>,
|
||||
subElements?: HTMLElement[],
|
||||
subNodes?: (Node | UINode)[],
|
||||
): HTMLElementTagNameMap[T] {
|
||||
const element = document.createElement(type);
|
||||
if (attributes) {
|
||||
@@ -63,8 +63,14 @@ export default abstract class UINode {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (subElements) {
|
||||
element.append(...subElements);
|
||||
if (subNodes) {
|
||||
for (const subElement of subNodes) {
|
||||
if (subElement instanceof UINode) {
|
||||
element.append(subElement.render());
|
||||
} else {
|
||||
element.append(subElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -50,16 +50,15 @@ export default class ActionButtonView extends UINode {
|
||||
protected build(): HTMLButtonElement {
|
||||
this.buttonElement = UINode.make("button", {
|
||||
classes: ["action-button", `action-button-${this.type}`],
|
||||
onclick: (event: MouseEvent) => this.disabled || this.onClick(event),
|
||||
subs: [
|
||||
onclick: (event: MouseEvent) => this.disabled || this.onClick(event)
|
||||
}, [
|
||||
this.icon !== null ? new IconView({
|
||||
iconName: this.icon,
|
||||
color: "var(--color-p-light)",
|
||||
}).render() : UINode.make("span", {
|
||||
innerText: this.label ?? ""
|
||||
}),
|
||||
],
|
||||
});
|
||||
]);
|
||||
if (this.alt) {
|
||||
this.buttonElement.title = this.alt;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,9 @@ export default class BoolBoxView extends UINode {
|
||||
});
|
||||
return UINode.make("div", {
|
||||
classes: ["bool-box"],
|
||||
subs: [
|
||||
},[
|
||||
this.labelElement,
|
||||
this.checkboxElement,
|
||||
],
|
||||
});
|
||||
]);
|
||||
}
|
||||
}
|
||||
14
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.css
Normal file
14
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.css
Normal 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);
|
||||
}
|
||||
71
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts
Normal file
71
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export default class NumberInputView extends UINode {
|
||||
});
|
||||
return UINode.make("div", {
|
||||
classes: ["number-input"],
|
||||
subs: [
|
||||
}, [
|
||||
this.labelElement,
|
||||
UINode.make("button", {
|
||||
innerText: "-",
|
||||
@@ -126,7 +126,6 @@ export default class NumberInputView extends UINode {
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user