add method for filtering

This commit is contained in:
marlon-erler
2026-09-23 00:09:09 +02:00
parent 3458799a3b
commit 26c99085d2
61 changed files with 1703 additions and 1623 deletions
View File
+86 -86
View File
@@ -70,150 +70,150 @@ export default class FileModel {
const didStore: boolean = this.storeFileContent(fileContent); const didStore: boolean = this.storeFileContent(fileContent);
if (didStore == false) return; if (didStore == false) return;
switch (fileContent.type) { switch (fileContent.type) {
case "board-info": case "board-info":
case "task": case "task":
this.boardsAndTasksModel.handleFileContent(fileContent); this.boardsAndTasksModel.handleFileContent(fileContent);
break; break;
case "reaction": case "reaction":
this.chatModel.handleReaction(fileContent); this.chatModel.handleReaction(fileContent);
break; break;
} }
}; };
// methods // methods
readonly addFileContentAndSend = ( readonly addFileContentAndSend = (
fileContent: FileContent<string>, fileContent: FileContent<string>,
): void => { ): void => {
this.handleFileContent(fileContent); this.handleFileContent(fileContent);
this.chatModel.sendMessage("", undefined, fileContent); this.chatModel.sendMessage("", undefined, fileContent);
}; };
// storage // storage
readonly storeFileContent = (fileContent: FileContent<string>): boolean => { readonly storeFileContent = (fileContent: FileContent<string>): boolean => {
const fileContentPath: string[] = this.getFileContentPath( const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId, fileContent.fileId,
fileContent.fileContentId, fileContent.fileContentId,
); );
// check if fileContent already exists // check if fileContent already exists
const existingFileContent: string | null = const existingFileContent: string | null =
this.storageModel.read(fileContentPath); this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false; if (existingFileContent != null) return false;
const stringifiedContent: string = stringify(fileContent); const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent); this.storageModel.write(fileContentPath, stringifiedContent);
return true; return true;
}; };
readonly listFileIds = (): string[] => { readonly listFileIds = (): string[] => {
return this.storageModel.list(this.basePath); return this.storageModel.list(this.basePath);
}; };
readonly listFileContentIds = (fileId: string): string[] => { readonly listFileContentIds = (fileId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId); const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath); return this.storageModel.list(filePath);
}; };
readonly selectLatestFileContentId = ( readonly selectLatestFileContentId = (
fileContentIds: string[], fileContentIds: string[],
): string | undefined => { ): string | undefined => {
return fileContentIds[fileContentIds.length - 1]; return fileContentIds[fileContentIds.length - 1];
}; };
readonly getFileContent = <T extends FileContent<string>>( readonly getFileContent = <T extends FileContent<string>>(
fileId: string, fileId: string,
fileContentName: string, fileContentName: string,
reference: T, reference: T,
): T | null => { ): T | null => {
const filePath: string[] = this.getFileContentPath( const filePath: string[] = this.getFileContentPath(
fileId, fileId,
fileContentName, fileContentName,
); );
const fileContentOrNull: T | null = this.storageModel.readStringifiable( const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath, filePath,
reference, reference,
); );
return fileContentOrNull; return fileContentOrNull;
}; };
readonly getLatestFileContent = <T extends FileContent<string>>( readonly getLatestFileContent = <T extends FileContent<string>>(
fileId: string, fileId: string,
reference: T, reference: T,
): T | null => { ): T | null => {
const fileContentsIds: string[] = this.listFileContentIds(fileId); const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined = const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds); this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null; if (latestFileContentId == undefined) return null;
const fileContent: T | null = this.getFileContent( const fileContent: T | null = this.getFileContent(
fileId, fileId,
latestFileContentId, latestFileContentId,
reference, reference,
); );
return fileContent; return fileContent;
}; };
// init // init
constructor( constructor(
storageModel: StorageModel, storageModel: StorageModel,
settingsModel: SettingsModel, settingsModel: SettingsModel,
chatModel: ChatModel, chatModel: ChatModel,
) { ) {
this.chatModel = chatModel; this.chatModel = chatModel;
this.settingsModel = settingsModel; this.settingsModel = settingsModel;
this.storageModel = storageModel; this.storageModel = storageModel;
this.boardsAndTasksModel = new BoardsAndTasksModel( this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel, this.storageModel,
this.settingsModel, this.settingsModel,
chatModel, chatModel,
this, this,
); );
} }
// utility // utility
static generateFileContentId = (creationDate: string): string => { static generateFileContentId = (creationDate: string): string => {
return creationDate + v4(); return creationDate + v4();
}; };
static createFileContent = <T extends string>( static createFileContent = <T extends string>(
fileId: string, fileId: string,
type: T, type: T,
): FileContent<T> => { ): FileContent<T> => {
const creationDate: string = createTimestamp(); const creationDate: string = createTimestamp();
const fileContentId: string = const fileContentId: string =
FileModel.generateFileContentId(creationDate); FileModel.generateFileContentId(creationDate);
return { return {
dataVersion: DATA_VERSION, dataVersion: DATA_VERSION,
fileId, fileId,
fileContentId, fileContentId,
creationDate, creationDate,
type, type,
}; };
}; };
} }
// paths // paths
export enum FileModelSubPath { export enum FileModelSubPath {
Data = "data", Data = "data",
Model = "model", Model = "model",
ModelView = "view", ModelView = "view",
ModelTask = "tasks", ModelTask = "tasks",
ModelCalendar = "calendar", ModelCalendar = "calendar",
} }
// types // types
export interface FileContent<T extends string> export interface FileContent<T extends string>
extends ValidObject, StringEntryObject { extends ValidObject, StringEntryObject {
readonly fileId: string; readonly fileId: string;
readonly fileContentId: string; readonly fileContentId: string;
readonly creationDate: string; readonly creationDate: string;
readonly type: T; readonly type: T;
} }
// references // references
export const FileContentReference: FileContent<string> = { export const FileContentReference: FileContent<string> = {
+3 -4
View File
@@ -37,7 +37,7 @@ export default class FileTransferModel {
}; };
readonly prepareToSend = (): void => { readonly prepareToSend = (): void => {
if (!this.transferData) return; if (!this.transferData) return;
this.direction = TransferDirections.Send; this.direction = TransferDirections.Send;
this.connectionModel.addChannel(this.transferData.channel); this.connectionModel.addChannel(this.transferData.channel);
}; };
@@ -157,8 +157,7 @@ export default class FileTransferModel {
}; };
readonly prepareFileForSending = (filePath: string[]): string => { readonly prepareFileForSending = (filePath: string[]): string => {
const fileContent: string | null = const fileContent: string | null = this.storageModel.read(filePath);
this.storageModel.read(filePath);
if (fileContent == null) return ""; if (fileContent == null) return "";
const fileData: FileData = { const fileData: FileData = {
@@ -169,7 +168,7 @@ export default class FileTransferModel {
}; };
readonly sendFile = async (stringifiedFileData: string): Promise<void> => { readonly sendFile = async (stringifiedFileData: string): Promise<void> => {
if (!this.transferData) return; if (!this.transferData) return;
const encryptedFileData: string = await encryptString( const encryptedFileData: string = await encryptString(
stringifiedFileData, stringifiedFileData,
+1 -1
View File
@@ -46,7 +46,7 @@ export default class SettingsModel {
// load // load
private readonly readSetting = ( private readonly readSetting = (
pathName: keyof typeof filePaths.settingsModel, pathName: keyof typeof filePaths.settingsModel,
): string|null=> { ): string | null => {
const path = StorageModel.getPath( const path = StorageModel.getPath(
StorageModelSubPaths.SettingsModel, StorageModelSubPaths.SettingsModel,
filePaths.settingsModel[pathName], filePaths.settingsModel[pathName],
Binary file not shown.
+171 -149
View File
@@ -1,5 +1,5 @@
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety"; import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
import * as Hammer from "hammerjs"; import * as React from "../../react";
export interface Stringifiable { export interface Stringifiable {
toString(): string; toString(): string;
@@ -18,7 +18,7 @@ export function createTimestamp(): string {
return new Date().toISOString(); return new Date().toISOString();
} }
// filters etc // filters
export type StringEntryObject = { [key: string]: Stringifiable | undefined }; export type StringEntryObject = { [key: string]: Stringifiable | undefined };
export function filterObjectsByStringEntries<T>( export function filterObjectsByStringEntries<T>(
@@ -28,7 +28,7 @@ export function filterObjectsByStringEntries<T>(
): Set<T> { ): Set<T> {
const matches: Set<T> = new Set(); const matches: Set<T> = new Set();
object_loop: for (const object of objects) { for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference( const doesMatch: boolean = checkDoesObjectMatchReference(
reference, reference,
converter(object), converter(object),
@@ -39,79 +39,6 @@ export function filterObjectsByStringEntries<T>(
return matches; return matches;
} }
export function checkDoesObjectMatchReference(
reference: StringEntryObject,
stringEntryObject: StringEntryObject,
explicitEmptyValue: boolean = false,
): boolean {
reference_entry_loop: for (const referenceEntry of Object.entries(
reference,
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
if (referenceValue == undefined) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
if (explicitEmptyValue == false) {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined ||
stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
return true;
}
export function collectObjectValuesForKey<T>(
key: string,
converter: (object: T) => StringEntryObject,
objects: T[],
): string[] {
const values: Set<string> = new Set();
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined) continue;
values.add(stringEntryObjectValue.toString());
}
return [...values.values()];
}
export function filterObjectsByWords<T>( export function filterObjectsByWords<T>(
query: string, query: string,
getStringsOfObject: (object: T) => string[], getStringsOfObject: (object: T) => string[],
@@ -119,7 +46,7 @@ export function filterObjectsByWords<T>(
): Set<T> { ): Set<T> {
const matches: Set<T> = new Set(); const matches: Set<T> = new Set();
object_loop: for (const object of objects) { for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch( const doesMatch: boolean = checkDoesObjectMatchSearch(
query, query,
getStringsOfObject, getStringsOfObject,
@@ -170,6 +97,26 @@ export function checkDoesObjectMatchSearch<T>(
return true; return true;
} }
export default function implementFilter<T>(
allItems: React.ListState<T>|React.MapState<T>,
matches: React.ListState<T>,
query: React.State<string>,
itemToString: (item: T) => string,
) {
function update() {
matches.clear();
allItems.value.forEach((item: T) => {
if (!itemToString(item).includes(query.value)) return;
matches.add(item);
allItems.handleRemoval(item, () => {
matches.remove(item);
});
});
}
query.subscribe(update);
allItems.handleAddition(update);
}
// handlers // handlers
export type Handler<T> = (item: T) => void; export type Handler<T> = (item: T) => void;
@@ -313,99 +260,174 @@ export function implementPinchZoom(canvas: HTMLElement) {
const MIN = 0.25; const MIN = 0.25;
const MAX = 5; const MAX = 5;
const element = (): HTMLElement|null=> canvas.querySelector(".zoom"); const element = (): HTMLElement | null => canvas.querySelector(".zoom");
const distance = (e: TouchEvent) => const distance = (e: TouchEvent) =>
Math.hypot( Math.hypot(
e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageX - e.touches[1].pageX,
e.touches[0].pageY - e.touches[1].pageY, e.touches[0].pageY - e.touches[1].pageY,
); );
const point = (e: TouchEvent, direction: "x" | "y", i: number) => const point = (e: TouchEvent, direction: "x" | "y", i: number) =>
e.touches[i][direction == "x" ? "clientX" : "clientY"]; e.touches[i][direction == "x" ? "clientX" : "clientY"];
const midpoint = (e: TouchEvent, direction: "x" | "y") => const midpoint = (e: TouchEvent, direction: "x" | "y") =>
(point(e, direction, 0) + point(e, direction, 1)) / 2; (point(e, direction, 0) + point(e, direction, 1)) / 2;
function apply(factor: number, _offset?: [number, number]) { function apply(factor: number, _offset?: [number, number]) {
const el = element(); const el = element();
if (!el) return; if (!el) return;
if (factor < MIN) return apply(MIN, _offset); if (factor < MIN) return apply(MIN, _offset);
if (factor > MAX) return apply(5, _offset); if (factor > MAX) return apply(5, _offset);
let x: number, y: number; let x: number, y: number;
if (_offset) [x, y] = _offset; if (_offset) [x, y] = _offset;
else { else {
x = currentX x = currentX;
y = currentY y = currentY;
} }
el.style.transform = `scale(${factor.toString()}) translate(${x}px, ${y}px)`; el.style.transform = `scale(${factor.toString()}) translate(${x}px, ${y}px)`;
currentX = x; currentX = x;
currentY = y; currentY = y;
currentZoom = factor; currentZoom = factor;
} }
canvas.addEventListener("wheel", (event: WheelEvent) => { canvas.addEventListener("wheel", (event: WheelEvent) => {
event.preventDefault(); event.preventDefault();
initialZoom = currentZoom; initialZoom = currentZoom;
initialX = currentX; initialX = currentX;
initialY = currentY; initialY = currentY;
if (!event.shiftKey) { if (!event.shiftKey) {
apply(currentZoom, [ apply(currentZoom, [
currentX - event.deltaX, currentX - event.deltaX,
currentY - event.deltaY, currentY - event.deltaY,
]); ]);
return; return;
} }
apply(currentZoom - event.deltaY * 0.01); apply(currentZoom - event.deltaY * 0.01);
}); });
canvas.addEventListener("scroll", (event: Event) => { canvas.addEventListener("scroll", (event: Event) => {
event.preventDefault(); event.preventDefault();
}); });
canvas.addEventListener("touchstart", (event: TouchEvent) => { canvas.addEventListener("touchstart", (event: TouchEvent) => {
(document.activeElement as HTMLElement).blur(); (document.activeElement as HTMLElement).blur();
const el = element(); const el = element();
if (!el) return; if (!el) return;
if (lastElement != undefined && lastElement != el) reset(); if (lastElement != undefined && lastElement != el) reset();
lastElement = el; lastElement = el;
initialZoom = currentZoom; initialZoom = currentZoom;
initialX = currentX; initialX = currentX;
initialY = currentY; initialY = currentY;
if (event.touches.length != 2) { if (event.touches.length != 2) {
pinching = false; pinching = false;
initialTouchX = event.touches[0].clientX; initialTouchX = event.touches[0].clientX;
initialTouchY = event.touches[0].clientY; initialTouchY = event.touches[0].clientY;
return; return;
} }
pinching = true; pinching = true;
initialDistance = distance(event); initialDistance = distance(event);
initialTouchX = midpoint(event, "x"); initialTouchX = midpoint(event, "x");
initialTouchY = midpoint(event, "y"); initialTouchY = midpoint(event, "y");
}); });
canvas.addEventListener("touchmove", (event: TouchEvent) => { canvas.addEventListener("touchmove", (event: TouchEvent) => {
if (!pinching) { if (!pinching) {
apply(currentZoom, [ apply(currentZoom, [
initialX + initialX +
(event.touches[0].clientX - initialTouchX) / initialZoom, (event.touches[0].clientX - initialTouchX) / initialZoom,
initialY + initialY +
(event.touches[0].clientY - initialTouchY) / initialZoom, (event.touches[0].clientY - initialTouchY) / initialZoom,
]); ]);
return; return;
} }
if (event.touches.length < 2) return; if (event.touches.length < 2) return;
event.preventDefault(); event.preventDefault();
const currentDistance = distance(event); const currentDistance = distance(event);
const midX = midpoint(event, "x"); const midX = midpoint(event, "x");
const midY = midpoint(event, "y"); const midY = midpoint(event, "y");
const ratio = currentDistance / initialDistance; const ratio = currentDistance / initialDistance;
const difference = currentDistance - initialDistance; const difference = currentDistance - initialDistance;
apply(initialZoom * ratio, [ apply(initialZoom * ratio, [
initialX + (midX - difference - initialTouchX) / initialZoom, initialX + (midX - difference - initialTouchX) / initialZoom,
initialY + (midY - difference - initialTouchY) / initialZoom, initialY + (midY - difference - initialTouchY) / initialZoom,
]); ]);
}); });
} }
// objecs
export function collectObjectValuesForKey<T>(
key: string,
converter: (object: T) => StringEntryObject,
objects: T[],
): string[] {
const values: Set<string> = new Set();
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined) continue;
values.add(stringEntryObjectValue.toString());
}
return [...values.values()];
}
// type safety
export function checkDoesObjectMatchReference(
reference: StringEntryObject,
stringEntryObject: StringEntryObject,
explicitEmptyValue: boolean = false,
): boolean {
reference_entry_loop: for (const referenceEntry of Object.entries(
reference,
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
if (referenceValue == undefined) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
if (explicitEmptyValue == false) {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined ||
stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
return true;
}
+1 -1
View File
@@ -1,4 +1,3 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { import {
@@ -6,6 +5,7 @@ import {
createPropertyValueIndexState, createPropertyValueIndexState,
} from "../Components/propertyValueList"; } from "../Components/propertyValueList";
import { FilteredList } from "../Components/filteredList"; import { FilteredList } from "../Components/filteredList";
import * as React from "../../react";
import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel"; import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
+1 -1
View File
@@ -1,4 +1,3 @@
import * as React from "../../react";
import { BoardStatusGridPage } from "./boardStatusGridPage"; import { BoardStatusGridPage } from "./boardStatusGridPage";
import { BoardKanbanPage } from "./boardKanbanPage"; import { BoardKanbanPage } from "./boardKanbanPage";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
@@ -7,6 +6,7 @@ import { SearchModal } from "../Modals/searchModal";
import { BoardSettingsModal } from "../Modals/boardSettingsModal"; import { BoardSettingsModal } from "../Modals/boardSettingsModal";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { BoardViewToggleButton } from "../Components/boardViewToggleButton"; import { BoardViewToggleButton } from "../Components/boardViewToggleButton";
import * as React from "../../react";
import BoardViewModel, { import BoardViewModel, {
BoardPageTypes, BoardPageTypes,
} from "../../ViewModel/Pages/boardViewModel"; } from "../../ViewModel/Pages/boardViewModel";
+1 -1
View File
@@ -1,4 +1,3 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { import {
@@ -8,6 +7,7 @@ import {
createSortedPropertyValueState, createSortedPropertyValueState,
} from "../Components/propertyValueList"; } from "../Components/propertyValueList";
import { FilteredList } from "../Components/filteredList"; import { FilteredList } from "../Components/filteredList";
import * as React from "../../react";
import { import {
TaskCategoryBulkChangeViewModel, TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel, TaskStatusBulkChangeViewModel,
+1 -1
View File
@@ -1,10 +1,10 @@
import * as React from "../../react";
import "./calendarPage.css"; import "./calendarPage.css";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { TaskSettingsModal } from "../Modals/taskSettingsModal"; import { TaskSettingsModal } from "../Modals/taskSettingsModal";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { PlaceholderView } from "../Components/placeholderView"; import { PlaceholderView } from "../Components/placeholderView";
import { MonthGrid } from "../Components/monthGrid"; import { MonthGrid } from "../Components/monthGrid";
import * as React from "../../react";
import CalendarPageViewModel from "../../ViewModel/Pages/calendarPageViewModel"; import CalendarPageViewModel from "../../ViewModel/Pages/calendarPageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
+1 -1
View File
@@ -1,9 +1,9 @@
import * as React from "../../react";
import "./messagePage.css"; import "./messagePage.css";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { MessageFilterModal } from "../Modals/messageFilterModal"; import { MessageFilterModal } from "../Modals/messageFilterModal";
import { ReplyPreview } from "../Components/replyPreview"; import { ReplyPreview } from "../Components/replyPreview";
import { ChatMessage } from "../Components/chatMessage"; import { ChatMessage } from "../Components/chatMessage";
import * as React from "../../react";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel"; import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "../../react";
import { DeletableListItem } from "../Components/deletableListItem"; import { DeletableListItem } from "../Components/deletableListItem";
import { DangerousActionButton } from "../Components/dangerousActionButton"; import { DangerousActionButton } from "../Components/dangerousActionButton";
import { ColorPicker } from "../Components/colorPicker"; import { ColorPicker } from "../Components/colorPicker";
import * as React from "../../react";
import SettingsPageViewModel from "../../ViewModel/Pages/settingsPageViewModel"; import SettingsPageViewModel from "../../ViewModel/Pages/settingsPageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
+3 -3
View File
@@ -1,9 +1,9 @@
import * as React from "../../react";
import "./taskPage.css"; import "./taskPage.css";
import { BoardPage } from "./boardPage"; import { BoardPage } from "./boardPage";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { PlaceholderView } from "../Components/placeholderView"; import { PlaceholderView } from "../Components/placeholderView";
import { BoardViewModelToEntry } from "../Components/boardEntry"; import { BoardViewModelToEntry } from "../Components/boardEntry";
import * as React from "../../react";
import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel"; import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
@@ -62,7 +62,7 @@ export function TaskPage(
<div class="flex-row width-input"> <div class="flex-row width-input">
<input <input
class="no-outline" class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput} bind:value={taskPageViewModel.boardQuery}
on:enter={taskPageViewModel.createBoard} on:enter={taskPageViewModel.createBoard}
placeholder={ placeholder={
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage.task
@@ -89,7 +89,7 @@ export function TaskPage(
class="grid gap" class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))" style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[ children:append={[
taskPageViewModel.boardViewModels, taskPageViewModel.boardMatches,
BoardViewModelToEntry, BoardViewModelToEntry,
]} ]}
></div> ></div>
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import * as React from "../../react";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
export function BoardEntry(boardViewModel: BoardViewModel) { export function BoardEntry(boardViewModel: BoardViewModel) {
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { RibbonButton } from "./ribbonButton"; import { RibbonButton } from "./ribbonButton";
import * as React from "../../react";
import BoardViewModel, { import BoardViewModel, {
BoardPageTypes, BoardPageTypes,
} from "../../ViewModel/Pages/boardViewModel"; } from "../../ViewModel/Pages/boardViewModel";
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "../../react";
import { MessageReactionButtonRow } from "./messageReactionButtonRow"; import { MessageReactionButtonRow } from "./messageReactionButtonRow";
import { InlineReply } from "./inlineReply"; import { InlineReply } from "./inlineReply";
import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal"; import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal";
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { ChatMessageStatuses } from "../../Model/Chat/chatModel"; import { ChatMessageStatuses } from "../../Model/Chat/chatModel";
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { RibbonButton } from "./ribbonButton"; import { RibbonButton } from "./ribbonButton";
import * as React from "../../react";
import ChatViewModel, { import ChatViewModel, {
ChatPageTypes, ChatPageTypes,
} from "../../ViewModel/Chat/chatViewModel"; } from "../../ViewModel/Chat/chatViewModel";
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import * as React from "../../react";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
export function InlineReply(chatMessageViewModel: ChatMessageViewModel) { export function InlineReply(chatMessageViewModel: ChatMessageViewModel) {
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { MessageReactionButton } from "./messageReactionButton"; import { MessageReactionButton } from "./messageReactionButton";
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { ReactionSymbols } from "../../Model/Chat/chatModel"; import { ReactionSymbols } from "../../Model/Chat/chatModel";
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from "../../react";
import "./monthGrid.css"; import "./monthGrid.css";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import * as React from "../../react";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { localeCompare } from "../../Model/Utility/utility"; import { localeCompare } from "../../Model/Utility/utility";
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { OptionButton } from "./optionButton"; import { OptionButton } from "./optionButton";
import * as React from "../../react";
export function OptionButtonList<T>( export function OptionButtonList<T>(
options: React.ListState<[string, T]>, options: React.ListState<[string, T]>,
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { DangerousActionButton } from "../Components/dangerousActionButton"; import { DangerousActionButton } from "../Components/dangerousActionButton";
import { ColorPicker } from "../Components/colorPicker"; import { ColorPicker } from "../Components/colorPicker";
import * as React from "../../react";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "../../react";
import { MessageReactionEntry } from "../Components/messageReactionEntry"; import { MessageReactionEntry } from "../Components/messageReactionEntry";
import { MessageReactionButtonRow } from "../Components/messageReactionButtonRow"; import { MessageReactionButtonRow } from "../Components/messageReactionButtonRow";
import { InfoTile } from "../Components/infoTile"; import { InfoTile } from "../Components/infoTile";
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import { DeletableListItem } from "../Components/deletableListItem"; import { DeletableListItem } from "../Components/deletableListItem";
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel"; import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel";
+148 -142
View File
@@ -1,6 +1,6 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { StringToTextSpan } from "../Components/textSpan"; import { StringToTextSpan } from "../Components/textSpan";
import * as React from "../../react";
import FileTransferViewModel, { import FileTransferViewModel, {
FileTransferModals, FileTransferModals,
FileTransferOption, FileTransferOption,
@@ -758,7 +758,9 @@ function ExportModal(
</span> </span>
<input <input
bind:value={fileTransferViewModel.exportKey} bind:value={fileTransferViewModel.exportKey}
on:enter={fileTransferViewModel.downloadFile} on:enter={
fileTransferViewModel.downloadFile
}
type="password" type="password"
></input> ></input>
</div> </div>
@@ -777,36 +779,38 @@ function ExportModal(
bind:value={ bind:value={
fileTransferViewModel.exportKeyConfirmation fileTransferViewModel.exportKeyConfirmation
} }
on:enter={fileTransferViewModel.downloadFile} on:enter={
type="password" fileTransferViewModel.downloadFile
></input> }
</div> type="password"
</label> ></input>
</div> </div>
</main> </label>
<div class="flex-row width-100"> </div>
<button </main>
class="flex" <div class="flex-row width-100">
on:click={ <button
fileTransferViewModel.showExportSelectionModal class="flex"
} on:click={
> fileTransferViewModel.showExportSelectionModal
{coreViewModel.translations.general.backButton} }
</button> >
<button {coreViewModel.translations.general.backButton}
class="primary flex" </button>
on:click={fileTransferViewModel.downloadFile} <button
toggle:disabled={fileTransferViewModel.cannotExport} class="primary flex"
> on:click={fileTransferViewModel.downloadFile}
{ toggle:disabled={fileTransferViewModel.cannotExport}
coreViewModel.translations.dataTransferModal >
.downloadFileButton {
} coreViewModel.translations.dataTransferModal
<span class="icon">download</span> .downloadFileButton
</button> }
</div> <span class="icon">download</span>
</div> </button>
</div> </div>
</div>
</div>
); );
} }
@@ -815,57 +819,57 @@ function ImportModal(
fileTransferViewModel: FileTransferViewModel, fileTransferViewModel: FileTransferViewModel,
) { ) {
const isPresented = React.createProxyState( const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal], [fileTransferViewModel.presentedModal],
() => () =>
fileTransferViewModel.presentedModal.value == fileTransferViewModel.presentedModal.value ==
FileTransferModals.ImportSelection, FileTransferModals.ImportSelection,
); );
return ( return (
<div class="modal" toggle:open={isPresented}> <div class="modal" toggle:open={isPresented}>
<div> <div>
<main> <main>
<h2> <h2>
{ {
coreViewModel.translations.dataTransferModal coreViewModel.translations.dataTransferModal
.importHeadline .importHeadline
} }
</h2> </h2>
<div class="flex-column gap content-margin-bottom"> <div class="flex-column gap content-margin-bottom">
<input <input
id="file-transfer-input" id="file-transfer-input"
type="file" type="file"
on:change={ on:change={
fileTransferViewModel.updateImportSelection fileTransferViewModel.updateImportSelection
} }
></input> ></input>
</div> </div>
</main> </main>
<div class="flex-row width-100"> <div class="flex-row width-100">
<button <button
class="flex" class="flex"
on:click={ on:click={
fileTransferViewModel.showDirectionSelectionModal fileTransferViewModel.showDirectionSelectionModal
} }
> >
{coreViewModel.translations.general.backButton} {coreViewModel.translations.general.backButton}
</button> </button>
<button <button
class="primary flex" class="primary flex"
on:click={fileTransferViewModel.importFile} on:click={fileTransferViewModel.importFile}
toggle:disabled={fileTransferViewModel.cannotImport} toggle:disabled={fileTransferViewModel.cannotImport}
> >
{ {
coreViewModel.translations.dataTransferModal coreViewModel.translations.dataTransferModal
.importFileButton .importFileButton
} }
<span class="icon">arrow_forward</span> <span class="icon">arrow_forward</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); );
} }
@@ -874,74 +878,76 @@ function ImportDecryptionDataModal(
fileTransferViewModel: FileTransferViewModel, fileTransferViewModel: FileTransferViewModel,
) { ) {
const isPresented = React.createProxyState( const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal], [fileTransferViewModel.presentedModal],
() => () =>
fileTransferViewModel.presentedModal.value == fileTransferViewModel.presentedModal.value ==
FileTransferModals.ImportDecryptData, FileTransferModals.ImportDecryptData,
); );
return ( return (
<div class="modal" toggle:open={isPresented}> <div class="modal" toggle:open={isPresented}>
<div> <div>
<main> <main>
<h2> <h2>
{ {
coreViewModel.translations.dataTransferModal coreViewModel.translations.dataTransferModal
.importHeadline .importHeadline
} }
</h2> </h2>
<div class="flex-column gap content-margin-bottom"> <div class="flex-column gap content-margin-bottom">
<label class="tile"> <label class="tile">
<span class="icon">key</span> <span class="icon">key</span>
<div> <div>
<span class="secondary"> <span class="secondary">
{ {
coreViewModel.translations coreViewModel.translations
.dataTransferModal.exportKey .dataTransferModal.exportKey
} }
</span> </span>
<input <input
bind:value={fileTransferViewModel.importKey} bind:value={fileTransferViewModel.importKey}
on:enter={fileTransferViewModel.decryptImport} on:enter={
type="password" fileTransferViewModel.decryptImport
></input> }
</div> type="password"
</label> ></input>
</div> </div>
</label>
</div>
<p <p
class="error" class="error"
toggle:hidden={fileTransferViewModel.isImportKeyCorrect} toggle:hidden={fileTransferViewModel.isImportKeyCorrect}
> >
{ {
coreViewModel.translations.dataTransferModal coreViewModel.translations.dataTransferModal
.incorrectPassphraseError .incorrectPassphraseError
} }
</p> </p>
</main> </main>
<div class="flex-row width-100"> <div class="flex-row width-100">
<button <button
class="flex" class="flex"
on:click={fileTransferViewModel.showImportModal} on:click={fileTransferViewModel.showImportModal}
> >
{coreViewModel.translations.general.backButton} {coreViewModel.translations.general.backButton}
</button> </button>
<button <button
class="primary flex" class="primary flex"
on:click={fileTransferViewModel.decryptImport} on:click={fileTransferViewModel.decryptImport}
toggle:disabled={ toggle:disabled={
fileTransferViewModel.importDecryptSuccessful fileTransferViewModel.importDecryptSuccessful
} }
> >
{ {
coreViewModel.translations.dataTransferModal coreViewModel.translations.dataTransferModal
.decryptImportButton .decryptImportButton
} }
<span class="icon">arrow_forward</span> <span class="icon">arrow_forward</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); );
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { MessageReactionFilterButton } from "../Components/messageReactionFilterButton"; import { MessageReactionFilterButton } from "../Components/messageReactionFilterButton";
import * as React from "../../react";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel"; import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ReactionSymbols } from "../../Model/Chat/chatModel"; import { ReactionSymbols } from "../../Model/Chat/chatModel";
+62 -44
View File
@@ -1,7 +1,7 @@
import { v4 } from "uuid"; import { v4 } from "uuid";
import * as React from "../../react";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { StringToOption } from "../Components/option"; import { StringToOption } from "../Components/option";
import * as React from "../../react";
import SearchViewModel from "../../ViewModel/Utility/searchViewModel"; import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
@@ -29,8 +29,8 @@ export function SearchModal<T>(
<h2>{headline}</h2> <h2>{headline}</h2>
<div class="flex-row"> <div class="flex-row">
<input <input
id="focused" id="focused"
style="max-width: unset" style="max-width: unset"
placeholder={ placeholder={
coreViewModel.translations.general.searchLabel coreViewModel.translations.general.searchLabel
} }
@@ -70,55 +70,73 @@ export function SearchModal<T>(
</button> </button>
</div> </div>
<div <div toggle:hidden={searchViewModel.hasNoSuggestions}>
toggle:hidden={searchViewModel.hasNoSuggestions}> <hr></hr>
<hr></hr> <h3>
<h3>{coreViewModel.translations.general.searchSuggestionsLabel}</h3> {
<div coreViewModel.translations.general
class="flex-column gap" .searchSuggestionsLabel
children:append={[ }
searchViewModel.suggestions, </h3>
(suggestion: string) => SuggestionView(suggestion, searchViewModel, coreViewModel), <div
]} class="flex-column gap"
></div> children:append={[
</div> searchViewModel.suggestions,
</main> (suggestion: string) =>
<button on:click={close}> SuggestionView(
{coreViewModel.translations.general.closeButton} suggestion,
<span class="icon">close</span> searchViewModel,
</button> coreViewModel,
</div> ),
</div> ]}
></div>
</div>
</main>
<button on:click={close}>
{coreViewModel.translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
); );
} }
function SuggestionView<T>(suggestion: string, searchViewModel: SearchViewModel<T>, coreViewModel: CoreViewModel) { function SuggestionView<T>(
const isApplied = React.createProxyState([searchViewModel.appliedQuery], ()=>searchViewModel.appliedQuery.value == suggestion); suggestion: string,
searchViewModel: SearchViewModel<T>,
coreViewModel: CoreViewModel,
) {
const isApplied = React.createProxyState(
[searchViewModel.appliedQuery],
() => searchViewModel.appliedQuery.value == suggestion,
);
function deleteSuggestion() { function deleteSuggestion() {
searchViewModel.deleteSuggestion(suggestion); searchViewModel.deleteSuggestion(suggestion);
} }
function applySuggestion() { function applySuggestion() {
searchViewModel.search(suggestion); searchViewModel.search(suggestion);
} }
return <div class="flex-row surface align-center"> return (
<span class="width-100 flex-1 padding-h">{suggestion}</span> <div class="flex-row surface align-center">
<button <span class="width-100 flex-1 padding-h">{suggestion}</span>
class="danger" <button
aria-label={coreViewModel.translations.general.deleteButton} class="danger"
on:click={deleteSuggestion} aria-label={coreViewModel.translations.general.deleteButton}
> on:click={deleteSuggestion}
<span class="icon">delete</span> >
</button> <span class="icon">delete</span>
<button </button>
class="primary" <button
aria-label={coreViewModel.translations.general.applyButton} class="primary"
on:click={applySuggestion} aria-label={coreViewModel.translations.general.applyButton}
toggle:disabled={isApplied} on:click={applySuggestion}
> toggle:disabled={isApplied}
<span class="icon">check</span> >
</button> <span class="icon">check</span>
</div> </button>
</div>
);
} }
+1 -1
View File
@@ -1,10 +1,10 @@
import * as React from "../../react";
import { SplitModal } from "./splitModal"; import { SplitModal } from "./splitModal";
import { languageNames } from "../translations"; import { languageNames } from "../translations";
import { PlaceholderView } from "../Components/placeholderView"; import { PlaceholderView } from "../Components/placeholderView";
import { OptionButtonList } from "../Components/optionButtonList"; import { OptionButtonList } from "../Components/optionButtonList";
import { NavigationButton } from "../Components/navigationButton"; import { NavigationButton } from "../Components/navigationButton";
import { InfoTile } from "../Components/infoTile"; import { InfoTile } from "../Components/infoTile";
import * as React from "../../react";
import SettingsViewModel, { import SettingsViewModel, {
SettingsModalPages, SettingsModalPages,
} from "../../ViewModel/Global/settingsViewModel"; } from "../../ViewModel/Global/settingsViewModel";
+2 -2
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import "./splitModal.css"; import "./splitModal.css";
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
export function SplitModal<T>( export function SplitModal<T>(
@@ -10,7 +10,7 @@ export function SplitModal<T>(
navigationState?: React.State<T | undefined>, navigationState?: React.State<T | undefined>,
) { ) {
function closePage() { function closePage() {
if (!navigationState) return; if (!navigationState) return;
navigationState.value = undefined; navigationState.value = undefined;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "../../react";
import { SplitModal } from "./splitModal"; import { SplitModal } from "./splitModal";
import { DirectoryItemList } from "../Components/directoryItemList"; import { DirectoryItemList } from "../Components/directoryItemList";
import { DangerousActionButton } from "../Components/dangerousActionButton"; import { DangerousActionButton } from "../Components/dangerousActionButton";
import * as React from "../../react";
import StorageViewModel from "../../ViewModel/Global/storageViewModel"; import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel"; import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel";
+247 -242
View File
@@ -1,5 +1,4 @@
import { v4 } from "uuid"; import { v4 } from "uuid";
import * as React from "../../react";
import { import {
Entry, Entry,
Option, Option,
@@ -7,6 +6,7 @@ import {
VersionIdToOption, VersionIdToOption,
} from "../Components/option"; } from "../Components/option";
import { DangerousActionButton } from "../Components/dangerousActionButton"; import { DangerousActionButton } from "../Components/dangerousActionButton";
import * as React from "../../react";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
@@ -18,264 +18,269 @@ export function TaskSettingsModal(
const statusSuggestionId = v4(); const statusSuggestionId = v4();
const BoardOptionConverter: React.StateItemConverter<Entry> = ( const BoardOptionConverter: React.StateItemConverter<Entry> = (
entry: Entry, entry: Entry,
) => { ) => {
const isSelected = entry[0] == taskViewModel.task.boardId; const isSelected = entry[0] == taskViewModel.task.boardId;
return Option(entry[1], entry[0], isSelected); return Option(entry[1], entry[0], isSelected);
}; };
return ( return (
<div class="modal task-settings" open toggle:full-description={taskViewModel.isPresentingFullScreenDescription}> <div
<div> class="modal task-settings"
<main id="standard-main"> open
<h2> toggle:full-description={
{ taskViewModel.isPresentingFullScreenDescription
coreViewModel.translations.chatPage.task }
.taskSettingsHeadline >
} <div>
</h2> <main id="standard-main">
<h2>
{
coreViewModel.translations.chatPage.task
.taskSettingsHeadline
}
</h2>
<div class="column-wrapper"> <div class="column-wrapper">
<div class="flex-column"> <div class="flex-column">
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">label</span> <span class="icon">label</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskNameLabel .task.taskNameLabel
} }
</span> </span>
<input <input
bind:value={taskViewModel.name} bind:value={taskViewModel.name}
></input> ></input>
</div> </div>
</label> </label>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">category</span> <span class="icon">category</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskBoardLabel .task.taskBoardLabel
} }
</span> </span>
<select <select
bind:value={taskViewModel.boardId} bind:value={taskViewModel.boardId}
children:append={[ children:append={[
taskViewModel.chatViewModel taskViewModel.chatViewModel
.taskBoardSuggestions, .taskBoardSuggestions,
BoardOptionConverter, BoardOptionConverter,
]} ]}
></select> ></select>
<span class="icon">arrow_drop_down</span> <span class="icon">arrow_drop_down</span>
</div> </div>
</label> </label>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">description</span> <span class="icon">description</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskDescriptionLabel .task.taskDescriptionLabel
} }
</span> </span>
<textarea <textarea
rows="10" rows="10"
bind:value={taskViewModel.description} bind:value={taskViewModel.description}
></textarea> ></textarea>
</div> </div>
</label> </label>
<div class="flex-row justify-end"> <div class="flex-row justify-end">
<button <button
class="width-50" class="width-50"
on:click={taskViewModel.openFullscreenDecription} on:click={
> taskViewModel.openFullscreenDecription
{ }
coreViewModel.translations.general.fullscreenButton >
} {
coreViewModel.translations.general
.fullscreenButton
}
<span class="icon">fullscreen</span> <span class="icon">fullscreen</span>
</button> </button>
</div> </div>
</div> </div>
<hr class="mobile-only"></hr> <hr class="mobile-only"></hr>
<div class="flex-column"> <div class="flex-column">
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">category</span> <span class="icon">category</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskCategoryLabel .task.taskCategoryLabel
} }
</span> </span>
<input <input
bind:value={taskViewModel.category} bind:value={taskViewModel.category}
list={categorySuggestionId} list={categorySuggestionId}
></input> ></input>
</div> </div>
</label> </label>
<datalist <datalist
hidden hidden
id={categorySuggestionId} id={categorySuggestionId}
children:append={[ children:append={[
taskViewModel.containingViewModel.taskCategorySuggestions, taskViewModel.containingViewModel
StringToOption, .taskCategorySuggestions,
]} StringToOption,
></datalist> ]}
></datalist>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">clock_loader_40</span> <span class="icon">clock_loader_40</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskStatusLabel .task.taskStatusLabel
} }
</span> </span>
<input <input
bind:value={taskViewModel.status} bind:value={taskViewModel.status}
list={statusSuggestionId} list={statusSuggestionId}
></input> ></input>
</div> </div>
</label> </label>
<datalist <datalist
hidden hidden
id={statusSuggestionId} id={statusSuggestionId}
children:append={[ children:append={[
taskViewModel.containingViewModel.taskStatusSuggestions, taskViewModel.containingViewModel
StringToOption, .taskStatusSuggestions,
]} StringToOption,
></datalist> ]}
></datalist>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">priority_high</span> <span class="icon">priority_high</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskPriorityLabel .task.taskPriorityLabel
} }
</span> </span>
<input <input
type="number" type="number"
bind:value={taskViewModel.priority} bind:value={taskViewModel.priority}
></input> ></input>
</div> </div>
</label> </label>
<hr></hr> <hr></hr>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">calendar_month</span> <span class="icon">calendar_month</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskDateLabel .task.taskDateLabel
} }
</span> </span>
<input <input
type="date" type="date"
bind:value={taskViewModel.date} bind:value={taskViewModel.date}
></input> ></input>
</div> </div>
</label> </label>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">schedule</span> <span class="icon">schedule</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage
.taskTimeLabel .task.taskTimeLabel
} }
</span> </span>
<input <input
type="time" type="time"
bind:value={taskViewModel.time} bind:value={taskViewModel.time}
></input> ></input>
</div> </div>
</label> </label>
</div> </div>
</div> </div>
<hr></hr> <hr></hr>
<label class="tile flex-no"> <label class="tile flex-no">
<span class="icon">history</span> <span class="icon">history</span>
<div> <div>
<span> <span>
{ {
coreViewModel.translations.general coreViewModel.translations.general
.fileVersionLabel .fileVersionLabel
} }
</span> </span>
<select <select
bind:value={taskViewModel.selectedVersionId} bind:value={taskViewModel.selectedVersionId}
children:append={[ children:append={[
taskViewModel.versionIds, taskViewModel.versionIds,
VersionIdToOption, VersionIdToOption,
]} ]}
></select> ></select>
<span class="icon">arrow_drop_down</span> <span class="icon">arrow_drop_down</span>
</div> </div>
</label> </label>
<hr></hr> <hr></hr>
<div class="width-input"> <div class="width-input">
{DangerousActionButton( {DangerousActionButton(
coreViewModel, coreViewModel,
coreViewModel.translations.chatPage.task coreViewModel.translations.chatPage.task
.deleteTaskButton, .deleteTaskButton,
"delete_forever", "delete_forever",
taskViewModel.deleteTask, taskViewModel.deleteTask,
)} )}
</div> </div>
</main> </main>
<main id="fullscreen-main"> <main id="fullscreen-main">
<textarea <textarea
style="height: 100%; width: 100%; max-width: unset" style="height: 100%; width: 100%; max-width: unset"
bind:value={taskViewModel.description} bind:value={taskViewModel.description}
></textarea> ></textarea>
</main> </main>
<div <div class="flex-row width-100" id="fullscreen-exit">
class="flex-row width-100" <button
id="fullscreen-exit" class="flex"
> on:click={taskViewModel.closeFullscreenDecription}
<button >
class="flex" {coreViewModel.translations.general.exitButton}
on:click={taskViewModel.closeFullscreenDecription} <span class="icon">fullscreen_exit</span>
> </button>
{coreViewModel.translations.general.exitButton} </div>
<span class="icon">fullscreen_exit</span> <div class="flex-row width-100" id="controls">
</button> <button
</div> class="flex"
<div on:click={taskViewModel.closeAndDiscard}
class="flex-row width-100" >
id="controls" {coreViewModel.translations.general.closeButton}
> </button>
<button <button
class="flex" class="flex primary"
on:click={taskViewModel.closeAndDiscard} on:click={taskViewModel.closeAndSave}
> >
{coreViewModel.translations.general.closeButton} {coreViewModel.translations.general.saveButton}
</button> <span class="icon">save</span>
<button </button>
class="flex primary" </div>
on:click={taskViewModel.closeAndSave} </div>
> </div>
{coreViewModel.translations.general.saveButton} );
<span class="icon">save</span>
</button>
</div>
</div>
</div>
);
} }
+47 -46
View File
@@ -65,9 +65,9 @@ body[showing-chat] #chat-page-wrapper {
overflow: hidden; overflow: hidden;
} }
@media (pointer: coarse) and (max-width: 480px){ @media (pointer: coarse) and (max-width: 480px) {
#chat-page > div > #main > div > .pane-wrapper > .pane { #chat-page > div > #main > div > .pane-wrapper > .pane {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
} }
@@ -78,13 +78,13 @@ body[showing-chat] #chat-page-wrapper {
@media (min-width: 1024px) { @media (min-width: 1024px) {
#chat-page > div > #main > div > .pane-wrapper { #chat-page > div > #main > div > .pane-wrapper {
min-width: unset; min-width: unset;
} }
#chat-page > div > #main > div > .pane-wrapper.side { #chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem; width: 18rem;
min-width: 18rem; min-width: 18rem;
max-width: 18rem; max-width: 18rem;
} }
} }
@@ -114,21 +114,21 @@ body[showing-chat] #chat-page-wrapper {
#chat-page > div > #ribbon > span, #chat-page > div > #ribbon > span,
#chat-page #chat-page
> div > div
> #main > #main
> div > div
> .pane-wrapper > .pane-wrapper
> .pane > .pane
> .toolbar > .toolbar
> span.ribbon { > span.ribbon {
gap: var(--gap-between-tiles); gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles); padding: var(--gap-between-tiles);
} }
@media (pointer: coarse) and (max-width: 480px){ @media (pointer: coarse) and (max-width: 480px) {
#chat-page > div > #ribbon #close-button { #chat-page > div > #ribbon #close-button {
order: 1; order: 1;
} }
} }
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar { #chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
@@ -192,13 +192,13 @@ body[showing-chat] #chat-page-wrapper {
#chat-page > div > #ribbon button.ribbon-button, #chat-page > div > #ribbon button.ribbon-button,
#chat-page #chat-page
> div > div
> #main > #main
> div > div
> .pane-wrapper > .pane-wrapper
> .pane > .pane
> .toolbar > .toolbar
button.ribbon-button { button.ribbon-button {
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles)); min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
width: calc(var(--control-height) - 2 * var(--gap-between-tiles)); width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles)); max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
@@ -211,14 +211,14 @@ button.ribbon-button {
#chat-page > div > #ribbon button > .icon, #chat-page > div > #ribbon button > .icon,
#chat-page #chat-page
> div > div
> #main > #main
> div > div
> .pane-wrapper > .pane-wrapper
> .pane > .pane
> .toolbar > .toolbar
button button
> .icon { > .icon {
font-size: 1.1rem; font-size: 1.1rem;
} }
@@ -244,13 +244,13 @@ button
} }
#chat-page #chat-page
> div > div
> #main > #main
> div > div
> .pane-wrapper > .pane-wrapper
> .pane > .pane
> .toolbar > .toolbar
:is(input, select, button) { :is(input, select, button) {
height: 100%; height: 100%;
flex: 1; flex: 1;
} }
@@ -262,18 +262,19 @@ button
@media (min-width: 640px) { @media (min-width: 640px) {
.modal.task-settings .column-wrapper { .modal.task-settings .column-wrapper {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: var(--content-padding); gap: var(--content-padding);
} }
.modal.task-settings .column-wrapper > .flex-column{ .modal.task-settings .column-wrapper > .flex-column {
width: 100%; width: 100%;
flex: 1; flex: 1;
} }
} }
.modal.task-settings:not([full-description]) :is(#fullscreen-exit, #fullscreen-main) { .modal.task-settings:not([full-description])
:is(#fullscreen-exit, #fullscreen-main) {
display: none; display: none;
} }
+44 -43
View File
@@ -1,4 +1,3 @@
import * as React from "../react";
import { ViewController } from "./viewController"; import { ViewController } from "./viewController";
import "./homePage.css"; import "./homePage.css";
import { ChatViewToggleButton } from "./Components/chatViewToggleButton"; import { ChatViewToggleButton } from "./Components/chatViewToggleButton";
@@ -6,6 +5,7 @@ import { TaskPage } from "./ChatPages/taskPage";
import { SettingsPage } from "./ChatPages/settingsPage"; import { SettingsPage } from "./ChatPages/settingsPage";
import { MessagePage } from "./ChatPages/messagePage"; import { MessagePage } from "./ChatPages/messagePage";
import { CalendarPage } from "./ChatPages/calendarPage"; import { CalendarPage } from "./ChatPages/calendarPage";
import * as React from "../react";
import CoreViewModel from "../ViewModel/Global/coreViewModel"; import CoreViewModel from "../ViewModel/Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../ViewModel/Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../ViewModel/Chat/chatViewModel";
@@ -73,8 +73,8 @@ export function ChatPage(
<div> <div>
<div id="ribbon"> <div id="ribbon">
<button <button
class="ghost" class="ghost"
id="close-button" id="close-button"
aria-label={ aria-label={
coreViewModel.translations.chatPage coreViewModel.translations.chatPage
.closeChatAudioLabe .closeChatAudioLabe
@@ -95,47 +95,48 @@ export function ChatPage(
toggle:hidden={ toggle:hidden={
chatViewModel.connectionViewModel.isConnected chatViewModel.connectionViewModel.isConnected
} }
toggle:disabled={ toggle:disabled={
chatViewModel.connectionViewModel.hasNoPreviousConnections chatViewModel.connectionViewModel
} .hasNoPreviousConnections
> }
<span class="icon">signal_disconnected</span> >
</button> <span class="icon">signal_disconnected</span>
</button>
<span class="marquee" children:set={marqueeContent}></span> <span class="marquee" children:set={marqueeContent}></span>
<span class="navigation-buttons"> <span class="navigation-buttons">
{ChatViewToggleButton( {ChatViewToggleButton(
coreViewModel.translations.chatPage.pages.calendar, coreViewModel.translations.chatPage.pages.calendar,
"calendar_month", "calendar_month",
ChatPageTypes.Calendar, ChatPageTypes.Calendar,
chatViewModel, chatViewModel,
)} )}
{ChatViewToggleButton( {ChatViewToggleButton(
coreViewModel.translations.chatPage.pages.tasks, coreViewModel.translations.chatPage.pages.tasks,
"task_alt", "task_alt",
ChatPageTypes.Tasks, ChatPageTypes.Tasks,
chatViewModel, chatViewModel,
)} )}
{ChatViewToggleButton( {ChatViewToggleButton(
coreViewModel.translations.chatPage.pages.messages, coreViewModel.translations.chatPage.pages.messages,
"forum", "forum",
ChatPageTypes.Messages, ChatPageTypes.Messages,
chatViewModel, chatViewModel,
)} )}
{ChatViewToggleButton( {ChatViewToggleButton(
coreViewModel.translations.chatPage.pages.settings, coreViewModel.translations.chatPage.pages.settings,
"settings", "settings",
ChatPageTypes.Settings, ChatPageTypes.Settings,
chatViewModel, chatViewModel,
)} )}
</span> </span>
</div> </div>
<div <div
id="main" id="main"
children:set={ViewController.chatPages.state} children:set={ViewController.chatPages.state}
></div> ></div>
</div> </div>
</article> </article>
); );
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from "../react";
import "./chatPage.css"; import "./chatPage.css";
import { ChatPage } from "./chatPage"; import { ChatPage } from "./chatPage";
import * as React from "../react";
import CoreViewModel from "../ViewModel/Global/coreViewModel"; import CoreViewModel from "../ViewModel/Global/coreViewModel";
import ChatListViewModel from "../ViewModel/Chat/chatListViewModel"; import ChatListViewModel from "../ViewModel/Chat/chatListViewModel";
+1 -1
View File
@@ -1,8 +1,8 @@
import * as React from "../react";
import "./homePage.css"; import "./homePage.css";
import { StringToOption } from "./Components/option"; import { StringToOption } from "./Components/option";
import { HomePageButton } from "./Components/homePageButton"; import { HomePageButton } from "./Components/homePageButton";
import { ChatViewModelToChatEntry } from "./Components/chatEntry"; import { ChatViewModelToChatEntry } from "./Components/chatEntry";
import * as React from "../react";
import StorageViewModel from "../ViewModel/Global/storageViewModel"; import StorageViewModel from "../ViewModel/Global/storageViewModel";
import SettingsViewModel from "../ViewModel/Global/settingsViewModel"; import SettingsViewModel from "../ViewModel/Global/settingsViewModel";
import FileTransferViewModel from "../ViewModel/Global/fileTransferViewModel"; import FileTransferViewModel from "../ViewModel/Global/fileTransferViewModel";
+536 -536
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ class Tracker<T> {
class TrackerMap<T> { class TrackerMap<T> {
trackers = new Map<string, Tracker<T>>(); trackers = new Map<string, Tracker<T>>();
private register = (key: string): Tracker<T>=> { private register = (key: string): Tracker<T> => {
if (this.trackers.has(key)) { if (this.trackers.has(key)) {
return this.trackers.get(key)!; return this.trackers.get(key)!;
} }
+1 -1
View File
@@ -1,9 +1,9 @@
import * as React from "../../react";
import ChatViewModel from "./chatViewModel"; import ChatViewModel from "./chatViewModel";
import SettingsViewModel from "../Global/settingsViewModel"; import SettingsViewModel from "../Global/settingsViewModel";
import NotificationViewModel from "../Global/notificationViewModel"; import NotificationViewModel from "../Global/notificationViewModel";
import CoreViewModel from "../Global/coreViewModel"; import CoreViewModel from "../Global/coreViewModel";
import ConnectionViewModel from "../Global/connectionViewModel"; import ConnectionViewModel from "../Global/connectionViewModel";
import * as React from "../../react";
import { IndexManager } from "../../Model/Utility/utility"; import { IndexManager } from "../../Model/Utility/utility";
import ChatModel from "../../Model/Chat/chatModel"; import ChatModel from "../../Model/Chat/chatModel";
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from "../../react";
import MessagePageViewModel from "../Pages/messagePageViewModel"; import MessagePageViewModel from "../Pages/messagePageViewModel";
import CoreViewModel from "../Global/coreViewModel"; import CoreViewModel from "../Global/coreViewModel";
import * as React from "../../react";
import { import {
ChatMessage, ChatMessage,
ChatMessageReaction, ChatMessageReaction,
+1 -1
View File
@@ -1,4 +1,3 @@
import * as React from "../../react";
import ChatListViewModel from "./chatListViewModel"; import ChatListViewModel from "./chatListViewModel";
import TaskPageViewModel from "../Pages/taskPageViewModel"; import TaskPageViewModel from "../Pages/taskPageViewModel";
import SettingsPageViewModel from "../Pages/settingsPageViewModel"; import SettingsPageViewModel from "../Pages/settingsPageViewModel";
@@ -10,6 +9,7 @@ import SettingsViewModel from "../Global/settingsViewModel";
import NotificationViewModel from "../Global/notificationViewModel"; import NotificationViewModel from "../Global/notificationViewModel";
import CoreViewModel, { Context, ContextHost } from "../Global/coreViewModel"; import CoreViewModel, { Context, ContextHost } from "../Global/coreViewModel";
import ConnectionViewModel from "../Global/connectionViewModel"; import ConnectionViewModel from "../Global/connectionViewModel";
import * as React from "../../react";
import { Colors } from "../../colors"; import { Colors } from "../../colors";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { Entry } from "../../View/Components/option"; import { Entry } from "../../View/Components/option";
Binary file not shown.
+4 -4
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import CoreViewModel from "./coreViewModel"; import CoreViewModel from "./coreViewModel";
import * as React from "../../react";
export default class ConnectionViewModel { export default class ConnectionViewModel {
// state // state
@@ -48,7 +48,7 @@ export default class ConnectionViewModel {
); );
} }
this.hideConnectionModal(); this.hideConnectionModal();
}; };
// methods // methods
@@ -69,8 +69,8 @@ export default class ConnectionViewModel {
this.coreViewModel.connectionModel.removeAddress(address); this.coreViewModel.connectionModel.removeAddress(address);
this.updatePreviousAddresses(); this.updatePreviousAddresses();
if (this.previousAddresses.value.size > 0) return; if (this.previousAddresses.value.size > 0) return;
this.hideConnectionModal(); this.hideConnectionModal();
}; };
// view // view
+15 -12
View File
@@ -40,8 +40,8 @@ export default class CoreViewModel {
return; return;
while (this.contexts.length > 0) { while (this.contexts.length > 0) {
const currentContext: Context|undefined= this.context; const currentContext: Context | undefined = this.context;
if (!currentContext) return; if (!currentContext) return;
currentContext.handleContextClose(fromHistoryEvent); currentContext.handleContextClose(fromHistoryEvent);
this.contextStack.delete(currentContext.contextId); this.contextStack.delete(currentContext.contextId);
if (currentContext.contextId == contextId) break; if (currentContext.contextId == contextId) break;
@@ -49,9 +49,10 @@ export default class CoreViewModel {
}; };
handleKeyDown = (e: KeyboardEvent): void => { handleKeyDown = (e: KeyboardEvent): void => {
if (!(e instanceof KeyboardEvent)) return console.trace("NOT A KEY EVENT"); if (!(e instanceof KeyboardEvent))
return console.trace("NOT A KEY EVENT");
if (CoreViewModel.checkIsKeystroke(e) == false) return; if (CoreViewModel.checkIsKeystroke(e) == false) return;
console.log(e.key); console.log(e.key);
e.preventDefault(); e.preventDefault();
const contexts: Context[] = this.contexts; const contexts: Context[] = this.contexts;
while (contexts.length > 0) { while (contexts.length > 0) {
@@ -97,7 +98,7 @@ export default class CoreViewModel {
document.body.addEventListener("keydown", this.handleKeyDown); document.body.addEventListener("keydown", this.handleKeyDown);
window.onpopstate = () => { window.onpopstate = () => {
if (!this.context) return; if (!this.context) return;
this.closeContext(this.context.contextId, true); this.closeContext(this.context.contextId, true);
}; };
@@ -164,11 +165,10 @@ export class ContextHost<T> extends Context {
updateContexts = (): void => { updateContexts = (): void => {
if (this.isOpen == false) return; if (this.isOpen == false) return;
const selection = this.contextSelection; const selection = this.contextSelection;
if (!selection) return; if (!selection) return;
const selectedContext: Context | undefined = this.contexts.get( const selectedContext: Context | undefined =
selection this.contexts.get(selection);
);
if (!selectedContext) return; if (!selectedContext) return;
if (selectedContext != this.currentContext.value) { if (selectedContext != this.currentContext.value) {
this.closeCurrentContext(); this.closeCurrentContext();
@@ -178,7 +178,10 @@ export class ContextHost<T> extends Context {
this.currentContext.value = selectedContext; this.currentContext.value = selectedContext;
}; };
constructor(contextDebugDescription: string, public coreViewModel: CoreViewModel) { constructor(
super(contextDebugDescription); contextDebugDescription: string,
public coreViewModel: CoreViewModel,
) {
super(contextDebugDescription);
} }
} }
@@ -1,5 +1,5 @@
import * as React from "../../react";
import CoreViewModel, { Context } from "./coreViewModel"; import CoreViewModel, { Context } from "./coreViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import StorageModel, { import StorageModel, {
StorageModelSubPaths, StorageModelSubPaths,
@@ -54,7 +54,9 @@ export default class FileTransferViewModel extends Context {
exportKey: React.State<string> = new React.State(""); exportKey: React.State<string> = new React.State("");
exportKeyConfirmation: React.State<string> = new React.State(""); exportKeyConfirmation: React.State<string> = new React.State("");
importedFile: React.State<File | undefined> = new React.State<File | undefined>(undefined); importedFile: React.State<File | undefined> = new React.State<
File | undefined
>(undefined);
importedFileString: React.State<string> = new React.State(""); importedFileString: React.State<string> = new React.State("");
importKey: React.State<string> = new React.State(""); importKey: React.State<string> = new React.State("");
isImportKeyCorrect: React.State<boolean> = new React.State(true); isImportKeyCorrect: React.State<boolean> = new React.State(true);
@@ -139,7 +141,7 @@ export default class FileTransferViewModel extends Context {
}; };
downloadFile = async (): Promise<void> => { downloadFile = async (): Promise<void> => {
if (this.cannotExport.value == true) return; if (this.cannotExport.value == true) return;
const date = new Date().toISOString().split("T")[0]; const date = new Date().toISOString().split("T")[0];
const backup: Blob = const backup: Blob =
@@ -158,7 +160,7 @@ export default class FileTransferViewModel extends Context {
if (!input) return; if (!input) return;
if (!(input instanceof HTMLInputElement)) return; if (!(input instanceof HTMLInputElement)) return;
if (!input.files) return; if (!input.files) return;
const file = input.files[0]; const file = input.files[0];
this.importedFile.value = file; this.importedFile.value = file;
}; };
@@ -171,7 +173,7 @@ export default class FileTransferViewModel extends Context {
reader.readAsText(file, "utf8"); reader.readAsText(file, "utf8");
reader.onload = () => { reader.onload = () => {
const result = reader.result; const result = reader.result;
if (!result) return; if (!result) return;
this.importedFileString.value = result.toString(); this.importedFileString.value = result.toString();
this.showImportDecryptDataModal(); this.showImportDecryptDataModal();
}; };
@@ -1,6 +1,6 @@
import * as React from "../../react";
import { ChatPageTypes } from "../Chat/chatViewModel"; import { ChatPageTypes } from "../Chat/chatViewModel";
import ChatListViewModel from "../Chat/chatListViewModel"; import ChatListViewModel from "../Chat/chatListViewModel";
import * as React from "../../react";
import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel"; import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel";
export default class NotificationViewModel { export default class NotificationViewModel {
@@ -35,7 +35,7 @@ export default class NotificationViewModel {
}; };
openNotification = () => { openNotification = () => {
const notification: Notification|undefined = this.marquee.value; const notification: Notification | undefined = this.marquee.value;
if (notification == undefined) return; if (notification == undefined) return;
const chat = [ const chat = [
@@ -44,7 +44,7 @@ export default class NotificationViewModel {
(chat) => (chat) =>
chat.chatModel.unwrappedPrimaryChannel == notification.chat, chat.chatModel.unwrappedPrimaryChannel == notification.chat,
); );
if (!chat) return; if (!chat) return;
chat.open(); chat.open();
chat.openPage(ChatPageTypes.Messages); chat.openPage(ChatPageTypes.Messages);
}; };
@@ -56,8 +56,9 @@ export default class NotificationViewModel {
return this.stopLoop(); return this.stopLoop();
} }
const notification: Notification|undefined = this.messagesInMarquee.shift(); const notification: Notification | undefined =
if (!notification) return; this.messagesInMarquee.shift();
if (!notification) return;
this.seenMessageIds.delete(notification.messageId); this.seenMessageIds.delete(notification.messageId);
this.marquee.value = notification; this.marquee.value = notification;
}; };
+103 -92
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import CoreViewModel, { Context } from "./coreViewModel"; import CoreViewModel, { Context } from "./coreViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { Languages, ThemeSettings } from "../../Model/Global/settingsModel"; import { Languages, ThemeSettings } from "../../Model/Global/settingsModel";
@@ -13,12 +13,12 @@ export default class SettingsViewModel extends Context {
requiresReload = new React.State<boolean>(false); requiresReload = new React.State<boolean>(false);
firstDayOfWeek: React.State<string> = new React.State("0"); firstDayOfWeek: React.State<string> = new React.State("0");
language: React.State<Languages | string> = new React.State<Languages | string>( language: React.State<Languages | string> = new React.State<
Languages.English, Languages | string
); >(Languages.English);
theme: React.State<ThemeSettings | string> = new React.State<ThemeSettings | string>( theme: React.State<ThemeSettings | string> = new React.State<
ThemeSettings.System, ThemeSettings | string
); >(ThemeSettings.System);
// guards // guards
cannotSetName: React.State<boolean> = React.createProxyState( cannotSetName: React.State<boolean> = React.createProxyState(
@@ -48,121 +48,132 @@ export default class SettingsViewModel extends Context {
}; };
showModalPage = (page: SettingsModalPages): void => { showModalPage = (page: SettingsModalPages): void => {
this.selectedModalPage.value = page; this.selectedModalPage.value = page;
}; };
// view // view
applyTheme = (): void => { applyTheme = (): void => {
let theme: string = this.theme.value; let theme: string = this.theme.value;
if (theme == ThemeSettings.Dynamic) { if (theme == ThemeSettings.Dynamic) {
const scene = this.selectDynamicScene(); const scene = this.selectDynamicScene();
this.setScene(scene); this.setScene(scene);
return; return;
} }
if (theme == ThemeSettings.System) { if (theme == ThemeSettings.System) {
theme = SettingsViewModel.getSystemTheme(); theme = SettingsViewModel.getSystemTheme();
} }
if (theme == ThemeSettings.Dark) { if (theme == ThemeSettings.Dark) {
this.setScene(DynamicSceneNight); this.setScene(DynamicSceneNight);
} else { } else {
this.setScene(DynamicSceneDay); this.setScene(DynamicSceneDay);
} }
}; };
selectDynamicScene = (): DynamicScene => { selectDynamicScene = (): DynamicScene => {
let hour = new Date().getHours(); let hour = new Date().getHours();
if (hour >= 21 || hour <= 6) { if (hour >= 21 || hour <= 6) {
return DynamicSceneNight; return DynamicSceneNight;
} else if (hour > 6 && hour < 10) { } else if (hour > 6 && hour < 10) {
return DynamicSceneSunrise; return DynamicSceneSunrise;
} else if (hour < 14) { } else if (hour < 14) {
return DynamicSceneDay; return DynamicSceneDay;
} else if (hour < 17) { } else if (hour < 17) {
return DynamicSceneAfternoon return DynamicSceneAfternoon;
} else { } else {
return DynamicSceneSunset return DynamicSceneSunset;
} }
} };
setScene = (scene: DynamicScene): void => { setScene = (scene: DynamicScene): void => {
document.body.style.setProperty("--backdrop-grass-filter", `brightness(${scene.brightness})`); document.body.style.setProperty(
"--backdrop-grass-filter",
`brightness(${scene.brightness})`,
);
function setSkyColor(tone: 1|2, hue: number, saturation: number, luma: number) { function setSkyColor(
document.body.style.setProperty(`--sky-${tone}`, `hsl(${hue}, ${saturation}%, ${luma}%)`); tone: 1 | 2,
} hue: number,
setSkyColor(1, scene.hue1, scene.saturation, scene.luma); saturation: number,
setSkyColor(2, scene.hue2, scene.saturation - 10, scene.luma-10); luma: number,
) {
document.body.style.setProperty(
`--sky-${tone}`,
`hsl(${hue}, ${saturation}%, ${luma}%)`,
);
}
setSkyColor(1, scene.hue1, scene.saturation, scene.luma);
setSkyColor(2, scene.hue2, scene.saturation - 10, scene.luma - 10);
document.body.setAttribute("theme", scene.baseTheme); document.body.setAttribute("theme", scene.baseTheme);
} };
// exit // exit
close = (): void => { close = (): void => {
this.coreViewModel.closeContext(this.contextId); this.coreViewModel.closeContext(this.contextId);
}; };
handleContextClose = (): void => { handleContextClose = (): void => {
this.isShowingSettingsModal.value = false; this.isShowingSettingsModal.value = false;
if (this.requiresReload.value == true) { if (this.requiresReload.value == true) {
window.location.reload(); window.location.reload();
} }
}; };
// init // init
constructor(public readonly coreViewModel: CoreViewModel) { constructor(public readonly coreViewModel: CoreViewModel) {
super("settings"); super("settings");
this.username.value = coreViewModel.settingsModel.username; this.username.value = coreViewModel.settingsModel.username;
this.usernameInput.value = coreViewModel.settingsModel.username; this.usernameInput.value = coreViewModel.settingsModel.username;
this.firstDayOfWeek.value = coreViewModel.settingsModel.firstDayOfWeek; this.firstDayOfWeek.value = coreViewModel.settingsModel.firstDayOfWeek;
this.language.value = coreViewModel.settingsModel.language; this.language.value = coreViewModel.settingsModel.language;
this.theme.value = coreViewModel.settingsModel.theme; this.theme.value = coreViewModel.settingsModel.theme;
// subscriptions // subscriptions
this.firstDayOfWeek.subscribe(this.setFirstDayofWeek); this.firstDayOfWeek.subscribe(this.setFirstDayofWeek);
this.language.subscribeSilent((newValue) => { this.language.subscribeSilent((newValue) => {
this.coreViewModel.settingsModel.setLanguage(newValue); this.coreViewModel.settingsModel.setLanguage(newValue);
this.requiresReload.value = true; this.requiresReload.value = true;
}); });
this.theme.subscribeSilent((newValue) => { this.theme.subscribeSilent((newValue) => {
this.coreViewModel.settingsModel.setTheme(newValue); this.coreViewModel.settingsModel.setTheme(newValue);
}); });
// set theme // set theme
this.theme.subscribe(() => { this.theme.subscribe(() => {
this.applyTheme(); this.applyTheme();
}); });
SettingsViewModel.generateThemeMedia().addEventListener("change", () => SettingsViewModel.generateThemeMedia().addEventListener("change", () =>
this.applyTheme(), this.applyTheme(),
); );
const seconds = new Date().getSeconds(); const seconds = new Date().getSeconds();
const secondsUntilNewMinute = 60-seconds; const secondsUntilNewMinute = 60 - seconds;
setTimeout(() => { setTimeout(() => {
setInterval(this.applyTheme, 1000*60); setInterval(this.applyTheme, 1000 * 60);
}, secondsUntilNewMinute); }, secondsUntilNewMinute);
// keystrokes // keystrokes
this.registerKeyStroke(CommonKeys.CloseOrCancel, this.close); this.registerKeyStroke(CommonKeys.CloseOrCancel, this.close);
} }
static generateThemeMedia(): MediaQueryList { static generateThemeMedia(): MediaQueryList {
return window.matchMedia("(prefers-color-scheme: dark)"); return window.matchMedia("(prefers-color-scheme: dark)");
} }
static getSystemTheme(): string { static getSystemTheme(): string {
const media = SettingsViewModel.generateThemeMedia(); const media = SettingsViewModel.generateThemeMedia();
return media.matches == true ? ThemeSettings.Dark : ThemeSettings.Light; return media.matches == true ? ThemeSettings.Dark : ThemeSettings.Light;
} }
} }
export enum SettingsModalPages { export enum SettingsModalPages {
Appearance, Appearance,
Regional, Regional,
Info, Info,
} }
export interface DynamicScene { export interface DynamicScene {
@@ -176,40 +187,40 @@ export interface DynamicScene {
export const DynamicSceneNight: DynamicScene = { export const DynamicSceneNight: DynamicScene = {
hue1: 215, hue1: 215,
hue2: 220, hue2: 220,
saturation : 50, saturation: 50,
luma: 25, luma: 25,
brightness: 0.4, brightness: 0.4,
baseTheme: ThemeSettings.Dark, baseTheme: ThemeSettings.Dark,
} };
export const DynamicSceneSunrise: DynamicScene = { export const DynamicSceneSunrise: DynamicScene = {
hue1: 45, hue1: 45,
hue2: 190, hue2: 190,
saturation : 100, saturation: 100,
luma: 50, luma: 50,
brightness: 1.2, brightness: 1.2,
baseTheme: ThemeSettings.Light, baseTheme: ThemeSettings.Light,
} };
export const DynamicSceneDay: DynamicScene = { export const DynamicSceneDay: DynamicScene = {
hue1: 190, hue1: 190,
hue2: 180, hue2: 180,
saturation : 100, saturation: 100,
luma: 70, luma: 70,
brightness: 1.4, brightness: 1.4,
baseTheme: ThemeSettings.Light, baseTheme: ThemeSettings.Light,
} };
export const DynamicSceneAfternoon: DynamicScene = { export const DynamicSceneAfternoon: DynamicScene = {
hue1: 195, hue1: 195,
hue2: 220, hue2: 220,
saturation : 100, saturation: 100,
luma: 50, luma: 50,
brightness: 0.8, brightness: 0.8,
baseTheme: ThemeSettings.Light, baseTheme: ThemeSettings.Light,
} };
export const DynamicSceneSunset: DynamicScene = { export const DynamicSceneSunset: DynamicScene = {
hue1: 20, hue1: 20,
hue2: 260, hue2: 260,
saturation : 100, saturation: 100,
luma: 50, luma: 50,
brightness: 0.4, brightness: 0.4,
baseTheme: ThemeSettings.Dark, baseTheme: ThemeSettings.Dark,
} };
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "../../react";
import CoreViewModel, { Context } from "./coreViewModel"; import CoreViewModel, { Context } from "./coreViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import StorageModel, { import StorageModel, {
PATH_COMPONENT_SEPARATOR, PATH_COMPONENT_SEPARATOR,
Binary file not shown.
+7 -11
View File
@@ -1,10 +1,10 @@
import * as React from "../../react";
import TaskViewModel from "./taskViewModel"; import TaskViewModel from "./taskViewModel";
import TaskPageViewModel from "./taskPageViewModel"; import TaskPageViewModel from "./taskPageViewModel";
import TaskContainingPageViewModel from "./taskContainingPageViewModel"; import TaskContainingPageViewModel from "./taskContainingPageViewModel";
import SearchViewModel from "../Utility/searchViewModel"; import SearchViewModel from "../Utility/searchViewModel";
import CoreViewModel from "../Global/coreViewModel"; import CoreViewModel from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react";
import { Colors } from "../../colors"; import { Colors } from "../../colors";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
@@ -115,11 +115,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
searchTerm, searchTerm,
]; ];
this.coreViewModel.storageModel.write(suggestionPath, ""); this.coreViewModel.storageModel.write(suggestionPath, "");
if ( if (!this.searchSuggestions.value.has(searchTerm) && searchTerm != "") {
!this.searchSuggestions.value.has(
searchTerm,
) && searchTerm != ""
) {
this.searchSuggestions.add(searchTerm); this.searchSuggestions.add(searchTerm);
} }
@@ -133,7 +129,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
searchTerm, searchTerm,
]; ];
this.coreViewModel.storageModel.remove(suggestionPath); this.coreViewModel.storageModel.remove(suggestionPath);
} };
// view // view
showTask = (taskFileContent: TaskFileContent): void => { showTask = (taskFileContent: TaskFileContent): void => {
@@ -154,7 +150,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
this, this,
taskFileContent, taskFileContent,
); );
console.log("STILL HERE", taskFileContent.fileId); console.log("STILL HERE", taskFileContent.fileId);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel); this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
}; };
@@ -234,7 +230,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
const dirPath: string[] = this.getPreviousSearchesPath(); const dirPath: string[] = this.getPreviousSearchesPath();
const searches: string[] = const searches: string[] =
this.coreViewModel.storageModel.list(dirPath); this.coreViewModel.storageModel.list(dirPath);
this.searchSuggestions.add(...searches.filter(x => x != "")); this.searchSuggestions.add(...searches.filter((x) => x != ""));
}; };
restoreSearch = (): void => { restoreSearch = (): void => {
@@ -307,13 +303,13 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
this.taskViewModels, this.taskViewModels,
this.filteredTaskViewModels, this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter, TaskViewModel.getStringsForFilter,
this.searchSuggestions, this.searchSuggestions,
); );
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => { this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
this.handleNewSearch(newQuery); this.handleNewSearch(newQuery);
}); });
this.restoreSearch(); this.restoreSearch();
this.searchSuggestions.handleRemovals(this.handleSearchRemoved); this.searchSuggestions.handleRemovals(this.handleSearchRemoved);
this.isFilterActive = React.createProxyState( this.isFilterActive = React.createProxyState(
[this.searchViewModel.appliedQuery], [this.searchViewModel.appliedQuery],
+1 -1
View File
@@ -1,8 +1,8 @@
import * as React from "../../react";
import TaskViewModel from "./taskViewModel"; import TaskViewModel from "./taskViewModel";
import TaskContainingPageViewModel from "./taskContainingPageViewModel"; import TaskContainingPageViewModel from "./taskContainingPageViewModel";
import CoreViewModel from "../Global/coreViewModel"; import CoreViewModel from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel"; import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
+4 -2
View File
@@ -1,8 +1,8 @@
import * as React from "../../react";
import SearchViewModel from "../Utility/searchViewModel"; import SearchViewModel from "../Utility/searchViewModel";
import CoreViewModel, { Context } from "../Global/coreViewModel"; import CoreViewModel, { Context } from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import ChatMessageViewModel from "../Chat/chatMessageViewModel"; import ChatMessageViewModel from "../Chat/chatMessageViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { import {
ChatMessage, ChatMessage,
@@ -68,7 +68,9 @@ export default class MessagePageViewModel extends Context {
); );
}; };
setReply = (chatMessageViewModel: ChatMessageViewModel|undefined): void => { setReply = (
chatMessageViewModel: ChatMessageViewModel | undefined,
): void => {
this.replyingMessage.value = chatMessageViewModel; this.replyingMessage.value = chatMessageViewModel;
this.setFocus(); this.setFocus();
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from "../../react";
import CoreViewModel, { Context } from "../Global/coreViewModel"; import CoreViewModel, { Context } from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react";
import { Colors } from "../../colors"; import { Colors } from "../../colors";
export default class SettingsPageViewModel extends Context { export default class SettingsPageViewModel extends Context {
@@ -1,7 +1,7 @@
import * as React from "../../react";
import TaskViewModel from "./taskViewModel"; import TaskViewModel from "./taskViewModel";
import CoreViewModel, { Context } from "../Global/coreViewModel"; import CoreViewModel, { Context } from "../Global/coreViewModel";
import ChatViewModel from "../Chat/chatViewModel"; import ChatViewModel from "../Chat/chatViewModel";
import * as React from "../../react";
import { IndexManager } from "../../Model/Utility/utility"; import { IndexManager } from "../../Model/Utility/utility";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
TaskFileContent, TaskFileContent,
@@ -21,7 +21,6 @@ export default class TaskContainingPageViewModel extends Context {
taskCategorySuggestions: React.ListState<string> = new React.ListState(); taskCategorySuggestions: React.ListState<string> = new React.ListState();
taskStatusSuggestions: React.ListState<string> = new React.ListState(); taskStatusSuggestions: React.ListState<string> = new React.ListState();
// methods // methods
createTaskFromBoardId = (boardId: string): void => { createTaskFromBoardId = (boardId: string): void => {
const taskFileContent: TaskFileContent = const taskFileContent: TaskFileContent =
+11 -7
View File
@@ -1,9 +1,9 @@
import * as React from "../../react";
import BoardViewModel from "./boardViewModel"; import BoardViewModel from "./boardViewModel";
import CoreViewModel, { ContextHost } from "../Global/coreViewModel"; import CoreViewModel, { ContextHost } from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { IndexManager } from "../../Model/Utility/utility"; import implementFilter, { IndexManager } from "../../Model/Utility/utility";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
BoardInfoFileContent, BoardInfoFileContent,
} from "../../Model/Files/boardsAndTasksModel"; } from "../../Model/Files/boardsAndTasksModel";
@@ -26,8 +26,9 @@ export default class TaskPageViewModel extends ContextHost<string> {
}; };
// state // state
newBoardNameInput: React.State<string> = new React.State(""); boardQuery: React.State<string> = new React.State("");
boardViewModels: React.MapState<BoardViewModel> = new React.MapState(); boardViewModels: React.MapState<BoardViewModel> = new React.MapState();
boardMatches: React.ListState<BoardViewModel> = new React.ListState();
isShowingBoadList: React.State<boolean> = new React.State(true); isShowingBoadList: React.State<boolean> = new React.State(true);
selectedBoardId: React.State<string | undefined> = new React.State<any>( selectedBoardId: React.State<string | undefined> = new React.State<any>(
undefined, undefined,
@@ -47,8 +48,8 @@ export default class TaskPageViewModel extends ContextHost<string> {
// guards // guards
cannotCreateBoard: React.State<boolean> = React.createProxyState( cannotCreateBoard: React.State<boolean> = React.createProxyState(
[this.newBoardNameInput], [this.boardQuery],
() => this.newBoardNameInput.value == "", () => this.boardQuery.value == "",
); );
// methods // methods
@@ -56,8 +57,8 @@ export default class TaskPageViewModel extends ContextHost<string> {
if (this.cannotCreateBoard.value == true) return; if (this.cannotCreateBoard.value == true) return;
const boardInfoFileContent: BoardInfoFileContent = const boardInfoFileContent: BoardInfoFileContent =
this.boardsAndTasksModel.createBoard(this.newBoardNameInput.value); this.boardsAndTasksModel.createBoard(this.boardQuery.value);
this.newBoardNameInput.value = ""; this.boardQuery.value = "";
this.showBoardInList(boardInfoFileContent); this.showBoardInList(boardInfoFileContent);
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent); this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
@@ -177,6 +178,9 @@ export default class TaskPageViewModel extends ContextHost<string> {
this.chatViewModel = chatViewModel; this.chatViewModel = chatViewModel;
// filter
implementFilter(this.boardViewModels, this.boardMatches, this.boardQuery, (board) => board.name.value);
// context // context
this.chatViewModel.registerContext(ChatPageTypes.Tasks, this); this.chatViewModel.registerContext(ChatPageTypes.Tasks, this);
this.selectedBoardId.subscribeSilent(this.updateContexts); this.selectedBoardId.subscribeSilent(this.updateContexts);
+14 -8
View File
@@ -1,7 +1,7 @@
import * as React from "../../react";
import TaskContainingPageViewModel from "./taskContainingPageViewModel"; import TaskContainingPageViewModel from "./taskContainingPageViewModel";
import CoreViewModel, { Context } from "../Global/coreViewModel"; import CoreViewModel, { Context } from "../Global/coreViewModel";
import ChatViewModel from "../Chat/chatViewModel"; import ChatViewModel from "../Chat/chatViewModel";
import * as React from "../../react";
import { ViewController } from "../../View/viewController"; import { ViewController } from "../../View/viewController";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { localeCompare, padZero } from "../../Model/Utility/utility"; import { localeCompare, padZero } from "../../Model/Utility/utility";
@@ -58,7 +58,9 @@ export default class TaskViewModel extends Context {
selectedVersionId: React.State<string> = new React.State(""); selectedVersionId: React.State<string> = new React.State("");
versionIds: React.ListState<string> = new React.ListState(); versionIds: React.ListState<string> = new React.ListState();
isPresentingFullScreenDescription: React.State<boolean> = new React.State(false); isPresentingFullScreenDescription: React.State<boolean> = new React.State(
false,
);
// methods // methods
dragStart = (event: DragEvent): void => { dragStart = (event: DragEvent): void => {
@@ -99,12 +101,12 @@ export default class TaskViewModel extends Context {
}; };
openFullscreenDecription = (): void => { openFullscreenDecription = (): void => {
this.isPresentingFullScreenDescription.value = true; this.isPresentingFullScreenDescription.value = true;
} };
closeFullscreenDecription = (): void => { closeFullscreenDecription = (): void => {
this.isPresentingFullScreenDescription.value = false; this.isPresentingFullScreenDescription.value = false;
} };
updateIndex = (): void => { updateIndex = (): void => {
const index: number = const index: number =
@@ -118,7 +120,9 @@ export default class TaskViewModel extends Context {
this.category.value, this.category.value,
) == false ) == false
) { ) {
this.containingViewModel.taskCategorySuggestions.add(this.category.value); this.containingViewModel.taskCategorySuggestions.add(
this.category.value,
);
} }
if ( if (
@@ -126,7 +130,9 @@ export default class TaskViewModel extends Context {
this.status.value, this.status.value,
) == false ) == false
) { ) {
this.containingViewModel.taskStatusSuggestions.add(this.status.value); this.containingViewModel.taskStatusSuggestions.add(
this.status.value,
);
} }
}; };
+8 -5
View File
@@ -15,7 +15,10 @@ export default class SearchViewModel<T> {
[this.searchInput], [this.searchInput],
() => this.searchInput.value == "", () => this.searchInput.value == "",
); );
hasNoSuggestions: React.State<boolean> = React.createProxyState([this.suggestions], ()=>this.suggestions.value.size == 0); hasNoSuggestions: React.State<boolean> = React.createProxyState(
[this.suggestions],
() => this.suggestions.value.size == 0,
);
// methods // methods
search = (searchTerm: string): void => { search = (searchTerm: string): void => {
@@ -36,12 +39,12 @@ export default class SearchViewModel<T> {
}; };
clear = (): void => { clear = (): void => {
this.searchInput.value = ""; this.searchInput.value = "";
} };
deleteSuggestion = (suggestion: string): void => { deleteSuggestion = (suggestion: string): void => {
this.suggestions.remove(suggestion); this.suggestions.remove(suggestion);
} };
// init // init
constructor( constructor(
@@ -1,5 +1,5 @@
import * as React from "../../react";
import TaskViewModel from "../Pages/taskViewModel"; import TaskViewModel from "../Pages/taskViewModel";
import * as React from "../../react";
export default class TaskPropertyBulkChangeViewModel { export default class TaskPropertyBulkChangeViewModel {
taskViewModels: React.ListState<TaskViewModel>; taskViewModels: React.ListState<TaskViewModel>;
+6 -4
View File
@@ -44,7 +44,9 @@ main {
} }
body { body {
transition: --sky-1 1s, --sky-2 5s; transition:
--sky-1 1s,
--sky-2 5s;
} }
#background-wrapper #grass-1 { #background-wrapper #grass-1 {
@@ -52,7 +54,7 @@ body {
--color-2: hsl(150, 60%, 25%); --color-2: hsl(150, 60%, 25%);
filter: var(--backdrop-grass-filter); filter: var(--backdrop-grass-filter);
opacity: .7; opacity: 0.7;
background-image: linear-gradient( background-image: linear-gradient(
350deg, 350deg,
@@ -65,9 +67,9 @@ body {
#background-wrapper #grass-2 { #background-wrapper #grass-2 {
--color-1: hsl(110, 60%, 40%); --color-1: hsl(110, 60%, 40%);
--color-2: hsl(110, 60%, 35%); --color-2: hsl(110, 60%, 35%);
filter: var(--backdrop-grass-filter); filter: var(--backdrop-grass-filter);
opacity: .7; opacity: 0.7;
background-image: linear-gradient( background-image: linear-gradient(
9deg, 9deg,
+136 -136
View File
@@ -85,7 +85,7 @@ export class ListState<T> extends State<Set<T>> {
items.forEach((item) => { items.forEach((item) => {
this.value.delete(item); this.value.delete(item);
this.genericRemovalHandlers.forEach(handler => handler(item)); this.genericRemovalHandlers.forEach((handler) => handler(item));
if (!this.removalHandlers.has(item)) return; if (!this.removalHandlers.has(item)) return;
this.removalHandlers.get(item)!.forEach((handler) => handler(item)); this.removalHandlers.get(item)!.forEach((handler) => handler(item));
this.removalHandlers.delete(item); this.removalHandlers.delete(item);
@@ -146,73 +146,73 @@ export class MapState<T> extends State<Map<string, T>> {
this.value.delete(key); this.value.delete(key);
this.callSubscriptions(); this.callSubscriptions();
this.genericRemovalHandlers.forEach(handler => handler(item)); this.genericRemovalHandlers.forEach((handler) => handler(item));
if (!this.removalHandlers.has(item)) return; if (!this.removalHandlers.has(item)) return;
this.removalHandlers.get(item)!.forEach((handler) => handler(item)); this.removalHandlers.get(item)!.forEach((handler) => handler(item));
this.removalHandlers.delete(item); this.removalHandlers.delete(item);
} }
clear() { clear() {
[...this.value.keys()].forEach((key) => this.remove(key)); [...this.value.keys()].forEach((key) => this.remove(key));
} }
// handlers // handlers
handleAddition(handler: AdditionSubscription<T>): void { handleAddition(handler: AdditionSubscription<T>): void {
this.additionHandlers.add(handler); this.additionHandlers.add(handler);
[...this.value.values()].forEach(handler); [...this.value.values()].forEach(handler);
} }
handleRemoval(item: T, handler: RemovalSubscription<T>): void { handleRemoval(item: T, handler: RemovalSubscription<T>): void {
if (!this.removalHandlers.has(item)) if (!this.removalHandlers.has(item))
this.removalHandlers.set(item, new Set()); this.removalHandlers.set(item, new Set());
this.removalHandlers.get(item)!.add(handler); this.removalHandlers.get(item)!.add(handler);
} }
handleRemovals(handler: RemovalSubscription<T>): void { handleRemovals(handler: RemovalSubscription<T>): void {
this.genericRemovalHandlers.add(handler); this.genericRemovalHandlers.add(handler);
} }
// stringification // stringification
toString(): string { toString(): string {
const array = [...this.value.entries()]; const array = [...this.value.entries()];
const json = JSON.stringify(array); const json = JSON.stringify(array);
return json; return json;
} }
} }
// UTILITY // UTILITY
export function createProxyState<T>( export function createProxyState<T>(
statesToSubscibe: State<any>[], statesToSubscibe: State<any>[],
fn: () => T fn: () => T,
): State<T> { ): State<T> {
const proxyState = new State<T>(fn()); const proxyState = new State<T>(fn());
statesToSubscibe.forEach((state) => statesToSubscibe.forEach((state) =>
state.subscribeSilent(() => (proxyState.value = fn())) state.subscribeSilent(() => (proxyState.value = fn())),
); );
return proxyState; return proxyState;
} }
export function bulkSubscribe( export function bulkSubscribe(
statesToSubscibe: State<any>[], statesToSubscibe: State<any>[],
fn: () => void fn: () => void,
): void { ): void {
statesToSubscibe.forEach((state) => state.subscribeSilent(fn)); statesToSubscibe.forEach((state) => state.subscribeSilent(fn));
} }
function persistState(localStorageKey: string, state: State<any>) { function persistState(localStorageKey: string, state: State<any>) {
state.subscribe(() => { state.subscribe(() => {
const stringifiedValue = state.toString(); const stringifiedValue = state.toString();
localStorage.setItem(localStorageKey, stringifiedValue); localStorage.setItem(localStorageKey, stringifiedValue);
}); });
} }
export function restoreState<T>( export function restoreState<T>(
localStorageKey: string, localStorageKey: string,
initialStateValue: T initialStateValue: T,
): State<T> { ): State<T> {
const storedString = const storedString =
localStorage.getItem(localStorageKey) ?? localStorage.getItem(localStorageKey) ??
JSON.stringify(initialStateValue); JSON.stringify(initialStateValue);
const convertedValue = JSON.parse(storedString); const convertedValue = JSON.parse(storedString);
const state = new State(convertedValue); const state = new State(convertedValue);
@@ -223,14 +223,14 @@ export function restoreState<T>(
export function restoreListState<T>( export function restoreListState<T>(
localStorageKey: string, localStorageKey: string,
initialItems: any[] = [] initialItems: any[] = [],
): ListState<T> { ): ListState<T> {
const storedString = localStorage.getItem(localStorageKey) ?? ""; const storedString = localStorage.getItem(localStorageKey) ?? "";
try { try {
const array = JSON.parse(storedString); const array = JSON.parse(storedString);
if (!Array.isArray(array)) throw ""; if (!Array.isArray(array)) throw "";
initialItems = array; initialItems = array;
} catch {} } catch {}
const state = new ListState<T>(initialItems); const state = new ListState<T>(initialItems);
@@ -241,14 +241,14 @@ export function restoreListState<T>(
export function restoreMapState<T>( export function restoreMapState<T>(
localStorageKey: string, localStorageKey: string,
initialItems: [string, any][] = [] initialItems: [string, any][] = [],
): MapState<T> { ): MapState<T> {
const storedString = localStorage.getItem(localStorageKey) ?? ""; const storedString = localStorage.getItem(localStorageKey) ?? "";
try { try {
const array = JSON.parse(storedString); const array = JSON.parse(storedString);
if (!Array.isArray(array)) throw ""; if (!Array.isArray(array)) throw "";
initialItems = array; initialItems = array;
} catch {} } catch {}
const state = new MapState<T>(initialItems); const state = new MapState<T>(initialItems);
@@ -271,115 +271,115 @@ export function createElement(
const element = document.createElement(tagName); const element = document.createElement(tagName);
if (attributes != null) if (attributes != null)
Object.entries(attributes).forEach((entry) => { Object.entries(attributes).forEach((entry) => {
const [attributename, value] = entry; const [attributename, value] = entry;
const [directiveKey, directiveValue] = attributename.split(":"); const [directiveKey, directiveValue] = attributename.split(":");
switch (directiveKey) { switch (directiveKey) {
case "on": { case "on": {
switch (directiveValue) { switch (directiveValue) {
case "enter": { case "enter": {
element.addEventListener("keydown", (e: Event) => { element.addEventListener("keydown", (e: Event) => {
if (!(e instanceof KeyboardEvent)) return; if (!(e instanceof KeyboardEvent)) return;
if (e.key != "Enter") return; if (e.key != "Enter") return;
value(e); value(e);
}); });
break; break;
} }
default: { default: {
element.addEventListener(directiveValue, value); element.addEventListener(directiveValue, value);
} }
} }
break; break;
} }
case "keystroke": { case "keystroke": {
element.addEventListener("keydown", (e: Event) => { element.addEventListener("keydown", (e: Event) => {
if (!(e instanceof KeyboardEvent)) return; if (!(e instanceof KeyboardEvent)) return;
if (e.metaKey == false && e.ctrlKey == false) return; if (e.metaKey == false && e.ctrlKey == false) return;
if (e.key != directiveValue) return; if (e.key != directiveValue) return;
value(e); value(e);
}) });
break; break;
} }
case "subscribe": { case "subscribe": {
const state = value as State<any>; const state = value as State<any>;
state.subscribe( state.subscribe(
(newValue) => (element[directiveValue] = newValue) (newValue) => (element[directiveValue] = newValue),
); );
break; break;
} }
case "bind": { case "bind": {
const state = value as State<any>; const state = value as State<any>;
state.subscribe( state.subscribe(
(newValue) => (element[directiveValue] = newValue) (newValue) => (element[directiveValue] = newValue),
); );
element.addEventListener( element.addEventListener(
"input", "input",
() => (state.value = (element as any)[directiveValue]) () => (state.value = (element as any)[directiveValue]),
); );
break; break;
} }
case "toggle": { case "toggle": {
if (value.subscribe) { if (value.subscribe) {
const state = value as State<any>; const state = value as State<any>;
state.subscribe((newValue) => state.subscribe((newValue) =>
element.toggleAttribute(directiveValue, newValue) element.toggleAttribute(directiveValue, newValue),
); );
} else { } else {
element.toggleAttribute(directiveValue, value); element.toggleAttribute(directiveValue, value);
} }
break; break;
} }
case "set": { case "set": {
const state = value as State<any>; const state = value as State<any>;
state.subscribe((newValue) => state.subscribe((newValue) =>
element.setAttribute(directiveValue, newValue) element.setAttribute(directiveValue, newValue),
); );
break; break;
} }
case "children": { case "children": {
switch (directiveValue) { switch (directiveValue) {
case "set": { case "set": {
const state = value as State<Node | Node[]>; const state = value as State<Node | Node[]>;
state.subscribe((newValue) => { state.subscribe((newValue) => {
element.innerHTML = ""; element.innerHTML = "";
element.append(...[newValue].flat()); element.append(...[newValue].flat());
}); });
break; break;
} }
case "append": case "append":
case "prepend": { case "prepend": {
try { try {
const [listState, toElement] = value as [ const [listState, toElement] = value as [
listState: ListState<any>, listState: ListState<any>,
StateItemConverter<any> StateItemConverter<any>,
]; ];
listState.handleAddition((newItem) => { listState.handleAddition((newItem) => {
const child = toElement(newItem); const child = toElement(newItem);
listState.handleRemoval(newItem, () => listState.handleRemoval(newItem, () =>
child.remove() child.remove(),
); );
if (directiveValue == "append") { if (directiveValue == "append") {
element.append(child); element.append(child);
} else if (directiveValue == "prepend") { } else if (directiveValue == "prepend") {
element.prepend(child); element.prepend(child);
} }
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw `error: cannot process subscribe:children directive. \n Usage: "children:append={[list, converter]}"; you can find a more detailed example in the documentation.`; throw `error: cannot process subscribe:children directive. \n Usage: "children:append={[list, converter]}"; you can find a more detailed example in the documentation.`;
} }
} }
} }
break; break;
} }
default: default:
element.setAttribute(attributename, value); element.setAttribute(attributename, value);
} }
}); });
children.filter((x) => x).forEach((child) => element.append(child)); children.filter((x) => x).forEach((child) => element.append(child));
+1 -2
View File
@@ -1,2 +1 @@
declare module "*.css" { declare module "*.css" {}
}