implement auto reconnect

This commit is contained in:
marlon-erler
2026-04-01 08:29:39 +00:00
parent c52d7b7685
commit 9d400a9929
82 changed files with 8340 additions and 7641 deletions
+22 -19
View File
@@ -4,29 +4,32 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { allowDrop } from "../utility";
export function BoardEntry(boardViewModel: BoardViewModel) {
const view = (
<button
set:color={boardViewModel.color}
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span class="shadow" subscribe:innerText={boardViewModel.name}></span>
<b subscribe:innerText={boardViewModel.name}></b>
</button>
);
const view = (
<button
set:color={boardViewModel.color}
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span
class="shadow"
subscribe:innerText={boardViewModel.name}
></span>
<b subscribe:innerText={boardViewModel.name}></b>
</button>
);
boardViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
boardViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const BoardViewModelToEntry: React.StateItemConverter<BoardViewModel> = (
boardViewModel: BoardViewModel
boardViewModel: BoardViewModel,
) => {
return BoardEntry(boardViewModel);
return BoardEntry(boardViewModel);
};
+13 -13
View File
@@ -1,25 +1,25 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
import { RibbonButton } from "./ribbonButton";
export function BoardViewToggleButton(
label: string,
icon: string,
page: BoardPageType,
boardViewModel: BoardViewModel
label: string,
icon: string,
page: BoardPageType,
boardViewModel: BoardViewModel,
) {
function select() {
boardViewModel.selectedPage.value = page;
}
function select() {
boardViewModel.selectedPage.value = page;
}
const isSelected = React.createProxyState(
[boardViewModel.selectedPage],
() => boardViewModel.selectedPage.value == page
);
const isSelected = React.createProxyState(
[boardViewModel.selectedPage],
() => boardViewModel.selectedPage.value == page,
);
return RibbonButton(label, icon, isSelected, select);
return RibbonButton(label, icon, isSelected, select);
}
+27 -23
View File
@@ -3,33 +3,37 @@ import * as React from "bloatless-react";
import ChatViewModel from "../../ViewModel/Chat/chatViewModel";
export function ChatEntry(chatViewModel: ChatViewModel) {
const view = (
<button
class="tile colored-tile chat-entry"
set:color={chatViewModel.settingsPageViewModel.color}
style="height: 8rem"
on:click={chatViewModel.open}
toggle:unread={chatViewModel.hasUnreadMessages}
>
<span
class="shadow"
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
></span>
<h2
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
></h2>
</button>
);
const view = (
<button
class="tile colored-tile chat-entry"
set:color={chatViewModel.settingsPageViewModel.color}
style="height: 8rem"
on:click={chatViewModel.open}
toggle:unread={chatViewModel.hasUnreadMessages}
>
<span
class="shadow"
subscribe:innerText={
chatViewModel.settingsPageViewModel.primaryChannel
}
></span>
<h2
subscribe:innerText={
chatViewModel.settingsPageViewModel.primaryChannel
}
></h2>
</button>
);
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const ChatViewModelToChatEntry: React.StateItemConverter<
ChatViewModel
ChatViewModel
> = (chatViewModel: ChatViewModel) => {
return ChatEntry(chatViewModel);
return ChatEntry(chatViewModel);
};
+55 -51
View File
@@ -6,61 +6,65 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { translations } from "../translations";
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case ChatMessageStatus.Outbox:
return "hourglass_top";
case ChatMessageStatus.Sent:
return "check";
case ChatMessageStatus.Received:
return "done_all";
default:
return "warning";
}
}
);
return (
<div
class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser}
>
<div class="main tile">
<div class="text-container">
<span class="sender-name ellipsis">
{chatMessageViewModel.sender}
</span>
<span
class="body"
subscribe:innerText={chatMessageViewModel.body}
></span>
<span class="timestamp ellipsis">
<span class="icon" subscribe:innerText={statusIcon}></span>
{chatMessageViewModel.dateSent}
</span>
</div>
<div class="button-container">
<button
on:click={chatMessageViewModel.showInfoModal}
aria-label={
translations.chatPage.message.showMessageInfoButtonAudioLabel
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case ChatMessageStatus.Outbox:
return "hourglass_top";
case ChatMessageStatus.Sent:
return "check";
case ChatMessageStatus.Received:
return "done_all";
default:
return "warning";
}
>
<span class="icon">info</span>
</button>
</div>
</div>
},
);
{ChatMessageInfoModal(chatMessageViewModel)}
</div>
);
return (
<div
class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser}
>
<div class="main tile">
<div class="text-container">
<span class="sender-name ellipsis">
{chatMessageViewModel.sender}
</span>
<span
class="body"
subscribe:innerText={chatMessageViewModel.body}
></span>
<span class="timestamp ellipsis">
<span
class="icon"
subscribe:innerText={statusIcon}
></span>
{chatMessageViewModel.dateSent}
</span>
</div>
<div class="button-container">
<button
on:click={chatMessageViewModel.showInfoModal}
aria-label={
translations.chatPage.message
.showMessageInfoButtonAudioLabel
}
>
<span class="icon">info</span>
</button>
</div>
</div>
{ChatMessageInfoModal(chatMessageViewModel)}
</div>
);
}
export const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(chatMessageViewModel);
return ChatMessage(chatMessageViewModel);
};
+13 -13
View File
@@ -1,25 +1,25 @@
import * as React from "bloatless-react";
import ChatViewModel, {
ChatPageType,
ChatPageType,
} from "../../ViewModel/Chat/chatViewModel";
import { RibbonButton } from "./ribbonButton";
export function ChatViewToggleButton(
label: string,
icon: string,
page: ChatPageType,
chatViewModel: ChatViewModel
label: string,
icon: string,
page: ChatPageType,
chatViewModel: ChatViewModel,
) {
function select() {
chatViewModel.selectedPage.value = page;
}
function select() {
chatViewModel.selectedPage.value = page;
}
const isSelected = React.createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page
);
const isSelected = React.createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page,
);
return RibbonButton(label, icon, isSelected, select);
return RibbonButton(label, icon, isSelected, select);
}
+22 -22
View File
@@ -3,28 +3,28 @@ import * as React from "bloatless-react";
import { Color } from "../../colors";
export function ColorPicker(selectedColor: React.State<Color>) {
return (
<div class="flex-row gap width-input">
{...Object.values(Color).map((color) => {
const isSelected = React.createProxyState(
[selectedColor],
() => selectedColor.value == color
);
return (
<div class="flex-row gap width-input">
{...Object.values(Color).map((color) => {
const isSelected = React.createProxyState(
[selectedColor],
() => selectedColor.value == color,
);
function setColor() {
selectedColor.value = color;
}
function setColor() {
selectedColor.value = color;
}
return (
<button
color={color}
class="fill-color width-100 flex"
style="height: 2rem"
toggle:selected={isSelected}
on:click={setColor}
></button>
);
})}
</div>
);
return (
<button
color={color}
class="fill-color width-100 flex"
style="height: 2rem"
toggle:selected={isSelected}
on:click={setColor}
></button>
);
})}
</div>
);
}
+38 -38
View File
@@ -3,45 +3,45 @@ import * as React from "bloatless-react";
import { translations } from "../translations";
export function DangerousActionButton(
label: string,
icon: string,
action: () => void
label: string,
icon: string,
action: () => void,
) {
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false
);
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false,
);
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
}
+18 -18
View File
@@ -3,25 +3,25 @@ import * as React from "bloatless-react";
import { translations } from "../translations";
export function DeletableListItem(
text: string,
primaryButton: HTMLElement,
ondelete: () => void
text: string,
primaryButton: HTMLElement,
ondelete: () => void,
) {
return (
<div class="tile flex-row justify-apart align-center padding-0">
<span class="padding-h ellipsis">{text}</span>
return (
<div class="tile flex-row justify-apart align-center padding-0">
<span class="padding-h ellipsis">{text}</span>
<div class="flex-row justify-end">
{primaryButton}
<div class="flex-row justify-end">
{primaryButton}
<button
class="danger"
aria-label={translations.general.deleteItemButtonAudioLabel}
on:click={ondelete}
>
<span class="icon">delete</span>
</button>
</div>
</div>
);
<button
class="danger"
aria-label={translations.general.deleteItemButtonAudioLabel}
on:click={ondelete}
>
<span class="icon">delete</span>
</button>
</div>
</div>
);
}
+56 -56
View File
@@ -1,79 +1,79 @@
import * as React from "bloatless-react";
import StorageModel, {
PATH_COMPONENT_SEPARATOR,
PATH_COMPONENT_SEPARATOR,
} from "../../Model/Global/storageModel";
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
export function DirectoryItemList(
storageViewModel: StorageViewModel,
pathString: string = PATH_COMPONENT_SEPARATOR
storageViewModel: StorageViewModel,
pathString: string = PATH_COMPONENT_SEPARATOR,
) {
// converter
const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string
) => DirectoryItemList(storageViewModel, pathString);
// converter
const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string,
) => DirectoryItemList(storageViewModel, pathString);
// data
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new React.ListState<string>();
// data
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new React.ListState<string>();
const style = `text-indent: ${path.length}rem`;
const style = `text-indent: ${path.length}rem`;
// methods
function loadItems() {
items.clear();
// methods
function loadItems() {
items.clear();
const directoryItems = storageViewModel.storageModel.list(path);
const directoryItems = storageViewModel.storageModel.list(path);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString);
}
}
}
function select() {
storageViewModel.selectedPath.value = pathString;
}
function select() {
storageViewModel.selectedPath.value = pathString;
}
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
select();
setTimeout(() => loadItems(), 50);
});
select();
setTimeout(() => loadItems(), 50);
});
// state
const isSelected = React.createProxyState(
[storageViewModel.selectedPath],
() => storageViewModel.selectedPath.value == pathString
);
// state
const isSelected = React.createProxyState(
[storageViewModel.selectedPath],
() => storageViewModel.selectedPath.value == pathString,
);
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
return (
<div class="flex-column">
<button
class="width-100 flex-1 clip"
toggle:selected={isSelected}
on:click={select}
>
<span class="ellipsis width-100 flex-1" style={style}>
{fileName}
</span>
</button>
return (
<div class="flex-column">
<button
class="width-100 flex-1 clip"
toggle:selected={isSelected}
on:click={select}
>
<span class="ellipsis width-100 flex-1" style={style}>
{fileName}
</span>
</button>
<div
class="flex-column"
children:append={[items, StringToDirectoryItemList]}
></div>
</div>
);
<div
class="flex-column"
children:append={[items, StringToDirectoryItemList]}
></div>
</div>
);
}
+33 -33
View File
@@ -1,62 +1,62 @@
.file-browser {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
display: flex;
display: flex;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
}
.file-browser > div {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
width: 100%;
min-width: 100%;
flex: 0;
width: 100%;
min-width: 100%;
flex: 0;
scroll-snap-align: center;
scroll-snap-align: center;
}
.file-browser .scroll-area {
flex: 1;
width: 100%;
overflow-y: auto;
flex: 1;
width: 100%;
overflow-y: auto;
padding: var(--content-padding);
padding: var(--content-padding);
}
.file-browser .detail-button-wrapper {
display: flex;
justify-content: end;
display: flex;
justify-content: end;
border-top: 1px solid var(--border);
padding: var(--content-padding);
border-top: 1px solid var(--border);
padding: var(--content-padding);
}
.file-browser .detail-button-wrapper button {
max-width: 100%;
max-width: 100%;
}
.file-browser code {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
font-size: .9rem;
font-size: 0.9rem;
}
@media (min-width: 640px) {
.file-browser > div {
width: 50%;
min-width: 50%;
.file-browser > div {
width: 50%;
min-width: 50%;
flex: 1;
}
flex: 1;
}
.file-browser .detail-button-wrapper {
display: none;
}
.file-browser .detail-button-wrapper {
display: none;
}
}
+73 -63
View File
@@ -9,75 +9,85 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import { translations } from "../translations";
export function FileBrowser(storageViewModel: StorageViewModel) {
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div>
<span class="secondary">{translations.storage.noItemSelected}</span>
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div>
<span class="secondary">
{translations.storage.noItemSelected}
</span>
<hr></hr>
<hr></hr>
{DangerousActionButton(
translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk
)}
</div>
);
{DangerousActionButton(
translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk,
)}
</div>
);
return (
<div class="flex-column gap">
<div class="tile flex-no">
return (
<div class="flex-column gap">
<div class="tile flex-no">
<div>
<b>{translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={
storageViewModel.selectedPath
}
></span>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{translations.storage.content}</b>
<code
subscribe:innerText={
storageViewModel.selectedFileContent
}
></code>
</div>
</div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem,
)}
</div>
);
},
);
const view = (
<div class="file-browser">
<div>
<b>{translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={storageViewModel.selectedPath}
></span>
<div class="scroll-area">
{DirectoryItemList(storageViewModel)}
</div>
<div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}>
<span
class="ellipsis"
subscribe:innerText={
storageViewModel.selectedFileName
}
></span>
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{translations.storage.content}</b>
<code
subscribe:innerText={storageViewModel.selectedFileContent}
></code>
</div>
</div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem
)}
<div class="scroll-area" children:set={detailView}></div>
</div>
);
);
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
);
const view = (
<div class="file-browser">
<div>
<div class="scroll-area">{DirectoryItemList(storageViewModel)}</div>
<div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}>
<span
class="ellipsis"
subscribe:innerText={storageViewModel.selectedFileName}
></span>
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
<div class="scroll-area" children:set={detailView}></div>
</div>
);
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
return view;
return view;
}
+18 -18
View File
@@ -1,30 +1,30 @@
import * as React from "bloatless-react";
import {
StringEntryObject,
checkDoesObjectMatchReference,
StringEntryObject,
checkDoesObjectMatchReference,
} from "../../Model/Utility/utility";
export function FilteredList<T>(
reference: StringEntryObject,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement
reference: StringEntryObject,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement,
) {
const matchingObjects: React.ListState<T> = new React.ListState();
const matchingObjects: React.ListState<T> = new React.ListState();
objects.handleAddition((newObject) => {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject)
);
if (doesMatch == false) return;
objects.handleAddition((newObject) => {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject),
);
if (doesMatch == false) return;
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
});
});
});
return viewBuilder(matchingObjects);
return viewBuilder(matchingObjects);
}
+55 -55
View File
@@ -1,106 +1,106 @@
.month-grid-wrapper {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
height: 100%;
overflow: auto;
height: 100%;
overflow: auto;
}
.month-grid-wrapper .day-labels {
flex: 0;
flex: 0;
padding: var(--content-padding);
padding-bottom: 0px;
padding: var(--content-padding);
padding-bottom: 0px;
}
.month-grid-wrapper > div {
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(7, minmax(0px, 1fr));
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(7, minmax(0px, 1fr));
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
.month-grid {
overflow: visible;
overflow: visible;
padding: var(--content-padding);
padding-top: 0px;
padding: var(--content-padding);
padding-top: 0px;
flex: 1;
flex: 1;
grid-auto-rows: 1fr;
grid-auto-rows: 1fr;
}
.month-grid .tile {
animation: slide-up var(--animation-duration);
overflow: hidden;
animation: slide-up var(--animation-duration);
overflow: hidden;
align-items: start;
align-items: start;
}
.month-grid .tile[today] b {
color: var(--primary);
color: var(--primary);
}
.month-grid .tile > div {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.month-grid .tile .event-count {
display: none;
display: none;
background-color: var(--text-normal);
color: var(--background);
background-color: var(--text-normal);
color: var(--background);
opacity: 0.6;
border-radius: 100%;
opacity: 0.6;
border-radius: 100%;
width: 1rem;
height: 1rem;
font-size: 0.6rem;
font-weight: bold;
width: 1rem;
height: 1rem;
font-size: 0.6rem;
font-weight: bold;
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
}
.month-grid .tile .event-count[has-events] {
background-color: var(--primary);
opacity: 1;
background-color: var(--primary);
opacity: 1;
}
.month-grid .tile .event-list {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
height: 100%;
width: 100%;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
.month-grid .tile .event-list span {
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
@media (max-width: 640px) and (max-height: 900px) {
.month-grid .tile {
padding: 0px;
}
.month-grid .tile {
padding: 0px;
}
.month-grid .tile > div {
align-items: center;
justify-content: center;
}
.month-grid .tile > div {
align-items: center;
justify-content: center;
}
.month-grid .tile .event-count {
display: flex;
}
.month-grid .tile .event-count {
display: flex;
}
.month-grid .tile .event-list {
display: none;
}
.month-grid .tile .event-list {
display: none;
}
}
+99 -97
View File
@@ -8,103 +8,105 @@ import { allowDrop } from "../utility";
import { localeCompare } from "../../Model/Utility/utility";
export function MonthGrid<T>(
monthGrid: MonthGrid<React.MapState<T>>,
selectedDate: React.State<number>,
handleDrop: (date: string) => void
monthGrid: MonthGrid<React.MapState<T>>,
selectedDate: React.State<number>,
handleDrop: (date: string) => void,
) {
const dayLabels: HTMLElement[] = [];
let currentWeekday = monthGrid.firstDayOfWeek;
while (dayLabels.length < 7) {
dayLabels.push(
<span class="secondary">
{translations.regional.weekdays.abbreviated[currentWeekday]}
</span>
const dayLabels: HTMLElement[] = [];
let currentWeekday = monthGrid.firstDayOfWeek;
while (dayLabels.length < 7) {
dayLabels.push(
<span class="secondary">
{translations.regional.weekdays.abbreviated[currentWeekday]}
</span>,
);
currentWeekday++;
if (currentWeekday == 7) currentWeekday = 0;
}
const offsetElements: HTMLElement[] = [];
for (let i = 0; i < monthGrid.offset; i++) {
offsetElements.push(<div></div>);
}
const converter: React.StateItemConverter<TaskViewModel> = (
taskViewModel,
) => {
const view = (
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
};
return (
<div class="month-grid-wrapper">
<div class="day-labels">{...dayLabels}</div>
<div class="month-grid">
{...offsetElements}
{...Object.entries(monthGrid.days)
.sort((a, b) => localeCompare(a[0], b[0]))
.map((entry) => {
const [date, mapState] = entry;
const isSelected = React.createProxyState(
[selectedDate],
() => selectedDate.value == parseInt(date),
);
const isToday: boolean =
monthGrid.isCurrentMonth == true &&
parseInt(date) == new Date().getDate();
const eventCount: React.State<number> =
React.createProxyState(
[mapState],
() => mapState.value.size,
);
const hasEvents: React.State<boolean> =
React.createProxyState(
[eventCount],
() => eventCount.value != 0,
);
function select() {
selectedDate.value = parseInt(date);
}
function drop() {
handleDrop(date);
}
return (
<button
class="tile"
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:drop={drop}
>
<div>
<b>{date}</b>
<span
class="event-count"
toggle:has-events={hasEvents}
subscribe:innerText={eventCount}
></span>
<div
class="event-list"
children:append={[mapState, converter]}
></div>
</div>
</button>
);
})}
</div>
</div>
);
currentWeekday++;
if (currentWeekday == 7) currentWeekday = 0;
}
const offsetElements: HTMLElement[] = [];
for (let i = 0; i < monthGrid.offset; i++) {
offsetElements.push(<div></div>);
}
const converter: React.StateItemConverter<TaskViewModel> = (
taskViewModel
) => {
const view = (
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
};
return (
<div class="month-grid-wrapper">
<div class="day-labels">{...dayLabels}</div>
<div class="month-grid">
{...offsetElements}
{...Object.entries(monthGrid.days)
.sort((a, b) => localeCompare(a[0], b[0]))
.map((entry) => {
const [date, mapState] = entry;
const isSelected = React.createProxyState(
[selectedDate],
() => selectedDate.value == parseInt(date)
);
const isToday: boolean =
monthGrid.isCurrentMonth == true &&
parseInt(date) == new Date().getDate();
const eventCount: React.State<number> = React.createProxyState(
[mapState],
() => mapState.value.size
);
const hasEvents: React.State<boolean> = React.createProxyState(
[eventCount],
() => eventCount.value != 0
);
function select() {
selectedDate.value = parseInt(date);
}
function drop() {
handleDrop(date);
}
return (
<button
class="tile"
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:drop={drop}
>
<div>
<b>{date}</b>
<span
class="event-count"
toggle:has-events={hasEvents}
subscribe:innerText={eventCount}
></span>
<div
class="event-list"
children:append={[mapState, converter]}
></div>
</div>
</button>
);
})}
</div>
</div>
);
}
+14 -14
View File
@@ -1,32 +1,32 @@
import * as React from "bloatless-react";
export function Option(text: string, value: string, selectedOnCreate: boolean) {
return (
<option value={value} toggle:selected={selectedOnCreate}>
{text}
</option>
);
return (
<option value={value} toggle:selected={selectedOnCreate}>
{text}
</option>
);
}
export const StringToOption: React.StateItemConverter<string> = (
string: string
string: string,
) => {
return Option(string, string, false);
return Option(string, string, false);
};
export const EntryToOption: React.StateItemConverter<Entry> = (
entry: Entry
entry: Entry,
) => {
return Option(entry[1], entry[0], false);
return Option(entry[1], entry[0], false);
};
export const VersionIdToOption: React.StateItemConverter<string> = (
versionId: string
versionId: string,
) => {
const [date, rest] = versionId.split("T");
const [time] = rest.split(".");
const readableName = `${date} ${time}`;
return Option(readableName, versionId, false);
const [date, rest] = versionId.split("T");
const [time] = rest.split(".");
const readableName = `${date} ${time}`;
return Option(readableName, versionId, false);
};
export type Entry = [string, string];
+49 -48
View File
@@ -1,70 +1,71 @@
import * as React from "bloatless-react";
import {
StringEntryObject,
collectObjectValuesForKey,
localeCompare,
StringEntryObject,
collectObjectValuesForKey,
localeCompare,
} from "../../Model/Utility/utility";
export function PropertyValueList<T>(
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (
keys: React.ListState<string>,
sortedKeys: React.State<string[]>
) => HTMLElement
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (
keys: React.ListState<string>,
sortedKeys: React.State<string[]>,
) => HTMLElement,
) {
const propertyValues: React.ListState<string> = new React.ListState();
const sortedPropertyValues: React.State<string[]> = createSortedPropertyValueState(propertyValues)
objects.subscribe(() => {
collectPropertyValuesToState(
propertyKey,
stringEntryObjectConverter,
objects,
propertyValues
);
});
const propertyValues: React.ListState<string> = new React.ListState();
const sortedPropertyValues: React.State<string[]> =
createSortedPropertyValueState(propertyValues);
objects.subscribe(() => {
collectPropertyValuesToState(
propertyKey,
stringEntryObjectConverter,
objects,
propertyValues,
);
});
return viewBuilder(propertyValues, sortedPropertyValues);
return viewBuilder(propertyValues, sortedPropertyValues);
}
export function collectPropertyValuesToState<T>(
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
propertyValues: React.ListState<string>
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
propertyValues: React.ListState<string>,
) {
const values: string[] = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()]
);
for (const existingValue of values) {
if (propertyValues.value.has(existingValue)) continue;
propertyValues.add(existingValue);
}
for (const displayedValue of propertyValues.value.values()) {
if (values.includes(displayedValue) == false) {
propertyValues.remove(displayedValue);
const values: string[] = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()],
);
for (const existingValue of values) {
if (propertyValues.value.has(existingValue)) continue;
propertyValues.add(existingValue);
}
for (const displayedValue of propertyValues.value.values()) {
if (values.includes(displayedValue) == false) {
propertyValues.remove(displayedValue);
}
}
}
}
export function createSortedPropertyValueState(
propertyValues: React.ListState<string>
propertyValues: React.ListState<string>,
): React.State<string[]> {
return React.createProxyState([propertyValues], () =>
[...propertyValues.value.values()].sort(localeCompare)
);
return React.createProxyState([propertyValues], () =>
[...propertyValues.value.values()].sort(localeCompare),
);
}
export function createPropertyValueIndexState(
sortedKeys: React.State<string[]>,
key: string
sortedKeys: React.State<string[]>,
key: string,
): React.State<number> {
return React.createProxyState([sortedKeys], () =>
sortedKeys.value.indexOf(key)
);
return React.createProxyState([sortedKeys], () =>
sortedKeys.value.indexOf(key),
);
}
+15 -15
View File
@@ -1,23 +1,23 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
export function RibbonButton(
label: string,
icon: string,
isSelected: React.State<boolean>,
select: () => void
label: string,
icon: string,
isSelected: React.State<boolean>,
select: () => void,
) {
return (
<button
class="ribbon-button"
aria-label={label}
toggle:selected={isSelected}
on:click={select}
>
<span class="icon">{icon}</span>
</button>
);
return (
<button
class="ribbon-button"
aria-label={label}
toggle:selected={isSelected}
on:click={select}
>
<span class="icon">{icon}</span>
</button>
);
}
+46 -43
View File
@@ -3,55 +3,58 @@ import * as React from "bloatless-react";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
export function TaskEntry(taskViewModel: TaskViewModel) {
const details: { [icon: string]: string } = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---",
};
const details: { [icon: string]: string } = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---",
};
const view = (
<button
draggable="true"
class="tile flex-no"
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
<div>
<b class="ellipsis" subscribe:innerText={taskViewModel.name}></b>
<hr></hr>
<div
class="grid secondary"
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
const view = (
<button
draggable="true"
class="tile flex-no"
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
{...Object.entries(details).map((entry) => (
<span
class="flex-row align-center width-100 flex-no clip"
style="gap: 1rem"
>
<span class="icon" style="font-size: 1.1rem">
{entry[0]}
</span>
<span class="ellipsis">{entry[1]}</span>
</span>
))}
</div>
</div>
</button>
);
<div>
<b
class="ellipsis"
subscribe:innerText={taskViewModel.name}
></b>
<hr></hr>
<div
class="grid secondary"
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
>
{...Object.entries(details).map((entry) => (
<span
class="flex-row align-center width-100 flex-no clip"
style="gap: 1rem"
>
<span class="icon" style="font-size: 1.1rem">
{entry[0]}
</span>
<span class="ellipsis">{entry[1]}</span>
</span>
))}
</div>
</div>
</button>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
taskViewModel: TaskViewModel
taskViewModel: TaskViewModel,
) => {
return TaskEntry(taskViewModel);
return TaskEntry(taskViewModel);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
export const StringToTextSpan: React.StateItemConverter<string> = (
string: string
string: string,
) => {
return <span class="ellipsis">{string}</span>
return <span class="ellipsis">{string}</span>;
};