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
+86 -86
View File
@@ -70,150 +70,150 @@ export default class FileModel {
const didStore: boolean = this.storeFileContent(fileContent);
if (didStore == false) return;
switch (fileContent.type) {
case "board-info":
case "task":
this.boardsAndTasksModel.handleFileContent(fileContent);
break;
case "reaction":
this.chatModel.handleReaction(fileContent);
break;
}
switch (fileContent.type) {
case "board-info":
case "task":
this.boardsAndTasksModel.handleFileContent(fileContent);
break;
case "reaction":
this.chatModel.handleReaction(fileContent);
break;
}
};
// methods
readonly addFileContentAndSend = (
fileContent: FileContent<string>,
fileContent: FileContent<string>,
): void => {
this.handleFileContent(fileContent);
this.chatModel.sendMessage("", undefined, fileContent);
this.handleFileContent(fileContent);
this.chatModel.sendMessage("", undefined, fileContent);
};
// storage
readonly storeFileContent = (fileContent: FileContent<string>): boolean => {
const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId,
fileContent.fileContentId,
);
const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId,
fileContent.fileContentId,
);
// check if fileContent already exists
const existingFileContent: string | null =
this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false;
// check if fileContent already exists
const existingFileContent: string | null =
this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false;
const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
return true;
const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
return true;
};
readonly listFileIds = (): string[] => {
return this.storageModel.list(this.basePath);
return this.storageModel.list(this.basePath);
};
readonly listFileContentIds = (fileId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath);
const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath);
};
readonly selectLatestFileContentId = (
fileContentIds: string[],
fileContentIds: string[],
): string | undefined => {
return fileContentIds[fileContentIds.length - 1];
return fileContentIds[fileContentIds.length - 1];
};
readonly getFileContent = <T extends FileContent<string>>(
fileId: string,
fileContentName: string,
reference: T,
fileId: string,
fileContentName: string,
reference: T,
): T | null => {
const filePath: string[] = this.getFileContentPath(
fileId,
fileContentName,
);
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath,
reference,
);
return fileContentOrNull;
const filePath: string[] = this.getFileContentPath(
fileId,
fileContentName,
);
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath,
reference,
);
return fileContentOrNull;
};
readonly getLatestFileContent = <T extends FileContent<string>>(
fileId: string,
reference: T,
fileId: string,
reference: T,
): T | null => {
const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null;
const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null;
const fileContent: T | null = this.getFileContent(
fileId,
latestFileContentId,
reference,
);
return fileContent;
const fileContent: T | null = this.getFileContent(
fileId,
latestFileContentId,
reference,
);
return fileContent;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
) {
this.chatModel = chatModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.chatModel = chatModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel,
this.settingsModel,
chatModel,
this,
);
this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel,
this.settingsModel,
chatModel,
this,
);
}
// utility
static generateFileContentId = (creationDate: string): string => {
return creationDate + v4();
return creationDate + v4();
};
static createFileContent = <T extends string>(
fileId: string,
type: T,
fileId: string,
type: T,
): FileContent<T> => {
const creationDate: string = createTimestamp();
const fileContentId: string =
FileModel.generateFileContentId(creationDate);
const creationDate: string = createTimestamp();
const fileContentId: string =
FileModel.generateFileContentId(creationDate);
return {
dataVersion: DATA_VERSION,
return {
dataVersion: DATA_VERSION,
fileId,
fileContentId,
creationDate,
type,
};
fileId,
fileContentId,
creationDate,
type,
};
};
}
// paths
export enum FileModelSubPath {
Data = "data",
Model = "model",
ModelView = "view",
ModelTask = "tasks",
ModelCalendar = "calendar",
Model = "model",
ModelView = "view",
ModelTask = "tasks",
ModelCalendar = "calendar",
}
// types
export interface FileContent<T extends string>
extends ValidObject, StringEntryObject {
readonly fileId: string;
readonly fileContentId: string;
readonly creationDate: string;
readonly fileId: string;
readonly fileContentId: string;
readonly creationDate: string;
readonly type: T;
}
readonly type: T;
}
// references
export const FileContentReference: FileContent<string> = {
+3 -4
View File
@@ -37,7 +37,7 @@ export default class FileTransferModel {
};
readonly prepareToSend = (): void => {
if (!this.transferData) return;
if (!this.transferData) return;
this.direction = TransferDirections.Send;
this.connectionModel.addChannel(this.transferData.channel);
};
@@ -157,8 +157,7 @@ export default class FileTransferModel {
};
readonly prepareFileForSending = (filePath: string[]): string => {
const fileContent: string | null =
this.storageModel.read(filePath);
const fileContent: string | null = this.storageModel.read(filePath);
if (fileContent == null) return "";
const fileData: FileData = {
@@ -169,7 +168,7 @@ export default class FileTransferModel {
};
readonly sendFile = async (stringifiedFileData: string): Promise<void> => {
if (!this.transferData) return;
if (!this.transferData) return;
const encryptedFileData: string = await encryptString(
stringifiedFileData,
+1 -1
View File
@@ -46,7 +46,7 @@ export default class SettingsModel {
// load
private readonly readSetting = (
pathName: keyof typeof filePaths.settingsModel,
): string|null=> {
): string | null => {
const path = StorageModel.getPath(
StorageModelSubPaths.SettingsModel,
filePaths.settingsModel[pathName],
Binary file not shown.
+171 -149
View File
@@ -1,5 +1,5 @@
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
import * as Hammer from "hammerjs";
import * as React from "../../react";
export interface Stringifiable {
toString(): string;
@@ -18,7 +18,7 @@ export function createTimestamp(): string {
return new Date().toISOString();
}
// filters etc
// filters
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
export function filterObjectsByStringEntries<T>(
@@ -28,7 +28,7 @@ export function filterObjectsByStringEntries<T>(
): Set<T> {
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object),
@@ -39,79 +39,6 @@ export function filterObjectsByStringEntries<T>(
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>(
query: string,
getStringsOfObject: (object: T) => string[],
@@ -119,7 +46,7 @@ export function filterObjectsByWords<T>(
): Set<T> {
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
@@ -170,6 +97,26 @@ export function checkDoesObjectMatchSearch<T>(
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
export type Handler<T> = (item: T) => void;
@@ -313,99 +260,174 @@ export function implementPinchZoom(canvas: HTMLElement) {
const MIN = 0.25;
const MAX = 5;
const element = (): HTMLElement|null=> canvas.querySelector(".zoom");
const element = (): HTMLElement | null => canvas.querySelector(".zoom");
const distance = (e: TouchEvent) =>
Math.hypot(
e.touches[0].pageX - e.touches[1].pageX,
e.touches[0].pageY - e.touches[1].pageY,
);
Math.hypot(
e.touches[0].pageX - e.touches[1].pageX,
e.touches[0].pageY - e.touches[1].pageY,
);
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") =>
(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]) {
const el = element();
if (!el) return;
if (factor < MIN) return apply(MIN, _offset);
if (factor > MAX) return apply(5, _offset);
const el = element();
if (!el) return;
if (factor < MIN) return apply(MIN, _offset);
if (factor > MAX) return apply(5, _offset);
let x: number, y: number;
if (_offset) [x, y] = _offset;
else {
x = currentX
y = currentY
}
el.style.transform = `scale(${factor.toString()}) translate(${x}px, ${y}px)`;
currentX = x;
currentY = y;
currentZoom = factor;
let x: number, y: number;
if (_offset) [x, y] = _offset;
else {
x = currentX;
y = currentY;
}
el.style.transform = `scale(${factor.toString()}) translate(${x}px, ${y}px)`;
currentX = x;
currentY = y;
currentZoom = factor;
}
canvas.addEventListener("wheel", (event: WheelEvent) => {
event.preventDefault();
event.preventDefault();
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
if (!event.shiftKey) {
apply(currentZoom, [
currentX - event.deltaX,
currentY - event.deltaY,
]);
return;
}
apply(currentZoom - event.deltaY * 0.01);
if (!event.shiftKey) {
apply(currentZoom, [
currentX - event.deltaX,
currentY - event.deltaY,
]);
return;
}
apply(currentZoom - event.deltaY * 0.01);
});
canvas.addEventListener("scroll", (event: Event) => {
event.preventDefault();
event.preventDefault();
});
canvas.addEventListener("touchstart", (event: TouchEvent) => {
(document.activeElement as HTMLElement).blur();
(document.activeElement as HTMLElement).blur();
const el = element();
if (!el) return;
if (lastElement != undefined && lastElement != el) reset();
lastElement = el;
const el = element();
if (!el) return;
if (lastElement != undefined && lastElement != el) reset();
lastElement = el;
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
if (event.touches.length != 2) {
pinching = false;
initialTouchX = event.touches[0].clientX;
initialTouchY = event.touches[0].clientY;
return;
}
if (event.touches.length != 2) {
pinching = false;
initialTouchX = event.touches[0].clientX;
initialTouchY = event.touches[0].clientY;
return;
}
pinching = true;
initialDistance = distance(event);
initialTouchX = midpoint(event, "x");
initialTouchY = midpoint(event, "y");
pinching = true;
initialDistance = distance(event);
initialTouchX = midpoint(event, "x");
initialTouchY = midpoint(event, "y");
});
canvas.addEventListener("touchmove", (event: TouchEvent) => {
if (!pinching) {
apply(currentZoom, [
initialX +
(event.touches[0].clientX - initialTouchX) / initialZoom,
initialY +
(event.touches[0].clientY - initialTouchY) / initialZoom,
]);
return;
}
if (event.touches.length < 2) return;
event.preventDefault();
const currentDistance = distance(event);
const midX = midpoint(event, "x");
const midY = midpoint(event, "y");
const ratio = currentDistance / initialDistance;
const difference = currentDistance - initialDistance;
apply(initialZoom * ratio, [
initialX + (midX - difference - initialTouchX) / initialZoom,
initialY + (midY - difference - initialTouchY) / initialZoom,
]);
if (!pinching) {
apply(currentZoom, [
initialX +
(event.touches[0].clientX - initialTouchX) / initialZoom,
initialY +
(event.touches[0].clientY - initialTouchY) / initialZoom,
]);
return;
}
if (event.touches.length < 2) return;
event.preventDefault();
const currentDistance = distance(event);
const midX = midpoint(event, "x");
const midY = midpoint(event, "y");
const ratio = currentDistance / initialDistance;
const difference = currentDistance - initialDistance;
apply(initialZoom * ratio, [
initialX + (midX - difference - initialTouchX) / 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;
}