improve zooming and scrolling

This commit is contained in:
marlon-erler
2026-08-30 22:24:55 +02:00
parent 7eb02f07dc
commit f159d45c75
13 changed files with 1042 additions and 924 deletions
+4 -5
View File
@@ -475,6 +475,7 @@ body[showing-chat] #chat-page-wrapper {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
padding: 0px; padding: 0px;
overflow: hidden;
} }
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content > div { #chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content > div {
animation: slide-up var(--animation-duration); animation: slide-up var(--animation-duration);
@@ -485,14 +486,12 @@ body[showing-chat] #chat-page-wrapper {
gap: var(--gap-between-tiles); gap: var(--gap-between-tiles);
padding: var(--content-padding); padding: var(--content-padding);
} }
#chat-page > div > #main > #task-page .kanban-board-wrapper, #chat-page > div > #main > #task-page .zoom {
#chat-page > div > #main > #task-page .status-page-content {
display: flex; display: flex;
align-items: start; align-items: start;
gap: var(--large-gap); gap: var(--large-gap);
width: 100%; width: auto;
height: 100%; height: auto;
overflow: visible;
transform-origin: top left; transform-origin: top left;
padding: var(--content-padding); padding: var(--content-padding);
} }
+125 -77
View File
@@ -1136,32 +1136,82 @@
function localeCompare(a, b) { function localeCompare(a, b) {
return a.localeCompare(b); return a.localeCompare(b);
} }
function implementPinchZoom(element) { function implementPinchZoom(targetSelector) {
let initialDistance = 0;
let pinching = false; let pinching = false;
let initialDistance = 0;
let currentZoom = 1;
let initialZoom = 1;
let currentX = 0;
let currentY = 0;
let initialX = 0;
let initialY = 0;
let initialTouchX = 0;
let initialTouchY = 0;
const MIN = 0.25;
const MAX = 5;
const element = () => canvas.querySelector(targetSelector);
const parent = () => element().parentElement;
const distance = (e) => Math.hypot( const distance = (e) => 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
); );
function apply(factor) { const point = (e, direction, i) => e.touches[i][direction == "x" ? "clientX" : "clientY"];
element.style.transform = `scale(${(Math.floor(factor * 100) / 100).toString()})`; const midpoint = (e, direction) => (point(e, direction, 0) + point(e, direction, 1)) / 2;
function apply(factor, offset) {
if (factor < MIN) return apply(MIN, offset);
if (factor > MAX) return apply(5, offset);
if (!element) return;
const [x, y] = offset;
element().style.transform = `scale(${(Math.floor(factor * 100) / 100).toString()}) translate(${x}px, ${y}px)`;
currentX = x;
currentY = y;
currentZoom = factor;
} }
element.addEventListener("touchstart", (event) => { const canvas = document.body;
if (event.touches.length != 2) return; canvas.addEventListener("touchstart", (event) => {
initialDistance = distance(event); document.activeElement.blur();
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
if (event.touches.length != 2) {
pinching = false;
initialTouchX = event.touches[0].clientX;
initialTouchY = event.touches[0].clientY;
return;
}
pinching = true; pinching = true;
apply(1); initialDistance = distance(event);
initialTouchX = midpoint(event, "x");
initialTouchY = midpoint(event, "y");
}); });
element.addEventListener("touchend", (event) => { canvas.addEventListener("touchmove", (event) => {
if (event.touches.length == 2) return; if (!pinching) {
pinching = false; apply(currentZoom, [
}); initialX + (event.touches[0].clientX - initialTouchX) / initialZoom,
element.addEventListener("touchmove", (event) => { initialY + (event.touches[0].clientY - initialTouchY) / initialZoom
if (!pinching) return; ]);
return;
}
if (event.touches.length < 2) return;
event.preventDefault(); event.preventDefault();
const currentDistance = distance(event); const currentDistance = distance(event);
const factor = currentDistance / initialDistance; const midX = midpoint(event, "x");
apply(factor); 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]);
});
canvas.addEventListener("wheel", (event) => {
event.preventDefault();
if (!event.shiftKey) {
apply(currentZoom, [currentX - event.deltaX, currentY - event.deltaY]);
return;
}
;
apply(currentZoom + (event.deltaY < 0 ? 1 : -1) * 0.1, [initialX, initialY]);
});
canvas.addEventListener("scroll", (event) => {
event.preventDefault();
}); });
} }
@@ -1174,7 +1224,7 @@
this.connectionModel = connectionModel2; this.connectionModel = connectionModel2;
this.chatListModel = chatListModel2; this.chatListModel = chatListModel2;
this.fileTransferModel = fileTransferModel2; this.fileTransferModel = fileTransferModel2;
this.BUILD = "Build 26.08.30.C"; this.BUILD = "Build 26.08.30.D";
// CONTEXT // CONTEXT
this.contextStack = /* @__PURE__ */ new Map(); this.contextStack = /* @__PURE__ */ new Map();
this.closeContext = (contextId, fromHistoryEvent = false) => { this.closeContext = (contextId, fromHistoryEvent = false) => {
@@ -1858,7 +1908,10 @@
}; };
this.downloadFile = async () => { this.downloadFile = async () => {
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]; const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const backup = await this.coreViewModel.fileTransferModel.generateBackup(this.selectedPaths.value.values(), this.exportKey.value); const backup = await this.coreViewModel.fileTransferModel.generateBackup(
this.selectedPaths.value.values(),
this.exportKey.value
);
const anchor = document.createElement("a"); const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(backup); anchor.href = window.URL.createObjectURL(backup);
anchor.download = `comms-${date}.bak`; anchor.download = `comms-${date}.bak`;
@@ -1888,7 +1941,10 @@
if (!fileString || !key) return; if (!fileString || !key) return;
this.cannotDecryptImport.value = true; this.cannotDecryptImport.value = true;
try { try {
await this.coreViewModel.fileTransferModel.handleBackupFile(fileString, key); await this.coreViewModel.fileTransferModel.handleBackupFile(
fileString,
key
);
window.location.reload(); window.location.reload();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -4907,7 +4963,7 @@
const index = createPropertyValueIndexState(sortedStatuses, statusName); const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(coreViewModel2, statusName, index, boardViewModel); return StatusNameCell(coreViewModel2, statusName, index, boardViewModel);
}; };
const main = /* @__PURE__ */ createElement("div", { class: "status-page-content" }, /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "status-page-content zoom" }, /* @__PURE__ */ createElement(
"div", "div",
{ {
class: "status-name-row", class: "status-name-row",
@@ -4941,8 +4997,6 @@
); );
} }
)); ));
implementPinchZoom(main);
return main;
} }
function StatusNameCell(coreViewModel2, statusName, index, boardViewModel) { function StatusNameCell(coreViewModel2, statusName, index, boardViewModel) {
const taskViewModelsWithMatchingStatus = new ListState(); const taskViewModelsWithMatchingStatus = new ListState();
@@ -5066,7 +5120,7 @@
// src/View/ChatPages/boardKanbanPage.tsx // src/View/ChatPages/boardKanbanPage.tsx
function BoardKanbanPage(coreViewModel2, boardViewModel) { function BoardKanbanPage(coreViewModel2, boardViewModel) {
const main = PropertyValueList( return PropertyValueList(
"category", "category",
(taskViewModel) => taskViewModel.task, (taskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels, boardViewModel.filteredTaskViewModels,
@@ -5086,14 +5140,12 @@
return /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement(
"div", "div",
{ {
class: "kanban-board-wrapper", class: "kanban-board-wrapper zoom",
"children:append": [categories, categoryNameConverter] "children:append": [categories, categoryNameConverter]
} }
); );
} }
); );
implementPinchZoom(main);
return main;
} }
function Column(coreViewModel2, categoryName, index, boardViewModel) { function Column(coreViewModel2, categoryName, index, boardViewModel) {
return FilteredList( return FilteredList(
@@ -6800,9 +6852,7 @@
function toggle() { function toggle() {
isSelected.value = !isSelected.value; isSelected.value = !isSelected.value;
} }
return /* @__PURE__ */ createElement("button", { class: "tile", "toggle:selected": isSelected, "on:click": toggle }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis" }, fileOption.label), /* @__PURE__ */ createElement("span", { class: "secondary ellipsis" }, StorageModel.pathComponentsToString( return /* @__PURE__ */ createElement("button", { class: "tile", "toggle:selected": isSelected, "on:click": toggle }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis" }, fileOption.label), /* @__PURE__ */ createElement("span", { class: "secondary ellipsis" }, StorageModel.pathComponentsToString(...fileOption.path))));
...fileOption.path
))));
} }
function DirectionSelectionModal(coreViewModel2, connectionViewModel2, fileTransferViewModel2) { function DirectionSelectionModal(coreViewModel2, connectionViewModel2, fileTransferViewModel2) {
const isPresented = createProxyState( const isPresented = createProxyState(
@@ -6813,55 +6863,45 @@
[connectionViewModel2.isConnected], [connectionViewModel2.isConnected],
() => connectionViewModel2.isConnected.value == false () => connectionViewModel2.isConnected.value == false
); );
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( 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("div", { class: "flex-column gap content-margin-bottom" }, /* @__PURE__ */ createElement(
"div", "button",
{ {
class: "flex-column gap content-margin-bottom" class: "tile",
"toggle:disabled": isDisconnected,
"on:click": fileTransferViewModel2.showFileSelectionModal
}, },
/* @__PURE__ */ createElement( /* @__PURE__ */ createElement("span", { class: "icon" }, "share_windows"),
"button", /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.fromThisDeviceButton)),
{ /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")
class: "tile", ), /* @__PURE__ */ createElement(
"toggle:disabled": isDisconnected, "button",
"on:click": fileTransferViewModel2.showFileSelectionModal {
}, class: "tile",
/* @__PURE__ */ createElement("span", { class: "icon" }, "share_windows"), "toggle:disabled": isDisconnected,
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.fromThisDeviceButton)), "on:click": fileTransferViewModel2.showTransferDataInputModal
/* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward") },
), /* @__PURE__ */ createElement("span", { class: "icon" }, "cloud_download"),
/* @__PURE__ */ createElement( /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.toThisDeviceButton)),
"button", /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")
{ ), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
class: "tile", "button",
"toggle:disabled": isDisconnected, {
"on:click": fileTransferViewModel2.showTransferDataInputModal class: "tile",
}, "on:click": fileTransferViewModel2.showExportSelectionModal
/* @__PURE__ */ createElement("span", { class: "icon" }, "cloud_download"), },
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.toThisDeviceButton)), /* @__PURE__ */ createElement("span", { class: "icon" }, "file_save"),
/* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward") /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.exportButton)),
), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")
/* @__PURE__ */ createElement("hr", null), ), /* @__PURE__ */ createElement(
/* @__PURE__ */ createElement( "button",
"button", {
{ class: "tile",
class: "tile", "on:click": fileTransferViewModel2.showImportModal
"on:click": fileTransferViewModel2.showExportSelectionModal },
}, /* @__PURE__ */ createElement("span", { class: "icon" }, "upload_file"),
/* @__PURE__ */ createElement("span", { class: "icon" }, "file_save"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.importButton)),
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.exportButton)), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")
/* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward") ))), /* @__PURE__ */ createElement("button", { "on:click": fileTransferViewModel2.close }, coreViewModel2.translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
),
/* @__PURE__ */ createElement(
"button",
{
class: "tile",
"on:click": fileTransferViewModel2.showImportModal
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "upload_file"),
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, coreViewModel2.translations.dataTransferModal.importButton)),
/* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")
)
)), /* @__PURE__ */ createElement("button", { "on:click": fileTransferViewModel2.close }, coreViewModel2.translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
} }
function FileSelectionModal(coreViewModel2, fileTransferViewModel2) { function FileSelectionModal(coreViewModel2, fileTransferViewModel2) {
const OptionConverter = (fileOption) => { const OptionConverter = (fileOption) => {
@@ -7118,7 +7158,14 @@
[fileTransferViewModel2.presentedModal], [fileTransferViewModel2.presentedModal],
() => fileTransferViewModel2.presentedModal.value == 8 /* ImportSelection */ () => fileTransferViewModel2.presentedModal.value == 8 /* ImportSelection */
); );
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.dataTransferModal.importHeadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap content-margin-bottom" }, /* @__PURE__ */ createElement("input", { id: "file-transfer-input", type: "file", "on:change": fileTransferViewModel2.updateImportSelection }))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.dataTransferModal.importHeadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap content-margin-bottom" }, /* @__PURE__ */ createElement(
"input",
{
id: "file-transfer-input",
type: "file",
"on:change": fileTransferViewModel2.updateImportSelection
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
"button", "button",
{ {
class: "flex", class: "flex",
@@ -8031,4 +8078,5 @@
StorageModal(coreViewModel, storageViewModel), StorageModal(coreViewModel, storageViewModel),
SettingsModal(coreViewModel, settingsViewModel) SettingsModal(coreViewModel, settingsViewModel)
); );
implementPinchZoom(".zoom");
})(); })();
+5 -4
View File
@@ -149,14 +149,15 @@ export default class FileTransferModel {
}); });
} }
const rawBackup = stringify(files); const rawBackup = stringify(files);
console.log(rawBackup); console.log(rawBackup);
const encrypted = await encryptString(rawBackup, passphrase); const encrypted = await encryptString(rawBackup, passphrase);
const blob = new Blob([encrypted], {type: "text/plain"}); const blob = new Blob([encrypted], { type: "text/plain" });
return blob; return blob;
}; };
readonly prepareFileForSending = (filePath: string[]): string => { readonly prepareFileForSending = (filePath: string[]): string => {
const fileContent: string | undefined = this.storageModel.read(filePath); const fileContent: string | undefined =
this.storageModel.read(filePath);
if (fileContent == null) return ""; if (fileContent == null) return "";
const fileData: FileData = { const fileData: FileData = {
+82 -19
View File
@@ -279,31 +279,94 @@ export function localeCompare(a: string, b: string): number {
} }
// ui // ui
export function implementPinchZoom(element: HTMLElement) { export function implementPinchZoom(targetSelector: string) {
let initialDistance = 0;
let pinching = false; let pinching = false;
const distance = (e: TouchEvent) => Math.hypot(e.touches[0].pageX - e.touches[1].pageX, let initialDistance = 0;
e.touches[0].pageY - e.touches[1].pageY);
function apply(factor) { let currentZoom = 1;
element.style.transform = `scale(${(Math.floor(factor*100)/100).toString()})`; let initialZoom = 1;
let currentX = 0;
let currentY = 0;
let initialX = 0;
let initialY = 0;
let initialTouchX = 0;
let initialTouchY = 0;
const MIN = 0.25;
const MAX = 5;
const element = (): HTMLElement => canvas.querySelector(targetSelector);
const parent = (): HTMLElement => element().parentElement;
const distance = (e: TouchEvent) =>
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"];
const midpoint = (e: TouchEvent, direction: "x" | "y") =>
(point(e, direction, 0) + point(e, direction, 1)) / 2;
function apply(factor: number, offset: [number, number]) {
if (factor < MIN) return apply(MIN, offset);
if (factor > MAX) return apply(5, offset);
if (!element) return;
const [x, y] = offset;
element().style.transform = `scale(${(Math.floor(factor * 100) / 100).toString()}) translate(${x}px, ${y}px)`;
currentX = x;
currentY = y;
currentZoom = factor;
} }
element.addEventListener("touchstart", (event: TouchEvent) => { const canvas = document.body;
if (event.touches.length != 2) return; canvas.addEventListener("touchstart", (event: TouchEvent) => {
initialDistance = distance(event); (document.activeElement as HTMLElement).blur();
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
if (event.touches.length != 2) {
pinching = false;
initialTouchX = event.touches[0].clientX;
initialTouchY = event.touches[0].clientY;
return;
}
pinching = true; pinching = true;
apply(1); initialDistance = distance(event);
initialTouchX = midpoint(event, "x");
initialTouchY = midpoint(event, "y");
}); });
element.addEventListener("touchend", (event: TouchEvent) => { canvas.addEventListener("touchmove", (event: TouchEvent) => {
if (event.touches.length == 2) return; if (!pinching) {
pinching = false; apply(currentZoom, [
}) initialX + (((event.touches[0].clientX - initialTouchX)/initialZoom)),
element.addEventListener("touchmove", (event: TouchEvent) => { initialY + ((event.touches[0].clientY - initialTouchY)/initialZoom),
if (!pinching) return; ]);
return;
}
if (event.touches.length < 2) return;
event.preventDefault(); event.preventDefault();
const currentDistance = distance(event); const currentDistance = distance(event);
const factor = currentDistance / initialDistance; const midX = midpoint(event, "x");
apply(factor); 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]);
});
canvas.addEventListener("wheel", (event: WheelEvent) => {
event.preventDefault();
if (!event.shiftKey) {
apply(currentZoom, [currentX - event.deltaX, currentY - event.deltaY]);
return;
};
apply(currentZoom + (event.deltaY < 0 ? 1 : -1) * 0.1, [initialX, initialY]);
});
canvas.addEventListener("scroll", (event: MouseEvent) => {
event.preventDefault();
});
} }
+3 -6
View File
@@ -1,6 +1,4 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import * as Utility from "../../Model/Utility/utility";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { import {
@@ -12,12 +10,13 @@ import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Utility/taskPro
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import * as Utility from "../../Model/Utility/utility";
export function BoardKanbanPage( export function BoardKanbanPage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
boardViewModel: BoardViewModel, boardViewModel: BoardViewModel,
) { ) {
const main = PropertyValueList( return PropertyValueList(
"category", "category",
(taskViewModel: TaskViewModel) => taskViewModel.task, (taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels, boardViewModel.filteredTaskViewModels,
@@ -43,14 +42,12 @@ export function BoardKanbanPage(
return ( return (
<div <div
class="kanban-board-wrapper" class="kanban-board-wrapper zoom"
children:append={[categories, categoryNameConverter]} children:append={[categories, categoryNameConverter]}
></div> ></div>
); );
}, },
); );
Utility.implementPinchZoom(main);
return main;
} }
function Column( function Column(
+2
View File
@@ -11,6 +11,7 @@ import BoardViewModel, {
BoardPageTypes, BoardPageTypes,
} from "../../ViewModel/Pages/boardViewModel"; } from "../../ViewModel/Pages/boardViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import * as Utility from "../../Model/Utility/utility";
export function BoardPage( export function BoardPage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -148,6 +149,7 @@ export function BoardPage(
</button> </button>
</span> </span>
</div> </div>
<div class="content main-content" children:set={pages}></div> <div class="content main-content" children:set={pages}></div>
{BoardSettingsModal(coreViewModel, boardViewModel)} {BoardSettingsModal(coreViewModel, boardViewModel)}
+3 -6
View File
@@ -1,6 +1,4 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import * as Utility from "../../Model/Utility/utility";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { import {
@@ -17,6 +15,7 @@ import {
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import * as Utility from "../../Model/Utility/utility";
export function BoardStatusGridPage( export function BoardStatusGridPage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -42,8 +41,8 @@ export function BoardStatusGridPage(
return StatusNameCell(coreViewModel, statusName, index, boardViewModel); return StatusNameCell(coreViewModel, statusName, index, boardViewModel);
}; };
const main = ( return (
<div class="status-page-content"> <div class="status-page-content zoom">
<div <div
class="status-name-row" class="status-name-row"
children:append={[statuses, statusNameCellConverter]} children:append={[statuses, statusNameCellConverter]}
@@ -85,8 +84,6 @@ export function BoardStatusGridPage(
)} )}
</div> </div>
); );
Utility.implementPinchZoom(main);
return main;
} }
function StatusNameCell( function StatusNameCell(
+4 -5
View File
@@ -3,6 +3,7 @@
max-width: 100%; max-width: 100%;
padding: 0px; padding: 0px;
overflow: hidden;
} }
#chat-page #chat-page
@@ -24,16 +25,14 @@
padding: var(--content-padding); padding: var(--content-padding);
} }
#chat-page > div > #main > #task-page .kanban-board-wrapper, #chat-page > div > #main > #task-page .zoom {
#chat-page > div > #main > #task-page .status-page-content {
display: flex; display: flex;
align-items: start; align-items: start;
gap: var(--large-gap); gap: var(--large-gap);
width: 100%; width: auto;
height: 100%; height: auto;
overflow: visible;
transform-origin: top left; transform-origin: top left;
padding: var(--content-padding); padding: var(--content-padding);
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -145,14 +145,14 @@ const englishTranslations = {
filesReceivedCount: (count: number) => `Files processed: ${count}.`, filesReceivedCount: (count: number) => `Files processed: ${count}.`,
// //
exportKey: "Encryption key", exportKey: "Encryption key",
exportKeyConfirmation: "Confirm", exportKeyConfirmation: "Confirm",
downloadFileButton: "Download", downloadFileButton: "Download",
importFileButton: "Import", importFileButton: "Import",
decryptImportButton: "Decrypt", decryptImportButton: "Decrypt",
incorrectPassphraseError: "The encryption key is incorrect", incorrectPassphraseError: "The encryption key is incorrect",
}, },
storage: { storage: {
+1 -1
View File
@@ -9,7 +9,7 @@ import ConnectionModel from "../../Model/Global/connectionModel";
import ChatListModel from "../../Model/Chat/chatListModel"; import ChatListModel from "../../Model/Chat/chatListModel";
export default class CoreViewModel { export default class CoreViewModel {
readonly BUILD = "Build 26.08.30.C"; readonly BUILD = "Build 26.08.30.D";
translations: Translations; translations: Translations;
+103 -97
View File
@@ -54,7 +54,7 @@ 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(undefined); importedFile: React.State<File | undefined> = new React.State(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);
@@ -75,17 +75,16 @@ export default class FileTransferViewModel extends Context {
cannotExport: React.State<boolean> = React.createProxyState( cannotExport: React.State<boolean> = React.createProxyState(
[this.exportKey, this.exportKeyConfirmation], [this.exportKey, this.exportKeyConfirmation],
() => () =>
this.exportKey.value == "" || this.exportKey.value != this.exportKeyConfirmation.value this.exportKey.value == "" ||
this.exportKey.value != this.exportKeyConfirmation.value,
); );
cannotImport: React.State<boolean> = React.createProxyState( cannotImport: React.State<boolean> = React.createProxyState(
[this.importedFile], [this.importedFile],
() => () => this.importedFile.value == undefined,
this.importedFile.value == undefined
); );
cannotDecryptImport: React.State<boolean> = React.createProxyState( cannotDecryptImport: React.State<boolean> = React.createProxyState(
[this.importKey], [this.importKey],
() => () => this.importKey.value == "",
this.importKey.value == ""
); );
// handlers // handlers
@@ -136,142 +135,149 @@ export default class FileTransferViewModel extends Context {
}; };
downloadFile = async (): Promise<void> => { downloadFile = async (): Promise<void> => {
const date = new Date().toISOString().split("T")[0]; const date = new Date().toISOString().split("T")[0];
const backup: Blob = await this.coreViewModel.fileTransferModel.generateBackup(this.selectedPaths.value.values(), this.exportKey.value); const backup: Blob =
const anchor = document.createElement("a"); await this.coreViewModel.fileTransferModel.generateBackup(
anchor.href = window.URL.createObjectURL(backup); this.selectedPaths.value.values(),
anchor.download = `comms-${date}.bak`; this.exportKey.value,
anchor.click(); );
} const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(backup);
anchor.download = `comms-${date}.bak`;
anchor.click();
};
updateImportSelection = (): void => { updateImportSelection = (): void => {
const input = document.getElementById("file-transfer-input"); const input = document.getElementById("file-transfer-input");
if (!input) return; if (!input) return;
if (!(input instanceof HTMLInputElement)) return; if (!(input instanceof HTMLInputElement)) return;
const file = input.files[0]; const file = input.files[0];
this.importedFile.value = file; this.importedFile.value = file;
} };
importFile = (): void => { importFile = (): void => {
const file = this.importedFile.value; const file = this.importedFile.value;
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.readAsText(file, "utf8"); reader.readAsText(file, "utf8");
reader.onload = () => { reader.onload = () => {
const result = reader.result; const result = reader.result;
this.importedFileString.value = result.toString(); this.importedFileString.value = result.toString();
this.showImportDecryptDataModal(); this.showImportDecryptDataModal();
} };
} };
decryptImport = async (): Promise<void> => { decryptImport = async (): Promise<void> => {
const fileString = this.importedFileString.value; const fileString = this.importedFileString.value;
const key = this.importKey.value; const key = this.importKey.value;
if (!fileString || !key) return; if (!fileString || !key) return;
this.cannotDecryptImport.value = true; this.cannotDecryptImport.value = true;
try { try {
await this.coreViewModel.fileTransferModel.handleBackupFile(fileString, key); await this.coreViewModel.fileTransferModel.handleBackupFile(
window.location.reload(); fileString,
} catch (e) { key,
console.error(e); );
this.isImportKeyCorrect.value = false; window.location.reload();
this.cannotDecryptImport.value = false; } catch (e) {
} console.error(e);
} this.isImportKeyCorrect.value = false;
this.cannotDecryptImport.value = false;
}
};
// view // view
showDirectionSelectionModal = (): void => { showDirectionSelectionModal = (): void => {
this.coreViewModel.context = this; this.coreViewModel.context = this;
this.presentedModal.value = FileTransferModals.DirectionSelection; this.presentedModal.value = FileTransferModals.DirectionSelection;
this.getOptions(); this.getOptions();
}; };
showFileSelectionModal = (): void => { showFileSelectionModal = (): void => {
this.presentedModal.value = FileTransferModals.FileSelection; this.presentedModal.value = FileTransferModals.FileSelection;
}; };
showTransferDataModal = (): void => { showTransferDataModal = (): void => {
this.presentedModal.value = FileTransferModals.TransferDataDisplay; this.presentedModal.value = FileTransferModals.TransferDataDisplay;
this.getTransferData(); this.getTransferData();
this.coreViewModel.fileTransferModel.prepareToSend(); this.coreViewModel.fileTransferModel.prepareToSend();
}; };
initiateTransfer = (): void => { initiateTransfer = (): void => {
this.presentedModal.value = FileTransferModals.TransferDisplay; this.presentedModal.value = FileTransferModals.TransferDisplay;
this.didNotFinishSending.value = true; 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(),
(path: string) => { (path: string) => {
this.filePathsSent.add(path); this.filePathsSent.add(path);
}, },
); );
this.didNotFinishSending.value = false; this.didNotFinishSending.value = false;
}; };
showTransferDataInputModal = (): void => { showTransferDataInputModal = (): void => {
this.presentedModal.value = FileTransferModals.TransferDataInput; this.presentedModal.value = FileTransferModals.TransferDataInput;
}; };
showExportSelectionModal = (): void => { showExportSelectionModal = (): void => {
this.presentedModal.value = FileTransferModals.ExportFileSelection; this.presentedModal.value = FileTransferModals.ExportFileSelection;
}; };
showExportModal = (): void => { showExportModal = (): void => {
this.presentedModal.value = FileTransferModals.Export; this.presentedModal.value = FileTransferModals.Export;
}; };
showImportModal = (): void => { showImportModal = (): void => {
this.presentedModal.value = FileTransferModals.ImportSelection; this.presentedModal.value = FileTransferModals.ImportSelection;
}; };
showImportDecryptDataModal = (): void => { showImportDecryptDataModal = (): void => {
this.presentedModal.value = FileTransferModals.ImportDecryptData; this.presentedModal.value = FileTransferModals.ImportDecryptData;
}; };
prepareReceivingData = (): void => { prepareReceivingData = (): void => {
if (this.cannotPrepareToReceive.value == true) return; if (this.cannotPrepareToReceive.value == true) return;
this.presentedModal.value = FileTransferModals.ReceptionDisplay; this.presentedModal.value = FileTransferModals.ReceptionDisplay;
this.filePathsReceived.clear(); this.filePathsReceived.clear();
const transferData: TransferData = { const transferData: TransferData = {
channel: this.receivingTransferChannel.value, channel: this.receivingTransferChannel.value,
key: this.receivingTransferKey.value, key: this.receivingTransferKey.value,
}; };
this.coreViewModel.fileTransferModel.prepareToReceive(transferData); this.coreViewModel.fileTransferModel.prepareToReceive(transferData);
}; };
// exit // exit
close = (): void => { close = (): void => {
this.coreViewModel.closeContext(this.contextId); this.coreViewModel.closeContext(this.contextId);
}; };
handleContextClose = (): void => { handleContextClose = (): void => {
this.presentedModal.value = undefined; this.presentedModal.value = undefined;
}; };
// init // init
constructor(public readonly coreViewModel: CoreViewModel) { constructor(public readonly coreViewModel: CoreViewModel) {
super("file-transfer"); super("file-transfer");
this.coreViewModel.fileTransferModel.fileHandlerManager.setHandler( this.coreViewModel.fileTransferModel.fileHandlerManager.setHandler(
"file-transfer-view-model", "file-transfer-view-model",
this.handleReceivedFile, this.handleReceivedFile,
); );
this.coreViewModel.fileTransferModel.readyToSendHandlerManager.setHandler( this.coreViewModel.fileTransferModel.readyToSendHandlerManager.setHandler(
"file-transfer-view-model", "file-transfer-view-model",
() => this.initiateTransfer(), () => this.initiateTransfer(),
); );
// keystrokes // keystrokes
this.registerKeyStroke(CommonKeys.CloseOrCancel, this.close); this.registerKeyStroke(CommonKeys.CloseOrCancel, this.close);
} }
} }
@@ -283,18 +289,18 @@ export interface FileTransferOption {
export enum FileTransferModals { export enum FileTransferModals {
DirectionSelection, DirectionSelection,
// sending // sending
FileSelection, FileSelection,
TransferDataDisplay, TransferDataDisplay,
TransferDisplay, TransferDisplay,
// receiving // receiving
TransferDataInput, TransferDataInput,
ReceptionDisplay, ReceptionDisplay,
// export // export
ExportFileSelection, ExportFileSelection,
Export, Export,
ImportSelection, ImportSelection,
ImportDecryptData, ImportDecryptData,
} }
+2 -1
View File
@@ -2,7 +2,6 @@ import * as React from "bloatless-react";
import "./colors.css"; import "./colors.css";
import "./coloredTile.css"; import "./coloredTile.css";
import "./base.css"; import "./base.css";
import * as Utility from "./Model/Utility/utility";
import HomeViewModel from "./ViewModel/Pages/homeViewModel"; import HomeViewModel from "./ViewModel/Pages/homeViewModel";
import StorageViewModel from "./ViewModel/Global/storageViewModel"; import StorageViewModel from "./ViewModel/Global/storageViewModel";
import SettingsViewModel from "./ViewModel/Global/settingsViewModel"; import SettingsViewModel from "./ViewModel/Global/settingsViewModel";
@@ -17,6 +16,7 @@ import { SettingsModal } from "./View/Modals/settingsModal";
import { DataTransferModalWrapper } from "./View/Modals/dataTransferModal"; import { DataTransferModalWrapper } from "./View/Modals/dataTransferModal";
import { ConnectionModal } from "./View/Modals/connectionModal"; import { ConnectionModal } from "./View/Modals/connectionModal";
import v1Upgrader from "./Upgrader/v1"; import v1Upgrader from "./Upgrader/v1";
import * as Utility from "./Model/Utility/utility";
import StorageModel from "./Model/Global/storageModel"; import StorageModel from "./Model/Global/storageModel";
import SettingsModel from "./Model/Global/settingsModel"; import SettingsModel from "./Model/Global/settingsModel";
import FileTransferModel from "./Model/Global/fileTransferModel"; import FileTransferModel from "./Model/Global/fileTransferModel";
@@ -100,3 +100,4 @@ document
StorageModal(coreViewModel, storageViewModel), StorageModal(coreViewModel, storageViewModel),
SettingsModal(coreViewModel, settingsViewModel), SettingsModal(coreViewModel, settingsViewModel),
); );
Utility.implementPinchZoom(".zoom");