add progress bar to data transfer

This commit is contained in:
marlon-erler
2026-09-30 18:57:09 +02:00
parent c78c5747cf
commit 397a5f76e4
25 changed files with 1125 additions and 1048 deletions
+67 -57
View File
@@ -436,9 +436,8 @@
sendButton: "Send", sendButton: "Send",
sendAgainButton: "Send again", sendAgainButton: "Send again",
/// ///
filesSentCount: (count) => `Files sent: ${count}.`, filesSentProgress: (progress, total) => `Sent ${progress} of ${total} files`,
allFilesSent: "Done.", filesReceivedProgress: (progress, total) => `Received ${progress} of ${total} files`,
filesReceivedCount: (count) => `Files processed: ${count}.`,
// //
exportKey: "Encryption key", exportKey: "Encryption key",
exportKeyConfirmation: "Confirm", exportKeyConfirmation: "Confirm",
@@ -702,9 +701,8 @@
transferKeyHeadline: "Schl\xFCssel", transferKeyHeadline: "Schl\xFCssel",
sendButton: "Senden", sendButton: "Senden",
sendAgainButton: "Erneut senden", sendAgainButton: "Erneut senden",
filesSentCount: (count) => `Dateien gesendet: ${count}.`, filesSentProgress: (progress, total) => `${progress} von ${total} Dateien gesendet`,
allFilesSent: "Fertig.", filesReceivedProgress: (progress, total) => `${progress} von ${total} Dateien empfangen`,
filesReceivedCount: (count) => `Dateien empfangen: ${count}.`,
exportKey: "Schl\xFCssel", exportKey: "Schl\xFCssel",
exportKeyConfirmation: "Schl\xFCssel best\xE4tigen", exportKeyConfirmation: "Schl\xFCssel best\xE4tigen",
downloadFileButton: "Herunterladen", downloadFileButton: "Herunterladen",
@@ -954,9 +952,8 @@
transferKeyHeadline: "Clave de encriptaci\xF3n de transferencia", transferKeyHeadline: "Clave de encriptaci\xF3n de transferencia",
sendButton: "Enviar", sendButton: "Enviar",
sendAgainButton: "Enviar otra vez", sendAgainButton: "Enviar otra vez",
filesSentCount: (count) => `Archivos enviados: ${count}.`, filesSentProgress: (progress, total) => `${progress} de ${total} enviados`,
allFilesSent: "Hecho.", filesReceivedProgress: (progress, total) => `${progress} de ${total} recibidos`,
filesReceivedCount: (count) => `Archivos recibidos: ${count}.`,
exportKey: "Clave de encriptaci\xF3n", exportKey: "Clave de encriptaci\xF3n",
exportKeyConfirmation: "Confirmar clave", exportKeyConfirmation: "Confirmar clave",
downloadFileButton: "Descargar", downloadFileButton: "Descargar",
@@ -2290,27 +2287,13 @@
this.receivingTransferChannel = new State(""); this.receivingTransferChannel = new State("");
this.receivingTransferKey = new State(""); this.receivingTransferKey = new State("");
this.filePathsSent = new ListState(); this.filePathsSent = new ListState();
this.filesSentCount = createProxyState( this.fileTransferTotal = new State(0);
[this.filePathsSent], this.fileTransferProgress = new State(0);
() => this.filePathsSent.value.size
);
this.filesSentText = createProxyState(
[this.filesSentCount],
() => this.coreViewModel.translations.dataTransferModal.filesSentCount(
this.filesSentCount.value
)
);
this.filePathsReceived = new ListState(); this.filePathsReceived = new ListState();
this.filesReceivedCount = createProxyState( this.filesReceivedCount = createProxyState(
[this.filePathsReceived], [this.filePathsReceived],
() => this.filePathsReceived.value.size () => this.filePathsReceived.value.size
); );
this.filesReceivedText = createProxyState(
[this.filesReceivedCount],
() => this.coreViewModel.translations.dataTransferModal.filesReceivedCount(
this.filesReceivedCount.value
)
);
this.exportKey = new State(""); this.exportKey = new State("");
this.exportKeyConfirmation = new State(""); this.exportKeyConfirmation = new State("");
this.importedFile = new State(void 0); this.importedFile = new State(void 0);
@@ -2322,7 +2305,6 @@
[this.selectedPaths], [this.selectedPaths],
() => this.selectedPaths.value.size == 0 () => this.selectedPaths.value.size == 0
); );
this.didNotFinishSending = new State(true);
this.cannotPrepareToReceive = createProxyState( this.cannotPrepareToReceive = createProxyState(
[this.receivingTransferChannel, this.receivingTransferKey], [this.receivingTransferChannel, this.receivingTransferKey],
() => this.receivingTransferChannel.value == "" || this.receivingTransferKey.value == "" () => this.receivingTransferChannel.value == "" || this.receivingTransferKey.value == ""
@@ -2346,6 +2328,7 @@
// handlers // handlers
this.handleReceivedFile = (path) => { this.handleReceivedFile = (path) => {
this.filePathsReceived.add(path); this.filePathsReceived.add(path);
this.fileTransferProgress.value = this.filePathsReceived.value.size;
}; };
// methods // methods
this.getOptions = () => { this.getOptions = () => {
@@ -2454,7 +2437,6 @@
}; };
this.initiateTransfer = () => { this.initiateTransfer = () => {
this.presentedModal.value = 3 /* TransferDisplay */; this.presentedModal.value = 3 /* TransferDisplay */;
this.didNotFinishSending.value = true;
this.filePathsSent.clear(); this.filePathsSent.clear();
this.coreViewModel.fileTransferModel.sendFiles( this.coreViewModel.fileTransferModel.sendFiles(
this.selectedPaths.value.values(), this.selectedPaths.value.values(),
@@ -2462,7 +2444,6 @@
this.filePathsSent.add(path); this.filePathsSent.add(path);
} }
); );
this.didNotFinishSending.value = false;
}; };
this.showTransferDataInputModal = () => { this.showTransferDataInputModal = () => {
this.presentedModal.value = 4 /* TransferDataInput */; this.presentedModal.value = 4 /* TransferDataInput */;
@@ -2496,6 +2477,10 @@
this.handleContextClose = () => { this.handleContextClose = () => {
this.presentedModal.value = void 0; this.presentedModal.value = void 0;
}; };
this.coreViewModel.fileTransferModel.transferSizeHandlerManager.setHandler(
"file-transfer-view-model",
(count) => this.fileTransferTotal.value = count
);
this.coreViewModel.fileTransferModel.fileHandlerManager.setHandler( this.coreViewModel.fileTransferModel.fileHandlerManager.setHandler(
"file-transfer-view-model", "file-transfer-view-model",
this.handleReceivedFile this.handleReceivedFile
@@ -4742,7 +4727,6 @@
}; };
this.randomizeKey = () => { this.randomizeKey = () => {
this.encryptionKeyInput.value = random(24); this.encryptionKeyInput.value = random(24);
this.setEncryptionKey();
}; };
this.applyColor = () => { this.applyColor = () => {
this.chatViewModel.setColor(this.color.value); this.chatViewModel.setColor(this.color.value);
@@ -8165,6 +8149,21 @@
return /* @__PURE__ */ createElement("span", { class: "ellipsis" }, string); return /* @__PURE__ */ createElement("span", { class: "ellipsis" }, string);
}; };
// src/View/Components/progressBar.tsx
function ProgressBar(valueLabel, value, maximum) {
const style = createProxyState(
[value, maximum],
() => `width: ${100 * (value.value / maximum.value)}%`
);
return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "width-100 surface-alt", style: "height: 18px" }, /* @__PURE__ */ createElement(
"div",
{
class: "height-100 background-primary",
"set:style": style
}
)), /* @__PURE__ */ createElement("span", { class: "secondary", "subscribe:innerText": valueLabel }));
}
// src/View/Modals/dataTransferModal.tsx // src/View/Modals/dataTransferModal.tsx
function DataTransferModalWrapper(coreViewModel2, connectionViewModel2, fileTransferViewModel2) { function DataTransferModalWrapper(coreViewModel2, connectionViewModel2, fileTransferViewModel2) {
return /* @__PURE__ */ createElement("div", null, DirectionSelectionModal( return /* @__PURE__ */ createElement("div", null, DirectionSelectionModal(
@@ -8320,20 +8319,8 @@
[fileTransferViewModel2.presentedModal], [fileTransferViewModel2.presentedModal],
() => fileTransferViewModel2.presentedModal.value == 3 /* TransferDisplay */ () => fileTransferViewModel2.presentedModal.value == 3 /* TransferDisplay */
); );
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.dataTransferModal.transferDataHeadline), /* @__PURE__ */ createElement( const progressLabel = createProxyState([fileTransferViewModel2.fileTransferProgress, fileTransferViewModel2.fileTransferTotal], () => coreViewModel2.translations.dataTransferModal.filesSentProgress(fileTransferViewModel2.fileTransferProgress.value, fileTransferViewModel2.fileTransferTotal.value));
"p", return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.dataTransferModal.transferDataHeadline), ProgressBar(progressLabel, fileTransferViewModel2.fileTransferProgress, fileTransferViewModel2.fileTransferTotal), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap content-margin-bottom" }, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", { class: "secondary" }, coreViewModel2.translations.dataTransferModal.transferChannelHeadline), /* @__PURE__ */ createElement(
{
class: "secondary",
"subscribe:innerText": fileTransferViewModel2.filesSentText
}
), /* @__PURE__ */ createElement(
"p",
{
class: "secondary",
"toggle:hidden": fileTransferViewModel2.didNotFinishSending
},
coreViewModel2.translations.dataTransferModal.allFilesSent
), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap content-margin-bottom" }, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", { class: "secondary" }, coreViewModel2.translations.dataTransferModal.transferChannelHeadline), /* @__PURE__ */ createElement(
"b", "b",
{ {
"subscribe:innerText": fileTransferViewModel2.transferChannel "subscribe:innerText": fileTransferViewModel2.transferChannel
@@ -8364,8 +8351,7 @@
"button", "button",
{ {
class: "flex", class: "flex",
"on:click": fileTransferViewModel2.close, "on:click": fileTransferViewModel2.close
"toggle:disabled": fileTransferViewModel2.didNotFinishSending
}, },
coreViewModel2.translations.general.closeButton, coreViewModel2.translations.general.closeButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "close") /* @__PURE__ */ createElement("span", { class: "icon" }, "close")
@@ -8413,13 +8399,8 @@
[fileTransferViewModel2.presentedModal], [fileTransferViewModel2.presentedModal],
() => fileTransferViewModel2.presentedModal.value == 5 /* ReceptionDisplay */ () => fileTransferViewModel2.presentedModal.value == 5 /* ReceptionDisplay */
); );
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.dataTransferModal.receiveHeadline), /* @__PURE__ */ createElement( const progressLabel = createProxyState([fileTransferViewModel2.fileTransferProgress, fileTransferViewModel2.fileTransferTotal], () => coreViewModel2.translations.dataTransferModal.filesReceivedProgress(fileTransferViewModel2.fileTransferProgress.value, fileTransferViewModel2.fileTransferTotal.value));
"p", return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.dataTransferModal.receiveHeadline), ProgressBar(progressLabel, fileTransferViewModel2.fileTransferProgress, fileTransferViewModel2.fileTransferTotal), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
{
class: "secondary",
"subscribe:innerText": fileTransferViewModel2.filesReceivedText
}
), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
"div", "div",
{ {
class: "tile flex-column align-start", class: "tile flex-column align-start",
@@ -8641,6 +8622,7 @@
this.direction = 0 /* Send */; this.direction = 0 /* Send */;
// handler managers // handler managers
this.fileHandlerManager = new HandlerManager(); this.fileHandlerManager = new HandlerManager();
this.transferSizeHandlerManager = new HandlerManager();
this.readyToSendHandlerManager = new HandlerManager(); this.readyToSendHandlerManager = new HandlerManager();
// general // general
this.generateTransferData = () => { this.generateTransferData = () => {
@@ -8700,6 +8682,11 @@
}; };
this.handleDecryptedFile = (data) => { this.handleDecryptedFile = (data) => {
const parsed = parse(data); const parsed = parse(data);
if (!parsed.type) return;
if (parsed.type == "transfer-size" && typeof parsed.count == "number") {
this.transferSizeHandlerManager.trigger(parsed.count);
return;
}
const isFileData = checkMatchesObjectStructure( const isFileData = checkMatchesObjectStructure(
parsed, parsed,
FileDataReference FileDataReference
@@ -8714,16 +8701,22 @@
}; };
// sending // sending
this.sendFiles = (directoryPaths, fileCallback) => { this.sendFiles = (directoryPaths, fileCallback) => {
const filesToSend = /* @__PURE__ */ new Set();
for (const directoryPath of directoryPaths) { for (const directoryPath of directoryPaths) {
this.storageModel.recurse(directoryPath, (filePath) => { this.storageModel.recurse(directoryPath, (filePath) => {
const stringifiedFileData = this.prepareFileForSending(filePath); const stringifiedFileData = this.prepareFileForSending(filePath);
this.sendFile(stringifiedFileData); filesToSend.add([stringifiedFileData, filePath]);
const pathString = StorageModel.pathComponentsToString(
...filePath
);
fileCallback(pathString);
}); });
} }
this.sendTransferSize(filesToSend.size);
for (const file of filesToSend) {
const [stringifiedFileData, filePath] = file;
this.sendFile(stringifiedFileData);
const pathString = StorageModel.pathComponentsToString(
...filePath
);
fileCallback(pathString);
}
}; };
this.generateBackup = async (directoryPaths, passphrase) => { this.generateBackup = async (directoryPaths, passphrase) => {
const files = []; const files = [];
@@ -8739,10 +8732,26 @@
const blob = new Blob([encrypted], { type: "text/plain" }); const blob = new Blob([encrypted], { type: "text/plain" });
return blob; return blob;
}; };
this.sendTransferSize = async (count) => {
if (!this.transferData) return;
const data = {
type: "transfer-size",
count
};
const encryptedData = await encryptString(
stringify(data),
this.transferData.key
);
this.connectionModel.sendPlainMessage(
this.transferData.channel,
encryptedData
);
};
this.prepareFileForSending = (filePath) => { this.prepareFileForSending = (filePath) => {
const fileContent = this.storageModel.read(filePath); const fileContent = this.storageModel.read(filePath);
if (fileContent == null) return ""; if (fileContent == null) return "";
const fileData = { const fileData = {
type: "file-data",
path: filePath, path: filePath,
body: fileContent body: fileContent
}; };
@@ -8771,6 +8780,7 @@
} }
}; };
var FileDataReference = { var FileDataReference = {
type: "file-data",
path: [""], path: [""],
body: "" body: ""
}; };
Vendored
+1 -1
View File
@@ -1,4 +1,4 @@
const VERSION = "2610 Beta 21.12" const VERSION = "2610 Beta 21.13"
function processUrl(event) { function processUrl(event) {
const url = event.request.url; const url = event.request.url;
+1 -1
View File
@@ -1 +1 @@
2610 Beta 21.12 2610 Beta 21.13
+44 -5
View File
@@ -24,6 +24,7 @@ export default class FileTransferModel {
// handler managers // handler managers
readonly fileHandlerManager = new HandlerManager<string>(); readonly fileHandlerManager = new HandlerManager<string>();
readonly transferSizeHandlerManager = new HandlerManager<number>();
readonly readyToSendHandlerManager = new HandlerManager<boolean>(); readonly readyToSendHandlerManager = new HandlerManager<boolean>();
// general // general
@@ -105,6 +106,12 @@ export default class FileTransferModel {
readonly handleDecryptedFile = (data: string): void => { readonly handleDecryptedFile = (data: string): void => {
const parsed: any = parse(data); const parsed: any = parse(data);
if (!parsed.type) return;
if (parsed.type == "transfer-size" && typeof parsed.count == "number") {
this.transferSizeHandlerManager.trigger(parsed.count);
return;
}
const isFileData: boolean = checkMatchesObjectStructure( const isFileData: boolean = checkMatchesObjectStructure(
parsed, parsed,
FileDataReference, FileDataReference,
@@ -126,17 +133,23 @@ export default class FileTransferModel {
directoryPaths: IteratorObject<string[]>, directoryPaths: IteratorObject<string[]>,
fileCallback: (path: string) => void, fileCallback: (path: string) => void,
): void => { ): void => {
const filesToSend = new Set<[string, string[]]>();
for (const directoryPath of directoryPaths) { for (const directoryPath of directoryPaths) {
this.storageModel.recurse(directoryPath, (filePath: string[]) => { this.storageModel.recurse(directoryPath, (filePath: string[]) => {
const stringifiedFileData = const stringifiedFileData =
this.prepareFileForSending(filePath); this.prepareFileForSending(filePath);
this.sendFile(stringifiedFileData); filesToSend.add([stringifiedFileData, filePath]);
const pathString: string = StorageModel.pathComponentsToString(
...filePath,
);
fileCallback(pathString);
}); });
} }
this.sendTransferSize(filesToSend.size);
for (const file of filesToSend) {
const [stringifiedFileData, filePath] = file;
this.sendFile(stringifiedFileData);
const pathString: string = StorageModel.pathComponentsToString(
...filePath,
);
fileCallback(pathString);
}
}; };
readonly generateBackup = async ( readonly generateBackup = async (
@@ -158,11 +171,30 @@ export default class FileTransferModel {
return blob; return blob;
}; };
readonly sendTransferSize = async (count: number): Promise<void> => {
if (!this.transferData) return;
const data: TransferSize = {
type: "transfer-size",
count,
};
const encryptedData: string = await encryptString(
stringify(data),
this.transferData.key,
);
this.connectionModel.sendPlainMessage(
this.transferData.channel,
encryptedData,
);
};
readonly prepareFileForSending = (filePath: string[]): string => { 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 ""; if (fileContent == null) return "";
const fileData: FileData = { const fileData: FileData = {
type: "file-data",
path: filePath, path: filePath,
body: fileContent, body: fileContent,
}; };
@@ -200,11 +232,18 @@ export interface TransferData {
} }
export interface FileData { export interface FileData {
type: "file-data";
path: string[]; path: string[];
body: string; body: string;
} }
export interface TransferSize {
type: "transfer-size";
count: number;
}
export const FileDataReference: FileData = { export const FileDataReference: FileData = {
type: "file-data",
path: [""], path: [""],
body: "", body: "",
}; };
+2 -2
View File
@@ -5,10 +5,10 @@ const ENCRYPTION_ALG = "AES-GCM";
// random // random
export function random(length: number): string { export function random(length: number): string {
return cryptoRandomString({length}) return cryptoRandomString({ length });
} }
// encrypt // encrypt
export async function encryptString( export async function encryptString(
plaintext: string, plaintext: string,
passphrase: string, passphrase: string,
+6 -5
View File
@@ -140,11 +140,12 @@ export function SettingsPage(
<div class="flex-row width-input"> <div class="flex-row width-input">
<button <button
class="width-50" class="width-50"
on:click={ on:click={settingsPageViewModel.randomizeKey}
settingsPageViewModel.randomizeKey
}
> >
{coreViewModel.translations.chatPage.settings.randomizeKey} {
coreViewModel.translations.chatPage.settings
.randomizeKey
}
<span class="icon">shuffle</span> <span class="icon">shuffle</span>
</button> </button>
<button <button
@@ -176,7 +177,7 @@ export function SettingsPage(
coreViewModel.translations.chatPage.settings coreViewModel.translations.chatPage.settings
.showEncryptionKey .showEncryptionKey
} }
</label> </label>
<hr></hr> <hr></hr>
+4 -4
View File
@@ -65,10 +65,10 @@ export function TaskPage(
class="no-outline" class="no-outline"
bind:value={taskPageViewModel.boardQuery} bind:value={taskPageViewModel.boardQuery}
on:enter={taskPageViewModel.createBoard} on:enter={taskPageViewModel.createBoard}
placeholder={ placeholder={coreViewModel.translations.general.filterOrCreateLabel(
coreViewModel.translations.general coreViewModel.translations.chatPage.task
.filterOrCreateLabel(coreViewModel.translations.chatPage.task.typeBoard) .typeBoard,
} )}
></input> ></input>
</div> </div>
</div> </div>
@@ -12,8 +12,8 @@ export function BoardViewToggleButton(
key: string, key: string,
) { ) {
function select() { function select() {
boardViewModel.selectedPage.value = page; boardViewModel.selectedPage.value = page;
boardViewModel.resetPinchZoom(); boardViewModel.resetPinchZoom();
} }
const isSelected = React.createProxyState( const isSelected = React.createProxyState(
+24
View File
@@ -0,0 +1,24 @@
import * as React from "../../react";
export function ProgressBar(
valueLabel: React.State<string>,
value: React.State<number>,
maximum: React.State<number>,
) {
const style = React.createProxyState(
[value, maximum],
() => `width: ${100 * (value.value / maximum.value)}%`,
);
return (
<div>
<div class="width-100 surface-alt" style="height: 18px">
<div
class="height-100 background-primary"
set:style={style}
></div>
</div>
<span class="secondary" subscribe:innerText={valueLabel}></span>
</div>
);
}
+36 -31
View File
@@ -1,6 +1,7 @@
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { CommonKeys, getKeySymbol } from "../keystrokes"; import { CommonKeys, getKeySymbol } from "../keystrokes";
import { StringToTextSpan } from "../Components/textSpan"; import { StringToTextSpan } from "../Components/textSpan";
import { ProgressBar } from "../Components/progressBar";
import * as React from "../../react"; import * as React from "../../react";
import FileTransferViewModel, { import FileTransferViewModel, {
FileTransferModals, FileTransferModals,
@@ -370,6 +371,18 @@ function TransferDisplayModal(
FileTransferModals.TransferDisplay, FileTransferModals.TransferDisplay,
); );
const progressLabel = React.createProxyState(
[
fileTransferViewModel.fileTransferProgress,
fileTransferViewModel.fileTransferTotal,
],
() =>
coreViewModel.translations.dataTransferModal.filesSentProgress(
fileTransferViewModel.fileTransferProgress.value,
fileTransferViewModel.fileTransferTotal.value,
),
);
return ( return (
<div class="modal" toggle:open={isPresented}> <div class="modal" toggle:open={isPresented}>
<div> <div>
@@ -381,24 +394,11 @@ function TransferDisplayModal(
} }
</h2> </h2>
<p {ProgressBar(
class="secondary" progressLabel,
subscribe:innerText={ fileTransferViewModel.fileTransferProgress,
fileTransferViewModel.filesSentText fileTransferViewModel.fileTransferTotal,
} )}
></p>
<p
class="secondary"
toggle:hidden={
fileTransferViewModel.didNotFinishSending
}
>
{
coreViewModel.translations.dataTransferModal
.allFilesSent
}
</p>
<hr></hr> <hr></hr>
@@ -460,13 +460,7 @@ function TransferDisplayModal(
} }
<span class="icon">restart_alt</span> <span class="icon">restart_alt</span>
</button> </button>
<button <button class="flex" on:click={fileTransferViewModel.close}>
class="flex"
on:click={fileTransferViewModel.close}
toggle:disabled={
fileTransferViewModel.didNotFinishSending
}
>
{coreViewModel.translations.general.closeButton} {coreViewModel.translations.general.closeButton}
<span class="icon">close</span> <span class="icon">close</span>
</button> </button>
@@ -588,6 +582,18 @@ function DataReceptionModal(
FileTransferModals.ReceptionDisplay, FileTransferModals.ReceptionDisplay,
); );
const progressLabel = React.createProxyState(
[
fileTransferViewModel.fileTransferProgress,
fileTransferViewModel.fileTransferTotal,
],
() =>
coreViewModel.translations.dataTransferModal.filesReceivedProgress(
fileTransferViewModel.fileTransferProgress.value,
fileTransferViewModel.fileTransferTotal.value,
),
);
return ( return (
<div class="modal" toggle:open={isPresented}> <div class="modal" toggle:open={isPresented}>
<div> <div>
@@ -599,12 +605,11 @@ function DataReceptionModal(
} }
</h2> </h2>
<p {ProgressBar(
class="secondary" progressLabel,
subscribe:innerText={ fileTransferViewModel.fileTransferProgress,
fileTransferViewModel.filesReceivedText fileTransferViewModel.fileTransferTotal,
} )}
></p>
<hr></hr> <hr></hr>
+4 -4
View File
@@ -50,7 +50,7 @@
@media (min-width: 640px) { @media (min-width: 640px) {
#home-page > div { #home-page > div {
gap: var(--content-padding); gap: var(--content-padding);
} }
#home-page #overview-section { #home-page #overview-section {
@@ -66,11 +66,11 @@
@media (min-width: 1000px) { @media (min-width: 1000px) {
#home-page > div { #home-page > div {
gap: var(--large-gap); gap: var(--large-gap);
} }
#home-page > h1 { #home-page > h1 {
padding-top: 3rem; padding-top: 3rem;
padding-bottom: 1rem; padding-bottom: 1rem;
} }
} }
+30 -21
View File
@@ -1,5 +1,6 @@
import "./homePage.css"; import "./homePage.css";
import { StringToOption } from "./Components/option"; import { StringToOption } from "./Components/option";
import { NewItemEntry } from "./Components/newItemEntry";
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 * as React from "../react";
@@ -9,7 +10,6 @@ import FileTransferViewModel from "../ViewModel/Global/fileTransferViewModel";
import CoreViewModel from "../ViewModel/Global/coreViewModel"; import CoreViewModel from "../ViewModel/Global/coreViewModel";
import ConnectionViewModel from "../ViewModel/Global/connectionViewModel"; import ConnectionViewModel from "../ViewModel/Global/connectionViewModel";
import ChatListViewModel from "../ViewModel/Chat/chatListViewModel"; import ChatListViewModel from "../ViewModel/Chat/chatListViewModel";
import {NewItemEntry} from "./Components/newItemEntry";
export function HomePage( export function HomePage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -19,9 +19,18 @@ export function HomePage(
fileTransferViewModel: FileTransferViewModel, fileTransferViewModel: FileTransferViewModel,
chatListViewModel: ChatListViewModel, chatListViewModel: ChatListViewModel,
) { ) {
const greeting = React.createProxyState([settingsViewModel.username], ()=>coreViewModel.translations.homePage.greeting(settingsViewModel.username.value)); const greeting = React.createProxyState([settingsViewModel.username], () =>
coreViewModel.translations.homePage.greeting(
settingsViewModel.username.value,
),
);
const isChatPlaceholderHidden = React.createProxyState([chatListViewModel.chatMatches, chatListViewModel.chatQuery], ()=>chatListViewModel.chatMatches.value.size > 0 || chatListViewModel.chatQuery.value != ""); const isChatPlaceholderHidden = React.createProxyState(
[chatListViewModel.chatMatches, chatListViewModel.chatQuery],
() =>
chatListViewModel.chatMatches.value.size > 0 ||
chatListViewModel.chatQuery.value != "",
);
// sections // sections
const overviewSection = ( const overviewSection = (
@@ -119,8 +128,8 @@ export function HomePage(
<span subscribe:innerText={coreViewModel.updateText}></span> <span subscribe:innerText={coreViewModel.updateText}></span>
<span class="icon">update</span> <span class="icon">update</span>
</button> </button>
<hr class="mobile-only"></hr> <hr class="mobile-only"></hr>
</div> </div>
); );
@@ -128,25 +137,25 @@ export function HomePage(
<div id="chat-section"> <div id="chat-section">
<div class="flex-row width-input"> <div class="flex-row width-input">
<input <input
placeholder={ placeholder={coreViewModel.translations.general.filterOrCreateLabel(
coreViewModel.translations.general coreViewModel.translations.homePage.typeChat,
.filterOrCreateLabel(coreViewModel.translations.homePage.typeChat) )}
}
bind:value={chatListViewModel.chatQuery} bind:value={chatListViewModel.chatQuery}
on:enter={chatListViewModel.createChat} on:enter={chatListViewModel.createChat}
></input> ></input>
</div> </div>
<hr></hr> <hr></hr>
{NewItemEntry(coreViewModel, chatListViewModel.chatQuery, chatListViewModel.createChat)} {NewItemEntry(
coreViewModel,
chatListViewModel.chatQuery,
chatListViewModel.createChat,
)}
<span <span class="secondary" toggle:hidden={isChatPlaceholderHidden}>
class="secondary" {coreViewModel.translations.homePage.noChatsPlaceholder}
toggle:hidden={isChatPlaceholderHidden} </span>
>
{coreViewModel.translations.homePage.noChatsPlaceholder}
</span>
<div <div
id="chat-grid" id="chat-grid"
@@ -154,14 +163,14 @@ export function HomePage(
chatListViewModel.chatMatches, chatListViewModel.chatMatches,
ChatViewModelToChatEntry, ChatViewModelToChatEntry,
]} ]}
></div> ></div>
</div> </div>
); );
// final // final
return ( return (
<article id="home-page"> <article id="home-page">
<h1 subscribe:innerText={greeting}></h1> <h1 subscribe:innerText={greeting}></h1>
<div> <div>
{overviewSection} {overviewSection}
{chatSection} {chatSection}
+567 -566
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -35,9 +35,7 @@ export default class ChatListViewModel {
// methods // methods
createChat = (): void => { createChat = (): void => {
const chatModel: ChatModel = const chatModel: ChatModel =
this.coreViewModel.chatListModel.createChat( this.coreViewModel.chatListModel.createChat(this.chatQuery.value);
this.chatQuery.value,
);
this.chatQuery.value = ""; this.chatQuery.value = "";
const chatViewModel: ChatViewModel = const chatViewModel: ChatViewModel =
@@ -108,8 +106,13 @@ export default class ChatListViewModel {
); );
this.loadChats(); this.loadChats();
implementFilter(this.chatViewModels, this.chatMatches, this.chatQuery, chat => chat.chatModel.info.name) implementFilter(
this.chatViewModels,
this.chatMatches,
this.chatQuery,
(chat) => chat.chatModel.info.name,
);
} }
// utility // utility
+13 -12
View File
@@ -52,22 +52,23 @@ export default class CoreViewModel {
) )
return; return;
let exitNext: boolean = false; let exitNext: boolean = false;
while (this.contexts.length > 0) { while (this.contexts.length > 0) {
const currentContext: Context | undefined = this.context; const currentContext: Context | undefined = this.context;
if (!currentContext) break; if (!currentContext) break;
const response = currentContext.handleContextClose(fromHistoryEvent); const response =
if (response == false) break; currentContext.handleContextClose(fromHistoryEvent);
if (response == false) break;
this.contextStack.delete(currentContext.contextId); this.contextStack.delete(currentContext.contextId);
if (response == true) { if (response == true) {
exitNext = true; exitNext = true;
continue; continue;
} else if (exitNext == true) { } else if (exitNext == true) {
break break;
} else if (currentContext.contextId == contextId) { } else if (currentContext.contextId == contextId) {
break; break;
} }
} }
}; };
@@ -206,7 +207,7 @@ export class Context {
}; };
close = (): void => {}; close = (): void => {};
handleContextClose = (fromHistoryEvent: boolean): void|boolean=> {}; handleContextClose = (fromHistoryEvent: boolean): void | boolean => {};
registerKeyStroke = (key: string, fn: () => void): void => { registerKeyStroke = (key: string, fn: () => void): void => {
this.keystrokes.set(key, fn); this.keystrokes.set(key, fn);
+9 -23
View File
@@ -11,7 +11,7 @@ import ChatModel from "../../Model/Chat/chatModel";
export default class FileTransferViewModel extends Context { export default class FileTransferViewModel extends Context {
// state // state
exitReception: () => void | undefined = undefined; exitReception: (() => void) | undefined = undefined;
presentedModal: React.State<FileTransferModals | undefined> = presentedModal: React.State<FileTransferModals | undefined> =
new React.State<any>(undefined); new React.State<any>(undefined);
@@ -29,30 +29,14 @@ export default class FileTransferViewModel extends Context {
receivingTransferKey: React.State<string> = new React.State(""); receivingTransferKey: React.State<string> = new React.State("");
filePathsSent: React.ListState<string> = new React.ListState(); filePathsSent: React.ListState<string> = new React.ListState();
filesSentCount: React.State<number> = React.createProxyState(
[this.filePathsSent],
() => this.filePathsSent.value.size,
);
filesSentText: React.State<string> = React.createProxyState(
[this.filesSentCount],
() =>
this.coreViewModel.translations.dataTransferModal.filesSentCount(
this.filesSentCount.value,
),
);
fileTransferTotal: React.State<number> = new React.State(0);
fileTransferProgress: React.State<number> = new React.State(0);
filePathsReceived: React.ListState<string> = new React.ListState(); filePathsReceived: React.ListState<string> = new React.ListState();
filesReceivedCount: React.State<number> = React.createProxyState( filesReceivedCount: React.State<number> = React.createProxyState(
[this.filePathsReceived], [this.filePathsReceived],
() => this.filePathsReceived.value.size, () => this.filePathsReceived.value.size,
); );
filesReceivedText: React.State<string> = React.createProxyState(
[this.filesReceivedCount],
() =>
this.coreViewModel.translations.dataTransferModal.filesReceivedCount(
this.filesReceivedCount.value,
),
);
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("");
@@ -69,7 +53,6 @@ export default class FileTransferViewModel extends Context {
[this.selectedPaths], [this.selectedPaths],
() => this.selectedPaths.value.size == 0, () => this.selectedPaths.value.size == 0,
); );
didNotFinishSending: React.State<boolean> = new React.State(true);
cannotPrepareToReceive: React.State<boolean> = React.createProxyState( cannotPrepareToReceive: React.State<boolean> = React.createProxyState(
[this.receivingTransferChannel, this.receivingTransferKey], [this.receivingTransferChannel, this.receivingTransferKey],
() => () =>
@@ -99,6 +82,7 @@ export default class FileTransferViewModel extends Context {
// handlers // handlers
handleReceivedFile = (path: string): void => { handleReceivedFile = (path: string): void => {
this.filePathsReceived.add(path); this.filePathsReceived.add(path);
this.fileTransferProgress.value = this.filePathsReceived.value.size;
}; };
// methods // methods
@@ -229,7 +213,6 @@ export default class FileTransferViewModel extends Context {
initiateTransfer = (): void => { initiateTransfer = (): void => {
this.presentedModal.value = FileTransferModals.TransferDisplay; this.presentedModal.value = FileTransferModals.TransferDisplay;
this.didNotFinishSending.value = true;
this.filePathsSent.clear(); this.filePathsSent.clear();
this.coreViewModel.fileTransferModel.sendFiles( this.coreViewModel.fileTransferModel.sendFiles(
@@ -238,8 +221,6 @@ export default class FileTransferViewModel extends Context {
this.filePathsSent.add(path); this.filePathsSent.add(path);
}, },
); );
this.didNotFinishSending.value = false;
}; };
showTransferDataInputModal = (): void => { showTransferDataInputModal = (): void => {
@@ -287,6 +268,11 @@ export default class FileTransferViewModel extends Context {
constructor(public readonly coreViewModel: CoreViewModel) { constructor(public readonly coreViewModel: CoreViewModel) {
super("file-transfer"); super("file-transfer");
this.coreViewModel.fileTransferModel.transferSizeHandlerManager.setHandler(
"file-transfer-view-model",
(count) => (this.fileTransferTotal.value = count),
);
this.coreViewModel.fileTransferModel.fileHandlerManager.setHandler( this.coreViewModel.fileTransferModel.fileHandlerManager.setHandler(
"file-transfer-view-model", "file-transfer-view-model",
this.handleReceivedFile, this.handleReceivedFile,
+201 -201
View File
@@ -21,7 +21,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
index: React.State<number> = new React.State(0); index: React.State<number> = new React.State(0);
selectedPage: React.State<BoardPageTypes> = new React.State<any>( selectedPage: React.State<BoardPageTypes> = new React.State<any>(
BoardPageTypes.List, BoardPageTypes.List,
); );
isSelected: React.State<boolean>; isSelected: React.State<boolean>;
@@ -31,339 +31,339 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
searchViewModel: SearchViewModel<TaskViewModel>; searchViewModel: SearchViewModel<TaskViewModel>;
searchSuggestions = new React.ListState<string>(); searchSuggestions = new React.ListState<string>();
filteredTaskViewModels: React.ListState<TaskViewModel> = filteredTaskViewModels: React.ListState<TaskViewModel> =
new React.ListState(); new React.ListState();
isFilterActive: React.State<boolean>; isFilterActive: React.State<boolean>;
pinchToZoomData: React.State<PinchToZoomData> = new React.State({ pinchToZoomData: React.State<PinchToZoomData> = new React.State({
zoom: 1, zoom: 1,
x: 0, x: 0,
y: 0, y: 0,
}); });
// paths // paths
getBasePath = (): string[] => { getBasePath = (): string[] => {
return [ return [
...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId), ...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId),
]; ];
}; };
getLastUsedBoardPath = (): string[] => { getLastUsedBoardPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPaths.LastUsedView]; return [...this.getBasePath(), BoardViewModelSubPaths.LastUsedView];
}; };
getPreviousSearchesPath = (): string[] => { getPreviousSearchesPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPaths.PreviousSearches]; return [...this.getBasePath(), BoardViewModelSubPaths.PreviousSearches];
}; };
getLastSearchPath = (): string[] => { getLastSearchPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPaths.LastSearch]; return [...this.getBasePath(), BoardViewModelSubPaths.LastSearch];
}; };
// settings // settings
saveSettings = (): void => { saveSettings = (): void => {
const newBoardInfoFileContent: BoardInfoFileContent = const newBoardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent( BoardsAndTasksModel.createBoardInfoFileContent(
this.boardInfo.fileId, this.boardInfo.fileId,
this.name.value, this.name.value,
this.color.value, this.color.value,
); );
this.taskPageViewModel.updateBoard(newBoardInfoFileContent); this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
}; };
applyColor = (): void => { applyColor = (): void => {
this.taskPageViewModel.chatViewModel.setDisplayedColor( this.taskPageViewModel.chatViewModel.setDisplayedColor(
this.color.value, this.color.value,
); );
}; };
deleteBoard = (): void => { deleteBoard = (): void => {
this.taskPageViewModel.deleteBoard(this.boardInfo); this.taskPageViewModel.deleteBoard(this.boardInfo);
this.chatViewModel.taskBoardSuggestions.remove(this.boardInfo.fileId); this.chatViewModel.taskBoardSuggestions.remove(this.boardInfo.fileId);
this.close(); this.close();
}; };
// methods // methods
createTask = (): void => { createTask = (): void => {
this.createTaskFromBoardId(this.boardInfo.fileId); this.createTaskFromBoardId(this.boardInfo.fileId);
}; };
handleDropWithinBoard = (category?: string, status?: string): void => { handleDropWithinBoard = (category?: string, status?: string): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value; const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return; if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setCategoryAndStatus(category, status); draggedObject.setCategoryAndStatus(category, status);
}; };
handleDropBetweenBoards = (): void => { handleDropBetweenBoards = (): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value; const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return; if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setBoardId(this.boardInfo.fileId); draggedObject.setBoardId(this.boardInfo.fileId);
}; };
// storage // storage
storeLastUsedView = (): void => { storeLastUsedView = (): void => {
const path: string[] = this.getLastUsedBoardPath(); const path: string[] = this.getLastUsedBoardPath();
const lastUsedView: string = this.selectedPage.value; const lastUsedView: string = this.selectedPage.value;
this.coreViewModel.storageModel.write(path, lastUsedView); this.coreViewModel.storageModel.write(path, lastUsedView);
}; };
restoreLastUsedView = (): void => { restoreLastUsedView = (): void => {
const path: string[] = this.getLastUsedBoardPath(); const path: string[] = this.getLastUsedBoardPath();
const lastUsedView: string | null = const lastUsedView: string | null =
this.coreViewModel.storageModel.read(path); this.coreViewModel.storageModel.read(path);
if (lastUsedView == null) return; if (lastUsedView == null) return;
this.selectedPage.value = lastUsedView as BoardPageTypes; this.selectedPage.value = lastUsedView as BoardPageTypes;
}; };
handleNewSearch = (searchTerm: string): void => { handleNewSearch = (searchTerm: string): void => {
const suggestionPath: string[] = [ const suggestionPath: string[] = [
...this.getPreviousSearchesPath(), ...this.getPreviousSearchesPath(),
searchTerm, searchTerm,
]; ];
this.coreViewModel.storageModel.write(suggestionPath, ""); this.coreViewModel.storageModel.write(suggestionPath, "");
if (!this.searchSuggestions.value.has(searchTerm) && searchTerm != "") { if (!this.searchSuggestions.value.has(searchTerm) && searchTerm != "") {
this.searchSuggestions.add(searchTerm); this.searchSuggestions.add(searchTerm);
} }
const lastSearchPath: string[] = this.getLastSearchPath(); const lastSearchPath: string[] = this.getLastSearchPath();
this.coreViewModel.storageModel.write(lastSearchPath, searchTerm); this.coreViewModel.storageModel.write(lastSearchPath, searchTerm);
}; };
handleSearchRemoved = (searchTerm: string): void => { handleSearchRemoved = (searchTerm: string): void => {
const suggestionPath: string[] = [ const suggestionPath: string[] = [
...this.getPreviousSearchesPath(), ...this.getPreviousSearchesPath(),
searchTerm, searchTerm,
]; ];
this.coreViewModel.storageModel.remove(suggestionPath); this.coreViewModel.storageModel.remove(suggestionPath);
}; };
// view // view
showTask = (taskFileContent: TaskFileContent): void => { showTask = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.boardId != this.boardInfo.fileId) { if (taskFileContent.boardId != this.boardInfo.fileId) {
// remove task that was moved to different board // remove task that was moved to different board
this.boardsAndTasksModel.deleteTaskReference( this.boardsAndTasksModel.deleteTaskReference(
this.boardInfo.fileId, this.boardInfo.fileId,
taskFileContent.fileId, taskFileContent.fileId,
); );
this.removeTaskFromView(taskFileContent); this.removeTaskFromView(taskFileContent);
return; return;
} }
const taskViewModel: TaskViewModel = new TaskViewModel( const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel, this.coreViewModel,
this.chatViewModel, this.chatViewModel,
this.boardsAndTasksModel, this.boardsAndTasksModel,
this, this,
taskFileContent, taskFileContent,
); );
this.taskViewModels.set(taskFileContent.fileId, taskViewModel); this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
}; };
removeTaskFromView = (taskFileContent: TaskFileContent): void => { removeTaskFromView = (taskFileContent: TaskFileContent): void => {
this.taskViewModels.remove(taskFileContent.fileId); this.taskViewModels.remove(taskFileContent.fileId);
this.updateIndex(); this.updateIndex();
}; };
select = (): void => { select = (): void => {
this.taskPageViewModel.selectBoard(this); this.taskPageViewModel.selectBoard(this);
}; };
close = (): void => { close = (): void => {
this.taskPageViewModel.closeBoard(); this.taskPageViewModel.closeBoard();
this.taskPageViewModel.storeLastUsedBoard(); this.taskPageViewModel.storeLastUsedBoard();
this.taskViewModels.clear(); this.taskViewModels.clear();
}; };
showSettings = (): void => { showSettings = (): void => {
this.isPresentingSettingsModal.value = true; this.isPresentingSettingsModal.value = true;
}; };
hideSettings = (): void => { hideSettings = (): void => {
this.saveSettings(); this.saveSettings();
this.isPresentingSettingsModal.value = false; this.isPresentingSettingsModal.value = false;
}; };
showFilterModal = (): void => { showFilterModal = (): void => {
this.isPresentingFilterModal.value = true; this.isPresentingFilterModal.value = true;
}; };
hideFilterModal = (): void => { hideFilterModal = (): void => {
this.isPresentingFilterModal.value = false; this.isPresentingFilterModal.value = false;
}; };
resetFilter = (): void => { resetFilter = (): void => {
this.searchViewModel.search(""); this.searchViewModel.search("");
}; };
updateIndex = (): void => { updateIndex = (): void => {
const index: number = const index: number =
this.taskPageViewModel.boardIndexManager.getIndex(this); this.taskPageViewModel.boardIndexManager.getIndex(this);
this.index.value = index; this.index.value = index;
}; };
resetPinchZoom = (): void => { resetPinchZoom = (): void => {
this.pinchToZoomData.value = {x: 0, y: 0, zoom: 1}; this.pinchToZoomData.value = { x: 0, y: 0, zoom: 1 };
} };
// load // load
preloadData = (): void => { preloadData = (): void => {
this.name.value = this.boardInfo.name; this.name.value = this.boardInfo.name;
this.color.value = this.boardInfo.color; this.color.value = this.boardInfo.color;
}; };
loadTasks = (): void => { loadTasks = (): void => {
const taskIds: string[] = this.boardsAndTasksModel.listTaskIds( const taskIds: string[] = this.boardsAndTasksModel.listTaskIds(
this.boardInfo.fileId, this.boardInfo.fileId,
); );
for (const taskId of taskIds) { for (const taskId of taskIds) {
if (this.taskViewModels.value.has(taskId)) continue; if (this.taskViewModels.value.has(taskId)) continue;
const taskFileContent: TaskFileContent | null = const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getLatestTaskFileContent(taskId); this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
if (taskFileContent == null) continue; if (taskFileContent == null) continue;
const taskViewModel: TaskViewModel = new TaskViewModel( const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel, this.coreViewModel,
this.chatViewModel, this.chatViewModel,
this.boardsAndTasksModel, this.boardsAndTasksModel,
this, this,
taskFileContent, taskFileContent,
); );
this.taskViewModels.set(taskFileContent.fileId, taskViewModel); this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
} }
this.updateTaskIndices(); this.updateTaskIndices();
}; };
loadSearchSuggestions = (): void => { loadSearchSuggestions = (): void => {
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 => {
const lastSearchPath: string[] = this.getLastSearchPath(); const lastSearchPath: string[] = this.getLastSearchPath();
const lastSearch: string | null = const lastSearch: string | null =
this.coreViewModel.storageModel.read(lastSearchPath); this.coreViewModel.storageModel.read(lastSearchPath);
if (lastSearch != null) { if (lastSearch != null) {
this.searchViewModel.search(lastSearch); this.searchViewModel.search(lastSearch);
} }
}; };
loadData = (): void => { loadData = (): void => {
this.restoreLastUsedView(); this.restoreLastUsedView();
this.loadTasks(); this.loadTasks();
this.loadSearchSuggestions(); this.loadSearchSuggestions();
}; };
// exit // exit
handleContextClose = (fromHistoryEvent: boolean): void => { handleContextClose = (fromHistoryEvent: boolean): void => {
this.taskPageViewModel.handleBoardClosed(this); this.taskPageViewModel.handleBoardClosed(this);
if (!fromHistoryEvent) return; if (!fromHistoryEvent) return;
this.taskPageViewModel.storeLastUsedBoard(); this.taskPageViewModel.storeLastUsedBoard();
}; };
// init // init
constructor( constructor(
public readonly coreViewModel: CoreViewModel, public readonly coreViewModel: CoreViewModel,
public readonly chatViewModel: ChatViewModel, public readonly chatViewModel: ChatViewModel,
public readonly boardsAndTasksModel: BoardsAndTasksModel, public readonly boardsAndTasksModel: BoardsAndTasksModel,
public readonly taskPageViewModel: TaskPageViewModel, public readonly taskPageViewModel: TaskPageViewModel,
public readonly boardInfo: BoardInfoFileContent, public readonly boardInfo: BoardInfoFileContent,
) { ) {
super(coreViewModel, chatViewModel, boardsAndTasksModel, "board"); super(coreViewModel, chatViewModel, boardsAndTasksModel, "board");
// load // load
this.preloadData(); this.preloadData();
// search
this.searchViewModel = new SearchViewModel(
this.taskViewModels,
this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.searchSuggestions,
);
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
this.handleNewSearch(newQuery);
});
this.restoreSearch();
this.searchSuggestions.handleRemovals(this.handleSearchRemoved);
this.isFilterActive = React.createProxyState( // search
[this.searchViewModel.appliedQuery], this.searchViewModel = new SearchViewModel(
() => this.searchViewModel.appliedQuery.value != "", this.taskViewModels,
); this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.searchSuggestions,
);
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
this.handleNewSearch(newQuery);
});
this.restoreSearch();
this.searchSuggestions.handleRemovals(this.handleSearchRemoved);
// subscriptions this.isFilterActive = React.createProxyState(
this.isSelected = React.createProxyState( [this.searchViewModel.appliedQuery],
[this.taskPageViewModel.selectedBoardId], () => this.searchViewModel.appliedQuery.value != "",
() => );
this.taskPageViewModel.selectedBoardId.value ==
this.boardInfo.fileId,
);
this.color.subscribe(() => { // subscriptions
if (this.isSelected.value == false) return; this.isSelected = React.createProxyState(
if (this.chatViewModel.selectedPage.value != ChatPageTypes.Tasks) [this.taskPageViewModel.selectedBoardId],
return; () =>
this.applyColor(); this.taskPageViewModel.selectedBoardId.value ==
}); this.boardInfo.fileId,
);
this.selectedPage.subscribeSilent(() => { this.color.subscribe(() => {
this.storeLastUsedView(); if (this.isSelected.value == false) return;
}); if (this.chatViewModel.selectedPage.value != ChatPageTypes.Tasks)
return;
this.applyColor();
});
this.searchViewModel.appliedQuery.subscribeSilent(()=> { this.selectedPage.subscribeSilent(() => {
this.resetPinchZoom(); this.storeLastUsedView();
}) });
// handlers this.searchViewModel.appliedQuery.subscribeSilent(() => {
boardsAndTasksModel.taskHandlerManager.setHandler( this.resetPinchZoom();
this.boardInfo.fileId, });
(taskFileContent: TaskFileContent) => {
if (taskFileContent.boardId != this.boardInfo.fileId) return;
this.showTask(taskFileContent);
this.updateTaskIndices();
},
);
// keystrokes // handlers
this.registerKeyStroke(CommonKeys.Filter, this.showFilterModal); boardsAndTasksModel.taskHandlerManager.setHandler(
this.registerKeyStroke(CommonKeys.Reset, this.resetFilter); // this.boardInfo.fileId,
this.registerKeyStroke(CommonKeys.CloseOrCancel, this.hideFilterModal); (taskFileContent: TaskFileContent) => {
this.registerKeyStroke(CommonKeys.Settings, this.showSettings); if (taskFileContent.boardId != this.boardInfo.fileId) return;
this.registerKeyStroke(CommonKeys.Apply, this.hideSettings); this.showTask(taskFileContent);
this.registerKeyStroke(CommonKeys.Create, this.createTask); this.updateTaskIndices();
},
);
this.registerKeyStroke( // keystrokes
"j", this.registerKeyStroke(CommonKeys.Filter, this.showFilterModal);
() => (this.selectedPage.value = BoardPageTypes.List), this.registerKeyStroke(CommonKeys.Reset, this.resetFilter); //
); this.registerKeyStroke(CommonKeys.CloseOrCancel, this.hideFilterModal);
this.registerKeyStroke( this.registerKeyStroke(CommonKeys.Settings, this.showSettings);
"k", this.registerKeyStroke(CommonKeys.Apply, this.hideSettings);
() => (this.selectedPage.value = BoardPageTypes.Kanban), this.registerKeyStroke(CommonKeys.Create, this.createTask);
);
this.registerKeyStroke(
"l",
() => (this.selectedPage.value = BoardPageTypes.StatusGrid),
);
this.taskPageViewModel.registerContext(this.boardInfo.fileId, this); this.registerKeyStroke(
"j",
() => (this.selectedPage.value = BoardPageTypes.List),
);
this.registerKeyStroke(
"k",
() => (this.selectedPage.value = BoardPageTypes.Kanban),
);
this.registerKeyStroke(
"l",
() => (this.selectedPage.value = BoardPageTypes.StatusGrid),
);
this.taskPageViewModel.registerContext(this.boardInfo.fileId, this);
} }
} }
export enum BoardViewModelSubPaths { export enum BoardViewModelSubPaths {
LastUsedView = "last-used-view", LastUsedView = "last-used-view",
PreviousSearches = "previous-searches", PreviousSearches = "previous-searches",
LastSearch = "last-search", LastSearch = "last-search",
} }
// types // types
export enum BoardPageTypes { export enum BoardPageTypes {
List = "list", List = "list",
Kanban = "kanban", Kanban = "kanban",
StatusGrid = "status-grid", StatusGrid = "status-grid",
} }
+4 -4
View File
@@ -219,12 +219,12 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
}; };
loadData = (): void => {}; loadData = (): void => {};
// context // context
handleContextClose = (fromHistoryEvent: boolean): void | boolean => { handleContextClose = (fromHistoryEvent: boolean): void | boolean => {
if (!fromHistoryEvent) return; if (!fromHistoryEvent) return;
return true; return true;
} };
// init // init
constructor( constructor(
+19 -20
View File
@@ -1,34 +1,33 @@
import * as React from "../../react";
import StorageViewModel from "../Global/storageViewModel"; import StorageViewModel from "../Global/storageViewModel";
import SettingsViewModel from "../Global/settingsViewModel"; import SettingsViewModel from "../Global/settingsViewModel";
import FileTransferViewModel from "../Global/fileTransferViewModel"; import FileTransferViewModel from "../Global/fileTransferViewModel";
import CoreViewModel, { Context } from "../Global/coreViewModel"; import CoreViewModel, { Context } from "../Global/coreViewModel";
import ConnectionViewModel from "../Global/connectionViewModel"; import ConnectionViewModel from "../Global/connectionViewModel";
import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
export default class HomeViewModel extends Context { export default class HomeViewModel extends Context {
constructor( constructor(
public readonly coreViewModel: CoreViewModel, public readonly coreViewModel: CoreViewModel,
public readonly settingsViewModel: SettingsViewModel, public readonly settingsViewModel: SettingsViewModel,
public readonly fileTransferViewModel: FileTransferViewModel, public readonly fileTransferViewModel: FileTransferViewModel,
public readonly storageViewModel: StorageViewModel, public readonly storageViewModel: StorageViewModel,
public readonly connectionViewModel: ConnectionViewModel, public readonly connectionViewModel: ConnectionViewModel,
) { ) {
super("home"); super("home");
this.coreViewModel.context = this; this.coreViewModel.context = this;
this.registerKeyStroke( this.registerKeyStroke(
CommonKeys.Settings, CommonKeys.Settings,
this.settingsViewModel.showSettingsModal, this.settingsViewModel.showSettingsModal,
); );
this.registerKeyStroke( this.registerKeyStroke(
"t", "t",
this.fileTransferViewModel.showDirectionSelectionModal, this.fileTransferViewModel.showDirectionSelectionModal,
); );
this.registerKeyStroke("e", this.storageViewModel.showStorageModal); this.registerKeyStroke("e", this.storageViewModel.showStorageModal);
this.registerKeyStroke("x", this.connectionViewModel.disconnect); this.registerKeyStroke("x", this.connectionViewModel.disconnect);
this.registerKeyStroke("c", this.connectionViewModel.connect); this.registerKeyStroke("c", this.connectionViewModel.connect);
} }
} }
+46 -46
View File
@@ -197,61 +197,61 @@ export default class MessagePageViewModel extends Context {
}; };
// context // context
handleContextClose = (fromHistoryEvent: boolean):boolean=> { handleContextClose = (fromHistoryEvent: boolean): boolean | void => {
if (fromHistoryEvent) { if (fromHistoryEvent) {
if (this.replyViewSelectedMessage.value == undefined) return true; if (this.replyViewSelectedMessage.value == undefined) return true;
this.resetReplyView(); this.resetReplyView();
return false; return false;
} }
} };
// init // init
constructor( constructor(
public readonly coreViewModel: CoreViewModel, public readonly coreViewModel: CoreViewModel,
public readonly chatViewModel: ChatViewModel, public readonly chatViewModel: ChatViewModel,
public readonly contactListViewModel: ContactListViewModel, public readonly contactListViewModel: ContactListViewModel,
) { ) {
super("message-page"); super("message-page");
this.restoreFilter(); this.restoreFilter();
// states // states
this.cannotSendMessage = React.createProxyState( this.cannotSendMessage = React.createProxyState(
[ [
this.chatViewModel.settingsViewModel.username, this.chatViewModel.settingsViewModel.username,
this.composingMessage, this.composingMessage,
], ],
() => () =>
this.chatViewModel.settingsViewModel.username.value == "" || this.chatViewModel.settingsViewModel.username.value == "" ||
this.composingMessage.value == "", this.composingMessage.value == "",
); );
this.searchViewModel = new SearchViewModel( this.searchViewModel = new SearchViewModel(
this.chatMessageViewModels, this.chatMessageViewModels,
this.filteredMessageViewModels, this.filteredMessageViewModels,
(chatMessageViewModel) => [chatMessageViewModel.body.value], (chatMessageViewModel) => [chatMessageViewModel.body.value],
new React.ListState(), new React.ListState(),
); );
this.isFilterActive = React.createProxyState( this.isFilterActive = React.createProxyState(
[this.searchViewModel.appliedQuery, this.reactionFilter], [this.searchViewModel.appliedQuery, this.reactionFilter],
() => () =>
this.searchViewModel.appliedQuery.value != "" || this.searchViewModel.appliedQuery.value != "" ||
this.reactionFilter.value != undefined, this.reactionFilter.value != undefined,
); );
// keystrokes // keystrokes
this.registerKeyStroke(CommonKeys.Filter, this.showFilterModal); this.registerKeyStroke(CommonKeys.Filter, this.showFilterModal);
this.registerKeyStroke(CommonKeys.CloseOrCancel, () => { this.registerKeyStroke(CommonKeys.CloseOrCancel, () => {
if (this.isFilterModalOpen.value == true) { if (this.isFilterModalOpen.value == true) {
this.hideFilterModal(); this.hideFilterModal();
} else { } else {
this.replyingMessage.value = undefined; this.replyingMessage.value = undefined;
} }
}); });
this.registerKeyStroke(CommonKeys.Reset, this.resetFilter); this.registerKeyStroke(CommonKeys.Reset, this.resetFilter);
this.registerKeyStroke(CommonKeys.Create, this.setFocus); this.registerKeyStroke(CommonKeys.Create, this.setFocus);
this.chatViewModel.registerContext(ChatPageTypes.Messages, this); this.chatViewModel.registerContext(ChatPageTypes.Messages, this);
} }
} }
+6 -7
View File
@@ -2,7 +2,7 @@ import CoreViewModel, { Context } from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react"; import * as React from "../../react";
import { Colors } from "../../colors"; import { Colors } from "../../colors";
import {random} from "../../Model/Utility/crypto"; import { random } from "../../Model/Utility/crypto";
export default class SettingsPageViewModel extends Context { export default class SettingsPageViewModel extends Context {
// state // state
@@ -75,9 +75,8 @@ export default class SettingsPageViewModel extends Context {
}; };
randomizeKey = (): void => { randomizeKey = (): void => {
this.encryptionKeyInput.value = random(24); this.encryptionKeyInput.value = random(24);
this.setEncryptionKey(); };
}
applyColor = (): void => { applyColor = (): void => {
this.chatViewModel.setColor(this.color.value); this.chatViewModel.setColor(this.color.value);
@@ -123,9 +122,9 @@ export default class SettingsPageViewModel extends Context {
// context // context
handleContextClose = (fromHistoryEvent: boolean): void | boolean => { handleContextClose = (fromHistoryEvent: boolean): void | boolean => {
if (!fromHistoryEvent) return; if (!fromHistoryEvent) return;
return true; return true;
} };
// init // init
constructor( constructor(
+4 -4
View File
@@ -165,12 +165,12 @@ export default class TaskPageViewModel extends ContextHost<string> {
this.updateBoardIndices(); this.updateBoardIndices();
this.openLastUsedBoard(); this.openLastUsedBoard();
}; };
// context // context
handleContextClose = (fromHistoryEvent: boolean): void | boolean => { handleContextClose = (fromHistoryEvent: boolean): void | boolean => {
if (!fromHistoryEvent) return; if (!fromHistoryEvent) return;
return true; return true;
} };
// init // init
constructor( constructor(
+1 -1
View File
@@ -40,7 +40,7 @@ export default class SearchViewModel<T> {
clear = (): void => { clear = (): void => {
this.searchInput.value = ""; this.searchInput.value = "";
this.applySearch(); this.applySearch();
}; };
deleteSuggestion = (suggestion: string): void => { deleteSuggestion = (suggestion: string): void => {
+3 -3
View File
@@ -29,8 +29,8 @@ main {
z-index: -1; z-index: -1;
pointer-events: none; pointer-events: none;
opacity: .3; opacity: 0.3;
} }
#background-wrapper > div > div { #background-wrapper > div > div {
@@ -143,6 +143,6 @@ body[theme="dark"] #background-wrapper #grass-2 {
} }
body[keystroke-active] [ctkeystroke]::after { body[keystroke-active] [ctkeystroke]::after {
opacity: .7; opacity: 0.7;
transform: translateY(0px); transform: translateY(0px);
} }
+23 -23
View File
@@ -85,32 +85,32 @@ chatListViewModel.selectedChat.subscribe(() => {
document.body.append( document.body.append(
<div id="background-wrapper"> <div id="background-wrapper">
<div> <div>
<div id="sky"></div> <div id="sky"></div>
<div id="grass-1"></div> <div id="grass-1"></div>
<div id="grass-2"></div> <div id="grass-2"></div>
</div> </div>
</div>, </div>,
); );
document document
.querySelector("main")! .querySelector("main")!
.append( .append(
HomePage( HomePage(
coreViewModel, coreViewModel,
storageViewModel, storageViewModel,
settingsViewModel, settingsViewModel,
connectionViewModel, connectionViewModel,
fileTransferViewModel, fileTransferViewModel,
chatListViewModel, chatListViewModel,
), ),
ChatPageWrapper(coreViewModel, chatListViewModel), ChatPageWrapper(coreViewModel, chatListViewModel),
OnboardingModalWrapper(coreViewModel, onboardingViewModel), OnboardingModalWrapper(coreViewModel, onboardingViewModel),
ConnectionModal(coreViewModel, connectionViewModel), ConnectionModal(coreViewModel, connectionViewModel),
DataTransferModalWrapper( DataTransferModalWrapper(
coreViewModel, coreViewModel,
connectionViewModel, connectionViewModel,
fileTransferViewModel, fileTransferViewModel,
), ),
StorageModal(coreViewModel, storageViewModel), StorageModal(coreViewModel, storageViewModel),
SettingsModal(coreViewModel, settingsViewModel), SettingsModal(coreViewModel, settingsViewModel),
); );