add connectionViewModel

This commit is contained in:
marlon-erler
2024-07-20 02:18:15 +02:00
parent 6d7d9e5250
commit fda853ebbe
6 changed files with 276 additions and 168 deletions
+198 -152
View File
@@ -1,4 +1,144 @@
(() => { (() => {
// node_modules/bloatless-react/index.ts
var State = class {
_value;
_bindings = /* @__PURE__ */ new Set();
// init
constructor(initialValue) {
this._value = initialValue;
}
// value
get value() {
return this._value;
}
set value(newValue) {
if (this._value == newValue) return;
this._value = newValue;
this.callSubscriptions();
}
// subscriptions
callSubscriptions() {
this._bindings.forEach((fn) => fn(this._value));
}
subscribe(fn) {
this._bindings.add(fn);
fn(this._value);
}
subscribeSilent(fn) {
this._bindings.add(fn);
}
// stringify
toString() {
return JSON.stringify(this._value);
}
};
function createProxyState(statesToSubscibe, fn) {
const proxyState = new State(fn());
statesToSubscibe.forEach(
(state) => state.subscribe(() => proxyState.value = fn())
);
return proxyState;
}
function createElement(tagName, attributes = {}, ...children) {
const element = document.createElement(tagName);
if (attributes != null)
Object.entries(attributes).forEach((entry) => {
const [attributename, value] = entry;
const [directiveKey, directiveValue] = attributename.split(":");
switch (directiveKey) {
case "on": {
switch (directiveValue) {
case "enter": {
element.addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
value();
});
break;
}
default: {
element.addEventListener(directiveValue, value);
}
}
break;
}
case "subscribe": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
break;
}
case "bind": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
element.addEventListener(
"input",
() => state.value = element[directiveValue]
);
break;
}
case "toggle": {
if (value.subscribe) {
const state = value;
state.subscribe(
(newValue) => element.toggleAttribute(directiveValue, newValue)
);
} else {
element.toggleAttribute(directiveValue, value);
}
break;
}
case "set": {
const state = value;
state.subscribe(
(newValue) => element.setAttribute(directiveValue, newValue)
);
break;
}
case "children": {
switch (directiveValue) {
case "set": {
const state = value;
state.subscribe((newValue) => {
element.innerHTML = "";
element.append(...[newValue].flat());
});
break;
}
case "append":
case "appendandscroll":
case "prepend": {
element.style.scrollBehavior = "smooth";
try {
const [listState, toElement] = value;
listState.handleAddition((newItem) => {
const child = toElement(newItem);
listState.handleRemoval(
newItem,
() => child.remove()
);
if (directiveValue == "append") {
element.append(child);
} else if (directiveValue == "prepend") {
element.prepend(child);
}
});
} catch {
throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
}
}
}
}
default:
element.setAttribute(attributename, value);
}
});
children.filter((x) => x).forEach((child) => element.append(child));
return element;
}
// node_modules/udn-frontend/index.ts // node_modules/udn-frontend/index.ts
var UDNFrontend = class { var UDNFrontend = class {
ws; ws;
@@ -162,145 +302,46 @@
} }
}; };
// node_modules/bloatless-react/index.ts // src/ViewModel/connectionViewModel.tsx
var State = class { var ConnectionViewModel = class {
_value; connectionModel;
_bindings = /* @__PURE__ */ new Set(); // state
// init serverAddressInput = new State("");
constructor(initialValue) { isConnected = new State(false);
this._value = initialValue; // toggles
} cannotConnect = createProxyState(
// value [this.serverAddressInput, this.isConnected],
get value() { () => this.isConnected.value == true && this.serverAddressInput.value == this.connectionModel.address || this.serverAddressInput.value == ""
return this._value; );
} cannotDisonnect = createProxyState(
set value(newValue) { [this.isConnected],
if (this._value == newValue) return; () => this.isConnected.value == false
this._value = newValue; );
this.callSubscriptions(); // handlers
} connectionChangeHandler = () => {
// subscriptions this.isConnected.value = this.connectionModel.isConnected;
callSubscriptions() { if (this.connectionModel.address) {
this._bindings.forEach((fn) => fn(this._value)); this.serverAddressInput.value = this.connectionModel.address;
}
subscribe(fn) {
this._bindings.add(fn);
fn(this._value);
}
subscribeSilent(fn) {
this._bindings.add(fn);
}
// stringify
toString() {
return JSON.stringify(this._value);
} }
}; };
function createProxyState(statesToSubscibe, fn) { messageHandler = (data) => {
const proxyState = new State(fn()); };
statesToSubscibe.forEach( // methods
(state) => state.subscribe(() => proxyState.value = fn()) connect = () => {
); this.connectionModel.connect(this.serverAddressInput.value);
return proxyState; };
} disconnect = () => {
function createElement(tagName, attributes = {}, ...children) { this.connectionModel.disconnect();
const element = document.createElement(tagName); };
if (attributes != null) // init
Object.entries(attributes).forEach((entry) => { constructor() {
const [attributename, value] = entry; const connectionModel = new ConnectionModel({
const [directiveKey, directiveValue] = attributename.split(":"); connectionChangeHandler: this.connectionChangeHandler,
switch (directiveKey) { messageHandler: this.messageHandler
case "on": {
switch (directiveValue) {
case "enter": {
element.addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
value();
}); });
break; this.connectionModel = connectionModel;
}
default: {
element.addEventListener(directiveValue, value);
}
}
break;
}
case "subscribe": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
break;
}
case "bind": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
element.addEventListener(
"input",
() => state.value = element[directiveValue]
);
break;
}
case "toggle": {
if (value.subscribe) {
const state = value;
state.subscribe(
(newValue) => element.toggleAttribute(directiveValue, newValue)
);
} else {
element.toggleAttribute(directiveValue, value);
}
break;
}
case "set": {
const state = value;
state.subscribe(
(newValue) => element.setAttribute(directiveValue, newValue)
);
break;
}
case "children": {
switch (directiveValue) {
case "set": {
const state = value;
state.subscribe((newValue) => {
element.innerHTML = "";
element.append(...[newValue].flat());
});
break;
}
case "append":
case "appendandscroll":
case "prepend": {
element.style.scrollBehavior = "smooth";
try {
const [listState, toElement] = value;
listState.handleAddition((newItem) => {
const child = toElement(newItem);
listState.handleRemoval(
newItem,
() => child.remove()
);
if (directiveValue == "append") {
element.append(child);
} else if (directiveValue == "prepend") {
element.prepend(child);
}
});
} catch {
throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
}
}
}
}
default:
element.setAttribute(attributename, value);
}
});
children.filter((x) => x).forEach((child) => element.append(child));
return element;
} }
};
// src/View/Components/option.tsx // src/View/Components/option.tsx
function Option(text, value, selectedOnCreate) { function Option(text, value, selectedOnCreate) {
@@ -370,12 +411,20 @@
var translations = allTranslations[language] || allTranslations.en; var translations = allTranslations[language] || allTranslations.en;
// src/View/homePage.tsx // src/View/homePage.tsx
function HomePage(settingsViewModel2) { function HomePage(settingsViewModel2, connectionViewModel2) {
const overviewSection = /* @__PURE__ */ createElement("div", { id: "overview-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.overviewHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.serverAddress), /* @__PURE__ */ createElement("input", null))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement( const overviewSection = /* @__PURE__ */ createElement("div", { id: "overview-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.overviewHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.serverAddress), /* @__PURE__ */ createElement(
"input",
{
"bind:value": connectionViewModel2.serverAddressInput,
"on:enter": connectionViewModel2.connect
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement(
"button", "button",
{ {
class: "danger flex justify-center", class: "danger flex justify-center",
"aria-label": translations.homePage.disconnectAudioLabel "aria-label": translations.homePage.disconnectAudioLabel,
"on:click": connectionViewModel2.disconnect,
"toggle:disabled": connectionViewModel2.cannotDisonnect
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "link_off") /* @__PURE__ */ createElement("span", { class: "icon" }, "link_off")
), /* @__PURE__ */ createElement( ), /* @__PURE__ */ createElement(
@@ -389,7 +438,9 @@
"button", "button",
{ {
class: "primary flex justify-center", class: "primary flex justify-center",
"aria-label": translations.homePage.connectAudioLabel "aria-label": translations.homePage.connectAudioLabel,
"on:click": connectionViewModel2.connect,
"toggle:disabled": connectionViewModel2.cannotConnect
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "link") /* @__PURE__ */ createElement("span", { class: "icon" }, "link")
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "inbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.homePage.mailboxHeadline), /* @__PURE__ */ createElement("span", { class: "error" }, translations.homePage.mailboxDisabled))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "outbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.homePage.outboxHeadline), /* @__PURE__ */ createElement("span", { class: "success" }, translations.homePage.outboxAllItemsSent))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.yourNameLabel), /* @__PURE__ */ createElement( )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "inbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.homePage.mailboxHeadline), /* @__PURE__ */ createElement("span", { class: "error" }, translations.homePage.mailboxDisabled))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "outbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.homePage.outboxHeadline), /* @__PURE__ */ createElement("span", { class: "success" }, translations.homePage.outboxAllItemsSent))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.yourNameLabel), /* @__PURE__ */ createElement(
@@ -577,23 +628,18 @@
this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value); this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value);
}; };
// init // init
constructor(settingsModel2) { constructor(storageModel2) {
this.settingsModel = settingsModel2; const settingsModel = new SettingsModel(storageModel2);
this.nameInput.value = settingsModel2.username; this.settingsModel = settingsModel;
this.firstDayOfWeekInput.value = settingsModel2.firstDayOfWeek; this.nameInput.value = settingsModel.username;
this.firstDayOfWeekInput.value = settingsModel.firstDayOfWeek;
this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek); this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek);
} }
}; };
// src/index.tsx // src/index.tsx
var storageModel = new StorageModel(); var storageModel = new StorageModel();
var settingsModel = new SettingsModel(storageModel); var settingsViewModel = new SettingsViewModel(storageModel);
var connectionModel = new ConnectionModel({ var connectionViewModel = new ConnectionViewModel();
connectionChangeHandler() { document.querySelector("main").append(HomePage(settingsViewModel, connectionViewModel));
},
messageHandler(data) {
}
});
var settingsViewModel = new SettingsViewModel(settingsModel);
document.querySelector("main").append(HomePage(settingsViewModel));
})(); })();
+1 -1
View File
@@ -27,7 +27,7 @@ export default class ConnectionModel {
this.udn.disconnect(); this.udn.disconnect();
}; };
handleConnectionChange = () => { handleConnectionChange = (): void => {
console.log("connection status:", this.isConnected, this.address); console.log("connection status:", this.isConnected, this.address);
}; };
+13 -2
View File
@@ -2,11 +2,15 @@ import "./homePage.css";
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import ConnectionViewModel from "../ViewModel/connectionViewModel";
import { Option } from "./Components/option"; import { Option } from "./Components/option";
import SettingsViewModel from "../ViewModel/settingsViewModel"; import SettingsViewModel from "../ViewModel/settingsViewModel";
import { translations } from "./translations"; import { translations } from "./translations";
export function HomePage(settingsViewModel: SettingsViewModel) { export function HomePage(
settingsViewModel: SettingsViewModel,
connectionViewModel: ConnectionViewModel
) {
// sections // sections
const overviewSection = ( const overviewSection = (
<div id="overview-section"> <div id="overview-section">
@@ -16,7 +20,10 @@ export function HomePage(settingsViewModel: SettingsViewModel) {
<span class="icon">cell_tower</span> <span class="icon">cell_tower</span>
<div> <div>
<span>{translations.homePage.serverAddress}</span> <span>{translations.homePage.serverAddress}</span>
<input></input> <input
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
</div> </div>
</label> </label>
@@ -24,6 +31,8 @@ export function HomePage(settingsViewModel: SettingsViewModel) {
<button <button
class="danger flex justify-center" class="danger flex justify-center"
aria-label={translations.homePage.disconnectAudioLabel} aria-label={translations.homePage.disconnectAudioLabel}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
> >
<span class="icon">link_off</span> <span class="icon">link_off</span>
</button> </button>
@@ -36,6 +45,8 @@ export function HomePage(settingsViewModel: SettingsViewModel) {
<button <button
class="primary flex justify-center" class="primary flex justify-center"
aria-label={translations.homePage.connectAudioLabel} aria-label={translations.homePage.connectAudioLabel}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
> >
<span class="icon">link</span> <span class="icon">link</span>
</button> </button>
+53
View File
@@ -0,0 +1,53 @@
import * as React from "bloatless-react";
import ConnectionModel from "../Model/connectionModel";
import { Message } from "udn-frontend";
export default class ConnectionViewModel {
connectionModel: ConnectionModel;
// state
serverAddressInput: React.State<string> = new React.State("");
isConnected: React.State<boolean> = new React.State(false);
// toggles
cannotConnect: React.State<boolean> = React.createProxyState(
[this.serverAddressInput, this.isConnected],
() =>
(this.isConnected.value == true &&
this.serverAddressInput.value == this.connectionModel.address) ||
this.serverAddressInput.value == ""
);
cannotDisonnect: React.State<boolean> = React.createProxyState(
[this.isConnected],
() => this.isConnected.value == false
);
// handlers
connectionChangeHandler = (): void => {
this.isConnected.value = this.connectionModel.isConnected;
if (this.connectionModel.address) {
this.serverAddressInput.value = this.connectionModel.address;
}
};
messageHandler = (data: Message): void => {};
// methods
connect = (): void => {
this.connectionModel.connect(this.serverAddressInput.value);
};
disconnect = (): void => {
this.connectionModel.disconnect();
};
// init
constructor() {
const connectionModel = new ConnectionModel({
connectionChangeHandler: this.connectionChangeHandler,
messageHandler: this.messageHandler,
});
this.connectionModel = connectionModel;
}
}
+4 -2
View File
@@ -1,6 +1,7 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import SettingsModel from "../Model/settingsModel"; import SettingsModel from "../Model/settingsModel";
import StorageModel from "../Model/storageModel";
export default class SettingsViewModel { export default class SettingsViewModel {
settingsModel: SettingsModel; settingsModel: SettingsModel;
@@ -10,7 +11,7 @@ export default class SettingsViewModel {
firstDayOfWeekInput: React.State<number> = new React.State(0); firstDayOfWeekInput: React.State<number> = new React.State(0);
// toggles // toggles
cannotSetName = React.createProxyState( cannotSetName: React.State<boolean> = React.createProxyState(
[this.nameInput], [this.nameInput],
() => () =>
this.nameInput.value == "" || this.nameInput.value == "" ||
@@ -28,7 +29,8 @@ export default class SettingsViewModel {
}; };
// init // init
constructor(settingsModel: SettingsModel) { constructor(storageModel: StorageModel) {
const settingsModel = new SettingsModel(storageModel);
this.settingsModel = settingsModel; this.settingsModel = settingsModel;
this.nameInput.value = settingsModel.username; this.nameInput.value = settingsModel.username;
+6 -10
View File
@@ -1,20 +1,16 @@
import ConnectionModel from "./Model/connectionModel"; import ConnectionViewModel from "./ViewModel/connectionViewModel";
import { HomePage } from "./View/homePage"; import { HomePage } from "./View/homePage";
import { MainPage } from "./View/mainPage";
import SettingsModel from "./Model/settingsModel";
import SettingsViewModel from "./ViewModel/settingsViewModel"; import SettingsViewModel from "./ViewModel/settingsViewModel";
import StorageModel from "./Model/storageModel"; import StorageModel from "./Model/storageModel";
// models // models
const storageModel = new StorageModel(); const storageModel = new StorageModel();
const settingsModel = new SettingsModel(storageModel);
const connectionModel = new ConnectionModel({
connectionChangeHandler() {},
messageHandler(data) {},
});
// viewModels // viewModels
const settingsViewModel = new SettingsViewModel(settingsModel); const settingsViewModel = new SettingsViewModel(storageModel);
const connectionViewModel = new ConnectionViewModel();
// view // view
document.querySelector("main")!.append(HomePage(settingsViewModel)); document
.querySelector("main")!
.append(HomePage(settingsViewModel, connectionViewModel));