implement auto reconnect
This commit is contained in:
Vendored
+1
-1
@@ -827,7 +827,7 @@ body[showing-chat] #chat-page-wrapper {
|
|||||||
.file-browser code {
|
.file-browser code {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: .9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.file-browser > div {
|
.file-browser > div {
|
||||||
|
|||||||
Vendored
+389
-159
@@ -532,7 +532,10 @@
|
|||||||
this.initializeTree();
|
this.initializeTree();
|
||||||
};
|
};
|
||||||
this.removeRecursively = (pathComponents) => {
|
this.removeRecursively = (pathComponents) => {
|
||||||
this.recurse(pathComponents, (path) => this.remove(path, false));
|
this.recurse(
|
||||||
|
pathComponents,
|
||||||
|
(path) => this.remove(path, false)
|
||||||
|
);
|
||||||
this.initializeTree();
|
this.initializeTree();
|
||||||
};
|
};
|
||||||
this.renameRecursively = (sourcePathComponents, destinationPathComponents) => {
|
this.renameRecursively = (sourcePathComponents, destinationPathComponents) => {
|
||||||
@@ -658,7 +661,9 @@
|
|||||||
constructor(storageModel2, settingsModel2, fileModel) {
|
constructor(storageModel2, settingsModel2, fileModel) {
|
||||||
// paths
|
// paths
|
||||||
this.getBasePath = () => {
|
this.getBasePath = () => {
|
||||||
return this.fileModel.getModelContainerPath("calendar" /* ModelCalendar */);
|
return this.fileModel.getModelContainerPath(
|
||||||
|
"calendar" /* ModelCalendar */
|
||||||
|
);
|
||||||
};
|
};
|
||||||
this.getViewPath = () => {
|
this.getViewPath = () => {
|
||||||
return [...this.getBasePath(), "view" /* ModelView */];
|
return [...this.getBasePath(), "view" /* ModelView */];
|
||||||
@@ -806,9 +811,15 @@
|
|||||||
};
|
};
|
||||||
// handlers
|
// handlers
|
||||||
this.handleFileContent = (fileContent) => {
|
this.handleFileContent = (fileContent) => {
|
||||||
if (checkMatchesObjectStructure(fileContent, BoardInfoFileContentReference) == true) {
|
if (checkMatchesObjectStructure(
|
||||||
|
fileContent,
|
||||||
|
BoardInfoFileContentReference
|
||||||
|
) == true) {
|
||||||
this.handleBoard(fileContent);
|
this.handleBoard(fileContent);
|
||||||
} else if (checkMatchesObjectStructure(fileContent, TaskFileContentReference) == true) {
|
} else if (checkMatchesObjectStructure(
|
||||||
|
fileContent,
|
||||||
|
TaskFileContentReference
|
||||||
|
) == true) {
|
||||||
this.handleTask(fileContent);
|
this.handleTask(fileContent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -897,7 +908,10 @@
|
|||||||
return versionIds;
|
return versionIds;
|
||||||
};
|
};
|
||||||
this.getLatestTaskFileContent = (taskId) => {
|
this.getLatestTaskFileContent = (taskId) => {
|
||||||
const taskFileContentOrNull = this.fileModel.getLatestFileContent(taskId, TaskFileContentReference);
|
const taskFileContentOrNull = this.fileModel.getLatestFileContent(
|
||||||
|
taskId,
|
||||||
|
TaskFileContentReference
|
||||||
|
);
|
||||||
return taskFileContentOrNull;
|
return taskFileContentOrNull;
|
||||||
};
|
};
|
||||||
this.getSpecificTaskFileContent = (taskId, versionId) => {
|
this.getSpecificTaskFileContent = (taskId, versionId) => {
|
||||||
@@ -933,10 +947,7 @@
|
|||||||
static {
|
static {
|
||||||
// utility
|
// utility
|
||||||
this.createBoardInfoFileContent = (fileId, name, color) => {
|
this.createBoardInfoFileContent = (fileId, name, color) => {
|
||||||
const fileContent = FileModel2.createFileContent(
|
const fileContent = FileModel2.createFileContent(fileId, "board-info");
|
||||||
fileId,
|
|
||||||
"board-info"
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...fileContent,
|
...fileContent,
|
||||||
name,
|
name,
|
||||||
@@ -1043,7 +1054,10 @@
|
|||||||
return fileContentIds[fileContentIds.length - 1];
|
return fileContentIds[fileContentIds.length - 1];
|
||||||
};
|
};
|
||||||
this.getFileContent = (fileId, fileContentName, reference) => {
|
this.getFileContent = (fileId, fileContentName, reference) => {
|
||||||
const filePath = this.getFileContentPath(fileId, fileContentName);
|
const filePath = this.getFileContentPath(
|
||||||
|
fileId,
|
||||||
|
fileContentName
|
||||||
|
);
|
||||||
const fileContentOrNull = this.storageModel.readStringifiable(
|
const fileContentOrNull = this.storageModel.readStringifiable(
|
||||||
filePath,
|
filePath,
|
||||||
reference
|
reference
|
||||||
@@ -1247,7 +1261,9 @@
|
|||||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||||
this.chatMessageHandlerManager.trigger(chatMessage);
|
this.chatMessageHandlerManager.trigger(chatMessage);
|
||||||
}
|
}
|
||||||
this.fileModel.handleStringifiedFileContent(chatMessage.stringifiedFile);
|
this.fileModel.handleStringifiedFileContent(
|
||||||
|
chatMessage.stringifiedFile
|
||||||
|
);
|
||||||
};
|
};
|
||||||
this.sendMessage = async (body, fileContent) => {
|
this.sendMessage = async (body, fileContent) => {
|
||||||
const senderName = this.settingsModel.username;
|
const senderName = this.settingsModel.username;
|
||||||
@@ -1328,7 +1344,11 @@
|
|||||||
this.loadInfo();
|
this.loadInfo();
|
||||||
this.loadColor();
|
this.loadColor();
|
||||||
this.subscribe();
|
this.subscribe();
|
||||||
this.fileModel = new FileModel2(this.storageModel, this.settingsModel, this);
|
this.fileModel = new FileModel2(
|
||||||
|
this.storageModel,
|
||||||
|
this.settingsModel,
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
get secondaryChannels() {
|
get secondaryChannels() {
|
||||||
return this.info.secondaryChannels.sort(localeCompare);
|
return this.info.secondaryChannels.sort(localeCompare);
|
||||||
@@ -1341,7 +1361,10 @@
|
|||||||
const chatMessages = [];
|
const chatMessages = [];
|
||||||
for (const messageId of messageIds) {
|
for (const messageId of messageIds) {
|
||||||
const messagePath = this.getMessagePath(messageId);
|
const messagePath = this.getMessagePath(messageId);
|
||||||
const chatMessage = this.storageModel.readStringifiable(messagePath, ChatMessageReference);
|
const chatMessage = this.storageModel.readStringifiable(
|
||||||
|
messagePath,
|
||||||
|
ChatMessageReference
|
||||||
|
);
|
||||||
chatMessages.push(chatMessage);
|
chatMessages.push(chatMessage);
|
||||||
}
|
}
|
||||||
const sorted = chatMessages.sort(
|
const sorted = chatMessages.sort(
|
||||||
@@ -1378,7 +1401,10 @@
|
|||||||
chatMessage.stringifiedFile = stringifiedFile;
|
chatMessage.stringifiedFile = stringifiedFile;
|
||||||
}
|
}
|
||||||
if (encryptionKey != "") {
|
if (encryptionKey != "") {
|
||||||
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
|
chatMessage.body = await encryptString(
|
||||||
|
chatMessage.body,
|
||||||
|
encryptionKey
|
||||||
|
);
|
||||||
chatMessage.stringifiedFile = await encryptString(
|
chatMessage.stringifiedFile = await encryptString(
|
||||||
chatMessage.stringifiedFile,
|
chatMessage.stringifiedFile,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
@@ -1463,7 +1489,10 @@
|
|||||||
};
|
};
|
||||||
// load
|
// load
|
||||||
this.loadChats = () => {
|
this.loadChats = () => {
|
||||||
const chatDir = StorageModel.getPath("chat" /* Chat */, filePaths.chat.base);
|
const chatDir = StorageModel.getPath(
|
||||||
|
"chat" /* Chat */,
|
||||||
|
filePaths.chat.base
|
||||||
|
);
|
||||||
const chatIds = this.storageModel.list(chatDir);
|
const chatIds = this.storageModel.list(chatDir);
|
||||||
for (const chatId of chatIds) {
|
for (const chatId of chatIds) {
|
||||||
const chatModel = new ChatModel(
|
const chatModel = new ChatModel(
|
||||||
@@ -1563,7 +1592,9 @@
|
|||||||
) == false) {
|
) == false) {
|
||||||
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
|
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
|
||||||
}
|
}
|
||||||
if (this.coreViewModel.taskStatusSuggestions.value.has(this.status.value) == false) {
|
if (this.coreViewModel.taskStatusSuggestions.value.has(
|
||||||
|
this.status.value
|
||||||
|
) == false) {
|
||||||
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
|
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1588,14 +1619,15 @@
|
|||||||
};
|
};
|
||||||
this.deleteTask = () => {
|
this.deleteTask = () => {
|
||||||
this.close();
|
this.close();
|
||||||
this.boardsAndTasksModel.deleteTask(this.task.boardId, this.task.fileId);
|
this.boardsAndTasksModel.deleteTask(
|
||||||
|
this.task.boardId,
|
||||||
|
this.task.fileId
|
||||||
|
);
|
||||||
this.containingModel.removeTaskFromView(this.task);
|
this.containingModel.removeTaskFromView(this.task);
|
||||||
};
|
};
|
||||||
// load
|
// load
|
||||||
this.loadVersionIds = () => {
|
this.loadVersionIds = () => {
|
||||||
const versionIds = this.boardsAndTasksModel.listTaskVersionIds(
|
const versionIds = this.boardsAndTasksModel.listTaskVersionIds(this.task.fileId);
|
||||||
this.task.fileId
|
|
||||||
);
|
|
||||||
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
|
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
|
||||||
this.versionIds.clear();
|
this.versionIds.clear();
|
||||||
this.versionIds.add(...sortedVersionIds);
|
this.versionIds.add(...sortedVersionIds);
|
||||||
@@ -1810,7 +1842,11 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handleDrop = (year, month, date) => {
|
this.handleDrop = (year, month, date) => {
|
||||||
const ISOString = CalendarModel.getISODateString(year, month, date);
|
const ISOString = CalendarModel.getISODateString(
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
date
|
||||||
|
);
|
||||||
const draggedObject = this.coreViewModel.draggedObject.value;
|
const draggedObject = this.coreViewModel.draggedObject.value;
|
||||||
if (draggedObject instanceof TaskViewModel == false) return;
|
if (draggedObject instanceof TaskViewModel == false) return;
|
||||||
draggedObject.setDate(ISOString);
|
draggedObject.setDate(ISOString);
|
||||||
@@ -1822,7 +1858,9 @@
|
|||||||
this.selectedMonth.value,
|
this.selectedMonth.value,
|
||||||
() => new MapState()
|
() => new MapState()
|
||||||
);
|
);
|
||||||
const taskIds = this.calendarModel.listTaskIds(this.monthString);
|
const taskIds = this.calendarModel.listTaskIds(
|
||||||
|
this.monthString
|
||||||
|
);
|
||||||
for (const taskId of taskIds) {
|
for (const taskId of taskIds) {
|
||||||
const taskFileContent = this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
|
const taskFileContent = this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
|
||||||
if (taskFileContent == null) continue;
|
if (taskFileContent == null) continue;
|
||||||
@@ -1935,7 +1973,10 @@
|
|||||||
existingChatMessageViewModel.body.value = chatMessage.body;
|
existingChatMessageViewModel.body.value = chatMessage.body;
|
||||||
existingChatMessageViewModel.status.value = chatMessage.status;
|
existingChatMessageViewModel.status.value = chatMessage.status;
|
||||||
} else {
|
} else {
|
||||||
this.chatMessageViewModels.set(chatMessage.id, chatMessageViewModel);
|
this.chatMessageViewModels.set(
|
||||||
|
chatMessage.id,
|
||||||
|
chatMessageViewModel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// load
|
// load
|
||||||
@@ -1947,7 +1988,10 @@
|
|||||||
};
|
};
|
||||||
this.chatViewModel = chatViewModel;
|
this.chatViewModel = chatViewModel;
|
||||||
this.cannotSendMessage = createProxyState(
|
this.cannotSendMessage = createProxyState(
|
||||||
[this.chatViewModel.settingsViewModel.username, this.composingMessage],
|
[
|
||||||
|
this.chatViewModel.settingsViewModel.username,
|
||||||
|
this.composingMessage
|
||||||
|
],
|
||||||
() => this.chatViewModel.settingsViewModel.username.value == "" || this.composingMessage.value == ""
|
() => this.chatViewModel.settingsViewModel.username.value == "" || this.composingMessage.value == ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2116,7 +2160,9 @@
|
|||||||
this.filteredTaskViewModels = new ListState();
|
this.filteredTaskViewModels = new ListState();
|
||||||
// paths
|
// paths
|
||||||
this.getBasePath = () => {
|
this.getBasePath = () => {
|
||||||
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)];
|
return [
|
||||||
|
...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)
|
||||||
|
];
|
||||||
};
|
};
|
||||||
this.getLastUsedBoardPath = () => {
|
this.getLastUsedBoardPath = () => {
|
||||||
return [...this.getBasePath(), "last-used-view" /* LastUsedView */];
|
return [...this.getBasePath(), "last-used-view" /* LastUsedView */];
|
||||||
@@ -2137,7 +2183,9 @@
|
|||||||
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
|
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
|
||||||
};
|
};
|
||||||
this.applyColor = () => {
|
this.applyColor = () => {
|
||||||
this.taskPageViewModel.chatViewModel.setDisplayedColor(this.color.value);
|
this.taskPageViewModel.chatViewModel.setDisplayedColor(
|
||||||
|
this.color.value
|
||||||
|
);
|
||||||
};
|
};
|
||||||
this.deleteBoard = () => {
|
this.deleteBoard = () => {
|
||||||
this.taskPageViewModel.deleteBoard(this.boardInfo);
|
this.taskPageViewModel.deleteBoard(this.boardInfo);
|
||||||
@@ -3266,7 +3314,10 @@
|
|||||||
"select",
|
"select",
|
||||||
{
|
{
|
||||||
"bind:value": taskViewModel.selectedVersionId,
|
"bind:value": taskViewModel.selectedVersionId,
|
||||||
"children:append": [taskViewModel.versionIds, VersionIdToOption]
|
"children:append": [
|
||||||
|
taskViewModel.versionIds,
|
||||||
|
VersionIdToOption
|
||||||
|
]
|
||||||
}
|
}
|
||||||
), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskNameLabel), /* @__PURE__ */ createElement("input", { "bind:value": taskViewModel.name }))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "category"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskBoardLabel), /* @__PURE__ */ createElement(
|
), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskNameLabel), /* @__PURE__ */ createElement("input", { "bind:value": taskViewModel.name }))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "category"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskBoardLabel), /* @__PURE__ */ createElement(
|
||||||
"select",
|
"select",
|
||||||
@@ -3315,11 +3366,44 @@
|
|||||||
StringToOption
|
StringToOption
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "priority_high"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskPriorityLabel), /* @__PURE__ */ createElement("input", { type: "number", "bind:value": taskViewModel.priority }))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "calendar_month"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskDateLabel), /* @__PURE__ */ createElement("input", { type: "date", "bind:value": taskViewModel.date }))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskTimeLabel), /* @__PURE__ */ createElement("input", { type: "time", "bind:value": taskViewModel.time }))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
|
), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "priority_high"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskPriorityLabel), /* @__PURE__ */ createElement(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
"bind:value": taskViewModel.priority
|
||||||
|
}
|
||||||
|
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "calendar_month"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskDateLabel), /* @__PURE__ */ createElement(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "date",
|
||||||
|
"bind:value": taskViewModel.date
|
||||||
|
}
|
||||||
|
))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskTimeLabel), /* @__PURE__ */ createElement(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "time",
|
||||||
|
"bind:value": taskViewModel.time
|
||||||
|
}
|
||||||
|
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
|
||||||
translations.chatPage.task.deleteTaskButton,
|
translations.chatPage.task.deleteTaskButton,
|
||||||
"delete_forever",
|
"delete_forever",
|
||||||
taskViewModel.deleteTask
|
taskViewModel.deleteTask
|
||||||
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": taskViewModel.closeAndDiscard }, translations.general.closeButton), /* @__PURE__ */ createElement("button", { class: "flex primary", "on:click": taskViewModel.closeAndSave }, translations.general.saveButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "save")))));
|
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "flex",
|
||||||
|
"on:click": taskViewModel.closeAndDiscard
|
||||||
|
},
|
||||||
|
translations.general.closeButton
|
||||||
|
), /* @__PURE__ */ createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "flex primary",
|
||||||
|
"on:click": taskViewModel.closeAndSave
|
||||||
|
},
|
||||||
|
translations.general.saveButton,
|
||||||
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "save")
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/View/Components/taskEntry.tsx
|
// src/View/Components/taskEntry.tsx
|
||||||
@@ -3341,7 +3425,13 @@
|
|||||||
"on:click": taskViewModel.open,
|
"on:click": taskViewModel.open,
|
||||||
"on:dragstart": taskViewModel.dragStart
|
"on:dragstart": taskViewModel.dragStart
|
||||||
},
|
},
|
||||||
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis", "subscribe:innerText": taskViewModel.name }), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
|
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement(
|
||||||
|
"b",
|
||||||
|
{
|
||||||
|
class: "ellipsis",
|
||||||
|
"subscribe:innerText": taskViewModel.name
|
||||||
|
}
|
||||||
|
), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: "grid secondary",
|
class: "grid secondary",
|
||||||
@@ -3384,7 +3474,11 @@
|
|||||||
date
|
date
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return MonthGrid2(monthGrid, calendarPageViewModel.selectedDate, drop);
|
return MonthGrid2(
|
||||||
|
monthGrid,
|
||||||
|
calendarPageViewModel.selectedDate,
|
||||||
|
drop
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -3399,19 +3493,25 @@
|
|||||||
if (listState == void 0) {
|
if (listState == void 0) {
|
||||||
return /* @__PURE__ */ createElement("div", null);
|
return /* @__PURE__ */ createElement("div", null);
|
||||||
} else {
|
} else {
|
||||||
const sidePaneContent = createProxyState([listState], () => {
|
const sidePaneContent = createProxyState(
|
||||||
if (listState.value.size == 0) {
|
[listState],
|
||||||
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100 flex-column justify-center align-center" }, /* @__PURE__ */ createElement("span", { class: "secondary slide-up" }, translations.chatPage.calendar.noEvents));
|
() => {
|
||||||
} else {
|
if (listState.value.size == 0) {
|
||||||
return /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100 flex-column justify-center align-center" }, /* @__PURE__ */ createElement("span", { class: "secondary slide-up" }, translations.chatPage.calendar.noEvents));
|
||||||
"div",
|
} else {
|
||||||
{
|
return /* @__PURE__ */ createElement(
|
||||||
class: "flex-column gap slide-up padding-bottom",
|
"div",
|
||||||
"children:append": [listState, TaskViewModelToEntry]
|
{
|
||||||
}
|
class: "flex-column gap slide-up padding-bottom",
|
||||||
);
|
"children:append": [
|
||||||
|
listState,
|
||||||
|
TaskViewModelToEntry
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
return /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@@ -3484,13 +3584,25 @@
|
|||||||
"on:click": calendarPageViewModel.createEvent
|
"on:click": calendarPageViewModel.createEvent
|
||||||
},
|
},
|
||||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
|
||||||
))), /* @__PURE__ */ createElement("div", { class: "content padding-0", "children:set": mainContent }))), /* @__PURE__ */ createElement(
|
))), /* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "content padding-0",
|
||||||
|
"children:set": mainContent
|
||||||
|
}
|
||||||
|
))), /* @__PURE__ */ createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: "pane-wrapper side background",
|
class: "pane-wrapper side background",
|
||||||
"set:color": calendarPageViewModel.chatViewModel.displayedColor
|
"set:color": calendarPageViewModel.chatViewModel.displayedColor
|
||||||
},
|
},
|
||||||
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement("div", { class: "content", "children:set": sidePaneContentWrapper }))
|
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "content",
|
||||||
|
"children:set": sidePaneContentWrapper
|
||||||
|
}
|
||||||
|
))
|
||||||
), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal }));
|
), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3522,13 +3634,20 @@
|
|||||||
|
|
||||||
// src/View/Modals/chatMessageInfoModal.tsx
|
// src/View/Modals/chatMessageInfoModal.tsx
|
||||||
function ChatMessageInfoModal(chatMessageViewModel) {
|
function ChatMessageInfoModal(chatMessageViewModel) {
|
||||||
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": chatMessageViewModel.isPresentingInfoModal }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.message.messageInfoHeadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.sentBy), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.sender))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.timeSent), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.dateSent))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.channel), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.channel))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "description"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.messageContent), /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement(
|
||||||
"b",
|
"div",
|
||||||
{
|
{
|
||||||
class: "break-word",
|
class: "modal",
|
||||||
"subscribe:innerText": chatMessageViewModel.body
|
"toggle:open": chatMessageViewModel.isPresentingInfoModal
|
||||||
}
|
},
|
||||||
)))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.copyMessage }, translations.chatPage.message.copyMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.resendMessage }, translations.chatPage.message.resendMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "redo")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.decryptMessage }, translations.chatPage.message.decryptMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "key")))), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.hideInfoModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
|
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.message.messageInfoHeadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.sentBy), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.sender))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.timeSent), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.dateSent))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.channel), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.channel))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "description"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.messageContent), /* @__PURE__ */ createElement(
|
||||||
|
"b",
|
||||||
|
{
|
||||||
|
class: "break-word",
|
||||||
|
"subscribe:innerText": chatMessageViewModel.body
|
||||||
|
}
|
||||||
|
)))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.copyMessage }, translations.chatPage.message.copyMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.resendMessage }, translations.chatPage.message.resendMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "redo")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.decryptMessage }, translations.chatPage.message.decryptMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "key")))), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.hideInfoModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/View/Components/chatMessage.tsx
|
// src/View/Components/chatMessage.tsx
|
||||||
@@ -3560,7 +3679,13 @@
|
|||||||
class: "body",
|
class: "body",
|
||||||
"subscribe:innerText": chatMessageViewModel.body
|
"subscribe:innerText": chatMessageViewModel.body
|
||||||
}
|
}
|
||||||
), /* @__PURE__ */ createElement("span", { class: "timestamp ellipsis" }, /* @__PURE__ */ createElement("span", { class: "icon", "subscribe:innerText": statusIcon }), chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement(
|
), /* @__PURE__ */ createElement("span", { class: "timestamp ellipsis" }, /* @__PURE__ */ createElement(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "icon",
|
||||||
|
"subscribe:innerText": statusIcon
|
||||||
|
}
|
||||||
|
), chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
"on:click": chatMessageViewModel.showInfoModal,
|
"on:click": chatMessageViewModel.showInfoModal,
|
||||||
@@ -3872,32 +3997,45 @@
|
|||||||
(taskViewModel) => taskViewModel.task,
|
(taskViewModel) => taskViewModel.task,
|
||||||
boardViewModel.filteredTaskViewModels,
|
boardViewModel.filteredTaskViewModels,
|
||||||
(taskViewModels) => {
|
(taskViewModels) => {
|
||||||
const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
|
const viewModel = new TaskCategoryBulkChangeViewModel(
|
||||||
|
taskViewModels,
|
||||||
|
categoryName
|
||||||
|
);
|
||||||
function drop() {
|
function drop() {
|
||||||
boardViewModel.handleDropWithinBoard(categoryName);
|
boardViewModel.handleDropWithinBoard(categoryName);
|
||||||
}
|
}
|
||||||
const view = /* @__PURE__ */ createElement("div", { class: "flex-column flex-no", "on:dragover": allowDrop, "on:drop": drop }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
|
const view = /* @__PURE__ */ createElement(
|
||||||
"input",
|
|
||||||
{
|
|
||||||
placeholder: translations.chatPage.task.renameCategoryInputPlaceholder,
|
|
||||||
"bind:value": viewModel.inputValue,
|
|
||||||
"on:enter": viewModel.set
|
|
||||||
}
|
|
||||||
), /* @__PURE__ */ createElement(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
class: "primary",
|
|
||||||
"on:click": viewModel.set,
|
|
||||||
"toggle:disabled": viewModel.cannotSet
|
|
||||||
},
|
|
||||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
|
|
||||||
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
|
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: "kanban-column",
|
class: "flex-column flex-no",
|
||||||
"children:append": [taskViewModels, TaskViewModelToEntry]
|
"on:dragover": allowDrop,
|
||||||
}
|
"on:drop": drop
|
||||||
));
|
},
|
||||||
|
/* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
placeholder: translations.chatPage.task.renameCategoryInputPlaceholder,
|
||||||
|
"bind:value": viewModel.inputValue,
|
||||||
|
"on:enter": viewModel.set
|
||||||
|
}
|
||||||
|
), /* @__PURE__ */ createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "primary",
|
||||||
|
"on:click": viewModel.set,
|
||||||
|
"toggle:disabled": viewModel.cannotSet
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
|
||||||
|
)),
|
||||||
|
/* @__PURE__ */ createElement("hr", null),
|
||||||
|
/* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "kanban-column",
|
||||||
|
"children:append": [taskViewModels, TaskViewModelToEntry]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
index.subscribe((newIndex) => {
|
index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
@@ -3908,17 +4046,24 @@
|
|||||||
|
|
||||||
// src/View/Modals/boardSettingsModal.tsx
|
// src/View/Modals/boardSettingsModal.tsx
|
||||||
function BoardSettingsModal(boardViewModel) {
|
function BoardSettingsModal(boardViewModel) {
|
||||||
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": boardViewModel.isPresentingSettingsModal }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.task.boardSettingsHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.boardNameInputLabel), /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement(
|
||||||
"input",
|
"div",
|
||||||
{
|
{
|
||||||
"on:enter": boardViewModel.saveSettings,
|
class: "modal",
|
||||||
"bind:value": boardViewModel.name
|
"toggle:open": boardViewModel.isPresentingSettingsModal
|
||||||
}
|
},
|
||||||
))), /* @__PURE__ */ createElement("hr", null), ColorPicker(boardViewModel.color), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
|
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.task.boardSettingsHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.boardNameInputLabel), /* @__PURE__ */ createElement(
|
||||||
translations.chatPage.task.deleteBoardButton,
|
"input",
|
||||||
"delete_forever",
|
{
|
||||||
boardViewModel.deleteBoard
|
"on:enter": boardViewModel.saveSettings,
|
||||||
))), /* @__PURE__ */ createElement("button", { "on:click": boardViewModel.hideSettings }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
|
"bind:value": boardViewModel.name
|
||||||
|
}
|
||||||
|
))), /* @__PURE__ */ createElement("hr", null), ColorPicker(boardViewModel.color), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
|
||||||
|
translations.chatPage.task.deleteBoardButton,
|
||||||
|
"delete_forever",
|
||||||
|
boardViewModel.deleteBoard
|
||||||
|
))), /* @__PURE__ */ createElement("button", { "on:click": boardViewModel.hideSettings }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/View/ChatPages/boardStatusGridPage.tsx
|
// src/View/ChatPages/boardStatusGridPage.tsx
|
||||||
@@ -3978,9 +4123,12 @@
|
|||||||
const doesMatchStatus = taskViewModel.task.status == statusName;
|
const doesMatchStatus = taskViewModel.task.status == statusName;
|
||||||
if (doesMatchStatus == false) return;
|
if (doesMatchStatus == false) return;
|
||||||
taskViewModelsWithMatchingStatus.add(taskViewModel);
|
taskViewModelsWithMatchingStatus.add(taskViewModel);
|
||||||
boardViewModel.filteredTaskViewModels.handleRemoval(taskViewModel, () => {
|
boardViewModel.filteredTaskViewModels.handleRemoval(
|
||||||
taskViewModelsWithMatchingStatus.remove(taskViewModel);
|
taskViewModel,
|
||||||
});
|
() => {
|
||||||
|
taskViewModelsWithMatchingStatus.remove(taskViewModel);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const viewModel = new TaskStatusBulkChangeViewModel(
|
const viewModel = new TaskStatusBulkChangeViewModel(
|
||||||
@@ -4015,7 +4163,10 @@
|
|||||||
boardViewModel.filteredTaskViewModels,
|
boardViewModel.filteredTaskViewModels,
|
||||||
(taskViewModels) => {
|
(taskViewModels) => {
|
||||||
const statusNameConverter = (statusName) => {
|
const statusNameConverter = (statusName) => {
|
||||||
const index2 = createPropertyValueIndexState(sortedStatuses, statusName);
|
const index2 = createPropertyValueIndexState(
|
||||||
|
sortedStatuses,
|
||||||
|
statusName
|
||||||
|
);
|
||||||
return CategoryStatusColumn(
|
return CategoryStatusColumn(
|
||||||
categoryName,
|
categoryName,
|
||||||
statusName,
|
statusName,
|
||||||
@@ -4024,7 +4175,10 @@
|
|||||||
taskViewModels
|
taskViewModels
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
|
const viewModel = new TaskCategoryBulkChangeViewModel(
|
||||||
|
taskViewModels,
|
||||||
|
categoryName
|
||||||
|
);
|
||||||
const view = /* @__PURE__ */ createElement("div", { class: "flex-row flex-no large-gap" }, /* @__PURE__ */ createElement("div", { class: "property-input-wrapper" }, /* @__PURE__ */ createElement(
|
const view = /* @__PURE__ */ createElement("div", { class: "flex-row flex-no large-gap" }, /* @__PURE__ */ createElement("div", { class: "property-input-wrapper" }, /* @__PURE__ */ createElement(
|
||||||
"input",
|
"input",
|
||||||
{
|
{
|
||||||
@@ -4113,7 +4267,10 @@
|
|||||||
{
|
{
|
||||||
hidden: true,
|
hidden: true,
|
||||||
id: suggestionId,
|
id: suggestionId,
|
||||||
"children:append": [searchViewModel.suggestions, StringToOption]
|
"children:append": [
|
||||||
|
searchViewModel.suggestions,
|
||||||
|
StringToOption
|
||||||
|
]
|
||||||
}
|
}
|
||||||
), /* @__PURE__ */ createElement(
|
), /* @__PURE__ */ createElement(
|
||||||
"button",
|
"button",
|
||||||
@@ -4129,7 +4286,10 @@
|
|||||||
{
|
{
|
||||||
class: "grid gap",
|
class: "grid gap",
|
||||||
style: "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))",
|
style: "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))",
|
||||||
"children:append": [searchViewModel.matchingObjects, converter]
|
"children:append": [
|
||||||
|
searchViewModel.matchingObjects,
|
||||||
|
converter
|
||||||
|
]
|
||||||
}
|
}
|
||||||
)), /* @__PURE__ */ createElement("button", { "on:click": close }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
|
)), /* @__PURE__ */ createElement("button", { "on:click": close }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
|
||||||
}
|
}
|
||||||
@@ -4168,7 +4328,9 @@
|
|||||||
if (boardViewModel.selectedTaskViewModel.value == void 0) {
|
if (boardViewModel.selectedTaskViewModel.value == void 0) {
|
||||||
return /* @__PURE__ */ createElement("div", null);
|
return /* @__PURE__ */ createElement("div", null);
|
||||||
} else {
|
} else {
|
||||||
return TaskSettingsModal(boardViewModel.selectedTaskViewModel.value);
|
return TaskSettingsModal(
|
||||||
|
boardViewModel.selectedTaskViewModel.value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -4248,7 +4410,13 @@
|
|||||||
"on:dragover": allowDrop,
|
"on:dragover": allowDrop,
|
||||||
"on:drop": boardViewModel.handleDropBetweenBoards
|
"on:drop": boardViewModel.handleDropBetweenBoards
|
||||||
},
|
},
|
||||||
/* @__PURE__ */ createElement("span", { class: "shadow", "subscribe:innerText": boardViewModel.name }),
|
/* @__PURE__ */ createElement(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "shadow",
|
||||||
|
"subscribe:innerText": boardViewModel.name
|
||||||
|
}
|
||||||
|
),
|
||||||
/* @__PURE__ */ createElement("b", { "subscribe:innerText": boardViewModel.name })
|
/* @__PURE__ */ createElement("b", { "subscribe:innerText": boardViewModel.name })
|
||||||
);
|
);
|
||||||
boardViewModel.index.subscribe((newIndex) => {
|
boardViewModel.index.subscribe((newIndex) => {
|
||||||
@@ -4281,49 +4449,58 @@
|
|||||||
return BoardPage(selectedBoard);
|
return BoardPage(selectedBoard);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return /* @__PURE__ */ createElement("div", { id: "task-page", "toggle:isshowingboard": isShowingBoard, "set:showingboardlist": taskPageViewModel.isShowingBoadList }, /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
id: "board-list",
|
id: "task-page",
|
||||||
class: "pane-wrapper side background",
|
"toggle:isshowingboard": isShowingBoard,
|
||||||
"set:color": taskPageViewModel.chatViewModel.displayedColor
|
"set:showingboardlist": taskPageViewModel.isShowingBoadList
|
||||||
},
|
},
|
||||||
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
|
/* @__PURE__ */ createElement(
|
||||||
"input",
|
|
||||||
{
|
|
||||||
class: "no-outline",
|
|
||||||
"bind:value": taskPageViewModel.newBoardNameInput,
|
|
||||||
"on:enter": taskPageViewModel.createBoard,
|
|
||||||
placeholder: translations.chatPage.task.newBoardNamePlaceholder
|
|
||||||
}
|
|
||||||
), /* @__PURE__ */ createElement(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
class: "primary",
|
|
||||||
"aria-label": translations.chatPage.task.createBoardButtonAudioLabel,
|
|
||||||
"on:click": taskPageViewModel.createBoard,
|
|
||||||
"toggle:disabled": taskPageViewModel.cannotCreateBoard
|
|
||||||
},
|
|
||||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
|
|
||||||
))), /* @__PURE__ */ createElement("div", { class: "content" }, /* @__PURE__ */ createElement(
|
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: "grid gap",
|
id: "board-list",
|
||||||
style: "grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))",
|
class: "pane-wrapper side background",
|
||||||
"children:append": [
|
"set:color": taskPageViewModel.chatViewModel.displayedColor
|
||||||
taskPageViewModel.boardViewModels,
|
},
|
||||||
BoardViewModelToEntry
|
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
|
||||||
]
|
"input",
|
||||||
|
{
|
||||||
|
class: "no-outline",
|
||||||
|
"bind:value": taskPageViewModel.newBoardNameInput,
|
||||||
|
"on:enter": taskPageViewModel.createBoard,
|
||||||
|
placeholder: translations.chatPage.task.newBoardNamePlaceholder
|
||||||
|
}
|
||||||
|
), /* @__PURE__ */ createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "primary",
|
||||||
|
"aria-label": translations.chatPage.task.createBoardButtonAudioLabel,
|
||||||
|
"on:click": taskPageViewModel.createBoard,
|
||||||
|
"toggle:disabled": taskPageViewModel.cannotCreateBoard
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
|
||||||
|
))), /* @__PURE__ */ createElement("div", { class: "content" }, /* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "grid gap",
|
||||||
|
style: "grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))",
|
||||||
|
"children:append": [
|
||||||
|
taskPageViewModel.boardViewModels,
|
||||||
|
BoardViewModelToEntry
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
),
|
||||||
|
/* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
id: "board-content",
|
||||||
|
class: "pane-wrapper",
|
||||||
|
"children:set": paneContent
|
||||||
}
|
}
|
||||||
)))
|
)
|
||||||
), /* @__PURE__ */ createElement(
|
);
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "board-content",
|
|
||||||
class: "pane-wrapper",
|
|
||||||
"children:set": paneContent
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/View/chatPage.tsx
|
// src/View/chatPage.tsx
|
||||||
@@ -4399,13 +4576,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement("div", { id: "chat-page-wrapper", "children:set": chatPageContent });
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "chat-page-wrapper",
|
|
||||||
"children:set": chatPageContent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/View/Modals/connectionModal.tsx
|
// src/View/Modals/connectionModal.tsx
|
||||||
@@ -4572,6 +4743,8 @@
|
|||||||
var ConnectionModel = class {
|
var ConnectionModel = class {
|
||||||
// init
|
// init
|
||||||
constructor(storageModel2) {
|
constructor(storageModel2) {
|
||||||
|
this.reconnectInterval = void 0;
|
||||||
|
this.shouldAttemptReconnect = false;
|
||||||
this.connectionChangeHandlerManager = new HandlerManager();
|
this.connectionChangeHandlerManager = new HandlerManager();
|
||||||
this.messageHandlerManager = new HandlerManager();
|
this.messageHandlerManager = new HandlerManager();
|
||||||
this.messageSentHandlerManager = new HandlerManager();
|
this.messageSentHandlerManager = new HandlerManager();
|
||||||
@@ -4583,11 +4756,22 @@
|
|||||||
this.handleConnectionChange = () => {
|
this.handleConnectionChange = () => {
|
||||||
console.log("connection status:", this.isConnected, this.address);
|
console.log("connection status:", this.isConnected, this.address);
|
||||||
this.connectionChangeHandlerManager.trigger();
|
this.connectionChangeHandlerManager.trigger();
|
||||||
if (this.isConnected == false) return;
|
if (this.isConnected == false) {
|
||||||
|
if (this.shouldAttemptReconnect == false) return;
|
||||||
|
this.reconnectInterval = setInterval(() => {
|
||||||
|
if (this.isConnected == true) {
|
||||||
|
clearInterval(this.reconnectInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.reconnect();
|
||||||
|
}, 5e3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.address == void 0) return;
|
if (this.address == void 0) return;
|
||||||
this.storeAddress(this.address);
|
this.storeAddress(this.address);
|
||||||
this.sendSubscriptionRequest();
|
this.sendSubscriptionRequest();
|
||||||
this.sendMessagesInOutbox();
|
this.sendMessagesInOutbox();
|
||||||
|
this.shouldAttemptReconnect = true;
|
||||||
};
|
};
|
||||||
// connection
|
// connection
|
||||||
this.connect = (address) => {
|
this.connect = (address) => {
|
||||||
@@ -4600,6 +4784,14 @@
|
|||||||
filePaths.connectionModel.reconnectAddress
|
filePaths.connectionModel.reconnectAddress
|
||||||
);
|
);
|
||||||
this.storageModel.remove(reconnectAddressPath);
|
this.storageModel.remove(reconnectAddressPath);
|
||||||
|
this.shouldAttemptReconnect = false;
|
||||||
|
};
|
||||||
|
this.reconnect = () => {
|
||||||
|
const reconnectAddressPath = this.getReconnectAddressPath();
|
||||||
|
const reconnectAddress = this.storageModel.read(reconnectAddressPath);
|
||||||
|
if (reconnectAddress == null) return;
|
||||||
|
console.log("reconnecting...");
|
||||||
|
this.connect(reconnectAddress);
|
||||||
};
|
};
|
||||||
// mailbox
|
// mailbox
|
||||||
this.getMailboxPath = (address) => {
|
this.getMailboxPath = (address) => {
|
||||||
@@ -4616,7 +4808,9 @@
|
|||||||
};
|
};
|
||||||
this.connectMailbox = () => {
|
this.connectMailbox = () => {
|
||||||
if (this.address == void 0) return;
|
if (this.address == void 0) return;
|
||||||
const mailboxId = this.storageModel.read(this.getMailboxPath(this.address));
|
const mailboxId = this.storageModel.read(
|
||||||
|
this.getMailboxPath(this.address)
|
||||||
|
);
|
||||||
console.log("connecting mailbox", mailboxId);
|
console.log("connecting mailbox", mailboxId);
|
||||||
if (mailboxId == null) return this.requestNewMailbox();
|
if (mailboxId == null) return this.requestNewMailbox();
|
||||||
this.udn.connectMailbox(mailboxId);
|
this.udn.connectMailbox(mailboxId);
|
||||||
@@ -4742,11 +4936,7 @@
|
|||||||
this.udn.onmailboxconnect = (mailboxId) => {
|
this.udn.onmailboxconnect = (mailboxId) => {
|
||||||
console.log(`using mailbox ${mailboxId}`);
|
console.log(`using mailbox ${mailboxId}`);
|
||||||
};
|
};
|
||||||
const reconnectAddressPath = this.getReconnectAddressPath();
|
this.reconnect();
|
||||||
const reconnectAddress = storageModel2.read(reconnectAddressPath);
|
|
||||||
if (reconnectAddress != null) {
|
|
||||||
this.connect(reconnectAddress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// data
|
// data
|
||||||
get isConnected() {
|
get isConnected() {
|
||||||
@@ -4860,7 +5050,9 @@
|
|||||||
);
|
);
|
||||||
this.filesSentText = createProxyState(
|
this.filesSentText = createProxyState(
|
||||||
[this.filesSentCount],
|
[this.filesSentCount],
|
||||||
() => translations.dataTransferModal.filesSentCount(this.filesSentCount.value)
|
() => translations.dataTransferModal.filesSentCount(
|
||||||
|
this.filesSentCount.value
|
||||||
|
)
|
||||||
);
|
);
|
||||||
this.filePathsReceived = new ListState();
|
this.filePathsReceived = new ListState();
|
||||||
this.filesReceivedCount = createProxyState(
|
this.filesReceivedCount = createProxyState(
|
||||||
@@ -4902,7 +5094,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translations.dataTransferModal.settingsData,
|
label: translations.dataTransferModal.settingsData,
|
||||||
path: StorageModel.getPath("settings" /* SettingsModel */, [])
|
path: StorageModel.getPath(
|
||||||
|
"settings" /* SettingsModel */,
|
||||||
|
[]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const chatModels = this.chatListModel.chatModels;
|
const chatModels = this.chatListModel.chatModels;
|
||||||
@@ -4973,7 +5168,10 @@
|
|||||||
|
|
||||||
// src/View/Modals/dataTransferModal.tsx
|
// src/View/Modals/dataTransferModal.tsx
|
||||||
function DataTransferModalWrapper(connectionViewModel2, fileTransferViewModel2) {
|
function DataTransferModalWrapper(connectionViewModel2, fileTransferViewModel2) {
|
||||||
return /* @__PURE__ */ createElement("div", null, DirectionSelectionModal(connectionViewModel2, fileTransferViewModel2), FileSelectionModal(fileTransferViewModel2), TransferDataDisplayModal(fileTransferViewModel2), TransferDisplayModal(fileTransferViewModel2), TransferDataInputModal(fileTransferViewModel2), DataReceptionModal(fileTransferViewModel2));
|
return /* @__PURE__ */ createElement("div", null, DirectionSelectionModal(
|
||||||
|
connectionViewModel2,
|
||||||
|
fileTransferViewModel2
|
||||||
|
), FileSelectionModal(fileTransferViewModel2), TransferDataDisplayModal(fileTransferViewModel2), TransferDisplayModal(fileTransferViewModel2), TransferDataInputModal(fileTransferViewModel2), DataReceptionModal(fileTransferViewModel2));
|
||||||
}
|
}
|
||||||
function DirectionSelectionModal(connectionViewModel2, fileTransferViewModel2) {
|
function DirectionSelectionModal(connectionViewModel2, fileTransferViewModel2) {
|
||||||
const isPresented = createProxyState(
|
const isPresented = createProxyState(
|
||||||
@@ -4984,7 +5182,14 @@
|
|||||||
[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, translations.dataTransferModal.transferDataHeadline), /* @__PURE__ */ createElement("p", { class: "error", "toggle:hidden": connectionViewModel2.isConnected }, translations.dataTransferModal.notConnectedError), /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.dataTransferModal.transferDataHeadline), /* @__PURE__ */ createElement(
|
||||||
|
"p",
|
||||||
|
{
|
||||||
|
class: "error",
|
||||||
|
"toggle:hidden": connectionViewModel2.isConnected
|
||||||
|
},
|
||||||
|
translations.dataTransferModal.notConnectedError
|
||||||
|
), /* @__PURE__ */ createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: "flex-column gap content-margin-bottom",
|
class: "flex-column gap content-margin-bottom",
|
||||||
@@ -5031,7 +5236,9 @@
|
|||||||
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(...fileOption.path))));
|
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
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
const isPresented = createProxyState(
|
const isPresented = createProxyState(
|
||||||
[fileTransferViewModel2.presentedModal],
|
[fileTransferViewModel2.presentedModal],
|
||||||
@@ -5083,7 +5290,12 @@
|
|||||||
{
|
{
|
||||||
"subscribe:innerText": fileTransferViewModel2.transferChannel
|
"subscribe:innerText": fileTransferViewModel2.transferChannel
|
||||||
}
|
}
|
||||||
))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.dataTransferModal.transferKeyHeadline), /* @__PURE__ */ createElement("b", { "subscribe:innerText": fileTransferViewModel2.transferKey }))))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
|
))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.dataTransferModal.transferKeyHeadline), /* @__PURE__ */ createElement(
|
||||||
|
"b",
|
||||||
|
{
|
||||||
|
"subscribe:innerText": fileTransferViewModel2.transferKey
|
||||||
|
}
|
||||||
|
))))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
class: "flex",
|
class: "flex",
|
||||||
@@ -5279,7 +5491,9 @@
|
|||||||
};
|
};
|
||||||
this.storageModel = storageModel2;
|
this.storageModel = storageModel2;
|
||||||
this.connectionModel = connectionModel2;
|
this.connectionModel = connectionModel2;
|
||||||
this.connectionModel.messageHandlerManager.addHandler(this.handleMessage);
|
this.connectionModel.messageHandlerManager.addHandler(
|
||||||
|
this.handleMessage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var FileDataReference = {
|
var FileDataReference = {
|
||||||
@@ -5399,7 +5613,15 @@
|
|||||||
},
|
},
|
||||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "sync_alt"),
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "sync_alt"),
|
||||||
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.transferDataButton))
|
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.transferDataButton))
|
||||||
), /* @__PURE__ */ createElement("button", { class: "tile flex-no", "on:click": storageViewModel2.showStorageModal }, /* @__PURE__ */ createElement("span", { class: "icon" }, "hard_drive_2"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.manageStorageButton))), /* @__PURE__ */ createElement("div", { class: "mobile-only" }, /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement("button", { class: "ghost width-50", "on:click": scrollToChat }, translations.homePage.scrollToChatButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))));
|
), /* @__PURE__ */ createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "tile flex-no",
|
||||||
|
"on:click": storageViewModel2.showStorageModal
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "hard_drive_2"),
|
||||||
|
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.manageStorageButton))
|
||||||
|
), /* @__PURE__ */ createElement("div", { class: "mobile-only" }, /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement("button", { class: "ghost width-50", "on:click": scrollToChat }, translations.homePage.scrollToChatButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))));
|
||||||
const chatSection = /* @__PURE__ */ createElement("div", { id: "chat-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.chatsHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
|
const chatSection = /* @__PURE__ */ createElement("div", { id: "chat-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.chatsHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
|
||||||
"input",
|
"input",
|
||||||
{
|
{
|
||||||
@@ -5611,17 +5833,23 @@
|
|||||||
this.coreViewModel = coreViewModel;
|
this.coreViewModel = coreViewModel;
|
||||||
// state
|
// state
|
||||||
this.isShowingStorageModal = new State(false);
|
this.isShowingStorageModal = new State(false);
|
||||||
this.selectedPath = new State(PATH_COMPONENT_SEPARATOR);
|
this.selectedPath = new State(
|
||||||
|
PATH_COMPONENT_SEPARATOR
|
||||||
|
);
|
||||||
this.didMakeChanges = new State(false);
|
this.didMakeChanges = new State(false);
|
||||||
this.lastDeletedItemPath = new State("");
|
this.lastDeletedItemPath = new State("");
|
||||||
// methods
|
// methods
|
||||||
this.getSelectedItemContent = () => {
|
this.getSelectedItemContent = () => {
|
||||||
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
|
const path = StorageModel.stringToPathComponents(
|
||||||
|
this.selectedPath.value
|
||||||
|
);
|
||||||
const content = this.storageModel.read(path);
|
const content = this.storageModel.read(path);
|
||||||
return (content ?? translations.storage.notAFile) || translations.storage.contentEmpty;
|
return (content ?? translations.storage.notAFile) || translations.storage.contentEmpty;
|
||||||
};
|
};
|
||||||
this.deleteSelectedItem = () => {
|
this.deleteSelectedItem = () => {
|
||||||
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
|
const path = StorageModel.stringToPathComponents(
|
||||||
|
this.selectedPath.value
|
||||||
|
);
|
||||||
this.lastDeletedItemPath.value = this.selectedPath.value;
|
this.lastDeletedItemPath.value = this.selectedPath.value;
|
||||||
this.storageModel.removeRecursively(path);
|
this.storageModel.removeRecursively(path);
|
||||||
this.didMakeChanges.value = true;
|
this.didMakeChanges.value = true;
|
||||||
@@ -5699,9 +5927,7 @@
|
|||||||
if (primaryChannel == null) return;
|
if (primaryChannel == null) return;
|
||||||
const parsedPriamryChannel = parseOrFallback(primaryChannel);
|
const parsedPriamryChannel = parseOrFallback(primaryChannel);
|
||||||
const chatModel = this.chatListModel.createChat(parsedPriamryChannel);
|
const chatModel = this.chatListModel.createChat(parsedPriamryChannel);
|
||||||
const secondaryChannelString = getLocalStorageItemAndClear(
|
const secondaryChannelString = getLocalStorageItemAndClear(storageKeys.secondaryChannels(id));
|
||||||
storageKeys.secondaryChannels(id)
|
|
||||||
);
|
|
||||||
if (secondaryChannelString != null) {
|
if (secondaryChannelString != null) {
|
||||||
const potentialSecondaryChannels = parseArray(
|
const potentialSecondaryChannels = parseArray(
|
||||||
secondaryChannelString
|
secondaryChannelString
|
||||||
@@ -5744,7 +5970,9 @@
|
|||||||
};
|
};
|
||||||
chatModel.addMessage(convertedChatMessage);
|
chatModel.addMessage(convertedChatMessage);
|
||||||
if (status == "outbox" /* Outbox */) {
|
if (status == "outbox" /* Outbox */) {
|
||||||
this.connectionModel.sendMessageOrStore(convertedChatMessage);
|
this.connectionModel.sendMessageOrStore(
|
||||||
|
convertedChatMessage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -5789,7 +6017,9 @@
|
|||||||
time: potentialVersion.time ?? ""
|
time: potentialVersion.time ?? ""
|
||||||
};
|
};
|
||||||
console.log(convertedTaskFileContent);
|
console.log(convertedTaskFileContent);
|
||||||
chatModel.fileModel.handleFileContent(convertedTaskFileContent);
|
chatModel.fileModel.handleFileContent(
|
||||||
|
convertedTaskFileContent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Comms",
|
"name": "Comms",
|
||||||
"display": "standalone",
|
"display": "fullscreen",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "icon.png",
|
"src": "icon.png",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import ChatModel, { ChatMessage } from "./chatModel";
|
import ChatModel, { ChatMessage } from "./chatModel";
|
||||||
import StorageModel, { StorageModelSubPath, filePaths } from "../Global/storageModel";
|
import StorageModel, {
|
||||||
|
StorageModelSubPath,
|
||||||
|
filePaths,
|
||||||
|
} from "../Global/storageModel";
|
||||||
|
|
||||||
import ConnectionModel from "../Global/connectionModel";
|
import ConnectionModel from "../Global/connectionModel";
|
||||||
import { Message } from "udn-frontend";
|
import { Message } from "udn-frontend";
|
||||||
@@ -7,104 +10,107 @@ import SettingsModel from "../Global/settingsModel";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export default class ChatListModel {
|
export default class ChatListModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
settingsModel: SettingsModel;
|
settingsModel: SettingsModel;
|
||||||
connectionModel: ConnectionModel;
|
connectionModel: ConnectionModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
chatModels = new Set<ChatModel>();
|
chatModels = new Set<ChatModel>();
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
messageHandler = (data: Message): void => {
|
messageHandler = (data: Message): void => {
|
||||||
const channel: string | undefined = data.messageChannel;
|
const channel: string | undefined = data.messageChannel;
|
||||||
const body: string | undefined = data.messageBody;
|
const body: string | undefined = data.messageBody;
|
||||||
if (channel == undefined) return;
|
if (channel == undefined) return;
|
||||||
if (body == undefined) return;
|
if (body == undefined) return;
|
||||||
|
|
||||||
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
||||||
chatModel.handleMessage(body)
|
chatModel.handleMessage(body),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
messageSentHandler = (chatMessage: ChatMessage): void => {
|
messageSentHandler = (chatMessage: ChatMessage): void => {
|
||||||
const channel: string = chatMessage.channel;
|
const channel: string = chatMessage.channel;
|
||||||
|
|
||||||
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
||||||
chatModel.handleMessageSent(chatMessage)
|
chatModel.handleMessageSent(chatMessage),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
routeMessageToCorrectChatModel = (
|
routeMessageToCorrectChatModel = (
|
||||||
channel: string,
|
channel: string,
|
||||||
fn: (chatModel: ChatModel) => void
|
fn: (chatModel: ChatModel) => void,
|
||||||
): void => {
|
): void => {
|
||||||
const allChannels = channel.split("/");
|
const allChannels = channel.split("/");
|
||||||
|
|
||||||
for (const chatModel of this.chatModels) {
|
for (const chatModel of this.chatModels) {
|
||||||
for (const channel of allChannels) {
|
for (const channel of allChannels) {
|
||||||
if (channel != chatModel.info.primaryChannel) continue;
|
if (channel != chatModel.info.primaryChannel) continue;
|
||||||
fn(chatModel);
|
fn(chatModel);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// storage
|
||||||
|
addChatModel = (chatModel: ChatModel) => {
|
||||||
|
this.chatModels.add(chatModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
createChat = (primaryChannel: string): ChatModel => {
|
||||||
|
const id: string = v4();
|
||||||
|
|
||||||
|
const chatModel = new ChatModel(
|
||||||
|
this.storageModel,
|
||||||
|
this.connectionModel,
|
||||||
|
this.settingsModel,
|
||||||
|
this,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
chatModel.setPrimaryChannel(primaryChannel);
|
||||||
|
|
||||||
|
this.addChatModel(chatModel);
|
||||||
|
return chatModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
untrackChat = (chat: ChatModel): void => {
|
||||||
|
this.chatModels.delete(chat);
|
||||||
|
};
|
||||||
|
|
||||||
|
// load
|
||||||
|
loadChats = (): void => {
|
||||||
|
const chatDir = StorageModel.getPath(
|
||||||
|
StorageModelSubPath.Chat,
|
||||||
|
filePaths.chat.base,
|
||||||
|
);
|
||||||
|
const chatIds = this.storageModel.list(chatDir);
|
||||||
|
for (const chatId of chatIds) {
|
||||||
|
const chatModel = new ChatModel(
|
||||||
|
this.storageModel,
|
||||||
|
this.connectionModel,
|
||||||
|
this.settingsModel,
|
||||||
|
this,
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
this.addChatModel(chatModel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
storageModel: StorageModel,
|
||||||
|
settingsModel: SettingsModel,
|
||||||
|
connectionModel: ConnectionModel,
|
||||||
|
) {
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.settingsModel = settingsModel;
|
||||||
|
this.connectionModel = connectionModel;
|
||||||
|
this.loadChats();
|
||||||
|
|
||||||
|
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
|
||||||
|
connectionModel.messageSentHandlerManager.addHandler(
|
||||||
|
this.messageSentHandler,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// storage
|
|
||||||
addChatModel = (chatModel: ChatModel) => {
|
|
||||||
this.chatModels.add(chatModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
createChat = (primaryChannel: string): ChatModel => {
|
|
||||||
const id: string = v4();
|
|
||||||
|
|
||||||
const chatModel = new ChatModel(
|
|
||||||
this.storageModel,
|
|
||||||
this.connectionModel,
|
|
||||||
this.settingsModel,
|
|
||||||
this,
|
|
||||||
id
|
|
||||||
);
|
|
||||||
chatModel.setPrimaryChannel(primaryChannel);
|
|
||||||
|
|
||||||
this.addChatModel(chatModel);
|
|
||||||
return chatModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
untrackChat = (chat: ChatModel): void => {
|
|
||||||
this.chatModels.delete(chat);
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
|
||||||
loadChats = (): void => {
|
|
||||||
const chatDir = StorageModel.getPath(StorageModelSubPath.Chat, filePaths.chat.base);
|
|
||||||
const chatIds = this.storageModel.list(chatDir);
|
|
||||||
for (const chatId of chatIds) {
|
|
||||||
const chatModel = new ChatModel(
|
|
||||||
this.storageModel,
|
|
||||||
this.connectionModel,
|
|
||||||
this.settingsModel,
|
|
||||||
this,
|
|
||||||
chatId
|
|
||||||
);
|
|
||||||
this.addChatModel(chatModel);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
storageModel: StorageModel,
|
|
||||||
settingsModel: SettingsModel,
|
|
||||||
connectionModel: ConnectionModel
|
|
||||||
) {
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.settingsModel = settingsModel;
|
|
||||||
this.connectionModel = connectionModel;
|
|
||||||
this.loadChats();
|
|
||||||
|
|
||||||
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
|
|
||||||
connectionModel.messageSentHandlerManager.addHandler(
|
|
||||||
this.messageSentHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+321
-308
@@ -3,15 +3,15 @@
|
|||||||
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||||
import FileModel, { FileContent } from "../Files/fileModel";
|
import FileModel, { FileContent } from "../Files/fileModel";
|
||||||
import {
|
import {
|
||||||
HandlerManager,
|
HandlerManager,
|
||||||
createTimestamp,
|
createTimestamp,
|
||||||
localeCompare,
|
localeCompare,
|
||||||
parseValidObject,
|
parseValidObject,
|
||||||
stringify,
|
stringify,
|
||||||
} from "../Utility/utility";
|
} from "../Utility/utility";
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
StorageModelSubPath,
|
StorageModelSubPath,
|
||||||
filePaths,
|
filePaths,
|
||||||
} from "../Global/storageModel";
|
} from "../Global/storageModel";
|
||||||
import { decryptString, encryptString } from "../Utility/crypto";
|
import { decryptString, encryptString } from "../Utility/crypto";
|
||||||
|
|
||||||
@@ -22,345 +22,358 @@ import SettingsModel from "../Global/settingsModel";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export default class ChatModel {
|
export default class ChatModel {
|
||||||
connectionModel: ConnectionModel;
|
connectionModel: ConnectionModel;
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
settingsModel: SettingsModel;
|
settingsModel: SettingsModel;
|
||||||
chatListModel: ChatListModel;
|
chatListModel: ChatListModel;
|
||||||
|
|
||||||
fileModel: FileModel;
|
fileModel: FileModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
id: string;
|
id: string;
|
||||||
info: ChatInfo;
|
info: ChatInfo;
|
||||||
color: Color;
|
color: Color;
|
||||||
|
|
||||||
chatMessageHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
|
chatMessageHandlerManager: HandlerManager<ChatMessage> =
|
||||||
|
new HandlerManager();
|
||||||
|
|
||||||
get secondaryChannels(): string[] {
|
get secondaryChannels(): string[] {
|
||||||
return this.info.secondaryChannels.sort(localeCompare);
|
return this.info.secondaryChannels.sort(localeCompare);
|
||||||
}
|
|
||||||
|
|
||||||
// paths
|
|
||||||
getBasePath = (): string[] => {
|
|
||||||
return StorageModel.getPath(
|
|
||||||
StorageModelSubPath.Chat,
|
|
||||||
filePaths.chat.chatBase(this.id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getInfoPath = (): string[] => {
|
|
||||||
return StorageModel.getPath(
|
|
||||||
StorageModelSubPath.Chat,
|
|
||||||
filePaths.chat.info(this.id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getColorPath = (): string[] => {
|
|
||||||
return StorageModel.getPath(
|
|
||||||
StorageModelSubPath.Chat,
|
|
||||||
filePaths.chat.color(this.id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getMessageDirPath = (): string[] => {
|
|
||||||
return StorageModel.getPath(
|
|
||||||
StorageModelSubPath.Chat,
|
|
||||||
filePaths.chat.messages(this.id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getMessagePath = (id: string): string[] => {
|
|
||||||
return [...this.getMessageDirPath(), id];
|
|
||||||
};
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
handleMessage = (body: string): void => {
|
|
||||||
const chatMessage: ChatMessage | null = parseValidObject(
|
|
||||||
body,
|
|
||||||
ChatMessageReference
|
|
||||||
);
|
|
||||||
if (chatMessage == null) return;
|
|
||||||
|
|
||||||
chatMessage.status = ChatMessageStatus.Received;
|
|
||||||
this.addMessage(chatMessage);
|
|
||||||
|
|
||||||
this.info.hasUnreadMessages = true;
|
|
||||||
this.storeInfo();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMessageSent = (chatMessage: ChatMessage): void => {
|
|
||||||
chatMessage.status = ChatMessageStatus.Sent;
|
|
||||||
this.addMessage(chatMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
// settings
|
|
||||||
setPrimaryChannel = (primaryChannel: string): void => {
|
|
||||||
this.info.primaryChannel = primaryChannel;
|
|
||||||
this.storeInfo();
|
|
||||||
this.subscribe();
|
|
||||||
};
|
|
||||||
|
|
||||||
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
|
||||||
this.info.secondaryChannels = secondaryChannels;
|
|
||||||
this.storeInfo();
|
|
||||||
};
|
|
||||||
|
|
||||||
setEncryptionKey = (key: string): void => {
|
|
||||||
this.info.encryptionKey = key;
|
|
||||||
this.storeInfo();
|
|
||||||
};
|
|
||||||
|
|
||||||
setColor = (color: Color): void => {
|
|
||||||
this.color = color;
|
|
||||||
this.storeColor();
|
|
||||||
};
|
|
||||||
|
|
||||||
// messaging
|
|
||||||
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
|
||||||
await this.decryptMessage(chatMessage);
|
|
||||||
|
|
||||||
// message
|
|
||||||
if (chatMessage.body != "") {
|
|
||||||
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
|
||||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
|
||||||
this.chatMessageHandlerManager.trigger(chatMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// paths
|
||||||
this.fileModel.handleStringifiedFileContent(chatMessage.stringifiedFile);
|
getBasePath = (): string[] => {
|
||||||
};
|
return StorageModel.getPath(
|
||||||
|
StorageModelSubPath.Chat,
|
||||||
sendMessage = async (
|
filePaths.chat.chatBase(this.id),
|
||||||
body: string,
|
);
|
||||||
fileContent?: FileContent<string>
|
|
||||||
): Promise<boolean> => {
|
|
||||||
const senderName = this.settingsModel.username;
|
|
||||||
if (senderName == "") return false;
|
|
||||||
|
|
||||||
const allChannels = [this.info.primaryChannel];
|
|
||||||
for (const secondaryChannel of this.info.secondaryChannels) {
|
|
||||||
allChannels.push(secondaryChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedChannel: string = allChannels.join("/");
|
|
||||||
|
|
||||||
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
|
||||||
combinedChannel,
|
|
||||||
senderName,
|
|
||||||
this.info.encryptionKey,
|
|
||||||
body,
|
|
||||||
fileContent
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addMessage(chatMessage);
|
|
||||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
|
||||||
const decryptedBody: string = await decryptString(
|
|
||||||
chatMessage.body,
|
|
||||||
this.info.encryptionKey
|
|
||||||
);
|
|
||||||
const decryptedFile: string = await decryptString(
|
|
||||||
chatMessage.stringifiedFile ?? "",
|
|
||||||
this.info.encryptionKey
|
|
||||||
);
|
|
||||||
chatMessage.body = decryptedBody;
|
|
||||||
chatMessage.stringifiedFile = decryptedFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
subscribe = (): void => {
|
|
||||||
this.connectionModel.addChannel(this.info.primaryChannel);
|
|
||||||
};
|
|
||||||
|
|
||||||
markRead = (): void => {
|
|
||||||
this.info.hasUnreadMessages = false;
|
|
||||||
this.storeInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// storage
|
|
||||||
storeInfo = (): void => {
|
|
||||||
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeColor = (): void => {
|
|
||||||
this.storageModel.write(this.getColorPath(), this.color);
|
|
||||||
};
|
|
||||||
|
|
||||||
delete = () => {
|
|
||||||
// untrack
|
|
||||||
this.chatListModel.untrackChat(this);
|
|
||||||
|
|
||||||
// delete
|
|
||||||
const dirPath: string[] = this.getBasePath();
|
|
||||||
this.storageModel.removeRecursively(dirPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
|
||||||
loadInfo = (): void => {
|
|
||||||
const info: ChatInfo | null = this.storageModel.readStringifiable(
|
|
||||||
this.getInfoPath(),
|
|
||||||
ChatInfoReference
|
|
||||||
);
|
|
||||||
if (info != null) {
|
|
||||||
this.info = info;
|
|
||||||
} else {
|
|
||||||
this.info = ChatModel.generateChatInfo("0");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadColor = (): void => {
|
|
||||||
const path: string[] = this.getColorPath();
|
|
||||||
const color: string | null = this.storageModel.read(path);
|
|
||||||
if (!color) {
|
|
||||||
this.color = Color.Standard;
|
|
||||||
} else {
|
|
||||||
this.color = color as any;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
get messages(): ChatMessage[] {
|
|
||||||
const messageIds: string[] = this.storageModel.list(
|
|
||||||
this.getMessageDirPath()
|
|
||||||
);
|
|
||||||
if (!Array.isArray(messageIds)) return [];
|
|
||||||
|
|
||||||
const chatMessages: ChatMessage[] = [];
|
|
||||||
for (const messageId of messageIds) {
|
|
||||||
const messagePath: string[] = this.getMessagePath(messageId);
|
|
||||||
const chatMessage: ChatMessage | any =
|
|
||||||
this.storageModel.readStringifiable(messagePath, ChatMessageReference);
|
|
||||||
chatMessages.push(chatMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sorted = chatMessages.sort((a, b) =>
|
|
||||||
a.dateSent.localeCompare(b.dateSent)
|
|
||||||
);
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
storageModel: StorageModel,
|
|
||||||
connectionModel: ConnectionModel,
|
|
||||||
settingsModel: SettingsModel,
|
|
||||||
chatListModel: ChatListModel,
|
|
||||||
chatId: string
|
|
||||||
) {
|
|
||||||
this.id = chatId;
|
|
||||||
this.connectionModel = connectionModel;
|
|
||||||
this.settingsModel = settingsModel;
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.chatListModel = chatListModel;
|
|
||||||
|
|
||||||
this.loadInfo();
|
|
||||||
this.loadColor();
|
|
||||||
this.subscribe();
|
|
||||||
|
|
||||||
this.fileModel = new FileModel(this.storageModel, this.settingsModel, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility
|
|
||||||
static generateChatInfo = (primaryChannel: string): ChatInfo => {
|
|
||||||
return {
|
|
||||||
dataVersion: DATA_VERSION,
|
|
||||||
|
|
||||||
primaryChannel,
|
|
||||||
secondaryChannels: [],
|
|
||||||
encryptionKey: "",
|
|
||||||
hasUnreadMessages: false,
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
static createChatMessage = async (
|
getInfoPath = (): string[] => {
|
||||||
channel: string,
|
return StorageModel.getPath(
|
||||||
sender: string,
|
StorageModelSubPath.Chat,
|
||||||
encryptionKey: string,
|
filePaths.chat.info(this.id),
|
||||||
body: string,
|
);
|
||||||
fileContent?: FileContent<string>
|
|
||||||
): Promise<ChatMessage> => {
|
|
||||||
const chatMessage: ChatMessage = {
|
|
||||||
dataVersion: DATA_VERSION,
|
|
||||||
|
|
||||||
id: v4(),
|
|
||||||
|
|
||||||
channel,
|
|
||||||
sender,
|
|
||||||
body,
|
|
||||||
dateSent: createTimestamp(),
|
|
||||||
|
|
||||||
status: ChatMessageStatus.Outbox,
|
|
||||||
stringifiedFile: "",
|
|
||||||
};
|
};
|
||||||
if (fileContent != undefined) {
|
|
||||||
const stringifiedFile = stringify(fileContent);
|
getColorPath = (): string[] => {
|
||||||
chatMessage.stringifiedFile = stringifiedFile;
|
return StorageModel.getPath(
|
||||||
|
StorageModelSubPath.Chat,
|
||||||
|
filePaths.chat.color(this.id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getMessageDirPath = (): string[] => {
|
||||||
|
return StorageModel.getPath(
|
||||||
|
StorageModelSubPath.Chat,
|
||||||
|
filePaths.chat.messages(this.id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getMessagePath = (id: string): string[] => {
|
||||||
|
return [...this.getMessageDirPath(), id];
|
||||||
|
};
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
handleMessage = (body: string): void => {
|
||||||
|
const chatMessage: ChatMessage | null = parseValidObject(
|
||||||
|
body,
|
||||||
|
ChatMessageReference,
|
||||||
|
);
|
||||||
|
if (chatMessage == null) return;
|
||||||
|
|
||||||
|
chatMessage.status = ChatMessageStatus.Received;
|
||||||
|
this.addMessage(chatMessage);
|
||||||
|
|
||||||
|
this.info.hasUnreadMessages = true;
|
||||||
|
this.storeInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMessageSent = (chatMessage: ChatMessage): void => {
|
||||||
|
chatMessage.status = ChatMessageStatus.Sent;
|
||||||
|
this.addMessage(chatMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
// settings
|
||||||
|
setPrimaryChannel = (primaryChannel: string): void => {
|
||||||
|
this.info.primaryChannel = primaryChannel;
|
||||||
|
this.storeInfo();
|
||||||
|
this.subscribe();
|
||||||
|
};
|
||||||
|
|
||||||
|
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||||
|
this.info.secondaryChannels = secondaryChannels;
|
||||||
|
this.storeInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
setEncryptionKey = (key: string): void => {
|
||||||
|
this.info.encryptionKey = key;
|
||||||
|
this.storeInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
setColor = (color: Color): void => {
|
||||||
|
this.color = color;
|
||||||
|
this.storeColor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// messaging
|
||||||
|
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||||
|
await this.decryptMessage(chatMessage);
|
||||||
|
|
||||||
|
// message
|
||||||
|
if (chatMessage.body != "") {
|
||||||
|
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||||
|
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||||
|
this.chatMessageHandlerManager.trigger(chatMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// file
|
||||||
|
this.fileModel.handleStringifiedFileContent(
|
||||||
|
chatMessage.stringifiedFile,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
sendMessage = async (
|
||||||
|
body: string,
|
||||||
|
fileContent?: FileContent<string>,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const senderName = this.settingsModel.username;
|
||||||
|
if (senderName == "") return false;
|
||||||
|
|
||||||
|
const allChannels = [this.info.primaryChannel];
|
||||||
|
for (const secondaryChannel of this.info.secondaryChannels) {
|
||||||
|
allChannels.push(secondaryChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedChannel: string = allChannels.join("/");
|
||||||
|
|
||||||
|
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
||||||
|
combinedChannel,
|
||||||
|
senderName,
|
||||||
|
this.info.encryptionKey,
|
||||||
|
body,
|
||||||
|
fileContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addMessage(chatMessage);
|
||||||
|
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||||
|
const decryptedBody: string = await decryptString(
|
||||||
|
chatMessage.body,
|
||||||
|
this.info.encryptionKey,
|
||||||
|
);
|
||||||
|
const decryptedFile: string = await decryptString(
|
||||||
|
chatMessage.stringifiedFile ?? "",
|
||||||
|
this.info.encryptionKey,
|
||||||
|
);
|
||||||
|
chatMessage.body = decryptedBody;
|
||||||
|
chatMessage.stringifiedFile = decryptedFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribe = (): void => {
|
||||||
|
this.connectionModel.addChannel(this.info.primaryChannel);
|
||||||
|
};
|
||||||
|
|
||||||
|
markRead = (): void => {
|
||||||
|
this.info.hasUnreadMessages = false;
|
||||||
|
this.storeInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
// storage
|
||||||
|
storeInfo = (): void => {
|
||||||
|
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
||||||
|
};
|
||||||
|
|
||||||
|
storeColor = (): void => {
|
||||||
|
this.storageModel.write(this.getColorPath(), this.color);
|
||||||
|
};
|
||||||
|
|
||||||
|
delete = () => {
|
||||||
|
// untrack
|
||||||
|
this.chatListModel.untrackChat(this);
|
||||||
|
|
||||||
|
// delete
|
||||||
|
const dirPath: string[] = this.getBasePath();
|
||||||
|
this.storageModel.removeRecursively(dirPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
// load
|
||||||
|
loadInfo = (): void => {
|
||||||
|
const info: ChatInfo | null = this.storageModel.readStringifiable(
|
||||||
|
this.getInfoPath(),
|
||||||
|
ChatInfoReference,
|
||||||
|
);
|
||||||
|
if (info != null) {
|
||||||
|
this.info = info;
|
||||||
|
} else {
|
||||||
|
this.info = ChatModel.generateChatInfo("0");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadColor = (): void => {
|
||||||
|
const path: string[] = this.getColorPath();
|
||||||
|
const color: string | null = this.storageModel.read(path);
|
||||||
|
if (!color) {
|
||||||
|
this.color = Color.Standard;
|
||||||
|
} else {
|
||||||
|
this.color = color as any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get messages(): ChatMessage[] {
|
||||||
|
const messageIds: string[] = this.storageModel.list(
|
||||||
|
this.getMessageDirPath(),
|
||||||
|
);
|
||||||
|
if (!Array.isArray(messageIds)) return [];
|
||||||
|
|
||||||
|
const chatMessages: ChatMessage[] = [];
|
||||||
|
for (const messageId of messageIds) {
|
||||||
|
const messagePath: string[] = this.getMessagePath(messageId);
|
||||||
|
const chatMessage: ChatMessage | any =
|
||||||
|
this.storageModel.readStringifiable(
|
||||||
|
messagePath,
|
||||||
|
ChatMessageReference,
|
||||||
|
);
|
||||||
|
chatMessages.push(chatMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = chatMessages.sort((a, b) =>
|
||||||
|
a.dateSent.localeCompare(b.dateSent),
|
||||||
|
);
|
||||||
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encryptionKey != "") {
|
// init
|
||||||
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
|
constructor(
|
||||||
chatMessage.stringifiedFile = await encryptString(
|
storageModel: StorageModel,
|
||||||
chatMessage.stringifiedFile,
|
connectionModel: ConnectionModel,
|
||||||
encryptionKey
|
settingsModel: SettingsModel,
|
||||||
);
|
chatListModel: ChatListModel,
|
||||||
|
chatId: string,
|
||||||
|
) {
|
||||||
|
this.id = chatId;
|
||||||
|
this.connectionModel = connectionModel;
|
||||||
|
this.settingsModel = settingsModel;
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.chatListModel = chatListModel;
|
||||||
|
|
||||||
|
this.loadInfo();
|
||||||
|
this.loadColor();
|
||||||
|
this.subscribe();
|
||||||
|
|
||||||
|
this.fileModel = new FileModel(
|
||||||
|
this.storageModel,
|
||||||
|
this.settingsModel,
|
||||||
|
this,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return chatMessage;
|
// utility
|
||||||
};
|
static generateChatInfo = (primaryChannel: string): ChatInfo => {
|
||||||
|
return {
|
||||||
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
|
primaryChannel,
|
||||||
|
secondaryChannels: [],
|
||||||
|
encryptionKey: "",
|
||||||
|
hasUnreadMessages: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static createChatMessage = async (
|
||||||
|
channel: string,
|
||||||
|
sender: string,
|
||||||
|
encryptionKey: string,
|
||||||
|
body: string,
|
||||||
|
fileContent?: FileContent<string>,
|
||||||
|
): Promise<ChatMessage> => {
|
||||||
|
const chatMessage: ChatMessage = {
|
||||||
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
|
id: v4(),
|
||||||
|
|
||||||
|
channel,
|
||||||
|
sender,
|
||||||
|
body,
|
||||||
|
dateSent: createTimestamp(),
|
||||||
|
|
||||||
|
status: ChatMessageStatus.Outbox,
|
||||||
|
stringifiedFile: "",
|
||||||
|
};
|
||||||
|
if (fileContent != undefined) {
|
||||||
|
const stringifiedFile = stringify(fileContent);
|
||||||
|
chatMessage.stringifiedFile = stringifiedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encryptionKey != "") {
|
||||||
|
chatMessage.body = await encryptString(
|
||||||
|
chatMessage.body,
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
|
chatMessage.stringifiedFile = await encryptString(
|
||||||
|
chatMessage.stringifiedFile,
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatMessage;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export interface ChatInfo extends ValidObject {
|
export interface ChatInfo extends ValidObject {
|
||||||
primaryChannel: string;
|
primaryChannel: string;
|
||||||
secondaryChannels: string[];
|
secondaryChannels: string[];
|
||||||
encryptionKey: string;
|
encryptionKey: string;
|
||||||
|
|
||||||
hasUnreadMessages: boolean;
|
hasUnreadMessages: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChatMessageStatus {
|
export enum ChatMessageStatus {
|
||||||
Outbox = "outbox",
|
Outbox = "outbox",
|
||||||
Sent = "sent",
|
Sent = "sent",
|
||||||
Received = "received",
|
Received = "received",
|
||||||
Other = "other",
|
Other = "other",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage extends ValidObject {
|
export interface ChatMessage extends ValidObject {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
channel: string;
|
channel: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
body: string;
|
body: string;
|
||||||
dateSent: string;
|
dateSent: string;
|
||||||
|
|
||||||
status: ChatMessageStatus;
|
status: ChatMessageStatus;
|
||||||
|
|
||||||
stringifiedFile: string;
|
stringifiedFile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// references
|
// references
|
||||||
export const ChatInfoReference: ChatInfo = {
|
export const ChatInfoReference: ChatInfo = {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
primaryChannel: "",
|
primaryChannel: "",
|
||||||
secondaryChannels: [""],
|
secondaryChannels: [""],
|
||||||
encryptionKey: "",
|
encryptionKey: "",
|
||||||
|
|
||||||
hasUnreadMessages: true,
|
hasUnreadMessages: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChatMessageReference: ChatMessage = {
|
export const ChatMessageReference: ChatMessage = {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
id: "",
|
id: "",
|
||||||
|
|
||||||
channel: "",
|
channel: "",
|
||||||
sender: "",
|
sender: "",
|
||||||
body: "",
|
body: "",
|
||||||
dateSent: "",
|
dateSent: "",
|
||||||
|
|
||||||
status: "" as ChatMessageStatus,
|
status: "" as ChatMessageStatus,
|
||||||
|
|
||||||
stringifiedFile: "",
|
stringifiedFile: "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// this file is responsible for managing boards within chats and tasks within boards.
|
// this file is responsible for managing boards within chats and tasks within boards.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DATA_VERSION,
|
DATA_VERSION,
|
||||||
checkMatchesObjectStructure,
|
checkMatchesObjectStructure,
|
||||||
} from "../Utility/typeSafety";
|
} from "../Utility/typeSafety";
|
||||||
import FileModel, { FileContent, FileModelSubPath } from "./fileModel";
|
import FileModel, { FileContent, FileModelSubPath } from "./fileModel";
|
||||||
|
|
||||||
@@ -15,316 +15,324 @@ import StorageModel from "../Global/storageModel";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export default class BoardsAndTasksModel {
|
export default class BoardsAndTasksModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
settingsModel: SettingsModel;
|
settingsModel: SettingsModel;
|
||||||
chatModel: ChatModel;
|
chatModel: ChatModel;
|
||||||
fileModel: FileModel;
|
fileModel: FileModel;
|
||||||
|
|
||||||
calendarModel: CalendarModel;
|
calendarModel: CalendarModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
|
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
|
||||||
new HandlerManager();
|
new HandlerManager();
|
||||||
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
|
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
getBasePath = (): string[] => {
|
getBasePath = (): string[] => {
|
||||||
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
|
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
|
||||||
};
|
};
|
||||||
|
|
||||||
getViewPath = (): string[] => {
|
getViewPath = (): string[] => {
|
||||||
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
||||||
};
|
};
|
||||||
|
|
||||||
getBoardFilePath = (boardId: string): string[] => {
|
getBoardFilePath = (boardId: string): string[] => {
|
||||||
return [...this.fileModel.getFilePath(boardId)];
|
return [...this.fileModel.getFilePath(boardId)];
|
||||||
};
|
};
|
||||||
|
|
||||||
getTaskFilePath = (taskId: string): string[] => {
|
getTaskFilePath = (taskId: string): string[] => {
|
||||||
return [...this.fileModel.getFilePath(taskId)];
|
return [...this.fileModel.getFilePath(taskId)];
|
||||||
};
|
};
|
||||||
|
|
||||||
getBoardContainerPath = (): string[] => {
|
getBoardContainerPath = (): string[] => {
|
||||||
return [...this.getBasePath(), TaskModelSubPaths.Boards];
|
return [...this.getBasePath(), TaskModelSubPaths.Boards];
|
||||||
};
|
};
|
||||||
|
|
||||||
getBoardDirectoryPath = (boardId: string): string[] => {
|
getBoardDirectoryPath = (boardId: string): string[] => {
|
||||||
return [...this.getBoardContainerPath(), boardId];
|
return [...this.getBoardContainerPath(), boardId];
|
||||||
};
|
};
|
||||||
|
|
||||||
getTaskContainerPath = (boardId: string): string[] => {
|
getTaskContainerPath = (boardId: string): string[] => {
|
||||||
return [
|
return [
|
||||||
...this.getBoardDirectoryPath(boardId),
|
...this.getBoardDirectoryPath(boardId),
|
||||||
TaskModelSubPaths.BoardTasks,
|
TaskModelSubPaths.BoardTasks,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
|
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
|
||||||
return [...this.getTaskContainerPath(boardId), fileId];
|
return [...this.getTaskContainerPath(boardId), fileId];
|
||||||
};
|
};
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
handleFileContent = (fileContent: FileContent<string>): void => {
|
handleFileContent = (fileContent: FileContent<string>): void => {
|
||||||
if (
|
if (
|
||||||
checkMatchesObjectStructure(fileContent, BoardInfoFileContentReference) ==
|
checkMatchesObjectStructure(
|
||||||
true
|
fileContent,
|
||||||
|
BoardInfoFileContentReference,
|
||||||
|
) == true
|
||||||
|
) {
|
||||||
|
this.handleBoard(fileContent as BoardInfoFileContent);
|
||||||
|
} else if (
|
||||||
|
checkMatchesObjectStructure(
|
||||||
|
fileContent,
|
||||||
|
TaskFileContentReference,
|
||||||
|
) == true
|
||||||
|
) {
|
||||||
|
this.handleTask(fileContent as TaskFileContent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
|
||||||
|
this.updateBoard(boardInfoFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTask = (taskFileContent: TaskFileContent) => {
|
||||||
|
this.updateTask(taskFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
// boards
|
||||||
|
createBoard = (name: string): BoardInfoFileContent => {
|
||||||
|
const boardInfoFileContent: BoardInfoFileContent =
|
||||||
|
BoardsAndTasksModel.createBoardInfoFileContent(
|
||||||
|
v4(),
|
||||||
|
name,
|
||||||
|
Color.Standard,
|
||||||
|
);
|
||||||
|
return boardInfoFileContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||||
|
this.storeBoard(boardInfoFileContent);
|
||||||
|
this.boardHandlerManager.trigger(boardInfoFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||||
|
this.updateBoard(boardInfoFileContent);
|
||||||
|
this.chatModel.sendMessage("", boardInfoFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||||
|
// store info
|
||||||
|
this.fileModel.storeFileContent(boardInfoFileContent);
|
||||||
|
|
||||||
|
// add to list
|
||||||
|
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
|
||||||
|
boardInfoFileContent.fileId,
|
||||||
|
);
|
||||||
|
this.storageModel.write(boardDirectoryPath, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteBoard = (boardId: string): void => {
|
||||||
|
const boardFilePath: string[] = this.getBoardFilePath(boardId);
|
||||||
|
const boardDirectoryPath: string[] =
|
||||||
|
this.getBoardDirectoryPath(boardId);
|
||||||
|
|
||||||
|
this.storageModel.removeRecursively(boardFilePath);
|
||||||
|
this.storageModel.removeRecursively(boardDirectoryPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
listBoardIds = (): string[] => {
|
||||||
|
const boardContainerPath: string[] = this.getBoardContainerPath();
|
||||||
|
const boardIds: string[] = this.storageModel.list(boardContainerPath);
|
||||||
|
return boardIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
|
||||||
|
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
|
||||||
|
this.fileModel.getLatestFileContent(
|
||||||
|
fileId,
|
||||||
|
BoardInfoFileContentReference,
|
||||||
|
);
|
||||||
|
return boardInfoFileContentOrNull;
|
||||||
|
};
|
||||||
|
|
||||||
|
getBoardName = (boardId: string): string => {
|
||||||
|
const boardInfo: BoardInfoFileContent | null =
|
||||||
|
this.getBoardInfo(boardId);
|
||||||
|
if (boardInfo == null) return "";
|
||||||
|
return boardInfo.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
//tasks
|
||||||
|
createTask = (boardId: string): TaskFileContent => {
|
||||||
|
const taskFileContent: TaskFileContent =
|
||||||
|
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
|
||||||
|
return taskFileContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTask = (taskFileContent: TaskFileContent): void => {
|
||||||
|
this.storeTask(taskFileContent);
|
||||||
|
this.taskHandlerManager.trigger(taskFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
|
||||||
|
this.updateTask(taskFileContent);
|
||||||
|
this.chatModel.sendMessage("", taskFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
storeTask = (taskFileContent: TaskFileContent): void => {
|
||||||
|
// store info
|
||||||
|
this.fileModel.storeFileContent(taskFileContent);
|
||||||
|
|
||||||
|
// add to board
|
||||||
|
const taskReferencePath: string[] = this.getTaskReferencePath(
|
||||||
|
taskFileContent.boardId,
|
||||||
|
taskFileContent.fileId,
|
||||||
|
);
|
||||||
|
this.storageModel.write(taskReferencePath, "");
|
||||||
|
|
||||||
|
// add to calendar
|
||||||
|
this.calendarModel.storeTaskReference(taskFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
listTaskIds = (boardId: string): string[] => {
|
||||||
|
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
|
||||||
|
const fileIds: string[] = this.storageModel.list(taskContainerPath);
|
||||||
|
return fileIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
listTaskVersionIds = (taskId: string): string[] => {
|
||||||
|
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
|
||||||
|
return versionIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
|
||||||
|
const taskFileContentOrNull: TaskFileContent | null =
|
||||||
|
this.fileModel.getLatestFileContent(
|
||||||
|
taskId,
|
||||||
|
TaskFileContentReference,
|
||||||
|
);
|
||||||
|
return taskFileContentOrNull;
|
||||||
|
};
|
||||||
|
|
||||||
|
getSpecificTaskFileContent = (
|
||||||
|
taskId: string,
|
||||||
|
versionId: string,
|
||||||
|
): TaskFileContent | null => {
|
||||||
|
const taskFileContentOrNull: TaskFileContent | null =
|
||||||
|
this.fileModel.getFileContent(
|
||||||
|
taskId,
|
||||||
|
versionId,
|
||||||
|
TaskFileContentReference,
|
||||||
|
);
|
||||||
|
return taskFileContentOrNull;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTask = (boardId: string, taskId: string): void => {
|
||||||
|
const taskFilePath: string[] = this.getTaskFilePath(taskId);
|
||||||
|
this.storageModel.removeRecursively(taskFilePath);
|
||||||
|
|
||||||
|
this.deleteTaskReference(boardId, taskId);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTaskReference = (boardId: string, taskId: string): void => {
|
||||||
|
const taskReferencePath: string[] = this.getTaskReferencePath(
|
||||||
|
boardId,
|
||||||
|
taskId,
|
||||||
|
);
|
||||||
|
this.storageModel.removeRecursively(taskReferencePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
storageModel: StorageModel,
|
||||||
|
settingsModel: SettingsModel,
|
||||||
|
chatModel: ChatModel,
|
||||||
|
fileModel: FileModel,
|
||||||
) {
|
) {
|
||||||
this.handleBoard(fileContent as BoardInfoFileContent);
|
this.storageModel = storageModel;
|
||||||
} else if (
|
this.settingsModel = settingsModel;
|
||||||
checkMatchesObjectStructure(fileContent, TaskFileContentReference) == true
|
this.chatModel = chatModel;
|
||||||
) {
|
this.fileModel = fileModel;
|
||||||
this.handleTask(fileContent as TaskFileContent);
|
|
||||||
|
this.calendarModel = new CalendarModel(
|
||||||
|
this.storageModel,
|
||||||
|
this.settingsModel,
|
||||||
|
this.fileModel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
|
// utility
|
||||||
this.updateBoard(boardInfoFileContent);
|
static createBoardInfoFileContent = (
|
||||||
};
|
fileId: string,
|
||||||
|
name: string,
|
||||||
|
color: Color,
|
||||||
|
): BoardInfoFileContent => {
|
||||||
|
const fileContent: FileContent<"board-info"> =
|
||||||
|
FileModel.createFileContent(fileId, "board-info");
|
||||||
|
return {
|
||||||
|
...fileContent,
|
||||||
|
|
||||||
handleTask = (taskFileContent: TaskFileContent) => {
|
name,
|
||||||
this.updateTask(taskFileContent);
|
color,
|
||||||
};
|
};
|
||||||
|
|
||||||
// boards
|
|
||||||
createBoard = (name: string): BoardInfoFileContent => {
|
|
||||||
const boardInfoFileContent: BoardInfoFileContent =
|
|
||||||
BoardsAndTasksModel.createBoardInfoFileContent(
|
|
||||||
v4(),
|
|
||||||
name,
|
|
||||||
Color.Standard
|
|
||||||
);
|
|
||||||
return boardInfoFileContent;
|
|
||||||
};
|
|
||||||
|
|
||||||
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
|
||||||
this.storeBoard(boardInfoFileContent);
|
|
||||||
this.boardHandlerManager.trigger(boardInfoFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
|
|
||||||
this.updateBoard(boardInfoFileContent);
|
|
||||||
this.chatModel.sendMessage("", boardInfoFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
|
||||||
// store info
|
|
||||||
this.fileModel.storeFileContent(boardInfoFileContent);
|
|
||||||
|
|
||||||
// add to list
|
|
||||||
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
|
|
||||||
boardInfoFileContent.fileId
|
|
||||||
);
|
|
||||||
this.storageModel.write(boardDirectoryPath, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteBoard = (boardId: string): void => {
|
|
||||||
const boardFilePath: string[] = this.getBoardFilePath(boardId);
|
|
||||||
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(boardId);
|
|
||||||
|
|
||||||
this.storageModel.removeRecursively(boardFilePath);
|
|
||||||
this.storageModel.removeRecursively(boardDirectoryPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
listBoardIds = (): string[] => {
|
|
||||||
const boardContainerPath: string[] = this.getBoardContainerPath();
|
|
||||||
const boardIds: string[] = this.storageModel.list(boardContainerPath);
|
|
||||||
return boardIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
|
|
||||||
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
|
|
||||||
this.fileModel.getLatestFileContent(
|
|
||||||
fileId,
|
|
||||||
BoardInfoFileContentReference
|
|
||||||
);
|
|
||||||
return boardInfoFileContentOrNull;
|
|
||||||
};
|
|
||||||
|
|
||||||
getBoardName = (boardId: string): string => {
|
|
||||||
const boardInfo: BoardInfoFileContent|null = this.getBoardInfo(boardId);
|
|
||||||
if (boardInfo == null) return "";
|
|
||||||
return boardInfo.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
//tasks
|
|
||||||
createTask = (boardId: string): TaskFileContent => {
|
|
||||||
const taskFileContent: TaskFileContent =
|
|
||||||
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
|
|
||||||
return taskFileContent;
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTask = (taskFileContent: TaskFileContent): void => {
|
|
||||||
this.storeTask(taskFileContent);
|
|
||||||
this.taskHandlerManager.trigger(taskFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
|
|
||||||
this.updateTask(taskFileContent);
|
|
||||||
this.chatModel.sendMessage("", taskFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeTask = (taskFileContent: TaskFileContent): void => {
|
|
||||||
// store info
|
|
||||||
this.fileModel.storeFileContent(taskFileContent);
|
|
||||||
|
|
||||||
// add to board
|
|
||||||
const taskReferencePath: string[] = this.getTaskReferencePath(
|
|
||||||
taskFileContent.boardId,
|
|
||||||
taskFileContent.fileId
|
|
||||||
);
|
|
||||||
this.storageModel.write(taskReferencePath, "");
|
|
||||||
|
|
||||||
// add to calendar
|
|
||||||
this.calendarModel.storeTaskReference(taskFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
listTaskIds = (boardId: string): string[] => {
|
|
||||||
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
|
|
||||||
const fileIds: string[] = this.storageModel.list(taskContainerPath);
|
|
||||||
return fileIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
listTaskVersionIds = (taskId: string): string[] => {
|
|
||||||
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
|
|
||||||
return versionIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
|
|
||||||
const taskFileContentOrNull: TaskFileContent | null =
|
|
||||||
this.fileModel.getLatestFileContent(taskId, TaskFileContentReference);
|
|
||||||
return taskFileContentOrNull;
|
|
||||||
};
|
|
||||||
|
|
||||||
getSpecificTaskFileContent = (
|
|
||||||
taskId: string,
|
|
||||||
versionId: string
|
|
||||||
): TaskFileContent | null => {
|
|
||||||
const taskFileContentOrNull: TaskFileContent | null =
|
|
||||||
this.fileModel.getFileContent(
|
|
||||||
taskId,
|
|
||||||
versionId,
|
|
||||||
TaskFileContentReference
|
|
||||||
);
|
|
||||||
return taskFileContentOrNull;
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteTask = (boardId: string, taskId: string): void => {
|
|
||||||
const taskFilePath: string[] = this.getTaskFilePath(taskId);
|
|
||||||
this.storageModel.removeRecursively(taskFilePath);
|
|
||||||
|
|
||||||
this.deleteTaskReference(boardId, taskId);
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteTaskReference = (boardId: string, taskId: string): void => {
|
|
||||||
const taskReferencePath: string[] = this.getTaskReferencePath(
|
|
||||||
boardId,
|
|
||||||
taskId
|
|
||||||
);
|
|
||||||
this.storageModel.removeRecursively(taskReferencePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
storageModel: StorageModel,
|
|
||||||
settingsModel: SettingsModel,
|
|
||||||
chatModel: ChatModel,
|
|
||||||
fileModel: FileModel
|
|
||||||
) {
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.settingsModel = settingsModel;
|
|
||||||
this.chatModel = chatModel;
|
|
||||||
this.fileModel = fileModel;
|
|
||||||
|
|
||||||
this.calendarModel = new CalendarModel(
|
|
||||||
this.storageModel,
|
|
||||||
this.settingsModel,
|
|
||||||
this.fileModel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility
|
|
||||||
static createBoardInfoFileContent = (
|
|
||||||
fileId: string,
|
|
||||||
name: string,
|
|
||||||
color: Color
|
|
||||||
): BoardInfoFileContent => {
|
|
||||||
const fileContent: FileContent<"board-info"> = FileModel.createFileContent(
|
|
||||||
fileId,
|
|
||||||
"board-info"
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...fileContent,
|
|
||||||
|
|
||||||
name,
|
|
||||||
color,
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
static createTaskFileContent = (
|
static createTaskFileContent = (
|
||||||
fileId: string,
|
fileId: string,
|
||||||
name: string,
|
name: string,
|
||||||
boardId: string
|
boardId: string,
|
||||||
): TaskFileContent => {
|
): TaskFileContent => {
|
||||||
const fileContent: FileContent<"task"> = FileModel.createFileContent(
|
const fileContent: FileContent<"task"> = FileModel.createFileContent(
|
||||||
fileId,
|
fileId,
|
||||||
"task"
|
"task",
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...fileContent,
|
...fileContent,
|
||||||
|
|
||||||
name,
|
name,
|
||||||
boardId,
|
boardId,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TaskModelSubPaths {
|
export enum TaskModelSubPaths {
|
||||||
Boards = "boards",
|
Boards = "boards",
|
||||||
BoardTasks = "tasks",
|
BoardTasks = "tasks",
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export interface BoardInfoFileContent extends FileContent<"board-info"> {
|
export interface BoardInfoFileContent extends FileContent<"board-info"> {
|
||||||
name: string;
|
name: string;
|
||||||
color: Color;
|
color: Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskFileContent extends FileContent<"task"> {
|
export interface TaskFileContent extends FileContent<"task"> {
|
||||||
name: string;
|
name: string;
|
||||||
boardId: string;
|
boardId: string;
|
||||||
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
category?: string;
|
category?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
priority?: string;
|
priority?: string;
|
||||||
|
|
||||||
date?: string;
|
date?: string;
|
||||||
time?: string;
|
time?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reference
|
// reference
|
||||||
export const BoardInfoFileContentReference: BoardInfoFileContent = {
|
export const BoardInfoFileContentReference: BoardInfoFileContent = {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
fileId: "string",
|
fileId: "string",
|
||||||
fileContentId: "",
|
fileContentId: "",
|
||||||
creationDate: "",
|
creationDate: "",
|
||||||
|
|
||||||
type: "board-info",
|
type: "board-info",
|
||||||
|
|
||||||
name: "",
|
name: "",
|
||||||
color: "" as Color,
|
color: "" as Color,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TaskFileContentReference: TaskFileContent = {
|
export const TaskFileContentReference: TaskFileContent = {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
fileId: "string",
|
fileId: "string",
|
||||||
fileContentId: "",
|
fileContentId: "",
|
||||||
creationDate: "",
|
creationDate: "",
|
||||||
type: "task",
|
type: "task",
|
||||||
|
|
||||||
name: "",
|
name: "",
|
||||||
boardId: "",
|
boardId: "",
|
||||||
};
|
};
|
||||||
|
|||||||
+138
-136
@@ -7,158 +7,160 @@ import StorageModel from "../Global/storageModel";
|
|||||||
import { TaskFileContent } from "./boardsAndTasksModel";
|
import { TaskFileContent } from "./boardsAndTasksModel";
|
||||||
|
|
||||||
export default class CalendarModel {
|
export default class CalendarModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
fileModel: FileModel;
|
fileModel: FileModel;
|
||||||
settingsModel: SettingsModel;
|
settingsModel: SettingsModel;
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
getBasePath = (): string[] => {
|
getBasePath = (): string[] => {
|
||||||
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelCalendar);
|
return this.fileModel.getModelContainerPath(
|
||||||
};
|
FileModelSubPath.ModelCalendar,
|
||||||
|
);
|
||||||
getViewPath = (): string[] => {
|
|
||||||
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
|
||||||
};
|
|
||||||
|
|
||||||
getMonthContainerPath = (): string[] => {
|
|
||||||
return [...this.getBasePath(), CalendarModelSubPaths.Months];
|
|
||||||
};
|
|
||||||
|
|
||||||
getMonthPath = (monthString: string): string[] => {
|
|
||||||
return [...this.getMonthContainerPath(), monthString];
|
|
||||||
};
|
|
||||||
|
|
||||||
// main
|
|
||||||
storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
|
||||||
if (taskFileContent.date == undefined) return;
|
|
||||||
|
|
||||||
const monthString: string = CalendarModel.isoToMonthString(
|
|
||||||
taskFileContent.date
|
|
||||||
);
|
|
||||||
|
|
||||||
const monthPath: string[] = this.getMonthPath(monthString);
|
|
||||||
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
|
||||||
|
|
||||||
this.storageModel.write(referencePath, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteTaskReference = (monthString: string, taskId: string): void => {
|
|
||||||
const monthPath: string[] = this.getMonthPath(monthString);
|
|
||||||
const referencePath: string[] = [...monthPath, taskId];
|
|
||||||
|
|
||||||
this.storageModel.write(referencePath, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
listTaskIds = (monthString: string): string[] => {
|
|
||||||
const monthPath: string[] = this.getMonthPath(monthString);
|
|
||||||
return this.storageModel.list(monthPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateMonthGrid = <T>(
|
|
||||||
year: number,
|
|
||||||
month: number,
|
|
||||||
defaultValueCreator: () => T
|
|
||||||
): MonthGrid<T> => {
|
|
||||||
// get today
|
|
||||||
const date: Date = new Date();
|
|
||||||
const isCurrentMonth: boolean =
|
|
||||||
year == date.getFullYear() && month == date.getMonth() + 1;
|
|
||||||
|
|
||||||
// get data for offset
|
|
||||||
date.setDate(1);
|
|
||||||
date.setMonth(month - 1);
|
|
||||||
date.setFullYear(year);
|
|
||||||
|
|
||||||
const firstWeekdayOfMonth = date.getDay();
|
|
||||||
const firstDayOfWeekSetting: number = parseInt(
|
|
||||||
this.settingsModel.firstDayOfWeek
|
|
||||||
);
|
|
||||||
|
|
||||||
const offset: number =
|
|
||||||
firstWeekdayOfMonth < firstDayOfWeekSetting
|
|
||||||
? 7 - firstDayOfWeekSetting
|
|
||||||
: firstWeekdayOfMonth - firstDayOfWeekSetting;
|
|
||||||
|
|
||||||
// get total day count
|
|
||||||
date.setMonth(month);
|
|
||||||
date.setDate(-1);
|
|
||||||
const daysInMonth: number = date.getDate() + 1;
|
|
||||||
|
|
||||||
const grid: MonthGrid<T> = {
|
|
||||||
offset,
|
|
||||||
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
|
|
||||||
isCurrentMonth,
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
days: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < daysInMonth; i++) {
|
getViewPath = (): string[] => {
|
||||||
const paddedDate: string = CalendarModel.padDateOrMonth(
|
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
||||||
(i + 1).toString()
|
};
|
||||||
);
|
|
||||||
grid.days[paddedDate] = defaultValueCreator();
|
getMonthContainerPath = (): string[] => {
|
||||||
|
return [...this.getBasePath(), CalendarModelSubPaths.Months];
|
||||||
|
};
|
||||||
|
|
||||||
|
getMonthPath = (monthString: string): string[] => {
|
||||||
|
return [...this.getMonthContainerPath(), monthString];
|
||||||
|
};
|
||||||
|
|
||||||
|
// main
|
||||||
|
storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
||||||
|
if (taskFileContent.date == undefined) return;
|
||||||
|
|
||||||
|
const monthString: string = CalendarModel.isoToMonthString(
|
||||||
|
taskFileContent.date,
|
||||||
|
);
|
||||||
|
|
||||||
|
const monthPath: string[] = this.getMonthPath(monthString);
|
||||||
|
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
||||||
|
|
||||||
|
this.storageModel.write(referencePath, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTaskReference = (monthString: string, taskId: string): void => {
|
||||||
|
const monthPath: string[] = this.getMonthPath(monthString);
|
||||||
|
const referencePath: string[] = [...monthPath, taskId];
|
||||||
|
|
||||||
|
this.storageModel.write(referencePath, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
listTaskIds = (monthString: string): string[] => {
|
||||||
|
const monthPath: string[] = this.getMonthPath(monthString);
|
||||||
|
return this.storageModel.list(monthPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
generateMonthGrid = <T>(
|
||||||
|
year: number,
|
||||||
|
month: number,
|
||||||
|
defaultValueCreator: () => T,
|
||||||
|
): MonthGrid<T> => {
|
||||||
|
// get today
|
||||||
|
const date: Date = new Date();
|
||||||
|
const isCurrentMonth: boolean =
|
||||||
|
year == date.getFullYear() && month == date.getMonth() + 1;
|
||||||
|
|
||||||
|
// get data for offset
|
||||||
|
date.setDate(1);
|
||||||
|
date.setMonth(month - 1);
|
||||||
|
date.setFullYear(year);
|
||||||
|
|
||||||
|
const firstWeekdayOfMonth = date.getDay();
|
||||||
|
const firstDayOfWeekSetting: number = parseInt(
|
||||||
|
this.settingsModel.firstDayOfWeek,
|
||||||
|
);
|
||||||
|
|
||||||
|
const offset: number =
|
||||||
|
firstWeekdayOfMonth < firstDayOfWeekSetting
|
||||||
|
? 7 - firstDayOfWeekSetting
|
||||||
|
: firstWeekdayOfMonth - firstDayOfWeekSetting;
|
||||||
|
|
||||||
|
// get total day count
|
||||||
|
date.setMonth(month);
|
||||||
|
date.setDate(-1);
|
||||||
|
const daysInMonth: number = date.getDate() + 1;
|
||||||
|
|
||||||
|
const grid: MonthGrid<T> = {
|
||||||
|
offset,
|
||||||
|
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
|
||||||
|
isCurrentMonth,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
days: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < daysInMonth; i++) {
|
||||||
|
const paddedDate: string = CalendarModel.padDateOrMonth(
|
||||||
|
(i + 1).toString(),
|
||||||
|
);
|
||||||
|
grid.days[paddedDate] = defaultValueCreator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
storageModel: StorageModel,
|
||||||
|
settingsModel: SettingsModel,
|
||||||
|
fileModel: FileModel,
|
||||||
|
) {
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.settingsModel = settingsModel;
|
||||||
|
this.fileModel = fileModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
return grid;
|
// utility
|
||||||
};
|
static isoToMonthString = (dateISOString: string): string => {
|
||||||
|
const [year, month, _] = dateISOString.split("-");
|
||||||
|
return CalendarModel.getMonthString(year, month);
|
||||||
|
};
|
||||||
|
|
||||||
// init
|
static isoToDateString = (dateISOString: string): string => {
|
||||||
constructor(
|
const [year, month, date, _] = dateISOString.split("-");
|
||||||
storageModel: StorageModel,
|
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
|
||||||
settingsModel: SettingsModel,
|
return paddedDate;
|
||||||
fileModel: FileModel
|
};
|
||||||
) {
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.settingsModel = settingsModel;
|
|
||||||
this.fileModel = fileModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility
|
static getMonthString = (year: string = "", month: string = ""): string => {
|
||||||
static isoToMonthString = (dateISOString: string): string => {
|
const paddedYear: string = year.padStart(4, "0");
|
||||||
const [year, month, _] = dateISOString.split("-");
|
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
|
||||||
return CalendarModel.getMonthString(year, month);
|
return `${paddedYear}-${paddedMonth}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
static isoToDateString = (dateISOString: string): string => {
|
static getISODateString = (
|
||||||
const [year, month, date, _] = dateISOString.split("-");
|
year: string,
|
||||||
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
|
month: string,
|
||||||
return paddedDate;
|
date: string,
|
||||||
};
|
): string => {
|
||||||
|
const monthString: string = CalendarModel.getMonthString(year, month);
|
||||||
|
const paddedDate: string = CalendarModel.padDateOrMonth(date);
|
||||||
|
return `${monthString}-${paddedDate}`;
|
||||||
|
};
|
||||||
|
|
||||||
static getMonthString = (year: string = "", month: string = ""): string => {
|
static padDateOrMonth = (input: string): string => {
|
||||||
const paddedYear: string = year.padStart(4, "0");
|
return input.padStart(2, "0");
|
||||||
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
|
};
|
||||||
return `${paddedYear}-${paddedMonth}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
static getISODateString = (
|
|
||||||
year: string,
|
|
||||||
month: string,
|
|
||||||
date: string
|
|
||||||
): string => {
|
|
||||||
const monthString: string = CalendarModel.getMonthString(year, month);
|
|
||||||
const paddedDate: string = CalendarModel.padDateOrMonth(date);
|
|
||||||
return `${monthString}-${paddedDate}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
static padDateOrMonth = (input: string): string => {
|
|
||||||
return input.padStart(2, "0");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CalendarModelSubPaths {
|
export enum CalendarModelSubPaths {
|
||||||
Months = "months",
|
Months = "months",
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export interface MonthGrid<T> {
|
export interface MonthGrid<T> {
|
||||||
offset: number;
|
offset: number;
|
||||||
firstDayOfWeek: number;
|
firstDayOfWeek: number;
|
||||||
isCurrentMonth: boolean;
|
isCurrentMonth: boolean;
|
||||||
|
|
||||||
year: number;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
|
|
||||||
days: { [date: string]: T };
|
days: { [date: string]: T };
|
||||||
}
|
}
|
||||||
|
|||||||
+178
-175
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
StorageModelSubPath,
|
StorageModelSubPath,
|
||||||
filePaths,
|
filePaths,
|
||||||
} from "../Global/storageModel";
|
} from "../Global/storageModel";
|
||||||
import {
|
import {
|
||||||
StringEntryObject,
|
StringEntryObject,
|
||||||
createTimestamp,
|
createTimestamp,
|
||||||
parseValidObject,
|
parseValidObject,
|
||||||
stringify,
|
stringify,
|
||||||
} from "../Utility/utility";
|
} from "../Utility/utility";
|
||||||
|
|
||||||
import BoardsAndTasksModel from "./boardsAndTasksModel";
|
import BoardsAndTasksModel from "./boardsAndTasksModel";
|
||||||
@@ -18,191 +18,194 @@ import SettingsModel from "../Global/settingsModel";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export default class FileModel {
|
export default class FileModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
settingsModel: SettingsModel;
|
settingsModel: SettingsModel;
|
||||||
|
|
||||||
chatModel: ChatModel;
|
chatModel: ChatModel;
|
||||||
boardsAndTasksModel: BoardsAndTasksModel;
|
boardsAndTasksModel: BoardsAndTasksModel;
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
getBasePath = (): string[] => {
|
getBasePath = (): string[] => {
|
||||||
return StorageModel.getPath(
|
return StorageModel.getPath(
|
||||||
StorageModelSubPath.Chat,
|
StorageModelSubPath.Chat,
|
||||||
filePaths.chat.files(this.chatModel.id)
|
filePaths.chat.files(this.chatModel.id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getFileContainerPath = (): string[] => {
|
getFileContainerPath = (): string[] => {
|
||||||
return [...this.getBasePath(), FileModelSubPath.Data];
|
return [...this.getBasePath(), FileModelSubPath.Data];
|
||||||
};
|
};
|
||||||
|
|
||||||
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
|
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
|
||||||
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
|
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
|
||||||
};
|
};
|
||||||
|
|
||||||
getFilePath = (fileId: string): string[] => {
|
getFilePath = (fileId: string): string[] => {
|
||||||
return [...this.getFileContainerPath(), fileId];
|
return [...this.getFileContainerPath(), fileId];
|
||||||
};
|
};
|
||||||
|
|
||||||
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
|
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
|
||||||
const filePath: string[] = this.getFilePath(fileId);
|
const filePath: string[] = this.getFilePath(fileId);
|
||||||
return [...filePath, fileContentId];
|
return [...filePath, fileContentId];
|
||||||
};
|
};
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
|
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
|
||||||
const fileContent: FileContent<string> | null = parseValidObject(
|
const fileContent: FileContent<string> | null = parseValidObject(
|
||||||
stringifiedFileContent,
|
stringifiedFileContent,
|
||||||
FileContentReference
|
FileContentReference,
|
||||||
);
|
);
|
||||||
if (fileContent == null) return;
|
if (fileContent == null) return;
|
||||||
|
|
||||||
this.handleFileContent(fileContent);
|
this.handleFileContent(fileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFileContent = (fileContent: FileContent<string>): void => {
|
handleFileContent = (fileContent: FileContent<string>): void => {
|
||||||
const didStore: boolean = this.storeFileContent(fileContent);
|
const didStore: boolean = this.storeFileContent(fileContent);
|
||||||
if (didStore == false) return;
|
if (didStore == false) return;
|
||||||
|
|
||||||
this.boardsAndTasksModel.handleFileContent(fileContent);
|
this.boardsAndTasksModel.handleFileContent(fileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
addFileContentAndSend = (fileContent: FileContent<string>): void => {
|
addFileContentAndSend = (fileContent: FileContent<string>): void => {
|
||||||
this.handleFileContent(fileContent);
|
this.handleFileContent(fileContent);
|
||||||
this.chatModel.sendMessage("", fileContent);
|
this.chatModel.sendMessage("", fileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// storage
|
// storage
|
||||||
storeFileContent = (fileContent: FileContent<string>): boolean => {
|
storeFileContent = (fileContent: FileContent<string>): boolean => {
|
||||||
const fileContentPath: string[] = this.getFileContentPath(
|
const fileContentPath: string[] = this.getFileContentPath(
|
||||||
fileContent.fileId,
|
fileContent.fileId,
|
||||||
fileContent.fileContentId
|
fileContent.fileContentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// check if fileContent already exists
|
// check if fileContent already exists
|
||||||
const existingFileContent: string | null =
|
const existingFileContent: string | null =
|
||||||
this.storageModel.read(fileContentPath);
|
this.storageModel.read(fileContentPath);
|
||||||
if (existingFileContent != null) return false;
|
if (existingFileContent != null) return false;
|
||||||
|
|
||||||
const stringifiedContent: string = stringify(fileContent);
|
const stringifiedContent: string = stringify(fileContent);
|
||||||
this.storageModel.write(fileContentPath, stringifiedContent);
|
this.storageModel.write(fileContentPath, stringifiedContent);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
listFileIds = (): string[] => {
|
listFileIds = (): string[] => {
|
||||||
return this.storageModel.list(this.getBasePath());
|
return this.storageModel.list(this.getBasePath());
|
||||||
};
|
};
|
||||||
|
|
||||||
listFileContentIds = (fileId: string): string[] => {
|
listFileContentIds = (fileId: string): string[] => {
|
||||||
const filePath: string[] = this.getFilePath(fileId);
|
const filePath: string[] = this.getFilePath(fileId);
|
||||||
return this.storageModel.list(filePath);
|
return this.storageModel.list(filePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
selectLatestFileContentId = (
|
selectLatestFileContentId = (
|
||||||
fileContentIds: string[]
|
fileContentIds: string[],
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
return fileContentIds[fileContentIds.length - 1];
|
return fileContentIds[fileContentIds.length - 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
getFileContent = <T extends FileContent<string>>(
|
getFileContent = <T extends FileContent<string>>(
|
||||||
fileId: string,
|
fileId: string,
|
||||||
fileContentName: string,
|
fileContentName: string,
|
||||||
reference: T
|
reference: T,
|
||||||
): T | null => {
|
): T | null => {
|
||||||
const filePath: string[] = this.getFileContentPath(fileId, fileContentName);
|
const filePath: string[] = this.getFileContentPath(
|
||||||
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
fileId,
|
||||||
filePath,
|
fileContentName,
|
||||||
reference
|
);
|
||||||
);
|
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
||||||
return fileContentOrNull;
|
filePath,
|
||||||
};
|
reference,
|
||||||
|
);
|
||||||
getLatestFileContent = <T extends FileContent<string>>(
|
return fileContentOrNull;
|
||||||
fileId: string,
|
};
|
||||||
reference: T
|
|
||||||
): T | null => {
|
getLatestFileContent = <T extends FileContent<string>>(
|
||||||
const fileContentsIds: string[] = this.listFileContentIds(fileId);
|
fileId: string,
|
||||||
const latestFileContentId: string | undefined =
|
reference: T,
|
||||||
this.selectLatestFileContentId(fileContentsIds);
|
): T | null => {
|
||||||
if (latestFileContentId == undefined) return null;
|
const fileContentsIds: string[] = this.listFileContentIds(fileId);
|
||||||
|
const latestFileContentId: string | undefined =
|
||||||
const fileContent: T | null = this.getFileContent(
|
this.selectLatestFileContentId(fileContentsIds);
|
||||||
fileId,
|
if (latestFileContentId == undefined) return null;
|
||||||
latestFileContentId,
|
|
||||||
reference
|
const fileContent: T | null = this.getFileContent(
|
||||||
);
|
fileId,
|
||||||
return fileContent;
|
latestFileContentId,
|
||||||
};
|
reference,
|
||||||
|
);
|
||||||
// init
|
return fileContent;
|
||||||
constructor(
|
};
|
||||||
storageModel: StorageModel,
|
|
||||||
settingsModel: SettingsModel,
|
// init
|
||||||
chatModel: ChatModel
|
constructor(
|
||||||
) {
|
storageModel: StorageModel,
|
||||||
this.chatModel = chatModel;
|
settingsModel: SettingsModel,
|
||||||
this.settingsModel = settingsModel;
|
chatModel: ChatModel,
|
||||||
this.storageModel = storageModel;
|
) {
|
||||||
|
this.chatModel = chatModel;
|
||||||
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
this.settingsModel = settingsModel;
|
||||||
this.storageModel,
|
this.storageModel = storageModel;
|
||||||
this.settingsModel,
|
|
||||||
chatModel,
|
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
||||||
this
|
this.storageModel,
|
||||||
);
|
this.settingsModel,
|
||||||
}
|
chatModel,
|
||||||
|
this,
|
||||||
// utility
|
);
|
||||||
static generateFileContentId = (creationDate: string): string => {
|
}
|
||||||
return creationDate + v4();
|
|
||||||
};
|
// utility
|
||||||
|
static generateFileContentId = (creationDate: string): string => {
|
||||||
static createFileContent = <T extends string>(
|
return creationDate + v4();
|
||||||
fileId: string,
|
};
|
||||||
type: T
|
|
||||||
): FileContent<T> => {
|
static createFileContent = <T extends string>(
|
||||||
const creationDate: string = createTimestamp();
|
fileId: string,
|
||||||
const fileContentId: string = FileModel.generateFileContentId(creationDate);
|
type: T,
|
||||||
|
): FileContent<T> => {
|
||||||
return {
|
const creationDate: string = createTimestamp();
|
||||||
dataVersion: DATA_VERSION,
|
const fileContentId: string =
|
||||||
|
FileModel.generateFileContentId(creationDate);
|
||||||
fileId,
|
|
||||||
fileContentId,
|
return {
|
||||||
creationDate,
|
dataVersion: DATA_VERSION,
|
||||||
type,
|
|
||||||
|
fileId,
|
||||||
|
fileContentId,
|
||||||
|
creationDate,
|
||||||
|
type,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
export enum FileModelSubPath {
|
export enum FileModelSubPath {
|
||||||
Data = "data",
|
Data = "data",
|
||||||
Model = "model",
|
Model = "model",
|
||||||
ModelView = "view",
|
ModelView = "view",
|
||||||
ModelTask = "tasks",
|
ModelTask = "tasks",
|
||||||
ModelCalendar = "calendar",
|
ModelCalendar = "calendar",
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export interface FileContent<T extends string>
|
export interface FileContent<T extends string>
|
||||||
extends ValidObject,
|
extends ValidObject, StringEntryObject {
|
||||||
StringEntryObject {
|
fileId: string;
|
||||||
fileId: string;
|
fileContentId: string;
|
||||||
fileContentId: string;
|
creationDate: string;
|
||||||
creationDate: string;
|
|
||||||
|
|
||||||
type: T;
|
type: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// references
|
// references
|
||||||
export const FileContentReference: FileContent<string> = {
|
export const FileContentReference: FileContent<string> = {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
fileId: "",
|
fileId: "",
|
||||||
fileContentId: "",
|
fileContentId: "",
|
||||||
creationDate: "",
|
creationDate: "",
|
||||||
|
|
||||||
type: "",
|
type: "",
|
||||||
};
|
};
|
||||||
|
|||||||
+257
-234
@@ -6,255 +6,278 @@ import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
|
|||||||
import UDNFrontend, { Message } from "udn-frontend";
|
import UDNFrontend, { Message } from "udn-frontend";
|
||||||
|
|
||||||
export default class ConnectionModel {
|
export default class ConnectionModel {
|
||||||
udn: UDNFrontend;
|
udn: UDNFrontend;
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
|
reconnectInterval: number | undefined = undefined;
|
||||||
|
shouldAttemptReconnect: boolean = false;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
get isConnected(): boolean {
|
get isConnected(): boolean {
|
||||||
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
|
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
|
||||||
}
|
|
||||||
|
|
||||||
get address(): string | undefined {
|
|
||||||
return this.udn.ws?.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
|
|
||||||
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
|
|
||||||
messageSentHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
|
|
||||||
|
|
||||||
channelsToSubscribe: Set<string> = new Set();
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
handleMessage = (data: Message): void => {
|
|
||||||
this.messageHandlerManager.trigger(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleConnectionChange = (): void => {
|
|
||||||
console.log("connection status:", this.isConnected, this.address);
|
|
||||||
this.connectionChangeHandlerManager.trigger();
|
|
||||||
|
|
||||||
if (this.isConnected == false) return;
|
|
||||||
if (this.address == undefined) return;
|
|
||||||
|
|
||||||
this.storeAddress(this.address);
|
|
||||||
this.sendSubscriptionRequest();
|
|
||||||
this.sendMessagesInOutbox();
|
|
||||||
};
|
|
||||||
|
|
||||||
// connection
|
|
||||||
connect = (address: string): void => {
|
|
||||||
this.udn.connect(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
disconnect = (): void => {
|
|
||||||
this.udn.disconnect();
|
|
||||||
|
|
||||||
// do not reconnect
|
|
||||||
const reconnectAddressPath: string[] = StorageModel.getPath(
|
|
||||||
StorageModelSubPath.ConnectionModel,
|
|
||||||
filePaths.connectionModel.reconnectAddress
|
|
||||||
);
|
|
||||||
this.storageModel.remove(reconnectAddressPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
// mailbox
|
|
||||||
getMailboxPath = (address: string): string[] => {
|
|
||||||
const mailboxDirPath = StorageModel.getPath(
|
|
||||||
StorageModelSubPath.ConnectionModel,
|
|
||||||
filePaths.connectionModel.mailboxes
|
|
||||||
);
|
|
||||||
const mailboxFilePath = [...mailboxDirPath, address];
|
|
||||||
return mailboxFilePath;
|
|
||||||
};
|
|
||||||
|
|
||||||
requestNewMailbox = (): void => {
|
|
||||||
console.log("requesting new mailbox");
|
|
||||||
this.udn.requestMailbox();
|
|
||||||
};
|
|
||||||
|
|
||||||
connectMailbox = (): void => {
|
|
||||||
if (this.address == undefined) return;
|
|
||||||
|
|
||||||
const mailboxId = this.storageModel.read(this.getMailboxPath(this.address));
|
|
||||||
console.log("connecting mailbox", mailboxId);
|
|
||||||
if (mailboxId == null) return this.requestNewMailbox();
|
|
||||||
|
|
||||||
this.udn.connectMailbox(mailboxId);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeMailbox = (mailboxId: string): void => {
|
|
||||||
if (this.address == undefined) return;
|
|
||||||
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// subscription
|
|
||||||
addChannel = (channel: string): void => {
|
|
||||||
this.channelsToSubscribe.add(channel);
|
|
||||||
this.sendSubscriptionRequest();
|
|
||||||
};
|
|
||||||
|
|
||||||
sendSubscriptionRequest = (): void => {
|
|
||||||
if (this.isConnected == false) return;
|
|
||||||
|
|
||||||
for (const channel of this.channelsToSubscribe) {
|
|
||||||
this.udn.subscribe(channel);
|
|
||||||
}
|
}
|
||||||
// update mailbox
|
|
||||||
this.connectMailbox();
|
|
||||||
};
|
|
||||||
|
|
||||||
// outbox
|
get address(): string | undefined {
|
||||||
getOutboxPath = (): string[] => {
|
return this.udn.ws?.url;
|
||||||
return StorageModel.getPath(
|
}
|
||||||
StorageModelSubPath.ConnectionModel,
|
|
||||||
filePaths.connectionModel.outbox
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getOutboxMessags = (): ChatMessage[] => {
|
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
|
||||||
const outboxPath: string[] = this.getOutboxPath();
|
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
|
||||||
const messageIds: string[] = this.storageModel.list(outboxPath);
|
messageSentHandlerManager: HandlerManager<ChatMessage> =
|
||||||
|
new HandlerManager();
|
||||||
|
|
||||||
let chatMessages: ChatMessage[] = [];
|
channelsToSubscribe: Set<string> = new Set();
|
||||||
for (const messageId of messageIds) {
|
|
||||||
const chatMessage: ChatMessage | null =
|
// handlers
|
||||||
this.storageModel.readStringifiable(
|
handleMessage = (data: Message): void => {
|
||||||
[...outboxPath, messageId],
|
this.messageHandlerManager.trigger(data);
|
||||||
ChatMessageReference
|
};
|
||||||
|
|
||||||
|
handleConnectionChange = (): void => {
|
||||||
|
console.log("connection status:", this.isConnected, this.address);
|
||||||
|
this.connectionChangeHandlerManager.trigger();
|
||||||
|
|
||||||
|
if (this.isConnected == false) {
|
||||||
|
if (this.shouldAttemptReconnect == false) return;
|
||||||
|
|
||||||
|
this.reconnectInterval = setInterval(() => {
|
||||||
|
if (this.isConnected == true) {
|
||||||
|
clearInterval(this.reconnectInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.reconnect();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.address == undefined) return;
|
||||||
|
|
||||||
|
this.storeAddress(this.address);
|
||||||
|
this.sendSubscriptionRequest();
|
||||||
|
this.sendMessagesInOutbox();
|
||||||
|
this.shouldAttemptReconnect = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// connection
|
||||||
|
connect = (address: string): void => {
|
||||||
|
this.udn.connect(address);
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnect = (): void => {
|
||||||
|
this.udn.disconnect();
|
||||||
|
|
||||||
|
// do not reconnect
|
||||||
|
const reconnectAddressPath: string[] = StorageModel.getPath(
|
||||||
|
StorageModelSubPath.ConnectionModel,
|
||||||
|
filePaths.connectionModel.reconnectAddress,
|
||||||
);
|
);
|
||||||
|
this.storageModel.remove(reconnectAddressPath);
|
||||||
|
this.shouldAttemptReconnect = false;
|
||||||
|
};
|
||||||
|
|
||||||
if (chatMessage == null) continue;
|
reconnect = (): void => {
|
||||||
chatMessages.push(chatMessage);
|
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
||||||
|
const reconnectAddress: string | null =
|
||||||
|
this.storageModel.read(reconnectAddressPath);
|
||||||
|
if (reconnectAddress == null) return;
|
||||||
|
|
||||||
|
console.log("reconnecting...");
|
||||||
|
this.connect(reconnectAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
// mailbox
|
||||||
|
getMailboxPath = (address: string): string[] => {
|
||||||
|
const mailboxDirPath = StorageModel.getPath(
|
||||||
|
StorageModelSubPath.ConnectionModel,
|
||||||
|
filePaths.connectionModel.mailboxes,
|
||||||
|
);
|
||||||
|
const mailboxFilePath = [...mailboxDirPath, address];
|
||||||
|
return mailboxFilePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
requestNewMailbox = (): void => {
|
||||||
|
console.log("requesting new mailbox");
|
||||||
|
this.udn.requestMailbox();
|
||||||
|
};
|
||||||
|
|
||||||
|
connectMailbox = (): void => {
|
||||||
|
if (this.address == undefined) return;
|
||||||
|
|
||||||
|
const mailboxId = this.storageModel.read(
|
||||||
|
this.getMailboxPath(this.address),
|
||||||
|
);
|
||||||
|
console.log("connecting mailbox", mailboxId);
|
||||||
|
if (mailboxId == null) return this.requestNewMailbox();
|
||||||
|
|
||||||
|
this.udn.connectMailbox(mailboxId);
|
||||||
|
};
|
||||||
|
|
||||||
|
storeMailbox = (mailboxId: string): void => {
|
||||||
|
if (this.address == undefined) return;
|
||||||
|
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// subscription
|
||||||
|
addChannel = (channel: string): void => {
|
||||||
|
this.channelsToSubscribe.add(channel);
|
||||||
|
this.sendSubscriptionRequest();
|
||||||
|
};
|
||||||
|
|
||||||
|
sendSubscriptionRequest = (): void => {
|
||||||
|
if (this.isConnected == false) return;
|
||||||
|
|
||||||
|
for (const channel of this.channelsToSubscribe) {
|
||||||
|
this.udn.subscribe(channel);
|
||||||
|
}
|
||||||
|
// update mailbox
|
||||||
|
this.connectMailbox();
|
||||||
|
};
|
||||||
|
|
||||||
|
// outbox
|
||||||
|
getOutboxPath = (): string[] => {
|
||||||
|
return StorageModel.getPath(
|
||||||
|
StorageModelSubPath.ConnectionModel,
|
||||||
|
filePaths.connectionModel.outbox,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getOutboxMessags = (): ChatMessage[] => {
|
||||||
|
const outboxPath: string[] = this.getOutboxPath();
|
||||||
|
const messageIds: string[] = this.storageModel.list(outboxPath);
|
||||||
|
|
||||||
|
let chatMessages: ChatMessage[] = [];
|
||||||
|
for (const messageId of messageIds) {
|
||||||
|
const chatMessage: ChatMessage | null =
|
||||||
|
this.storageModel.readStringifiable(
|
||||||
|
[...outboxPath, messageId],
|
||||||
|
ChatMessageReference,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chatMessage == null) continue;
|
||||||
|
chatMessages.push(chatMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatMessages;
|
||||||
|
};
|
||||||
|
|
||||||
|
addToOutbox = (chatMessage: ChatMessage): void => {
|
||||||
|
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
||||||
|
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeFromOutbox = (chatMessage: ChatMessage): void => {
|
||||||
|
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
||||||
|
this.storageModel.remove(messagePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
sendMessagesInOutbox = (): void => {
|
||||||
|
const messages: ChatMessage[] = this.getOutboxMessags();
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
const isSent: boolean = this.tryToSendMessage(message);
|
||||||
|
if (isSent == false) return;
|
||||||
|
|
||||||
|
this.removeFromOutbox(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// messaging
|
||||||
|
sendMessageOrStore = (chatMessage: ChatMessage): void => {
|
||||||
|
const isSent = this.tryToSendMessage(chatMessage);
|
||||||
|
if (isSent == true) return;
|
||||||
|
|
||||||
|
this.addToOutbox(chatMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
|
||||||
|
const stringifiedBody: string = stringify(chatMessage);
|
||||||
|
const isSent: boolean = this.sendPlainMessage(
|
||||||
|
chatMessage.channel,
|
||||||
|
stringifiedBody,
|
||||||
|
);
|
||||||
|
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
|
||||||
|
return isSent;
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPlainMessage = (channel: string, body: string): boolean => {
|
||||||
|
return this.udn.sendMessage(channel, body);
|
||||||
|
};
|
||||||
|
|
||||||
|
// storage
|
||||||
|
getPreviousAddressPath = (): string[] => {
|
||||||
|
return StorageModel.getPath(
|
||||||
|
StorageModelSubPath.ConnectionModel,
|
||||||
|
filePaths.connectionModel.previousAddresses,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getAddressPath = (address: string): string[] => {
|
||||||
|
const dirPath = this.getPreviousAddressPath();
|
||||||
|
return [...dirPath, address];
|
||||||
|
};
|
||||||
|
|
||||||
|
getReconnectAddressPath = (): string[] => {
|
||||||
|
return StorageModel.getPath(
|
||||||
|
StorageModelSubPath.ConnectionModel,
|
||||||
|
filePaths.connectionModel.reconnectAddress,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
storeAddress = (address: string): void => {
|
||||||
|
// history
|
||||||
|
const addressPath = this.getAddressPath(address);
|
||||||
|
this.storageModel.write(addressPath, "");
|
||||||
|
|
||||||
|
// reconnect
|
||||||
|
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
||||||
|
this.storageModel.write(reconnectAddressPath, address);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeAddress = (address: string): void => {
|
||||||
|
const addressPath = this.getAddressPath(address);
|
||||||
|
this.storageModel.remove(addressPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
get addresses(): string[] {
|
||||||
|
const dirPath = this.getPreviousAddressPath();
|
||||||
|
return this.storageModel.list(dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return chatMessages;
|
// init
|
||||||
};
|
constructor(storageModel: StorageModel) {
|
||||||
|
// create frontend
|
||||||
|
this.udn = new UDNFrontend();
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
|
||||||
addToOutbox = (chatMessage: ChatMessage): void => {
|
// setup handlers
|
||||||
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
this.udn.onmessage = (data: Message) => {
|
||||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
this.handleMessage(data);
|
||||||
};
|
};
|
||||||
|
this.udn.onconnect = () => {
|
||||||
|
this.handleConnectionChange();
|
||||||
|
};
|
||||||
|
this.udn.ondisconnect = () => {
|
||||||
|
this.handleConnectionChange();
|
||||||
|
};
|
||||||
|
|
||||||
removeFromOutbox = (chatMessage: ChatMessage): void => {
|
this.udn.onmailboxcreate = (mailboxId: string) => {
|
||||||
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
console.log("created mailbox", mailboxId);
|
||||||
this.storageModel.remove(messagePath);
|
this.storeMailbox(mailboxId);
|
||||||
};
|
this.connectMailbox();
|
||||||
|
};
|
||||||
|
this.udn.onmailboxdelete = (mailboxId: string) => {
|
||||||
|
console.log(`mailbox ${mailboxId} deleted`);
|
||||||
|
this.requestNewMailbox();
|
||||||
|
};
|
||||||
|
this.udn.onmailboxconnect = (mailboxId: string) => {
|
||||||
|
console.log(`using mailbox ${mailboxId}`);
|
||||||
|
};
|
||||||
|
|
||||||
sendMessagesInOutbox = (): void => {
|
this.reconnect();
|
||||||
const messages: ChatMessage[] = this.getOutboxMessags();
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
const isSent: boolean = this.tryToSendMessage(message);
|
|
||||||
if (isSent == false) return;
|
|
||||||
|
|
||||||
this.removeFromOutbox(message);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// messaging
|
|
||||||
sendMessageOrStore = (chatMessage: ChatMessage): void => {
|
|
||||||
const isSent = this.tryToSendMessage(chatMessage);
|
|
||||||
if (isSent == true) return;
|
|
||||||
|
|
||||||
this.addToOutbox(chatMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
|
|
||||||
const stringifiedBody: string = stringify(chatMessage);
|
|
||||||
const isSent: boolean = this.sendPlainMessage(
|
|
||||||
chatMessage.channel,
|
|
||||||
stringifiedBody
|
|
||||||
);
|
|
||||||
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
|
|
||||||
return isSent;
|
|
||||||
};
|
|
||||||
|
|
||||||
sendPlainMessage = (channel: string, body: string): boolean => {
|
|
||||||
return this.udn.sendMessage(channel, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
// storage
|
|
||||||
getPreviousAddressPath = (): string[] => {
|
|
||||||
return StorageModel.getPath(
|
|
||||||
StorageModelSubPath.ConnectionModel,
|
|
||||||
filePaths.connectionModel.previousAddresses
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getAddressPath = (address: string): string[] => {
|
|
||||||
const dirPath = this.getPreviousAddressPath();
|
|
||||||
return [...dirPath, address];
|
|
||||||
};
|
|
||||||
|
|
||||||
getReconnectAddressPath = (): string[] => {
|
|
||||||
return StorageModel.getPath(
|
|
||||||
StorageModelSubPath.ConnectionModel,
|
|
||||||
filePaths.connectionModel.reconnectAddress
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeAddress = (address: string): void => {
|
|
||||||
// history
|
|
||||||
const addressPath = this.getAddressPath(address);
|
|
||||||
this.storageModel.write(addressPath, "");
|
|
||||||
|
|
||||||
// reconnect
|
|
||||||
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
|
||||||
this.storageModel.write(reconnectAddressPath, address);
|
|
||||||
};
|
|
||||||
|
|
||||||
removeAddress = (address: string): void => {
|
|
||||||
const addressPath = this.getAddressPath(address);
|
|
||||||
this.storageModel.remove(addressPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
get addresses(): string[] {
|
|
||||||
const dirPath = this.getPreviousAddressPath();
|
|
||||||
return this.storageModel.list(dirPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(storageModel: StorageModel) {
|
|
||||||
// create frontend
|
|
||||||
this.udn = new UDNFrontend();
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
|
|
||||||
// setup handlers
|
|
||||||
this.udn.onmessage = (data: Message) => {
|
|
||||||
this.handleMessage(data);
|
|
||||||
};
|
|
||||||
this.udn.onconnect = () => {
|
|
||||||
this.handleConnectionChange();
|
|
||||||
};
|
|
||||||
this.udn.ondisconnect = () => {
|
|
||||||
this.handleConnectionChange();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.udn.onmailboxcreate = (mailboxId: string) => {
|
|
||||||
console.log("created mailbox", mailboxId);
|
|
||||||
this.storeMailbox(mailboxId);
|
|
||||||
this.connectMailbox();
|
|
||||||
};
|
|
||||||
this.udn.onmailboxdelete = (mailboxId: string) => {
|
|
||||||
console.log(`mailbox ${mailboxId} deleted`);
|
|
||||||
this.requestNewMailbox();
|
|
||||||
};
|
|
||||||
this.udn.onmailboxconnect = (mailboxId: string) => {
|
|
||||||
console.log(`using mailbox ${mailboxId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// reconnect
|
|
||||||
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
|
||||||
const reconnectAddress: string | null =
|
|
||||||
storageModel.read(reconnectAddressPath);
|
|
||||||
if (reconnectAddress != null) {
|
|
||||||
this.connect(reconnectAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
export enum settingsModelStorageFiles {
|
export enum settingsModelStorageFiles {
|
||||||
username = "user-name",
|
username = "user-name",
|
||||||
firstDayOfWeek = "first-day-of-week",
|
firstDayOfWeek = "first-day-of-week",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// this file is responsible for sending and handling files.
|
// this file is responsible for sending and handling files.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HandlerManager,
|
HandlerManager,
|
||||||
generateRandomToken,
|
generateRandomToken,
|
||||||
parse,
|
parse,
|
||||||
stringify,
|
stringify,
|
||||||
} from "../Utility/utility";
|
} from "../Utility/utility";
|
||||||
import { decryptString, encryptString } from "../Utility/crypto";
|
import { decryptString, encryptString } from "../Utility/crypto";
|
||||||
|
|
||||||
@@ -14,118 +14,120 @@ import StorageModel from "./storageModel";
|
|||||||
import { checkMatchesObjectStructure } from "../Utility/typeSafety";
|
import { checkMatchesObjectStructure } from "../Utility/typeSafety";
|
||||||
|
|
||||||
export default class FileTransferModel {
|
export default class FileTransferModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
connectionModel: ConnectionModel;
|
connectionModel: ConnectionModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
transferData: TransferData | undefined;
|
transferData: TransferData | undefined;
|
||||||
fileHandlerManager: HandlerManager<string> = new HandlerManager();
|
fileHandlerManager: HandlerManager<string> = new HandlerManager();
|
||||||
|
|
||||||
// general
|
// general
|
||||||
generateTransferData = (): TransferData => {
|
generateTransferData = (): TransferData => {
|
||||||
const transferData: TransferData = {
|
const transferData: TransferData = {
|
||||||
channel: generateRandomToken(2).substring(0, 4),
|
channel: generateRandomToken(2).substring(0, 4),
|
||||||
key: generateRandomToken(3).substring(0, 6),
|
key: generateRandomToken(3).substring(0, 6),
|
||||||
|
};
|
||||||
|
this.transferData = transferData;
|
||||||
|
return transferData;
|
||||||
};
|
};
|
||||||
this.transferData = transferData;
|
|
||||||
return transferData;
|
|
||||||
};
|
|
||||||
|
|
||||||
prepareToReceive = (transferData: TransferData) => {
|
prepareToReceive = (transferData: TransferData) => {
|
||||||
this.connectionModel.addChannel(transferData.channel);
|
this.connectionModel.addChannel(transferData.channel);
|
||||||
this.transferData = transferData;
|
this.transferData = transferData;
|
||||||
};
|
};
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
handleMessage = (data: Message): void => {
|
handleMessage = (data: Message): void => {
|
||||||
if (this.transferData == undefined) return;
|
if (this.transferData == undefined) return;
|
||||||
if (data.messageBody == undefined) return;
|
if (data.messageBody == undefined) return;
|
||||||
if (data.messageChannel != this.transferData.channel) return;
|
if (data.messageChannel != this.transferData.channel) return;
|
||||||
|
|
||||||
this.handleFile(data.messageBody);
|
this.handleFile(data.messageBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFile = async (encryptedFileData: string): Promise<void> => {
|
handleFile = async (encryptedFileData: string): Promise<void> => {
|
||||||
if (this.transferData == undefined) return;
|
if (this.transferData == undefined) return;
|
||||||
|
|
||||||
const decrypted: string = await decryptString(
|
const decrypted: string = await decryptString(
|
||||||
encryptedFileData,
|
encryptedFileData,
|
||||||
this.transferData.key
|
this.transferData.key,
|
||||||
);
|
|
||||||
console.log(decrypted);
|
|
||||||
|
|
||||||
const parsed: any = parse(decrypted);
|
|
||||||
const isFileData: boolean = checkMatchesObjectStructure(
|
|
||||||
parsed,
|
|
||||||
FileDataReference
|
|
||||||
);
|
|
||||||
if (isFileData == false) return;
|
|
||||||
|
|
||||||
const fileData: FileData = parsed;
|
|
||||||
this.storageModel.write(fileData.path, fileData.body);
|
|
||||||
|
|
||||||
const pathString: string = StorageModel.pathComponentsToString(
|
|
||||||
...fileData.path
|
|
||||||
);
|
|
||||||
this.fileHandlerManager.trigger(pathString);
|
|
||||||
};
|
|
||||||
|
|
||||||
// sending
|
|
||||||
sendFiles = (
|
|
||||||
directoryPaths: IteratorObject<string[]>,
|
|
||||||
callback: (path: string) => void
|
|
||||||
): void => {
|
|
||||||
for (const directoryPath of directoryPaths) {
|
|
||||||
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
|
|
||||||
this.sendFile(filePath);
|
|
||||||
const pathString: string = StorageModel.pathComponentsToString(
|
|
||||||
...filePath
|
|
||||||
);
|
);
|
||||||
callback(pathString);
|
console.log(decrypted);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sendFile = async (filePath: string[]): Promise<void> => {
|
const parsed: any = parse(decrypted);
|
||||||
if (this.transferData == undefined) return;
|
const isFileData: boolean = checkMatchesObjectStructure(
|
||||||
const fileContent: string | null = this.storageModel.read(filePath);
|
parsed,
|
||||||
if (fileContent == null) return;
|
FileDataReference,
|
||||||
|
);
|
||||||
|
if (isFileData == false) return;
|
||||||
|
|
||||||
const fileData: FileData = {
|
const fileData: FileData = parsed;
|
||||||
path: filePath,
|
this.storageModel.write(fileData.path, fileData.body);
|
||||||
body: fileContent,
|
|
||||||
|
const pathString: string = StorageModel.pathComponentsToString(
|
||||||
|
...fileData.path,
|
||||||
|
);
|
||||||
|
this.fileHandlerManager.trigger(pathString);
|
||||||
};
|
};
|
||||||
const stringifiedFileData: string = stringify(fileData);
|
|
||||||
const encryptedFileData: string = await encryptString(
|
|
||||||
stringifiedFileData,
|
|
||||||
this.transferData.key
|
|
||||||
);
|
|
||||||
this.connectionModel.sendPlainMessage(
|
|
||||||
this.transferData.channel,
|
|
||||||
encryptedFileData
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
// sending
|
||||||
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
|
sendFiles = (
|
||||||
this.storageModel = storageModel;
|
directoryPaths: IteratorObject<string[]>,
|
||||||
this.connectionModel = connectionModel;
|
callback: (path: string) => void,
|
||||||
|
): void => {
|
||||||
|
for (const directoryPath of directoryPaths) {
|
||||||
|
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
|
||||||
|
this.sendFile(filePath);
|
||||||
|
const pathString: string = StorageModel.pathComponentsToString(
|
||||||
|
...filePath,
|
||||||
|
);
|
||||||
|
callback(pathString);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.connectionModel.messageHandlerManager.addHandler(this.handleMessage);
|
sendFile = async (filePath: string[]): Promise<void> => {
|
||||||
}
|
if (this.transferData == undefined) return;
|
||||||
|
const fileContent: string | null = this.storageModel.read(filePath);
|
||||||
|
if (fileContent == null) return;
|
||||||
|
|
||||||
|
const fileData: FileData = {
|
||||||
|
path: filePath,
|
||||||
|
body: fileContent,
|
||||||
|
};
|
||||||
|
const stringifiedFileData: string = stringify(fileData);
|
||||||
|
const encryptedFileData: string = await encryptString(
|
||||||
|
stringifiedFileData,
|
||||||
|
this.transferData.key,
|
||||||
|
);
|
||||||
|
this.connectionModel.sendPlainMessage(
|
||||||
|
this.transferData.channel,
|
||||||
|
encryptedFileData,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.connectionModel = connectionModel;
|
||||||
|
|
||||||
|
this.connectionModel.messageHandlerManager.addHandler(
|
||||||
|
this.handleMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransferData {
|
export interface TransferData {
|
||||||
channel: string;
|
channel: string;
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileData {
|
export interface FileData {
|
||||||
path: string[];
|
path: string[];
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileDataReference: FileData = {
|
export const FileDataReference: FileData = {
|
||||||
path: [""],
|
path: [""],
|
||||||
body: "",
|
body: "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,55 +3,55 @@
|
|||||||
import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
|
import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
|
||||||
|
|
||||||
export default class SettingsModel {
|
export default class SettingsModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
username: string;
|
username: string;
|
||||||
firstDayOfWeek: string;
|
firstDayOfWeek: string;
|
||||||
|
|
||||||
// storage
|
// storage
|
||||||
setName(newValue: string): void {
|
setName(newValue: string): void {
|
||||||
this.username = newValue;
|
this.username = newValue;
|
||||||
const path = StorageModel.getPath(
|
const path = StorageModel.getPath(
|
||||||
StorageModelSubPath.SettingsModel,
|
StorageModelSubPath.SettingsModel,
|
||||||
filePaths.settingsModel.username
|
filePaths.settingsModel.username,
|
||||||
);
|
);
|
||||||
this.storageModel.write(path, newValue);
|
this.storageModel.write(path, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFirstDayOfWeek(newValue: string): void {
|
setFirstDayOfWeek(newValue: string): void {
|
||||||
this.firstDayOfWeek = newValue;
|
this.firstDayOfWeek = newValue;
|
||||||
const path = StorageModel.getPath(
|
const path = StorageModel.getPath(
|
||||||
StorageModelSubPath.SettingsModel,
|
StorageModelSubPath.SettingsModel,
|
||||||
filePaths.settingsModel.firstDayOfWeek
|
filePaths.settingsModel.firstDayOfWeek,
|
||||||
);
|
);
|
||||||
this.storageModel.write(path, newValue);
|
this.storageModel.write(path, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// load
|
// load
|
||||||
loadUsernam(): void {
|
loadUsernam(): void {
|
||||||
const path = StorageModel.getPath(
|
const path = StorageModel.getPath(
|
||||||
StorageModelSubPath.SettingsModel,
|
StorageModelSubPath.SettingsModel,
|
||||||
filePaths.settingsModel.username
|
filePaths.settingsModel.username,
|
||||||
);
|
);
|
||||||
const content = this.storageModel.read(path);
|
const content = this.storageModel.read(path);
|
||||||
this.username = content ?? "";
|
this.username = content ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFirstDayofWeek(): void {
|
loadFirstDayofWeek(): void {
|
||||||
const path = StorageModel.getPath(
|
const path = StorageModel.getPath(
|
||||||
StorageModelSubPath.SettingsModel,
|
StorageModelSubPath.SettingsModel,
|
||||||
filePaths.settingsModel.firstDayOfWeek
|
filePaths.settingsModel.firstDayOfWeek,
|
||||||
);
|
);
|
||||||
const content = this.storageModel.read(path);
|
const content = this.storageModel.read(path);
|
||||||
this.firstDayOfWeek = content ?? "0";
|
this.firstDayOfWeek = content ?? "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(storageModel: StorageModel) {
|
constructor(storageModel: StorageModel) {
|
||||||
this.storageModel = storageModel;
|
this.storageModel = storageModel;
|
||||||
|
|
||||||
this.loadUsernam();
|
this.loadUsernam();
|
||||||
this.loadFirstDayofWeek();
|
this.loadFirstDayofWeek();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+229
-219
@@ -6,207 +6,217 @@ import { localeCompare, parseValidObject, stringify } from "../Utility/utility";
|
|||||||
export const PATH_COMPONENT_SEPARATOR = "\\";
|
export const PATH_COMPONENT_SEPARATOR = "\\";
|
||||||
|
|
||||||
export default class StorageModel {
|
export default class StorageModel {
|
||||||
storageEntryTree: StorageEntry = {};
|
storageEntryTree: StorageEntry = {};
|
||||||
|
|
||||||
// read
|
// read
|
||||||
read = (pathComponents: string[]): string | null => {
|
read = (pathComponents: string[]): string | null => {
|
||||||
const pathString: string = StorageModel.pathComponentsToString(
|
const pathString: string = StorageModel.pathComponentsToString(
|
||||||
...pathComponents
|
...pathComponents,
|
||||||
);
|
);
|
||||||
return localStorage.getItem(pathString);
|
return localStorage.getItem(pathString);
|
||||||
};
|
};
|
||||||
|
|
||||||
list = (pathComponents: string[]): string[] => {
|
list = (pathComponents: string[]): string[] => {
|
||||||
let currentParent: StorageEntry = this.storageEntryTree;
|
let currentParent: StorageEntry = this.storageEntryTree;
|
||||||
for (const component of pathComponents) {
|
for (const component of pathComponents) {
|
||||||
const nextParent: StorageEntry | undefined = currentParent[component];
|
const nextParent: StorageEntry | undefined =
|
||||||
if (nextParent == undefined) return [];
|
currentParent[component];
|
||||||
currentParent = nextParent;
|
if (nextParent == undefined) return [];
|
||||||
|
currentParent = nextParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...Object.keys(currentParent).sort(localeCompare)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// write
|
||||||
|
write = (pathComponents: string[], value: string): void => {
|
||||||
|
const pathString: string = StorageModel.pathComponentsToString(
|
||||||
|
...pathComponents,
|
||||||
|
);
|
||||||
|
localStorage.setItem(pathString, value);
|
||||||
|
this.updateTree(...pathComponents);
|
||||||
|
};
|
||||||
|
|
||||||
|
remove = (
|
||||||
|
pathComponents: string[],
|
||||||
|
shouldInitialize: boolean = true,
|
||||||
|
): void => {
|
||||||
|
const pathString: string = StorageModel.pathComponentsToString(
|
||||||
|
...pathComponents,
|
||||||
|
);
|
||||||
|
localStorage.removeItem(pathString);
|
||||||
|
|
||||||
|
if (shouldInitialize == true) {
|
||||||
|
this.initializeTree();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rename = (
|
||||||
|
sourcePathComponents: string[],
|
||||||
|
destinationPathComponents: string[],
|
||||||
|
shouldInitialize: boolean = true,
|
||||||
|
): boolean => {
|
||||||
|
const content: string | null = this.read(sourcePathComponents);
|
||||||
|
if (content == null) return false;
|
||||||
|
|
||||||
|
this.write(destinationPathComponents, content);
|
||||||
|
this.remove(sourcePathComponents);
|
||||||
|
|
||||||
|
if (shouldInitialize == true) {
|
||||||
|
this.initializeTree();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// recursion
|
||||||
|
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
|
||||||
|
loop_over_files: for (const key of Object.keys(localStorage)) {
|
||||||
|
// get path of current entity
|
||||||
|
const pathComponentsOfCurrentEntity: string[] =
|
||||||
|
StorageModel.stringToPathComponents(key);
|
||||||
|
|
||||||
|
// exit if entity does not match
|
||||||
|
loop_over_path_components: for (
|
||||||
|
let i = 0;
|
||||||
|
i < rootDirectory.length;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
|
||||||
|
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
|
||||||
|
continue loop_over_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute
|
||||||
|
fn(pathComponentsOfCurrentEntity);
|
||||||
|
}
|
||||||
|
this.initializeTree();
|
||||||
|
};
|
||||||
|
|
||||||
|
removeRecursively = (pathComponents: string[]): void => {
|
||||||
|
this.recurse(pathComponents, (path: string[]) =>
|
||||||
|
this.remove(path, false),
|
||||||
|
);
|
||||||
|
this.initializeTree();
|
||||||
|
};
|
||||||
|
|
||||||
|
renameRecursively = (
|
||||||
|
sourcePathComponents: string[],
|
||||||
|
destinationPathComponents: string[],
|
||||||
|
): void => {
|
||||||
|
this.recurse(sourcePathComponents, (path: string[]) => {
|
||||||
|
// assemble destinationPath
|
||||||
|
const relativePathOfCurrentEntity = path.slice(
|
||||||
|
sourcePathComponents.length,
|
||||||
|
);
|
||||||
|
const destinationPathComponentsOfCurrentEntity = [
|
||||||
|
...destinationPathComponents,
|
||||||
|
...relativePathOfCurrentEntity,
|
||||||
|
];
|
||||||
|
|
||||||
|
// rename
|
||||||
|
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
|
||||||
|
});
|
||||||
|
this.initializeTree();
|
||||||
|
};
|
||||||
|
|
||||||
|
// stringifiable
|
||||||
|
writeStringifiable = (
|
||||||
|
pathComponents: string[],
|
||||||
|
value: ValidObject,
|
||||||
|
): void => {
|
||||||
|
const valueString: string = stringify(value);
|
||||||
|
this.write(pathComponents, valueString);
|
||||||
|
};
|
||||||
|
|
||||||
|
readStringifiable = <T extends ValidObject>(
|
||||||
|
pathComponents: string[],
|
||||||
|
reference: T,
|
||||||
|
): T | null => {
|
||||||
|
const valueString: string | null = this.read(pathComponents);
|
||||||
|
if (!valueString) return null;
|
||||||
|
|
||||||
|
const object: any | null = parseValidObject(valueString, reference);
|
||||||
|
if (object == null) return null;
|
||||||
|
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
// cleaning
|
||||||
|
removeJunk = (): void => {
|
||||||
|
this.recurse([], (path: string[]) => {
|
||||||
|
if (path[0] == DATA_VERSION) return;
|
||||||
|
this.remove(path);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// tree
|
||||||
|
initializeTree = (): void => {
|
||||||
|
console.log("initializing tree");
|
||||||
|
|
||||||
|
this.storageEntryTree = {};
|
||||||
|
for (const key of Object.keys(localStorage)) {
|
||||||
|
const components: string[] =
|
||||||
|
StorageModel.stringToPathComponents(key);
|
||||||
|
this.updateTree(...components);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTree = (...pathComponents: string[]): void => {
|
||||||
|
let currentParent: StorageEntry = this.storageEntryTree;
|
||||||
|
for (const pathPart of pathComponents) {
|
||||||
|
if (!currentParent[pathPart]) {
|
||||||
|
currentParent[pathPart] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
currentParent = currentParent[pathPart];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
printTree = (): string => {
|
||||||
|
return stringify(this.storageEntryTree);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor() {
|
||||||
|
this.initializeTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...Object.keys(currentParent).sort(localeCompare)];
|
// utility
|
||||||
};
|
static getFileName = (pathComponents: string[]): string => {
|
||||||
|
return pathComponents[pathComponents.length - 1] || "\\";
|
||||||
|
};
|
||||||
|
|
||||||
// write
|
static getFileNameFromString = (pathString: string): string => {
|
||||||
write = (pathComponents: string[], value: string): void => {
|
const pathComponents: string[] =
|
||||||
const pathString: string = StorageModel.pathComponentsToString(
|
this.stringToPathComponents(pathString);
|
||||||
...pathComponents
|
return pathComponents[pathComponents.length - 1] || "\\";
|
||||||
);
|
};
|
||||||
localStorage.setItem(pathString, value);
|
|
||||||
this.updateTree(...pathComponents);
|
|
||||||
};
|
|
||||||
|
|
||||||
remove = (
|
static pathComponentsToString = (...pathComponents: string[]): string => {
|
||||||
pathComponents: string[],
|
return pathComponents
|
||||||
shouldInitialize: boolean = true
|
.filter((x) => x != "")
|
||||||
): void => {
|
.join(PATH_COMPONENT_SEPARATOR);
|
||||||
const pathString: string = StorageModel.pathComponentsToString(
|
};
|
||||||
...pathComponents
|
|
||||||
);
|
|
||||||
localStorage.removeItem(pathString);
|
|
||||||
|
|
||||||
if (shouldInitialize == true) {
|
static stringToPathComponents = (string: string): string[] => {
|
||||||
this.initializeTree();
|
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
|
||||||
|
};
|
||||||
|
|
||||||
|
static join = (...items: string[]): string => {
|
||||||
|
let allComponents: string[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const parts = this.stringToPathComponents(item);
|
||||||
|
allComponents.push(...parts);
|
||||||
|
}
|
||||||
|
return StorageModel.pathComponentsToString(...allComponents);
|
||||||
|
};
|
||||||
|
|
||||||
|
static getPath(
|
||||||
|
locationName: StorageModelSubPath,
|
||||||
|
filePath: string[],
|
||||||
|
): string[] {
|
||||||
|
return [DATA_VERSION, locationName, ...filePath];
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
rename = (
|
|
||||||
sourcePathComponents: string[],
|
|
||||||
destinationPathComponents: string[],
|
|
||||||
shouldInitialize: boolean = true
|
|
||||||
): boolean => {
|
|
||||||
const content: string | null = this.read(sourcePathComponents);
|
|
||||||
if (content == null) return false;
|
|
||||||
|
|
||||||
this.write(destinationPathComponents, content);
|
|
||||||
this.remove(sourcePathComponents);
|
|
||||||
|
|
||||||
if (shouldInitialize == true) {
|
|
||||||
this.initializeTree();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// recursion
|
|
||||||
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
|
|
||||||
loop_over_files: for (const key of Object.keys(localStorage)) {
|
|
||||||
// get path of current entity
|
|
||||||
const pathComponentsOfCurrentEntity: string[] =
|
|
||||||
StorageModel.stringToPathComponents(key);
|
|
||||||
|
|
||||||
// exit if entity does not match
|
|
||||||
loop_over_path_components: for (
|
|
||||||
let i = 0;
|
|
||||||
i < rootDirectory.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
|
|
||||||
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
|
|
||||||
continue loop_over_files;
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute
|
|
||||||
fn(pathComponentsOfCurrentEntity);
|
|
||||||
}
|
|
||||||
this.initializeTree();
|
|
||||||
};
|
|
||||||
|
|
||||||
removeRecursively = (pathComponents: string[]): void => {
|
|
||||||
this.recurse(pathComponents, (path: string[]) => this.remove(path, false));
|
|
||||||
this.initializeTree();
|
|
||||||
};
|
|
||||||
|
|
||||||
renameRecursively = (
|
|
||||||
sourcePathComponents: string[],
|
|
||||||
destinationPathComponents: string[]
|
|
||||||
): void => {
|
|
||||||
this.recurse(sourcePathComponents, (path: string[]) => {
|
|
||||||
// assemble destinationPath
|
|
||||||
const relativePathOfCurrentEntity = path.slice(
|
|
||||||
sourcePathComponents.length
|
|
||||||
);
|
|
||||||
const destinationPathComponentsOfCurrentEntity = [
|
|
||||||
...destinationPathComponents,
|
|
||||||
...relativePathOfCurrentEntity,
|
|
||||||
];
|
|
||||||
|
|
||||||
// rename
|
|
||||||
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
|
|
||||||
});
|
|
||||||
this.initializeTree();
|
|
||||||
};
|
|
||||||
|
|
||||||
// stringifiable
|
|
||||||
writeStringifiable = (pathComponents: string[], value: ValidObject): void => {
|
|
||||||
const valueString: string = stringify(value);
|
|
||||||
this.write(pathComponents, valueString);
|
|
||||||
};
|
|
||||||
|
|
||||||
readStringifiable = <T extends ValidObject>(
|
|
||||||
pathComponents: string[],
|
|
||||||
reference: T
|
|
||||||
): T | null => {
|
|
||||||
const valueString: string | null = this.read(pathComponents);
|
|
||||||
if (!valueString) return null;
|
|
||||||
|
|
||||||
const object: any | null = parseValidObject(valueString, reference);
|
|
||||||
if (object == null) return null;
|
|
||||||
|
|
||||||
return object;
|
|
||||||
};
|
|
||||||
|
|
||||||
// cleaning
|
|
||||||
removeJunk = (): void => {
|
|
||||||
this.recurse([], (path: string[]) => {
|
|
||||||
if (path[0] == DATA_VERSION) return;
|
|
||||||
this.remove(path);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// tree
|
|
||||||
initializeTree = (): void => {
|
|
||||||
console.log("initializing tree");
|
|
||||||
|
|
||||||
this.storageEntryTree = {};
|
|
||||||
for (const key of Object.keys(localStorage)) {
|
|
||||||
const components: string[] = StorageModel.stringToPathComponents(key);
|
|
||||||
this.updateTree(...components);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTree = (...pathComponents: string[]): void => {
|
|
||||||
let currentParent: StorageEntry = this.storageEntryTree;
|
|
||||||
for (const pathPart of pathComponents) {
|
|
||||||
if (!currentParent[pathPart]) {
|
|
||||||
currentParent[pathPart] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
currentParent = currentParent[pathPart];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
printTree = (): string => {
|
|
||||||
return stringify(this.storageEntryTree);
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor() {
|
|
||||||
this.initializeTree();
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility
|
|
||||||
static getFileName = (pathComponents: string[]): string => {
|
|
||||||
return pathComponents[pathComponents.length - 1] || "\\";
|
|
||||||
};
|
|
||||||
|
|
||||||
static getFileNameFromString = (pathString: string): string => {
|
|
||||||
const pathComponents: string[] = this.stringToPathComponents(pathString);
|
|
||||||
return pathComponents[pathComponents.length - 1] || "\\";
|
|
||||||
};
|
|
||||||
|
|
||||||
static pathComponentsToString = (...pathComponents: string[]): string => {
|
|
||||||
return pathComponents.filter((x) => x != "").join(PATH_COMPONENT_SEPARATOR);
|
|
||||||
};
|
|
||||||
|
|
||||||
static stringToPathComponents = (string: string): string[] => {
|
|
||||||
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
|
|
||||||
};
|
|
||||||
|
|
||||||
static join = (...items: string[]): string => {
|
|
||||||
let allComponents: string[] = [];
|
|
||||||
for (const item of items) {
|
|
||||||
const parts = this.stringToPathComponents(item);
|
|
||||||
allComponents.push(...parts);
|
|
||||||
}
|
|
||||||
return StorageModel.pathComponentsToString(...allComponents);
|
|
||||||
};
|
|
||||||
|
|
||||||
static getPath(
|
|
||||||
locationName: StorageModelSubPath,
|
|
||||||
filePath: string[]
|
|
||||||
): string[] {
|
|
||||||
return [DATA_VERSION, locationName, ...filePath];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
@@ -214,37 +224,37 @@ export type StorageEntry = { [key: string]: StorageEntry };
|
|||||||
|
|
||||||
// locations
|
// locations
|
||||||
export enum StorageModelSubPath {
|
export enum StorageModelSubPath {
|
||||||
Chat = "chat",
|
Chat = "chat",
|
||||||
|
|
||||||
ConnectionModel = "connection",
|
ConnectionModel = "connection",
|
||||||
SettingsModel = "settings",
|
SettingsModel = "settings",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filePaths = {
|
export const filePaths = {
|
||||||
connectionModel: {
|
connectionModel: {
|
||||||
socketAddress: ["socket-address"],
|
socketAddress: ["socket-address"],
|
||||||
reconnectAddress: ["reconnect-address"],
|
reconnectAddress: ["reconnect-address"],
|
||||||
outbox: ["outbox"],
|
outbox: ["outbox"],
|
||||||
mailboxes: ["mailboxes"],
|
mailboxes: ["mailboxes"],
|
||||||
|
|
||||||
previousAddresses: ["previous-addresses"],
|
previousAddresses: ["previous-addresses"],
|
||||||
},
|
},
|
||||||
|
|
||||||
chat: {
|
chat: {
|
||||||
base: [],
|
base: [],
|
||||||
chatBase: (id: string) => [id],
|
chatBase: (id: string) => [id],
|
||||||
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
|
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
|
||||||
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
|
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
|
||||||
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
|
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
|
||||||
lastUsedPage: (id: string) => [
|
lastUsedPage: (id: string) => [
|
||||||
...filePaths.chat.chatBase(id),
|
...filePaths.chat.chatBase(id),
|
||||||
"last-used-page",
|
"last-used-page",
|
||||||
],
|
],
|
||||||
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
|
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
|
||||||
},
|
},
|
||||||
|
|
||||||
settingsModel: {
|
settingsModel: {
|
||||||
username: ["user-name"],
|
username: ["user-name"],
|
||||||
firstDayOfWeek: ["first-day-of-week"],
|
firstDayOfWeek: ["first-day-of-week"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+60
-60
@@ -3,113 +3,113 @@ const ENCRYPTION_ALG = "AES-GCM";
|
|||||||
|
|
||||||
// minimal
|
// minimal
|
||||||
export async function encryptString(
|
export async function encryptString(
|
||||||
plaintext: string,
|
plaintext: string,
|
||||||
passphrase: string
|
passphrase: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!window.crypto.subtle) return plaintext;
|
if (!window.crypto.subtle) return plaintext;
|
||||||
|
|
||||||
const iv = generateIV();
|
const iv = generateIV();
|
||||||
const key = await importKey(passphrase, "encrypt");
|
const key = await importKey(passphrase, "encrypt");
|
||||||
const encryptedArray = await encrypt(iv, key, plaintext);
|
const encryptedArray = await encrypt(iv, key, plaintext);
|
||||||
|
|
||||||
const encryptionData = {
|
const encryptionData = {
|
||||||
iv: uInt8ToArray(iv),
|
iv: uInt8ToArray(iv),
|
||||||
encryptedArray: uInt8ToArray(encryptedArray),
|
encryptedArray: uInt8ToArray(encryptedArray),
|
||||||
};
|
};
|
||||||
return btoa(JSON.stringify(encryptionData));
|
return btoa(JSON.stringify(encryptionData));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptString(
|
export async function decryptString(
|
||||||
cyphertext: string,
|
cyphertext: string,
|
||||||
passphrase: string
|
passphrase: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const encrypionData = JSON.parse(atob(cyphertext));
|
const encrypionData = JSON.parse(atob(cyphertext));
|
||||||
const iv = arrayToUint8(encrypionData.iv);
|
const iv = arrayToUint8(encrypionData.iv);
|
||||||
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
||||||
|
|
||||||
const key = await importKey(passphrase, "decrypt");
|
const key = await importKey(passphrase, "decrypt");
|
||||||
return await decrypt(iv, key, encryptedArray);
|
return await decrypt(iv, key, encryptedArray);
|
||||||
} catch {
|
} catch {
|
||||||
return cyphertext;
|
return cyphertext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// process
|
// process
|
||||||
export function encode(string: string): Uint8Array {
|
export function encode(string: string): Uint8Array {
|
||||||
return new TextEncoder().encode(string);
|
return new TextEncoder().encode(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decode(array: Uint8Array): string {
|
export function decode(array: Uint8Array): string {
|
||||||
return new TextDecoder("utf-8").decode(array);
|
return new TextDecoder("utf-8").decode(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encrypt(
|
export async function encrypt(
|
||||||
iv: Uint8Array,
|
iv: Uint8Array,
|
||||||
key: CryptoKey,
|
key: CryptoKey,
|
||||||
message: string
|
message: string,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const arrayBuffer = await window.crypto.subtle.encrypt(
|
const arrayBuffer = await window.crypto.subtle.encrypt(
|
||||||
{ name: ENCRYPTION_ALG, iv },
|
{ name: ENCRYPTION_ALG, iv },
|
||||||
key,
|
key,
|
||||||
encode(message)
|
encode(message),
|
||||||
);
|
);
|
||||||
return new Uint8Array(arrayBuffer);
|
return new Uint8Array(arrayBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decrypt(
|
export async function decrypt(
|
||||||
iv: Uint8Array,
|
iv: Uint8Array,
|
||||||
key: CryptoKey,
|
key: CryptoKey,
|
||||||
cyphertext: Uint8Array
|
cyphertext: Uint8Array,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const arrayBuffer = await crypto.subtle.decrypt(
|
const arrayBuffer = await crypto.subtle.decrypt(
|
||||||
{ name: ENCRYPTION_ALG, iv },
|
{ name: ENCRYPTION_ALG, iv },
|
||||||
key,
|
key,
|
||||||
cyphertext
|
cyphertext,
|
||||||
);
|
);
|
||||||
return arrayBufferToString(arrayBuffer);
|
return arrayBufferToString(arrayBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
|
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
|
||||||
return await crypto.subtle.digest("SHA-256", encoded);
|
return await crypto.subtle.digest("SHA-256", encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// iv
|
// iv
|
||||||
export function generateIV(): Uint8Array {
|
export function generateIV(): Uint8Array {
|
||||||
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitMergedString(mergedString: string): [Uint8Array, string] {
|
export function splitMergedString(mergedString: string): [Uint8Array, string] {
|
||||||
const iv = mergedString.slice(0, IV_SIZE);
|
const iv = mergedString.slice(0, IV_SIZE);
|
||||||
const encryptedString = mergedString.slice(IV_SIZE);
|
const encryptedString = mergedString.slice(IV_SIZE);
|
||||||
|
|
||||||
return [JSON.parse(iv), encryptedString];
|
return [JSON.parse(iv), encryptedString];
|
||||||
}
|
}
|
||||||
|
|
||||||
// key
|
// key
|
||||||
export async function importKey(
|
export async function importKey(
|
||||||
passphrase: string,
|
passphrase: string,
|
||||||
purpose: "encrypt" | "decrypt"
|
purpose: "encrypt" | "decrypt",
|
||||||
): Promise<CryptoKey> {
|
): Promise<CryptoKey> {
|
||||||
return await crypto.subtle.importKey(
|
return await crypto.subtle.importKey(
|
||||||
"raw",
|
"raw",
|
||||||
await hash(encode(passphrase)),
|
await hash(encode(passphrase)),
|
||||||
{ name: ENCRYPTION_ALG },
|
{ name: ENCRYPTION_ALG },
|
||||||
false,
|
false,
|
||||||
[purpose]
|
[purpose],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// conversion
|
// conversion
|
||||||
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
|
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
|
||||||
const uInt8Array = new Uint8Array(arrayBuffer);
|
const uInt8Array = new Uint8Array(arrayBuffer);
|
||||||
return decode(uInt8Array);
|
return decode(uInt8Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
|
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
|
||||||
return Array.from(uInt8Array);
|
return Array.from(uInt8Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayToUint8(array: number[]): Uint8Array {
|
export function arrayToUint8(array: number[]): Uint8Array {
|
||||||
return new Uint8Array(array);
|
return new Uint8Array(array);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
export const DATA_VERSION = "v2";
|
export const DATA_VERSION = "v2";
|
||||||
|
|
||||||
export function checkIsValidObject(object: any): boolean {
|
export function checkIsValidObject(object: any): boolean {
|
||||||
return object.dataVersion == DATA_VERSION;
|
return object.dataVersion == DATA_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkMatchesObjectStructure(
|
export function checkMatchesObjectStructure(
|
||||||
objectToCheck: any,
|
objectToCheck: any,
|
||||||
reference: any
|
reference: any,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (typeof reference != "object") {
|
if (typeof reference != "object") {
|
||||||
return typeof objectToCheck == typeof reference;
|
return typeof objectToCheck == typeof reference;
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of Object.keys(reference)) {
|
|
||||||
const requiredType = typeof reference[key];
|
|
||||||
const actualType = typeof objectToCheck[key];
|
|
||||||
if (requiredType != actualType) return false;
|
|
||||||
|
|
||||||
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
|
|
||||||
// only check if array is not empty
|
|
||||||
if (objectToCheck[key].length == 0) continue;
|
|
||||||
if (objectToCheck[key].size == 0) continue;
|
|
||||||
|
|
||||||
// check first item of array
|
|
||||||
const [firstOfObjectToCheck] = objectToCheck[key];
|
|
||||||
const [fisrtOfReference] = reference[key];
|
|
||||||
const doesFirstItemMatch = checkMatchesObjectStructure(
|
|
||||||
firstOfObjectToCheck,
|
|
||||||
fisrtOfReference
|
|
||||||
);
|
|
||||||
if (doesFirstItemMatch == false) return false;
|
|
||||||
} else if (requiredType == "object") {
|
|
||||||
// recurse into objects
|
|
||||||
const doesNestedObjectMatch = checkMatchesObjectStructure(
|
|
||||||
objectToCheck[key],
|
|
||||||
reference[key]
|
|
||||||
);
|
|
||||||
if (doesNestedObjectMatch == false) return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
for (const key of Object.keys(reference)) {
|
||||||
|
const requiredType = typeof reference[key];
|
||||||
|
const actualType = typeof objectToCheck[key];
|
||||||
|
if (requiredType != actualType) return false;
|
||||||
|
|
||||||
|
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
|
||||||
|
// only check if array is not empty
|
||||||
|
if (objectToCheck[key].length == 0) continue;
|
||||||
|
if (objectToCheck[key].size == 0) continue;
|
||||||
|
|
||||||
|
// check first item of array
|
||||||
|
const [firstOfObjectToCheck] = objectToCheck[key];
|
||||||
|
const [fisrtOfReference] = reference[key];
|
||||||
|
const doesFirstItemMatch = checkMatchesObjectStructure(
|
||||||
|
firstOfObjectToCheck,
|
||||||
|
fisrtOfReference,
|
||||||
|
);
|
||||||
|
if (doesFirstItemMatch == false) return false;
|
||||||
|
} else if (requiredType == "object") {
|
||||||
|
// recurse into objects
|
||||||
|
const doesNestedObjectMatch = checkMatchesObjectStructure(
|
||||||
|
objectToCheck[key],
|
||||||
|
reference[key],
|
||||||
|
);
|
||||||
|
if (doesNestedObjectMatch == false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidObject {
|
export interface ValidObject {
|
||||||
dataVersion: "v2";
|
dataVersion: "v2";
|
||||||
}
|
}
|
||||||
|
|||||||
+182
-181
@@ -1,277 +1,278 @@
|
|||||||
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
||||||
|
|
||||||
export interface Stringifiable {
|
export interface Stringifiable {
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// crypto
|
// crypto
|
||||||
export function generateRandomToken(length: number): string {
|
export function generateRandomToken(length: number): string {
|
||||||
const array = new Uint8Array(length);
|
const array = new Uint8Array(length);
|
||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array);
|
||||||
return array.join("");
|
return array.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// date
|
// date
|
||||||
export function createTimestamp(): string {
|
export function createTimestamp(): string {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// filters etc
|
// filters etc
|
||||||
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
|
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
|
||||||
|
|
||||||
export function filterObjectsByStringEntries<T>(
|
export function filterObjectsByStringEntries<T>(
|
||||||
reference: StringEntryObject,
|
reference: StringEntryObject,
|
||||||
converter: (object: T) => StringEntryObject,
|
converter: (object: T) => StringEntryObject,
|
||||||
objects: T[]
|
objects: T[],
|
||||||
): Set<T> {
|
): Set<T> {
|
||||||
const matches: Set<T> = new Set();
|
const matches: Set<T> = new Set();
|
||||||
|
|
||||||
object_loop: for (const object of objects) {
|
object_loop: for (const object of objects) {
|
||||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||||
reference,
|
reference,
|
||||||
converter(object)
|
converter(object),
|
||||||
);
|
);
|
||||||
if (doesMatch) matches.add(object);
|
if (doesMatch) matches.add(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkDoesObjectMatchReference(
|
export function checkDoesObjectMatchReference(
|
||||||
reference: StringEntryObject,
|
reference: StringEntryObject,
|
||||||
stringEntryObject: StringEntryObject
|
stringEntryObject: StringEntryObject,
|
||||||
): boolean {
|
): boolean {
|
||||||
reference_entry_loop: for (const referenceEntry of Object.entries(
|
reference_entry_loop: for (const referenceEntry of Object.entries(
|
||||||
reference
|
reference,
|
||||||
)) {
|
)) {
|
||||||
const [referenceKey, referenceValue] = referenceEntry;
|
const [referenceKey, referenceValue] = referenceEntry;
|
||||||
const stringEntryObjectValue: Stringifiable | undefined =
|
const stringEntryObjectValue: Stringifiable | undefined =
|
||||||
stringEntryObject[referenceKey];
|
stringEntryObject[referenceKey];
|
||||||
|
|
||||||
if (referenceValue == undefined) return false;
|
if (referenceValue == undefined) return false;
|
||||||
|
|
||||||
if (referenceValue[0] == "-") {
|
if (referenceValue[0] == "-") {
|
||||||
const strippedReferenceValue: string = referenceValue
|
const strippedReferenceValue: string = referenceValue
|
||||||
.toString()
|
.toString()
|
||||||
.substring(1);
|
.substring(1);
|
||||||
// property may not exist
|
// property may not exist
|
||||||
if (
|
if (
|
||||||
strippedReferenceValue == "" &&
|
strippedReferenceValue == "" &&
|
||||||
stringEntryObjectValue != undefined &&
|
stringEntryObjectValue != undefined &&
|
||||||
stringEntryObjectValue != ""
|
stringEntryObjectValue != ""
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// property may not match
|
// property may not match
|
||||||
if (stringEntryObjectValue == strippedReferenceValue) {
|
if (stringEntryObjectValue == strippedReferenceValue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// property must exist but be anything
|
// property must exist but be anything
|
||||||
if (
|
if (
|
||||||
referenceValue == "" &&
|
referenceValue == "" &&
|
||||||
(stringEntryObjectValue == undefined || stringEntryObjectValue == "")
|
(stringEntryObjectValue == undefined ||
|
||||||
) {
|
stringEntryObjectValue == "")
|
||||||
return false;
|
) {
|
||||||
} else if (referenceValue == "") {
|
return false;
|
||||||
continue reference_entry_loop;
|
} else if (referenceValue == "") {
|
||||||
}
|
continue reference_entry_loop;
|
||||||
|
}
|
||||||
|
|
||||||
// property must match
|
// property must match
|
||||||
if (stringEntryObjectValue != referenceValue) {
|
if (stringEntryObjectValue != referenceValue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectObjectValuesForKey<T>(
|
export function collectObjectValuesForKey<T>(
|
||||||
key: string,
|
key: string,
|
||||||
converter: (object: T) => StringEntryObject,
|
converter: (object: T) => StringEntryObject,
|
||||||
objects: T[]
|
objects: T[],
|
||||||
): string[] {
|
): string[] {
|
||||||
const values: Set<string> = new Set();
|
const values: Set<string> = new Set();
|
||||||
|
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
const stringEntryObject: StringEntryObject = converter(object);
|
const stringEntryObject: StringEntryObject = converter(object);
|
||||||
const stringEntryObjectValue: Stringifiable | undefined =
|
const stringEntryObjectValue: Stringifiable | undefined =
|
||||||
stringEntryObject[key];
|
stringEntryObject[key];
|
||||||
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
|
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
values.add(stringEntryObjectValue.toString());
|
values.add(stringEntryObjectValue.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...values.values()];
|
return [...values.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterObjectsByWords<T>(
|
export function filterObjectsByWords<T>(
|
||||||
query: string,
|
query: string,
|
||||||
getStringsOfObject: (object: T) => string[],
|
getStringsOfObject: (object: T) => string[],
|
||||||
objects: T[]
|
objects: T[],
|
||||||
): Set<T> {
|
): Set<T> {
|
||||||
const matches: Set<T> = new Set();
|
const matches: Set<T> = new Set();
|
||||||
|
|
||||||
object_loop: for (const object of objects) {
|
object_loop: for (const object of objects) {
|
||||||
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
||||||
query,
|
query,
|
||||||
getStringsOfObject,
|
getStringsOfObject,
|
||||||
object
|
object,
|
||||||
);
|
);
|
||||||
if (doesMatch) matches.add(object);
|
if (doesMatch) matches.add(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkDoesObjectMatchSearch<T>(
|
export function checkDoesObjectMatchSearch<T>(
|
||||||
query: string,
|
query: string,
|
||||||
getStringsOfObject: (object: T) => string[],
|
getStringsOfObject: (object: T) => string[],
|
||||||
object: T
|
object: T,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (query == "") return true;
|
if (query == "") return true;
|
||||||
|
|
||||||
const stringsInObject: string[] = getStringsOfObject(object);
|
const stringsInObject: string[] = getStringsOfObject(object);
|
||||||
const wordsInObject: string[] = [];
|
const wordsInObject: string[] = [];
|
||||||
for (const string of stringsInObject) {
|
for (const string of stringsInObject) {
|
||||||
const lowercaseWordsInString = string
|
const lowercaseWordsInString = string
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter((word) => word != "");
|
.filter((word) => word != "");
|
||||||
wordsInObject.push(...lowercaseWordsInString);
|
wordsInObject.push(...lowercaseWordsInString);
|
||||||
}
|
|
||||||
|
|
||||||
const lowercaseWordsInQuery = query
|
|
||||||
.toLowerCase()
|
|
||||||
.split(" ")
|
|
||||||
.filter((word) => word != "");
|
|
||||||
for (const queryWord of lowercaseWordsInQuery) {
|
|
||||||
if (queryWord[0] == "-") {
|
|
||||||
// exclusion
|
|
||||||
const wordContent = queryWord.substring(1);
|
|
||||||
if (wordsInObject.includes(wordContent)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (wordsInObject.includes(queryWord) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
const lowercaseWordsInQuery = query
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ")
|
||||||
|
.filter((word) => word != "");
|
||||||
|
for (const queryWord of lowercaseWordsInQuery) {
|
||||||
|
if (queryWord[0] == "-") {
|
||||||
|
// exclusion
|
||||||
|
const wordContent = queryWord.substring(1);
|
||||||
|
if (wordsInObject.includes(wordContent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wordsInObject.includes(queryWord) == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
export type Handler<T> = (item: T) => void;
|
export type Handler<T> = (item: T) => void;
|
||||||
|
|
||||||
export class HandlerManager<T> {
|
export class HandlerManager<T> {
|
||||||
handlers: Set<Handler<T>> = new Set();
|
handlers: Set<Handler<T>> = new Set();
|
||||||
|
|
||||||
// manage
|
// manage
|
||||||
addHandler = (handler: Handler<T>): void => {
|
addHandler = (handler: Handler<T>): void => {
|
||||||
this.handlers.add(handler);
|
this.handlers.add(handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteHandler = (handler: Handler<T>): void => {
|
deleteHandler = (handler: Handler<T>): void => {
|
||||||
this.handlers.delete(handler);
|
this.handlers.delete(handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
// trigger
|
// trigger
|
||||||
trigger = (item: T): void => {
|
trigger = (item: T): void => {
|
||||||
for (const handler of this.handlers) {
|
for (const handler of this.handlers) {
|
||||||
handler(item);
|
handler(item);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// sorting
|
// sorting
|
||||||
export class IndexManager<T> {
|
export class IndexManager<T> {
|
||||||
private itemToString: (item: T) => string;
|
private itemToString: (item: T) => string;
|
||||||
|
|
||||||
sortedStrings: string[] = [];
|
sortedStrings: string[] = [];
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
update = (items: T[]): void => {
|
update = (items: T[]): void => {
|
||||||
this.sortedStrings = [];
|
this.sortedStrings = [];
|
||||||
|
|
||||||
let strings: string[] = [];
|
let strings: string[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const string: string = this.itemToString(item);
|
const string: string = this.itemToString(item);
|
||||||
strings.push(string);
|
strings.push(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sortedStrings = strings.sort(localeCompare);
|
||||||
|
};
|
||||||
|
|
||||||
|
getIndex = (item: T): number => {
|
||||||
|
const string: string = this.itemToString(item);
|
||||||
|
const index: number = this.sortedStrings.indexOf(string);
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(itemToString: (item: T) => string) {
|
||||||
|
this.itemToString = itemToString;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortedStrings = strings.sort(localeCompare);
|
|
||||||
};
|
|
||||||
|
|
||||||
getIndex = (item: T): number => {
|
|
||||||
const string: string = this.itemToString(item);
|
|
||||||
const index: number = this.sortedStrings.indexOf(string);
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(itemToString: (item: T) => string) {
|
|
||||||
this.itemToString = itemToString;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// storage
|
// storage
|
||||||
export function getLocalStorageItemAndClear(key: string): string | null {
|
export function getLocalStorageItemAndClear(key: string): string | null {
|
||||||
const value: string | null = localStorage.getItem(key);
|
const value: string | null = localStorage.getItem(key);
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
if (value != null) localStorage.setItem(`_${key}`, value);
|
if (value != null) localStorage.setItem(`_${key}`, value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// string & parsing
|
// string & parsing
|
||||||
export function stringify(data: any): string {
|
export function stringify(data: any): string {
|
||||||
return JSON.stringify(data, null, 4);
|
return JSON.stringify(data, null, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function padZero(string: string | undefined, length: number): string {
|
export function padZero(string: string | undefined, length: number): string {
|
||||||
return (string ?? "").padStart(length, "0");
|
return (string ?? "").padStart(length, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parse(string: string): any {
|
export function parse(string: string): any {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(string);
|
return JSON.parse(string);
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseValidObject<T>(string: string, reference: T): T | null {
|
export function parseValidObject<T>(string: string, reference: T): T | null {
|
||||||
const parsed: any = parse(string);
|
const parsed: any = parse(string);
|
||||||
if (checkIsValidObject(parsed) == false) return null;
|
if (checkIsValidObject(parsed) == false) return null;
|
||||||
|
|
||||||
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
||||||
parsed,
|
parsed,
|
||||||
reference
|
reference,
|
||||||
);
|
);
|
||||||
if (doesMatchReference == false) return null;
|
if (doesMatchReference == false) return null;
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseOrFallback(inputString: string): any {
|
export function parseOrFallback(inputString: string): any {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(inputString);
|
return JSON.parse(inputString);
|
||||||
} catch {
|
} catch {
|
||||||
return inputString;
|
return inputString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseArray(inputString: string): any[] {
|
export function parseArray(inputString: string): any[] {
|
||||||
const parsed: any = parseOrFallback(inputString);
|
const parsed: any = parseOrFallback(inputString);
|
||||||
if (Array.isArray(parsed) == false) return [];
|
if (Array.isArray(parsed) == false) return [];
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort
|
// sort
|
||||||
export function localeCompare(a: string, b: string): number {
|
export function localeCompare(a: string, b: string): number {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
}
|
}
|
||||||
|
|||||||
+239
-228
@@ -1,14 +1,18 @@
|
|||||||
// this file is responsible for migrating data from v1 to v2.
|
// this file is responsible for migrating data from v1 to v2.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BoardInfoFileContent,
|
BoardInfoFileContent,
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../Model/Files/boardsAndTasksModel";
|
} from "../Model/Files/boardsAndTasksModel";
|
||||||
import ChatModel, {
|
import ChatModel, {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ChatMessageStatus,
|
ChatMessageStatus,
|
||||||
} from "../Model/Chat/chatModel";
|
} from "../Model/Chat/chatModel";
|
||||||
import { getLocalStorageItemAndClear, parseArray, parseOrFallback } from "../Model/Utility/utility";
|
import {
|
||||||
|
getLocalStorageItemAndClear,
|
||||||
|
parseArray,
|
||||||
|
parseOrFallback,
|
||||||
|
} from "../Model/Utility/utility";
|
||||||
|
|
||||||
import ChatListModel from "../Model/Chat/chatListModel";
|
import ChatListModel from "../Model/Chat/chatListModel";
|
||||||
import ConnectionModel from "../Model/Global/connectionModel";
|
import ConnectionModel from "../Model/Global/connectionModel";
|
||||||
@@ -19,263 +23,270 @@ import { translations } from "../View/translations";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export default class v1Upgrader {
|
export default class v1Upgrader {
|
||||||
// general
|
// general
|
||||||
migrateSettings = (): void => {
|
migrateSettings = (): void => {
|
||||||
const name: string | null = getLocalStorageItemAndClear("sender-name");
|
const name: string | null = getLocalStorageItemAndClear("sender-name");
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
const parsedName: string = parseOrFallback(name);
|
const parsedName: string = parseOrFallback(name);
|
||||||
this.settingsModel.setName(parsedName);
|
this.settingsModel.setName(parsedName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstDayOfWeek: string | null =
|
const firstDayOfWeek: string | null =
|
||||||
getLocalStorageItemAndClear("first-day-of-week");
|
getLocalStorageItemAndClear("first-day-of-week");
|
||||||
if (firstDayOfWeek != null) {
|
if (firstDayOfWeek != null) {
|
||||||
const parsedFirstDayOfWeek: string = parseOrFallback(firstDayOfWeek);
|
const parsedFirstDayOfWeek: string =
|
||||||
this.settingsModel.setFirstDayOfWeek(parsedFirstDayOfWeek);
|
parseOrFallback(firstDayOfWeek);
|
||||||
}
|
this.settingsModel.setFirstDayOfWeek(parsedFirstDayOfWeek);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
migrateConnections = (): void => {
|
migrateConnections = (): void => {
|
||||||
const previousAddressString: string | null =
|
const previousAddressString: string | null =
|
||||||
getLocalStorageItemAndClear("previous-addresses");
|
getLocalStorageItemAndClear("previous-addresses");
|
||||||
if (previousAddressString == null) return;
|
if (previousAddressString == null) return;
|
||||||
|
|
||||||
const previousAddresses: any[] = parseArray(previousAddressString);
|
const previousAddresses: any[] = parseArray(previousAddressString);
|
||||||
for (const address of previousAddresses) {
|
for (const address of previousAddresses) {
|
||||||
if (typeof address != "string") continue;
|
if (typeof address != "string") continue;
|
||||||
this.connectionModel.storeAddress(address);
|
this.connectionModel.storeAddress(address);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// chats
|
// chats
|
||||||
migrateChats = (): void => {
|
migrateChats = (): void => {
|
||||||
const chatIdString: string | null = getLocalStorageItemAndClear("chat-ids");
|
const chatIdString: string | null =
|
||||||
if (chatIdString == null) return;
|
getLocalStorageItemAndClear("chat-ids");
|
||||||
|
if (chatIdString == null) return;
|
||||||
|
|
||||||
const chatIds: any[] = parseArray(chatIdString);
|
const chatIds: any[] = parseArray(chatIdString);
|
||||||
for (const chatId of chatIds) {
|
for (const chatId of chatIds) {
|
||||||
if (typeof chatId != "string") continue;
|
if (typeof chatId != "string") continue;
|
||||||
this.migrateChatById(chatId);
|
this.migrateChatById(chatId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
migrateChatById = (id: string): void => {
|
migrateChatById = (id: string): void => {
|
||||||
// get primary channel
|
// get primary channel
|
||||||
const primaryChannel: string | null = getLocalStorageItemAndClear(
|
const primaryChannel: string | null = getLocalStorageItemAndClear(
|
||||||
storageKeys.primaryChannel(id)
|
storageKeys.primaryChannel(id),
|
||||||
);
|
|
||||||
if (primaryChannel == null) return;
|
|
||||||
const parsedPriamryChannel: string = parseOrFallback(primaryChannel);
|
|
||||||
|
|
||||||
// create chat
|
|
||||||
const chatModel: ChatModel =
|
|
||||||
this.chatListModel.createChat(parsedPriamryChannel);
|
|
||||||
|
|
||||||
// secondary channels
|
|
||||||
const secondaryChannelString: string | null = getLocalStorageItemAndClear(
|
|
||||||
storageKeys.secondaryChannels(id)
|
|
||||||
);
|
|
||||||
if (secondaryChannelString != null) {
|
|
||||||
const potentialSecondaryChannels: any[] = parseArray(
|
|
||||||
secondaryChannelString
|
|
||||||
);
|
|
||||||
|
|
||||||
const confirmedSecondaryChannels: string[] = [];
|
|
||||||
for (const secondaryChannel of potentialSecondaryChannels) {
|
|
||||||
if (typeof secondaryChannel != "string") continue;
|
|
||||||
confirmedSecondaryChannels.push(secondaryChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
chatModel.setSecondaryChannels(confirmedSecondaryChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryption key
|
|
||||||
const encryptionKey: string | null = getLocalStorageItemAndClear(
|
|
||||||
storageKeys.encyptionKey(id)
|
|
||||||
);
|
|
||||||
if (encryptionKey != null) {
|
|
||||||
const parsedEncryptionKey: string = parseOrFallback(encryptionKey);
|
|
||||||
chatModel.setEncryptionKey(parsedEncryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// messages
|
|
||||||
const messagesString: string =
|
|
||||||
getLocalStorageItemAndClear(storageKeys.messages(id)) ?? "";
|
|
||||||
const messageOutboxString: string =
|
|
||||||
getLocalStorageItemAndClear(storageKeys.outbox(id)) ?? "";
|
|
||||||
const potentialMessages: any[] = parseArray(messagesString);
|
|
||||||
const potentialMessagesInOutbox: any[] = parseArray(messageOutboxString);
|
|
||||||
|
|
||||||
const addMessages = (
|
|
||||||
potentialMessages: any[],
|
|
||||||
status: ChatMessageStatus
|
|
||||||
) => {
|
|
||||||
for (const potentialMessage of potentialMessages) {
|
|
||||||
const isV1ChatMessage: boolean = checkMatchesObjectStructure(
|
|
||||||
potentialMessage,
|
|
||||||
V1ChatMessageReference
|
|
||||||
);
|
);
|
||||||
if (isV1ChatMessage == false) continue;
|
if (primaryChannel == null) return;
|
||||||
const v1ChatMessage: typeof V1ChatMessageReference = potentialMessage;
|
const parsedPriamryChannel: string = parseOrFallback(primaryChannel);
|
||||||
|
|
||||||
const convertedChatMessage: ChatMessage = {
|
// create chat
|
||||||
dataVersion: "v2",
|
const chatModel: ChatModel =
|
||||||
|
this.chatListModel.createChat(parsedPriamryChannel);
|
||||||
|
|
||||||
id: v4(),
|
// secondary channels
|
||||||
|
const secondaryChannelString: string | null =
|
||||||
|
getLocalStorageItemAndClear(storageKeys.secondaryChannels(id));
|
||||||
|
if (secondaryChannelString != null) {
|
||||||
|
const potentialSecondaryChannels: any[] = parseArray(
|
||||||
|
secondaryChannelString,
|
||||||
|
);
|
||||||
|
|
||||||
channel: v1ChatMessage.channel,
|
const confirmedSecondaryChannels: string[] = [];
|
||||||
sender: v1ChatMessage.sender,
|
for (const secondaryChannel of potentialSecondaryChannels) {
|
||||||
body: v1ChatMessage.body,
|
if (typeof secondaryChannel != "string") continue;
|
||||||
dateSent: v1ChatMessage.isoDate,
|
confirmedSecondaryChannels.push(secondaryChannel);
|
||||||
|
}
|
||||||
|
|
||||||
status,
|
chatModel.setSecondaryChannels(confirmedSecondaryChannels);
|
||||||
stringifiedFile: "",
|
}
|
||||||
|
|
||||||
|
// encryption key
|
||||||
|
const encryptionKey: string | null = getLocalStorageItemAndClear(
|
||||||
|
storageKeys.encyptionKey(id),
|
||||||
|
);
|
||||||
|
if (encryptionKey != null) {
|
||||||
|
const parsedEncryptionKey: string = parseOrFallback(encryptionKey);
|
||||||
|
chatModel.setEncryptionKey(parsedEncryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages
|
||||||
|
const messagesString: string =
|
||||||
|
getLocalStorageItemAndClear(storageKeys.messages(id)) ?? "";
|
||||||
|
const messageOutboxString: string =
|
||||||
|
getLocalStorageItemAndClear(storageKeys.outbox(id)) ?? "";
|
||||||
|
const potentialMessages: any[] = parseArray(messagesString);
|
||||||
|
const potentialMessagesInOutbox: any[] =
|
||||||
|
parseArray(messageOutboxString);
|
||||||
|
|
||||||
|
const addMessages = (
|
||||||
|
potentialMessages: any[],
|
||||||
|
status: ChatMessageStatus,
|
||||||
|
) => {
|
||||||
|
for (const potentialMessage of potentialMessages) {
|
||||||
|
const isV1ChatMessage: boolean = checkMatchesObjectStructure(
|
||||||
|
potentialMessage,
|
||||||
|
V1ChatMessageReference,
|
||||||
|
);
|
||||||
|
if (isV1ChatMessage == false) continue;
|
||||||
|
const v1ChatMessage: typeof V1ChatMessageReference =
|
||||||
|
potentialMessage;
|
||||||
|
|
||||||
|
const convertedChatMessage: ChatMessage = {
|
||||||
|
dataVersion: "v2",
|
||||||
|
|
||||||
|
id: v4(),
|
||||||
|
|
||||||
|
channel: v1ChatMessage.channel,
|
||||||
|
sender: v1ChatMessage.sender,
|
||||||
|
body: v1ChatMessage.body,
|
||||||
|
dateSent: v1ChatMessage.isoDate,
|
||||||
|
|
||||||
|
status,
|
||||||
|
stringifiedFile: "",
|
||||||
|
};
|
||||||
|
chatModel.addMessage(convertedChatMessage);
|
||||||
|
if (status == ChatMessageStatus.Outbox) {
|
||||||
|
this.connectionModel.sendMessageOrStore(
|
||||||
|
convertedChatMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
chatModel.addMessage(convertedChatMessage);
|
|
||||||
if (status == ChatMessageStatus.Outbox) {
|
addMessages(potentialMessages, ChatMessageStatus.Received);
|
||||||
this.connectionModel.sendMessageOrStore(convertedChatMessage);
|
addMessages(potentialMessagesInOutbox, ChatMessageStatus.Outbox);
|
||||||
}
|
|
||||||
}
|
// objects
|
||||||
|
const objectsString: string =
|
||||||
|
getLocalStorageItemAndClear(storageKeys.objects(id)) ?? "";
|
||||||
|
const objectOutboxString: string =
|
||||||
|
getLocalStorageItemAndClear(storageKeys.itemOutbox(id)) ?? "";
|
||||||
|
const potentialObjects: any[] = parseArray(objectsString);
|
||||||
|
const potentialObjectsInOutbox: any[] = parseArray(objectOutboxString);
|
||||||
|
|
||||||
|
const addObjects = (potentialObjects: any[]): void => {
|
||||||
|
const board: BoardInfoFileContent =
|
||||||
|
chatModel.fileModel.boardsAndTasksModel.createBoard(
|
||||||
|
translations.updater.migrated,
|
||||||
|
);
|
||||||
|
chatModel.fileModel.boardsAndTasksModel.updateBoard(board);
|
||||||
|
|
||||||
|
for (const potentialObjectEntry of potentialObjects) {
|
||||||
|
const potentialObject = potentialObjectEntry[1];
|
||||||
|
const isV1MessageObject: boolean =
|
||||||
|
checkIsV1MessageObject(potentialObject);
|
||||||
|
if (isV1MessageObject == false) continue;
|
||||||
|
|
||||||
|
const objectId: string = potentialObject.id;
|
||||||
|
const objectName: string = potentialObject.title;
|
||||||
|
|
||||||
|
const contentVersions: any[] = Object.values(
|
||||||
|
potentialObject.contentVersions,
|
||||||
|
);
|
||||||
|
for (const potentialVersion of contentVersions) {
|
||||||
|
const isV1MessageObjectContent: boolean =
|
||||||
|
checkIsV1MessageObjectContent(potentialVersion);
|
||||||
|
if (isV1MessageObjectContent == false) continue;
|
||||||
|
|
||||||
|
const convertedTaskFileContent: TaskFileContent = {
|
||||||
|
dataVersion: "v2",
|
||||||
|
|
||||||
|
fileId: objectId,
|
||||||
|
fileContentId: FileModel.generateFileContentId(
|
||||||
|
potentialVersion.isoDateVersionCreated,
|
||||||
|
),
|
||||||
|
creationDate: potentialVersion.isoDateVersionCreated,
|
||||||
|
type: "task",
|
||||||
|
|
||||||
|
name: objectName ?? "",
|
||||||
|
boardId: board.fileId ?? "",
|
||||||
|
|
||||||
|
description: potentialVersion.noteContent ?? "",
|
||||||
|
|
||||||
|
category: potentialVersion.categoryName ?? "",
|
||||||
|
status: potentialVersion.status ?? "",
|
||||||
|
priority: potentialVersion.priority ?? "",
|
||||||
|
|
||||||
|
date: potentialVersion.date ?? "",
|
||||||
|
time: potentialVersion.time ?? "",
|
||||||
|
};
|
||||||
|
console.log(convertedTaskFileContent);
|
||||||
|
chatModel.fileModel.handleFileContent(
|
||||||
|
convertedTaskFileContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addObjects([...potentialObjects, ...potentialObjectsInOutbox]);
|
||||||
};
|
};
|
||||||
|
|
||||||
addMessages(potentialMessages, ChatMessageStatus.Received);
|
// init
|
||||||
addMessages(potentialMessagesInOutbox, ChatMessageStatus.Outbox);
|
constructor(
|
||||||
|
public settingsModel: SettingsModel,
|
||||||
// objects
|
public connectionModel: ConnectionModel,
|
||||||
const objectsString: string =
|
public chatListModel: ChatListModel,
|
||||||
getLocalStorageItemAndClear(storageKeys.objects(id)) ?? "";
|
) {
|
||||||
const objectOutboxString: string =
|
this.migrateSettings();
|
||||||
getLocalStorageItemAndClear(storageKeys.itemOutbox(id)) ?? "";
|
this.migrateConnections();
|
||||||
const potentialObjects: any[] = parseArray(objectsString);
|
this.migrateChats();
|
||||||
const potentialObjectsInOutbox: any[] = parseArray(objectOutboxString);
|
}
|
||||||
|
|
||||||
const addObjects = (potentialObjects: any[]): void => {
|
|
||||||
const board: BoardInfoFileContent =
|
|
||||||
chatModel.fileModel.boardsAndTasksModel.createBoard(
|
|
||||||
translations.updater.migrated
|
|
||||||
);
|
|
||||||
chatModel.fileModel.boardsAndTasksModel.updateBoard(board);
|
|
||||||
|
|
||||||
for (const potentialObjectEntry of potentialObjects) {
|
|
||||||
const potentialObject = potentialObjectEntry[1];
|
|
||||||
const isV1MessageObject: boolean =
|
|
||||||
checkIsV1MessageObject(potentialObject);
|
|
||||||
if (isV1MessageObject == false) continue;
|
|
||||||
|
|
||||||
const objectId: string = potentialObject.id;
|
|
||||||
const objectName: string = potentialObject.title;
|
|
||||||
|
|
||||||
const contentVersions: any[] = Object.values(
|
|
||||||
potentialObject.contentVersions
|
|
||||||
);
|
|
||||||
for (const potentialVersion of contentVersions) {
|
|
||||||
const isV1MessageObjectContent: boolean =
|
|
||||||
checkIsV1MessageObjectContent(potentialVersion);
|
|
||||||
if (isV1MessageObjectContent == false) continue;
|
|
||||||
|
|
||||||
const convertedTaskFileContent: TaskFileContent = {
|
|
||||||
dataVersion: "v2",
|
|
||||||
|
|
||||||
fileId: objectId,
|
|
||||||
fileContentId: FileModel.generateFileContentId(
|
|
||||||
potentialVersion.isoDateVersionCreated
|
|
||||||
),
|
|
||||||
creationDate: potentialVersion.isoDateVersionCreated,
|
|
||||||
type: "task",
|
|
||||||
|
|
||||||
name: objectName ?? "",
|
|
||||||
boardId: board.fileId ?? "",
|
|
||||||
|
|
||||||
description: potentialVersion.noteContent ?? "",
|
|
||||||
|
|
||||||
category: potentialVersion.categoryName ?? "",
|
|
||||||
status: potentialVersion.status ?? "",
|
|
||||||
priority: potentialVersion.priority ?? "",
|
|
||||||
|
|
||||||
date: potentialVersion.date ?? "",
|
|
||||||
time: potentialVersion.time ?? "",
|
|
||||||
};
|
|
||||||
console.log(convertedTaskFileContent);
|
|
||||||
chatModel.fileModel.handleFileContent(convertedTaskFileContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
addObjects([...potentialObjects, ...potentialObjectsInOutbox]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public settingsModel: SettingsModel,
|
|
||||||
public connectionModel: ConnectionModel,
|
|
||||||
public chatListModel: ChatListModel
|
|
||||||
) {
|
|
||||||
this.migrateSettings();
|
|
||||||
this.migrateConnections();
|
|
||||||
this.migrateChats();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageKeys = {
|
const storageKeys = {
|
||||||
viewType(id: string): string {
|
viewType(id: string): string {
|
||||||
return id + "view-type";
|
return id + "view-type";
|
||||||
},
|
},
|
||||||
|
|
||||||
hasUnread(id: string): string {
|
hasUnread(id: string): string {
|
||||||
return id + "has-unread-messages";
|
return id + "has-unread-messages";
|
||||||
},
|
},
|
||||||
|
|
||||||
primaryChannel(id: string): string {
|
primaryChannel(id: string): string {
|
||||||
return id + "primary-channel";
|
return id + "primary-channel";
|
||||||
},
|
},
|
||||||
|
|
||||||
secondaryChannels(id: string): string {
|
secondaryChannels(id: string): string {
|
||||||
return id + "secondary-channels";
|
return id + "secondary-channels";
|
||||||
},
|
},
|
||||||
|
|
||||||
encyptionKey(id: string): string {
|
encyptionKey(id: string): string {
|
||||||
return id + "encryption-key";
|
return id + "encryption-key";
|
||||||
},
|
},
|
||||||
|
|
||||||
messages(id: string): string {
|
messages(id: string): string {
|
||||||
return id + "messages";
|
return id + "messages";
|
||||||
},
|
},
|
||||||
|
|
||||||
objects(id: string): string {
|
objects(id: string): string {
|
||||||
return id + "items";
|
return id + "items";
|
||||||
},
|
},
|
||||||
|
|
||||||
outbox(id: string): string {
|
outbox(id: string): string {
|
||||||
return id + "outbox";
|
return id + "outbox";
|
||||||
},
|
},
|
||||||
|
|
||||||
itemOutbox(id: string): string {
|
itemOutbox(id: string): string {
|
||||||
return id + "item-outbox";
|
return id + "item-outbox";
|
||||||
},
|
},
|
||||||
|
|
||||||
composingMessage(id: string): string {
|
composingMessage(id: string): string {
|
||||||
return id + "composing-message";
|
return id + "composing-message";
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const V1ChatMessageReference = {
|
const V1ChatMessageReference = {
|
||||||
channel: "",
|
channel: "",
|
||||||
sender: "",
|
sender: "",
|
||||||
body: "",
|
body: "",
|
||||||
isoDate: "",
|
isoDate: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkIsV1MessageObject(object: any): boolean {
|
function checkIsV1MessageObject(object: any): boolean {
|
||||||
if (object.id == undefined) return false;
|
if (object.id == undefined) return false;
|
||||||
if (object.title == undefined) return false;
|
if (object.title == undefined) return false;
|
||||||
if (object.contentVersions == undefined) return false;
|
if (object.contentVersions == undefined) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIsV1MessageObjectContent(object: any): boolean {
|
function checkIsV1MessageObjectContent(object: any): boolean {
|
||||||
if (object.id == undefined) return false;
|
if (object.id == undefined) return false;
|
||||||
if (object.isoDateVersionCreated == undefined) return false;
|
if (object.isoDateVersionCreated == undefined) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PropertyValueList,
|
PropertyValueList,
|
||||||
createPropertyValueIndexState,
|
createPropertyValueIndexState,
|
||||||
} from "../Components/propertyValueList";
|
} from "../Components/propertyValueList";
|
||||||
|
|
||||||
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
|
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
|
||||||
@@ -14,84 +14,93 @@ import { allowDrop } from "../utility";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function BoardKanbanPage(boardViewModel: BoardViewModel) {
|
export function BoardKanbanPage(boardViewModel: BoardViewModel) {
|
||||||
return PropertyValueList(
|
return PropertyValueList(
|
||||||
"category",
|
"category",
|
||||||
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
||||||
boardViewModel.taskViewModels,
|
boardViewModel.taskViewModels,
|
||||||
(
|
(
|
||||||
categories: React.ListState<string>,
|
categories: React.ListState<string>,
|
||||||
sortedCategories: React.State<string[]>
|
sortedCategories: React.State<string[]>,
|
||||||
) => {
|
) => {
|
||||||
const categoryNameConverter: React.StateItemConverter<string> = (
|
const categoryNameConverter: React.StateItemConverter<string> = (
|
||||||
categoryName: string
|
categoryName: string,
|
||||||
) => {
|
) => {
|
||||||
const index: React.State<number> = createPropertyValueIndexState(
|
const index: React.State<number> =
|
||||||
sortedCategories,
|
createPropertyValueIndexState(
|
||||||
categoryName
|
sortedCategories,
|
||||||
);
|
categoryName,
|
||||||
return KanbanBoard(categoryName, index, boardViewModel);
|
);
|
||||||
};
|
return KanbanBoard(categoryName, index, boardViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="kanban-board-wrapper"
|
class="kanban-board-wrapper"
|
||||||
children:append={[categories, categoryNameConverter]}
|
children:append={[categories, categoryNameConverter]}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function KanbanBoard(
|
function KanbanBoard(
|
||||||
categoryName: string,
|
categoryName: string,
|
||||||
index: React.State<number>,
|
index: React.State<number>,
|
||||||
boardViewModel: BoardViewModel
|
boardViewModel: BoardViewModel,
|
||||||
) {
|
) {
|
||||||
return FilteredList(
|
return FilteredList(
|
||||||
{ category: categoryName },
|
{ category: categoryName },
|
||||||
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
||||||
boardViewModel.filteredTaskViewModels,
|
boardViewModel.filteredTaskViewModels,
|
||||||
(taskViewModels: React.ListState<TaskViewModel>) => {
|
(taskViewModels: React.ListState<TaskViewModel>) => {
|
||||||
const viewModel: TaskCategoryBulkChangeViewModel =
|
const viewModel: TaskCategoryBulkChangeViewModel =
|
||||||
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
|
new TaskCategoryBulkChangeViewModel(
|
||||||
|
taskViewModels,
|
||||||
|
categoryName,
|
||||||
|
);
|
||||||
|
|
||||||
function drop() {
|
function drop() {
|
||||||
boardViewModel.handleDropWithinBoard(categoryName);
|
boardViewModel.handleDropWithinBoard(categoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = (
|
const view = (
|
||||||
<div class="flex-column flex-no" on:dragover={allowDrop} on:drop={drop}>
|
<div
|
||||||
<div class="flex-row width-input">
|
class="flex-column flex-no"
|
||||||
<input
|
on:dragover={allowDrop}
|
||||||
placeholder={
|
on:drop={drop}
|
||||||
translations.chatPage.task.renameCategoryInputPlaceholder
|
>
|
||||||
}
|
<div class="flex-row width-input">
|
||||||
bind:value={viewModel.inputValue}
|
<input
|
||||||
on:enter={viewModel.set}
|
placeholder={
|
||||||
></input>
|
translations.chatPage.task
|
||||||
<button
|
.renameCategoryInputPlaceholder
|
||||||
class="primary"
|
}
|
||||||
on:click={viewModel.set}
|
bind:value={viewModel.inputValue}
|
||||||
toggle:disabled={viewModel.cannotSet}
|
on:enter={viewModel.set}
|
||||||
>
|
></input>
|
||||||
<span class="icon">check</span>
|
<button
|
||||||
</button>
|
class="primary"
|
||||||
</div>
|
on:click={viewModel.set}
|
||||||
|
toggle:disabled={viewModel.cannotSet}
|
||||||
|
>
|
||||||
|
<span class="icon">check</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="kanban-column"
|
class="kanban-column"
|
||||||
children:append={[taskViewModels, TaskViewModelToEntry]}
|
children:append={[taskViewModels, TaskViewModelToEntry]}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
index.subscribe((newIndex) => {
|
index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-115
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardViewModel, {
|
import BoardViewModel, {
|
||||||
BoardPageType,
|
BoardPageType,
|
||||||
} from "../../ViewModel/Pages/boardViewModel";
|
} from "../../ViewModel/Pages/boardViewModel";
|
||||||
|
|
||||||
import { BoardKanbanPage } from "./boardKanbanPage";
|
import { BoardKanbanPage } from "./boardKanbanPage";
|
||||||
@@ -14,120 +14,140 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function BoardPage(boardViewModel: BoardViewModel) {
|
export function BoardPage(boardViewModel: BoardViewModel) {
|
||||||
boardViewModel.loadData();
|
boardViewModel.loadData();
|
||||||
|
|
||||||
// main content
|
// main content
|
||||||
const mainContent = React.createProxyState(
|
const mainContent = React.createProxyState(
|
||||||
[boardViewModel.selectedPage],
|
[boardViewModel.selectedPage],
|
||||||
() => {
|
() => {
|
||||||
switch (boardViewModel.selectedPage.value) {
|
switch (boardViewModel.selectedPage.value) {
|
||||||
case BoardPageType.Kanban: {
|
case BoardPageType.Kanban: {
|
||||||
return BoardKanbanPage(boardViewModel);
|
return BoardKanbanPage(boardViewModel);
|
||||||
}
|
}
|
||||||
case BoardPageType.StatusGrid: {
|
case BoardPageType.StatusGrid: {
|
||||||
return BoardStatusGridPage(boardViewModel);
|
return BoardStatusGridPage(boardViewModel);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="task-grid"
|
class="task-grid"
|
||||||
children:append={[
|
children:append={[
|
||||||
boardViewModel.filteredTaskViewModels,
|
boardViewModel.filteredTaskViewModels,
|
||||||
|
TaskViewModelToEntry,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// task modal
|
||||||
|
const taskSettingsModal = React.createProxyState(
|
||||||
|
[boardViewModel.selectedTaskViewModel],
|
||||||
|
() => {
|
||||||
|
if (boardViewModel.selectedTaskViewModel.value == undefined) {
|
||||||
|
return <div></div>;
|
||||||
|
} else {
|
||||||
|
return TaskSettingsModal(
|
||||||
|
boardViewModel.selectedTaskViewModel.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="pane">
|
||||||
|
<div class="toolbar">
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
class="ghost board-close-button"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task
|
||||||
|
.closeBoardButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={boardViewModel.close}
|
||||||
|
>
|
||||||
|
<span class="icon">arrow_back</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ghost board-toggle-button inset-outline"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task
|
||||||
|
.toggleBoardButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={
|
||||||
|
boardViewModel.taskPageViewModel.toggleBoardList
|
||||||
|
}
|
||||||
|
toggle:selected={
|
||||||
|
boardViewModel.taskPageViewModel.isShowingBoadList
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">dock_to_right</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task.boardSettingsHeadline
|
||||||
|
}
|
||||||
|
on:click={boardViewModel.showSettings}
|
||||||
|
>
|
||||||
|
<span class="icon">settings</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="scroll-h ribbon">
|
||||||
|
{BoardViewToggleButton(
|
||||||
|
translations.chatPage.task.listViewButtonAudioLabel,
|
||||||
|
"view_list",
|
||||||
|
BoardPageType.List,
|
||||||
|
boardViewModel,
|
||||||
|
)}
|
||||||
|
{BoardViewToggleButton(
|
||||||
|
translations.chatPage.task.kanbanViewButtonAudioLabel,
|
||||||
|
"view_kanban",
|
||||||
|
BoardPageType.Kanban,
|
||||||
|
boardViewModel,
|
||||||
|
)}
|
||||||
|
{BoardViewToggleButton(
|
||||||
|
translations.chatPage.task.statusViewButtonAudioLabel,
|
||||||
|
"grid_view",
|
||||||
|
BoardPageType.StatusGrid,
|
||||||
|
boardViewModel,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task
|
||||||
|
.filterTasksButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={boardViewModel.showFilterModal}
|
||||||
|
>
|
||||||
|
<span class="icon">filter_alt</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task
|
||||||
|
.createTaskButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={boardViewModel.createTask}
|
||||||
|
>
|
||||||
|
<span class="icon">add</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="content main-content" children:set={mainContent}></div>
|
||||||
|
|
||||||
|
{BoardSettingsModal(boardViewModel)}
|
||||||
|
{SearchModal(
|
||||||
|
boardViewModel.searchViewModel,
|
||||||
|
translations.chatPage.task.filterTasksHeadline,
|
||||||
TaskViewModelToEntry,
|
TaskViewModelToEntry,
|
||||||
]}
|
boardViewModel.isPresentingFilterModal,
|
||||||
></div>
|
)}
|
||||||
);
|
<div children:set={taskSettingsModal}></div>
|
||||||
}
|
</div>
|
||||||
}
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// task modal
|
|
||||||
const taskSettingsModal = React.createProxyState(
|
|
||||||
[boardViewModel.selectedTaskViewModel],
|
|
||||||
() => {
|
|
||||||
if (boardViewModel.selectedTaskViewModel.value == undefined) {
|
|
||||||
return <div></div>;
|
|
||||||
} else {
|
|
||||||
return TaskSettingsModal(boardViewModel.selectedTaskViewModel.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="pane">
|
|
||||||
<div class="toolbar">
|
|
||||||
<span>
|
|
||||||
<button
|
|
||||||
class="ghost board-close-button"
|
|
||||||
aria-label={translations.chatPage.task.closeBoardButtonAudioLabel}
|
|
||||||
on:click={boardViewModel.close}
|
|
||||||
>
|
|
||||||
<span class="icon">arrow_back</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ghost board-toggle-button inset-outline"
|
|
||||||
aria-label={translations.chatPage.task.toggleBoardButtonAudioLabel}
|
|
||||||
on:click={boardViewModel.taskPageViewModel.toggleBoardList}
|
|
||||||
toggle:selected={boardViewModel.taskPageViewModel.isShowingBoadList}
|
|
||||||
>
|
|
||||||
<span class="icon">dock_to_right</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ghost"
|
|
||||||
aria-label={translations.chatPage.task.boardSettingsHeadline}
|
|
||||||
on:click={boardViewModel.showSettings}
|
|
||||||
>
|
|
||||||
<span class="icon">settings</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span class="scroll-h ribbon">
|
|
||||||
{BoardViewToggleButton(
|
|
||||||
translations.chatPage.task.listViewButtonAudioLabel,
|
|
||||||
"view_list",
|
|
||||||
BoardPageType.List,
|
|
||||||
boardViewModel
|
|
||||||
)}
|
|
||||||
{BoardViewToggleButton(
|
|
||||||
translations.chatPage.task.kanbanViewButtonAudioLabel,
|
|
||||||
"view_kanban",
|
|
||||||
BoardPageType.Kanban,
|
|
||||||
boardViewModel
|
|
||||||
)}
|
|
||||||
{BoardViewToggleButton(
|
|
||||||
translations.chatPage.task.statusViewButtonAudioLabel,
|
|
||||||
"grid_view",
|
|
||||||
BoardPageType.StatusGrid,
|
|
||||||
boardViewModel
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<button
|
|
||||||
class="ghost"
|
|
||||||
aria-label={translations.chatPage.task.filterTasksButtonAudioLabel}
|
|
||||||
on:click={boardViewModel.showFilterModal}
|
|
||||||
>
|
|
||||||
<span class="icon">filter_alt</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ghost"
|
|
||||||
aria-label={translations.chatPage.task.createTaskButtonAudioLabel}
|
|
||||||
on:click={boardViewModel.createTask}
|
|
||||||
>
|
|
||||||
<span class="icon">add</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="content main-content" children:set={mainContent}></div>
|
|
||||||
|
|
||||||
{BoardSettingsModal(boardViewModel)}
|
|
||||||
{SearchModal(
|
|
||||||
boardViewModel.searchViewModel,
|
|
||||||
translations.chatPage.task.filterTasksHeadline,
|
|
||||||
TaskViewModelToEntry,
|
|
||||||
boardViewModel.isPresentingFilterModal
|
|
||||||
)}
|
|
||||||
<div children:set={taskSettingsModal}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PropertyValueList,
|
PropertyValueList,
|
||||||
collectPropertyValuesToState,
|
collectPropertyValuesToState,
|
||||||
createPropertyValueIndexState,
|
createPropertyValueIndexState,
|
||||||
createSortedPropertyValueState,
|
createSortedPropertyValueState,
|
||||||
} from "../Components/propertyValueList";
|
} from "../Components/propertyValueList";
|
||||||
import {
|
import {
|
||||||
TaskCategoryBulkChangeViewModel,
|
TaskCategoryBulkChangeViewModel,
|
||||||
TaskStatusBulkChangeViewModel,
|
TaskStatusBulkChangeViewModel,
|
||||||
} from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
|
} from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
|
||||||
|
|
||||||
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
|
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
|
||||||
@@ -19,221 +19,236 @@ import { allowDrop } from "../utility";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function BoardStatusGridPage(boardViewModel: BoardViewModel) {
|
export function BoardStatusGridPage(boardViewModel: BoardViewModel) {
|
||||||
const statuses: React.ListState<string> = new React.ListState();
|
const statuses: React.ListState<string> = new React.ListState();
|
||||||
const sortedStatuses: React.State<string[]> =
|
const sortedStatuses: React.State<string[]> =
|
||||||
createSortedPropertyValueState(statuses);
|
createSortedPropertyValueState(statuses);
|
||||||
|
|
||||||
boardViewModel.taskViewModels.subscribe(() => {
|
boardViewModel.taskViewModels.subscribe(() => {
|
||||||
collectPropertyValuesToState(
|
collectPropertyValuesToState(
|
||||||
"status",
|
"status",
|
||||||
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
||||||
boardViewModel.taskViewModels,
|
boardViewModel.taskViewModels,
|
||||||
statuses
|
statuses,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusNameCellConverter: React.StateItemConverter<string> = (
|
const statusNameCellConverter: React.StateItemConverter<string> = (
|
||||||
statusName: string
|
statusName: string,
|
||||||
) => {
|
) => {
|
||||||
const index = createPropertyValueIndexState(sortedStatuses, statusName);
|
const index = createPropertyValueIndexState(sortedStatuses, statusName);
|
||||||
return StatusNameCell(statusName, index, boardViewModel);
|
return StatusNameCell(statusName, index, boardViewModel);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="status-page-content">
|
<div class="status-page-content">
|
||||||
<div
|
|
||||||
class="status-name-row"
|
|
||||||
children:append={[statuses, statusNameCellConverter]}
|
|
||||||
></div>
|
|
||||||
{PropertyValueList(
|
|
||||||
"category",
|
|
||||||
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
|
||||||
boardViewModel.filteredTaskViewModels,
|
|
||||||
(
|
|
||||||
categories: React.ListState<string>,
|
|
||||||
sortedCategories: React.State<string[]>
|
|
||||||
) => {
|
|
||||||
const categoryRowConverter: React.StateItemConverter<string> = (
|
|
||||||
categoryName: string
|
|
||||||
) => {
|
|
||||||
const index: React.State<number> = createPropertyValueIndexState(
|
|
||||||
sortedCategories,
|
|
||||||
categoryName
|
|
||||||
);
|
|
||||||
|
|
||||||
return CategoryRow(
|
|
||||||
categoryName,
|
|
||||||
index,
|
|
||||||
statuses,
|
|
||||||
sortedStatuses,
|
|
||||||
boardViewModel
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
class="status-grid-wrapper"
|
class="status-name-row"
|
||||||
children:append={[categories, categoryRowConverter]}
|
children:append={[statuses, statusNameCellConverter]}
|
||||||
></div>
|
></div>
|
||||||
);
|
{PropertyValueList(
|
||||||
}
|
"category",
|
||||||
)}
|
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
||||||
</div>
|
boardViewModel.filteredTaskViewModels,
|
||||||
);
|
(
|
||||||
|
categories: React.ListState<string>,
|
||||||
|
sortedCategories: React.State<string[]>,
|
||||||
|
) => {
|
||||||
|
const categoryRowConverter: React.StateItemConverter<
|
||||||
|
string
|
||||||
|
> = (categoryName: string) => {
|
||||||
|
const index: React.State<number> =
|
||||||
|
createPropertyValueIndexState(
|
||||||
|
sortedCategories,
|
||||||
|
categoryName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return CategoryRow(
|
||||||
|
categoryName,
|
||||||
|
index,
|
||||||
|
statuses,
|
||||||
|
sortedStatuses,
|
||||||
|
boardViewModel,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="status-grid-wrapper"
|
||||||
|
children:append={[categories, categoryRowConverter]}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusNameCell(
|
function StatusNameCell(
|
||||||
statusName: string,
|
statusName: string,
|
||||||
index: React.State<number>,
|
index: React.State<number>,
|
||||||
boardViewModel: BoardViewModel
|
boardViewModel: BoardViewModel,
|
||||||
) {
|
) {
|
||||||
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
|
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
|
||||||
new React.ListState();
|
new React.ListState();
|
||||||
|
|
||||||
boardViewModel.filteredTaskViewModels.handleAddition(
|
boardViewModel.filteredTaskViewModels.handleAddition(
|
||||||
(taskViewModel: TaskViewModel) => {
|
(taskViewModel: TaskViewModel) => {
|
||||||
const doesMatchStatus: boolean = taskViewModel.task.status == statusName;
|
const doesMatchStatus: boolean =
|
||||||
if (doesMatchStatus == false) return;
|
taskViewModel.task.status == statusName;
|
||||||
|
if (doesMatchStatus == false) return;
|
||||||
|
|
||||||
taskViewModelsWithMatchingStatus.add(taskViewModel);
|
taskViewModelsWithMatchingStatus.add(taskViewModel);
|
||||||
boardViewModel.filteredTaskViewModels.handleRemoval(taskViewModel, () => {
|
boardViewModel.filteredTaskViewModels.handleRemoval(
|
||||||
taskViewModelsWithMatchingStatus.remove(taskViewModel);
|
taskViewModel,
|
||||||
});
|
() => {
|
||||||
}
|
taskViewModelsWithMatchingStatus.remove(taskViewModel);
|
||||||
);
|
},
|
||||||
|
);
|
||||||
const viewModel: TaskStatusBulkChangeViewModel =
|
},
|
||||||
new TaskStatusBulkChangeViewModel(
|
|
||||||
taskViewModelsWithMatchingStatus,
|
|
||||||
statusName
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const view = (
|
const viewModel: TaskStatusBulkChangeViewModel =
|
||||||
<div class="flex-row">
|
new TaskStatusBulkChangeViewModel(
|
||||||
<div class="property-input-wrapper">
|
taskViewModelsWithMatchingStatus,
|
||||||
<input
|
statusName,
|
||||||
placeholder={
|
);
|
||||||
translations.chatPage.task.renameCategoryInputPlaceholder
|
|
||||||
}
|
|
||||||
bind:value={viewModel.inputValue}
|
|
||||||
on:enter={viewModel.set}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
on:click={viewModel.set}
|
|
||||||
toggle:disabled={viewModel.cannotSet}
|
|
||||||
>
|
|
||||||
<span class="icon">check</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
index.subscribe((newIndex) => {
|
const view = (
|
||||||
view.style.order = newIndex;
|
<div class="flex-row">
|
||||||
});
|
<div class="property-input-wrapper">
|
||||||
|
<input
|
||||||
|
placeholder={
|
||||||
|
translations.chatPage.task
|
||||||
|
.renameCategoryInputPlaceholder
|
||||||
|
}
|
||||||
|
bind:value={viewModel.inputValue}
|
||||||
|
on:enter={viewModel.set}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
on:click={viewModel.set}
|
||||||
|
toggle:disabled={viewModel.cannotSet}
|
||||||
|
>
|
||||||
|
<span class="icon">check</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return view;
|
index.subscribe((newIndex) => {
|
||||||
|
view.style.order = newIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategoryRow(
|
function CategoryRow(
|
||||||
categoryName: string,
|
categoryName: string,
|
||||||
index: React.State<number>,
|
index: React.State<number>,
|
||||||
allStatuses: React.ListState<string>,
|
allStatuses: React.ListState<string>,
|
||||||
sortedStatuses: React.State<string[]>,
|
sortedStatuses: React.State<string[]>,
|
||||||
boardViewModel: BoardViewModel
|
boardViewModel: BoardViewModel,
|
||||||
) {
|
) {
|
||||||
return FilteredList(
|
return FilteredList(
|
||||||
{ category: categoryName },
|
{ category: categoryName },
|
||||||
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
(taskViewModel: TaskViewModel) => taskViewModel.task,
|
||||||
boardViewModel.filteredTaskViewModels,
|
boardViewModel.filteredTaskViewModels,
|
||||||
(taskViewModels: React.ListState<TaskViewModel>) => {
|
(taskViewModels: React.ListState<TaskViewModel>) => {
|
||||||
const statusNameConverter: React.StateItemConverter<string> = (
|
const statusNameConverter: React.StateItemConverter<string> = (
|
||||||
statusName: string
|
statusName: string,
|
||||||
) => {
|
) => {
|
||||||
const index = createPropertyValueIndexState(sortedStatuses, statusName);
|
const index = createPropertyValueIndexState(
|
||||||
return CategoryStatusColumn(
|
sortedStatuses,
|
||||||
categoryName,
|
statusName,
|
||||||
statusName,
|
);
|
||||||
index,
|
return CategoryStatusColumn(
|
||||||
boardViewModel,
|
categoryName,
|
||||||
taskViewModels
|
statusName,
|
||||||
);
|
index,
|
||||||
};
|
boardViewModel,
|
||||||
|
taskViewModels,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const viewModel: TaskCategoryBulkChangeViewModel =
|
const viewModel: TaskCategoryBulkChangeViewModel =
|
||||||
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
|
new TaskCategoryBulkChangeViewModel(
|
||||||
|
taskViewModels,
|
||||||
|
categoryName,
|
||||||
|
);
|
||||||
|
|
||||||
const view = (
|
const view = (
|
||||||
<div class="flex-row flex-no large-gap">
|
<div class="flex-row flex-no large-gap">
|
||||||
<div class="property-input-wrapper">
|
<div class="property-input-wrapper">
|
||||||
<input
|
<input
|
||||||
placeholder={
|
placeholder={
|
||||||
translations.chatPage.task.renameCategoryInputPlaceholder
|
translations.chatPage.task
|
||||||
}
|
.renameCategoryInputPlaceholder
|
||||||
bind:value={viewModel.inputValue}
|
}
|
||||||
on:enter={viewModel.set}
|
bind:value={viewModel.inputValue}
|
||||||
></input>
|
on:enter={viewModel.set}
|
||||||
<button
|
></input>
|
||||||
class="primary"
|
<button
|
||||||
on:click={viewModel.set}
|
class="primary"
|
||||||
toggle:disabled={viewModel.cannotSet}
|
on:click={viewModel.set}
|
||||||
>
|
toggle:disabled={viewModel.cannotSet}
|
||||||
<span class="icon">check</span>
|
>
|
||||||
</button>
|
<span class="icon">check</span>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex-row large-gap padding-right"
|
class="flex-row large-gap padding-right"
|
||||||
children:append={[allStatuses, statusNameConverter]}
|
children:append={[allStatuses, statusNameConverter]}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
index.subscribe((newIndex) => {
|
index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategoryStatusColumn(
|
function CategoryStatusColumn(
|
||||||
categoryName: string,
|
categoryName: string,
|
||||||
statusName: string,
|
statusName: string,
|
||||||
index: React.State<number>,
|
index: React.State<number>,
|
||||||
boardViewModel: BoardViewModel,
|
boardViewModel: BoardViewModel,
|
||||||
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>
|
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>,
|
||||||
) {
|
) {
|
||||||
const taskViewModels: React.ListState<TaskViewModel> = new React.ListState();
|
const taskViewModels: React.ListState<TaskViewModel> =
|
||||||
|
new React.ListState();
|
||||||
|
|
||||||
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
|
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
|
||||||
const doesMatchStatus: boolean = taskViewModel.status.value == statusName;
|
const doesMatchStatus: boolean =
|
||||||
if (doesMatchStatus == false) return;
|
taskViewModel.status.value == statusName;
|
||||||
|
if (doesMatchStatus == false) return;
|
||||||
|
|
||||||
taskViewModels.add(taskViewModel);
|
taskViewModels.add(taskViewModel);
|
||||||
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
|
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
|
||||||
taskViewModels.remove(taskViewModel);
|
taskViewModels.remove(taskViewModel);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function drop() {
|
function drop() {
|
||||||
boardViewModel.handleDropWithinBoard(categoryName, statusName);
|
boardViewModel.handleDropWithinBoard(categoryName, statusName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = (
|
const view = (
|
||||||
<div
|
<div
|
||||||
class="status-column gap"
|
class="status-column gap"
|
||||||
on:dragover={allowDrop}
|
on:dragover={allowDrop}
|
||||||
on:drop={drop}
|
on:drop={drop}
|
||||||
children:append={[taskViewModels, TaskViewModelToEntry]}
|
children:append={[taskViewModels, TaskViewModelToEntry]}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
|
||||||
index.subscribe((newIndex) => {
|
index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,73 @@
|
|||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> #calendar-page
|
> #calendar-page
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
padding: var(--gap-between-tiles);
|
padding: var(--gap-between-tiles);
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> #calendar-page
|
> #calendar-page
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
.input-wrapper
|
.input-wrapper
|
||||||
> input {
|
> input {
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
||||||
|
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
|
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#calendar-page .input-wrapper > input::-webkit-outer-spin-button,
|
#calendar-page .input-wrapper > input::-webkit-outer-spin-button,
|
||||||
#calendar-page .input-wrapper > input::-webkit-innner-spin-button {
|
#calendar-page .input-wrapper > input::-webkit-innner-spin-button {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> #calendar-page
|
> #calendar-page
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
.input-wrapper
|
.input-wrapper
|
||||||
> .year-input {
|
> .year-input {
|
||||||
width: 4.4rem;
|
width: 4.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> #calendar-page
|
> #calendar-page
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
.input-wrapper
|
.input-wrapper
|
||||||
> .month-input {
|
> .month-input {
|
||||||
width: 3.1rem;
|
width: 3.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
#chat-page > div > #main > #calendar-page {
|
#chat-page > div > #main > #calendar-page {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
|
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+195
-160
@@ -10,168 +10,203 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function CalendarPage(calendarPageViewModel: CalendarPageViewModel) {
|
export function CalendarPage(calendarPageViewModel: CalendarPageViewModel) {
|
||||||
calendarPageViewModel.loadData();
|
calendarPageViewModel.loadData();
|
||||||
|
|
||||||
const mainContent = React.createProxyState(
|
const mainContent = React.createProxyState(
|
||||||
[calendarPageViewModel.monthGrid],
|
[calendarPageViewModel.monthGrid],
|
||||||
() => {
|
() => {
|
||||||
const monthGrid = calendarPageViewModel.monthGrid.value;
|
const monthGrid = calendarPageViewModel.monthGrid.value;
|
||||||
|
|
||||||
if (monthGrid == undefined) {
|
if (monthGrid == undefined) {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
} else {
|
} else {
|
||||||
function drop(date: string) {
|
function drop(date: string) {
|
||||||
calendarPageViewModel.handleDrop(
|
calendarPageViewModel.handleDrop(
|
||||||
monthGrid!.year.toString(),
|
monthGrid!.year.toString(),
|
||||||
monthGrid!.month.toString(),
|
monthGrid!.month.toString(),
|
||||||
date
|
date,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return MonthGrid(monthGrid, calendarPageViewModel.selectedDate, drop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const sidePaneContentWrapper = React.createProxyState(
|
|
||||||
[
|
|
||||||
calendarPageViewModel.selectedYear,
|
|
||||||
calendarPageViewModel.selectedMonth,
|
|
||||||
calendarPageViewModel.selectedDate,
|
|
||||||
],
|
|
||||||
() => {
|
|
||||||
const listState = calendarPageViewModel.getEventsForDate();
|
|
||||||
|
|
||||||
if (listState == undefined) {
|
|
||||||
return <div></div>;
|
|
||||||
} else {
|
|
||||||
const sidePaneContent = React.createProxyState([listState], () => {
|
|
||||||
if (listState.value.size == 0) {
|
|
||||||
return (
|
|
||||||
<div class="width-100 height-100 flex-column justify-center align-center">
|
|
||||||
<span class="secondary slide-up">
|
|
||||||
{translations.chatPage.calendar.noEvents}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex-column gap slide-up padding-bottom"
|
|
||||||
children:append={[listState, TaskViewModelToEntry]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="width-100 height-100"
|
|
||||||
children:set={sidePaneContent}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const taskSettingsModal = React.createProxyState(
|
|
||||||
[calendarPageViewModel.selectedTaskViewModel],
|
|
||||||
() => {
|
|
||||||
if (calendarPageViewModel.selectedTaskViewModel.value == undefined) {
|
|
||||||
return <div></div>;
|
|
||||||
} else {
|
|
||||||
return TaskSettingsModal(
|
|
||||||
calendarPageViewModel.selectedTaskViewModel.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="calendar-page">
|
|
||||||
<div class="pane-wrapper grid-pane-wrapper">
|
|
||||||
<div class="pane">
|
|
||||||
<div class="toolbar">
|
|
||||||
<span>
|
|
||||||
<button
|
|
||||||
class="ghost"
|
|
||||||
aria-label={
|
|
||||||
translations.chatPage.calendar.todayButtonAudioLabel
|
|
||||||
}
|
}
|
||||||
on:click={calendarPageViewModel.showToday}
|
|
||||||
>
|
return MonthGrid(
|
||||||
<span class="icon">today</span>
|
monthGrid,
|
||||||
</button>
|
calendarPageViewModel.selectedDate,
|
||||||
</span>
|
drop,
|
||||||
<span>
|
);
|
||||||
<button
|
}
|
||||||
class="ghost"
|
},
|
||||||
aria-label={
|
);
|
||||||
translations.chatPage.calendar.previousMonthButtonAudioLabel
|
|
||||||
}
|
const sidePaneContentWrapper = React.createProxyState(
|
||||||
on:click={calendarPageViewModel.showPreviousMonth}
|
[
|
||||||
>
|
calendarPageViewModel.selectedYear,
|
||||||
<span class="icon">arrow_back</span>
|
calendarPageViewModel.selectedMonth,
|
||||||
</button>
|
calendarPageViewModel.selectedDate,
|
||||||
<span class="input-wrapper">
|
],
|
||||||
<input
|
() => {
|
||||||
class="year-input"
|
const listState = calendarPageViewModel.getEventsForDate();
|
||||||
type="number"
|
|
||||||
aria-label={
|
if (listState == undefined) {
|
||||||
translations.chatPage.calendar.yearInputAudioLabel
|
return <div></div>;
|
||||||
}
|
} else {
|
||||||
placeholder={
|
const sidePaneContent = React.createProxyState(
|
||||||
translations.chatPage.calendar.yearInputPlaceholder
|
[listState],
|
||||||
}
|
() => {
|
||||||
bind:value={calendarPageViewModel.selectedYear}
|
if (listState.value.size == 0) {
|
||||||
></input>
|
return (
|
||||||
<input
|
<div class="width-100 height-100 flex-column justify-center align-center">
|
||||||
class="month-input"
|
<span class="secondary slide-up">
|
||||||
type="number"
|
{
|
||||||
aria-label={
|
translations.chatPage.calendar
|
||||||
translations.chatPage.calendar.monthInputAudioLabel
|
.noEvents
|
||||||
}
|
}
|
||||||
placeholder={
|
</span>
|
||||||
translations.chatPage.calendar.monthInputPlaceholder
|
</div>
|
||||||
}
|
);
|
||||||
bind:value={calendarPageViewModel.selectedMonth}
|
} else {
|
||||||
></input>
|
return (
|
||||||
</span>
|
<div
|
||||||
<button
|
class="flex-column gap slide-up padding-bottom"
|
||||||
class="ghost"
|
children:append={[
|
||||||
aria-label={
|
listState,
|
||||||
translations.chatPage.calendar.nextMonthButtonAudioLabel
|
TaskViewModelToEntry,
|
||||||
}
|
]}
|
||||||
on:click={calendarPageViewModel.showNextMonth}
|
></div>
|
||||||
>
|
);
|
||||||
<span class="icon">arrow_forward</span>
|
}
|
||||||
</button>
|
},
|
||||||
</span>
|
);
|
||||||
<span>
|
|
||||||
<button
|
return (
|
||||||
class="ghost"
|
<div
|
||||||
aria-label={
|
class="width-100 height-100"
|
||||||
translations.chatPage.task.createTaskButtonAudioLabel
|
children:set={sidePaneContent}
|
||||||
}
|
></div>
|
||||||
on:click={calendarPageViewModel.createEvent}
|
);
|
||||||
>
|
}
|
||||||
<span class="icon">add</span>
|
},
|
||||||
</button>
|
);
|
||||||
</span>
|
|
||||||
</div>
|
const taskSettingsModal = React.createProxyState(
|
||||||
<div class="content padding-0" children:set={mainContent}></div>
|
[calendarPageViewModel.selectedTaskViewModel],
|
||||||
|
() => {
|
||||||
|
if (
|
||||||
|
calendarPageViewModel.selectedTaskViewModel.value == undefined
|
||||||
|
) {
|
||||||
|
return <div></div>;
|
||||||
|
} else {
|
||||||
|
return TaskSettingsModal(
|
||||||
|
calendarPageViewModel.selectedTaskViewModel.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="calendar-page">
|
||||||
|
<div class="pane-wrapper grid-pane-wrapper">
|
||||||
|
<div class="pane">
|
||||||
|
<div class="toolbar">
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.todayButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={calendarPageViewModel.showToday}
|
||||||
|
>
|
||||||
|
<span class="icon">today</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.previousMonthButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={
|
||||||
|
calendarPageViewModel.showPreviousMonth
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">arrow_back</span>
|
||||||
|
</button>
|
||||||
|
<span class="input-wrapper">
|
||||||
|
<input
|
||||||
|
class="year-input"
|
||||||
|
type="number"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.yearInputAudioLabel
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.yearInputPlaceholder
|
||||||
|
}
|
||||||
|
bind:value={
|
||||||
|
calendarPageViewModel.selectedYear
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
<input
|
||||||
|
class="month-input"
|
||||||
|
type="number"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.monthInputAudioLabel
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.monthInputPlaceholder
|
||||||
|
}
|
||||||
|
bind:value={
|
||||||
|
calendarPageViewModel.selectedMonth
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.calendar
|
||||||
|
.nextMonthButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={calendarPageViewModel.showNextMonth}
|
||||||
|
>
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
class="ghost"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task
|
||||||
|
.createTaskButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={calendarPageViewModel.createEvent}
|
||||||
|
>
|
||||||
|
<span class="icon">add</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="content padding-0"
|
||||||
|
children:set={mainContent}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pane-wrapper side background"
|
||||||
|
set:color={calendarPageViewModel.chatViewModel.displayedColor}
|
||||||
|
>
|
||||||
|
<div class="pane">
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
children:set={sidePaneContentWrapper}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div children:set={taskSettingsModal}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
<div
|
|
||||||
class="pane-wrapper side background"
|
|
||||||
set:color={calendarPageViewModel.chatViewModel.displayedColor}
|
|
||||||
>
|
|
||||||
<div class="pane">
|
|
||||||
<div class="content" children:set={sidePaneContentWrapper}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div children:set={taskSettingsModal}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +1,168 @@
|
|||||||
/* layout */
|
/* layout */
|
||||||
#chat-page #main #message-page .content {
|
#chat-page #main #message-page .content {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
|
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page #message-container {
|
#message-page #message-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* composer */
|
/* composer */
|
||||||
#message-page #composer {
|
#message-page #composer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page #composer input {
|
#message-page #composer input {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
max-width: unset;
|
|
||||||
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
#message-page #composer div {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
|
|
||||||
display: flex;
|
flex: 1;
|
||||||
}
|
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
#message-page #composer div {
|
||||||
|
width: 100%;
|
||||||
|
max-width: unset;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 700px) {
|
@media (min-width: 700px) {
|
||||||
#message-page #composer .content-width-constraint,
|
#message-page #composer .content-width-constraint,
|
||||||
#message-page #composer .input-width-constraint {
|
#message-page #composer .input-width-constraint {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--max-content-width);
|
max-width: var(--max-content-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page #composer .content-width-constraint {
|
#message-page #composer .content-width-constraint {
|
||||||
max-width: var(--max-content-width);
|
max-width: var(--max-content-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page #composer .input-width-constraint {
|
#message-page #composer .input-width-constraint {
|
||||||
max-width: var(--max-input-width);
|
max-width: var(--max-input-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* message */
|
/* message */
|
||||||
#message-page #message-container {
|
#message-page #message-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
|
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--max-content-width);
|
max-width: var(--max-content-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main {
|
.message-bubble .tile.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--max-input-width);
|
max-width: var(--max-input-width);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .text-container {
|
.message-bubble .tile.main .text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .button-container {
|
.message-bubble .tile.main .button-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
width: var(--control-height);
|
width: var(--control-height);
|
||||||
max-width: var(--control-height);
|
max-width: var(--control-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main button {
|
.message-bubble .tile.main button {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
--surface: transparent;
|
--surface: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .text-container {
|
.message-bubble .tile.main .text-container {
|
||||||
padding: var(--control-padding);
|
padding: var(--control-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .timestamp,
|
.message-bubble .tile.main .timestamp,
|
||||||
.message-bubble .tile.main .timestamp .icon,
|
.message-bubble .tile.main .timestamp .icon,
|
||||||
.message-bubble .tile.main .sender-name {
|
.message-bubble .tile.main .sender-name {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .timestamp {
|
.message-bubble .tile.main .timestamp {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .timestamp .icon {
|
.message-bubble .tile.main .timestamp .icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .sender-name {
|
.message-bubble .tile.main .sender-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble .tile.main .body {
|
.message-bubble .tile.main .body {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble[sentbyuser] {
|
.message-bubble[sentbyuser] {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble[sentbyuser] {
|
.message-bubble[sentbyuser] {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble[sentbyuser] .tile.main,
|
.message-bubble[sentbyuser] .tile.main,
|
||||||
.message-bubble[sentbyuser] .tile.main .timestamp {
|
.message-bubble[sentbyuser] .tile.main .timestamp {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble[sentbyuser] .tile.main .timestamp .icon {
|
.message-bubble[sentbyuser] .tile.main .timestamp .icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble[sentbyuser] .tile.main span {
|
.message-bubble[sentbyuser] .tile.main span {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,69 +7,81 @@ import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function MessagePage(messagePageViewModel: MessagePageViewModel) {
|
export function MessagePage(messagePageViewModel: MessagePageViewModel) {
|
||||||
messagePageViewModel.loadData();
|
messagePageViewModel.loadData();
|
||||||
|
|
||||||
const messageContainer = (
|
const messageContainer = (
|
||||||
<div
|
<div
|
||||||
id="message-container"
|
id="message-container"
|
||||||
children:append={[
|
children:append={[
|
||||||
messagePageViewModel.chatMessageViewModels,
|
messagePageViewModel.chatMessageViewModels,
|
||||||
ChatMessageViewModelToView,
|
ChatMessageViewModelToView,
|
||||||
]}
|
]}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
|
||||||
function scrollDown() {
|
function scrollDown() {
|
||||||
messageContainer.scrollTop = messageContainer.scrollHeight;
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
function scrollDownIfApplicable() {
|
function scrollDownIfApplicable() {
|
||||||
const scrollFromBottom =
|
const scrollFromBottom =
|
||||||
messageContainer.scrollHeight -
|
messageContainer.scrollHeight -
|
||||||
(messageContainer.scrollTop + messageContainer.offsetHeight);
|
(messageContainer.scrollTop + messageContainer.offsetHeight);
|
||||||
if (scrollFromBottom > 400) return;
|
if (scrollFromBottom > 400) return;
|
||||||
|
|
||||||
scrollDown();
|
scrollDown();
|
||||||
}
|
}
|
||||||
messagePageViewModel.chatMessageViewModels.subscribeSilent(
|
messagePageViewModel.chatMessageViewModels.subscribeSilent(
|
||||||
scrollDownIfApplicable
|
scrollDownIfApplicable,
|
||||||
);
|
);
|
||||||
setTimeout(() => scrollDown(), 100);
|
setTimeout(() => scrollDown(), 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="message-page">
|
<div id="message-page">
|
||||||
<div class="pane-wrapper">
|
<div class="pane-wrapper">
|
||||||
<div class="pane">
|
<div class="pane">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="title">{translations.chatPage.message.messagesHeadline}</span>
|
<span class="title">
|
||||||
</div>
|
{translations.chatPage.message.messagesHeadline}
|
||||||
<div class="content">
|
</span>
|
||||||
{messageContainer}
|
</div>
|
||||||
<div id="composer">
|
<div class="content">
|
||||||
<div class="content-width-constraint">
|
{messageContainer}
|
||||||
<div class="input-width-constraint">
|
<div id="composer">
|
||||||
<input
|
<div class="content-width-constraint">
|
||||||
bind:value={messagePageViewModel.composingMessage}
|
<div class="input-width-constraint">
|
||||||
on:enter={messagePageViewModel.sendMessage}
|
<input
|
||||||
placeholder={
|
bind:value={
|
||||||
translations.chatPage.message.composerInputPlaceholder
|
messagePageViewModel.composingMessage
|
||||||
}
|
}
|
||||||
></input>
|
on:enter={
|
||||||
<button
|
messagePageViewModel.sendMessage
|
||||||
class="primary"
|
}
|
||||||
aria-label={
|
placeholder={
|
||||||
translations.chatPage.message.sendMessageButtonAudioLabel
|
translations.chatPage.message
|
||||||
}
|
.composerInputPlaceholder
|
||||||
on:click={messagePageViewModel.sendMessage}
|
}
|
||||||
toggle:disabled={messagePageViewModel.cannotSendMessage}
|
></input>
|
||||||
>
|
<button
|
||||||
<span class="icon">send</span>
|
class="primary"
|
||||||
</button>
|
aria-label={
|
||||||
|
translations.chatPage.message
|
||||||
|
.sendMessageButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={
|
||||||
|
messagePageViewModel.sendMessage
|
||||||
|
}
|
||||||
|
toggle:disabled={
|
||||||
|
messagePageViewModel.cannotSendMessage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">send</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-133
@@ -8,141 +8,177 @@ import SettingsPageViewModel from "../../ViewModel/Pages/settingsPageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function SettingsPage(settingsPageViewModel: SettingsPageViewModel) {
|
export function SettingsPage(settingsPageViewModel: SettingsPageViewModel) {
|
||||||
settingsPageViewModel.loadData();
|
settingsPageViewModel.loadData();
|
||||||
|
|
||||||
const secondaryChannelConverter: React.StateItemConverter<string> = (
|
const secondaryChannelConverter: React.StateItemConverter<string> = (
|
||||||
secondaryChannel: string
|
secondaryChannel: string,
|
||||||
) => {
|
) => {
|
||||||
return DeletableListItem(secondaryChannel, <span></span>, () => {
|
return DeletableListItem(secondaryChannel, <span></span>, () => {
|
||||||
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
|
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="settings-page">
|
<div id="settings-page">
|
||||||
<div class="pane-wrapper">
|
<div class="pane-wrapper">
|
||||||
<div class="pane">
|
<div class="pane">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{translations.chatPage.settings.settingsHeadline}
|
{translations.chatPage.settings.settingsHeadline}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<label class="tile flex-no">
|
<label class="tile flex-no">
|
||||||
<span class="icon">forum</span>
|
<span class="icon">forum</span>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
{translations.chatPage.settings.primaryChannelLabel}
|
{
|
||||||
</span>
|
translations.chatPage.settings
|
||||||
<input
|
.primaryChannelLabel
|
||||||
bind:value={settingsPageViewModel.primaryChannelInput}
|
}
|
||||||
on:enter={settingsPageViewModel.setPrimaryChannel}
|
</span>
|
||||||
></input>
|
<input
|
||||||
</div>
|
bind:value={
|
||||||
</label>
|
settingsPageViewModel.primaryChannelInput
|
||||||
<div class="flex-row justify-end width-input">
|
}
|
||||||
<button
|
on:enter={
|
||||||
class="width-50"
|
settingsPageViewModel.setPrimaryChannel
|
||||||
aria-label={
|
}
|
||||||
translations.chatPage.settings
|
></input>
|
||||||
.setPrimaryChannelButtonAudioLabel
|
</div>
|
||||||
}
|
</label>
|
||||||
on:click={settingsPageViewModel.setPrimaryChannel}
|
<div class="flex-row justify-end width-input">
|
||||||
toggle:disabled={settingsPageViewModel.cannotSetPrimaryChannel}
|
<button
|
||||||
>
|
class="width-50"
|
||||||
{translations.general.setButton}
|
aria-label={
|
||||||
<span class="icon">check</span>
|
translations.chatPage.settings
|
||||||
</button>
|
.setPrimaryChannelButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={
|
||||||
|
settingsPageViewModel.setPrimaryChannel
|
||||||
|
}
|
||||||
|
toggle:disabled={
|
||||||
|
settingsPageViewModel.cannotSetPrimaryChannel
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translations.general.setButton}
|
||||||
|
<span class="icon">check</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<div class="flex-row width-input margin-bottom">
|
||||||
|
<input
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.settings
|
||||||
|
.newSecondaryChannelAudioLabel
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
translations.chatPage.settings
|
||||||
|
.newSecondaryChannelPlaceholder
|
||||||
|
}
|
||||||
|
bind:value={
|
||||||
|
settingsPageViewModel.newSecondaryChannelInput
|
||||||
|
}
|
||||||
|
on:enter={
|
||||||
|
settingsPageViewModel.addSecondaryChannel
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.settings
|
||||||
|
.addSecondaryChannelButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={
|
||||||
|
settingsPageViewModel.addSecondaryChannel
|
||||||
|
}
|
||||||
|
toggle:disabled={
|
||||||
|
settingsPageViewModel.cannotAddSecondaryChannel
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex-column gap width-input"
|
||||||
|
children:append={[
|
||||||
|
settingsPageViewModel.secondaryChannels,
|
||||||
|
secondaryChannelConverter,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">key</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
translations.chatPage.settings
|
||||||
|
.encryptionKeyLabel
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
bind:value={
|
||||||
|
settingsPageViewModel.encryptionKeyInput
|
||||||
|
}
|
||||||
|
on:enter={
|
||||||
|
settingsPageViewModel.setEncryptionKey
|
||||||
|
}
|
||||||
|
set:type={
|
||||||
|
settingsPageViewModel.encryptionKeyInputType
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="flex-row justify-end width-input">
|
||||||
|
<button
|
||||||
|
class="width-50"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.settings
|
||||||
|
.setEncryptionKeyButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={
|
||||||
|
settingsPageViewModel.setEncryptionKey
|
||||||
|
}
|
||||||
|
toggle:disabled={
|
||||||
|
settingsPageViewModel.cannotSetEncryptionKey
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translations.general.setButton}
|
||||||
|
<span class="icon">check</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="inline">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={
|
||||||
|
settingsPageViewModel.shouldShowEncryptionKey
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
{translations.chatPage.settings.showEncryptionKey}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
{ColorPicker(settingsPageViewModel.color)}
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<div class="width-input">
|
||||||
|
{DangerousActionButton(
|
||||||
|
translations.chatPage.settings.deleteChatButton,
|
||||||
|
"chat_error",
|
||||||
|
settingsPageViewModel.remove,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-row width-input margin-bottom">
|
|
||||||
<input
|
|
||||||
aria-label={
|
|
||||||
translations.chatPage.settings.newSecondaryChannelAudioLabel
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
translations.chatPage.settings.newSecondaryChannelPlaceholder
|
|
||||||
}
|
|
||||||
bind:value={settingsPageViewModel.newSecondaryChannelInput}
|
|
||||||
on:enter={settingsPageViewModel.addSecondaryChannel}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
aria-label={
|
|
||||||
translations.chatPage.settings
|
|
||||||
.addSecondaryChannelButtonAudioLabel
|
|
||||||
}
|
|
||||||
on:click={settingsPageViewModel.addSecondaryChannel}
|
|
||||||
toggle:disabled={
|
|
||||||
settingsPageViewModel.cannotAddSecondaryChannel
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="icon">add</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex-column gap width-input"
|
|
||||||
children:append={[
|
|
||||||
settingsPageViewModel.secondaryChannels,
|
|
||||||
secondaryChannelConverter,
|
|
||||||
]}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">key</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.settings.encryptionKeyLabel}</span>
|
|
||||||
<input
|
|
||||||
bind:value={settingsPageViewModel.encryptionKeyInput}
|
|
||||||
on:enter={settingsPageViewModel.setEncryptionKey}
|
|
||||||
set:type={settingsPageViewModel.encryptionKeyInputType}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div class="flex-row justify-end width-input">
|
|
||||||
<button
|
|
||||||
class="width-50"
|
|
||||||
aria-label={
|
|
||||||
translations.chatPage.settings
|
|
||||||
.setEncryptionKeyButtonAudioLabel
|
|
||||||
}
|
|
||||||
on:click={settingsPageViewModel.setEncryptionKey}
|
|
||||||
toggle:disabled={settingsPageViewModel.cannotSetEncryptionKey}
|
|
||||||
>
|
|
||||||
{translations.general.setButton}
|
|
||||||
<span class="icon">check</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="inline">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={settingsPageViewModel.shouldShowEncryptionKey}
|
|
||||||
></input>
|
|
||||||
{translations.chatPage.settings.showEncryptionKey}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
{ColorPicker(settingsPageViewModel.color)}
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="width-input">
|
|
||||||
{DangerousActionButton(
|
|
||||||
translations.chatPage.settings.deleteChatButton,
|
|
||||||
"chat_error",
|
|
||||||
settingsPageViewModel.remove
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
|
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> #task-page
|
> #task-page
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .main-content
|
> .main-content
|
||||||
> div {
|
> div {
|
||||||
animation: slide-up var(--animation-duration);
|
animation: slide-up var(--animation-duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .task-grid {
|
#chat-page > div > #main > #task-page .task-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
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 .kanban-board-wrapper,
|
||||||
#chat-page > div > #main > #task-page .status-page-content {
|
#chat-page > div > #main > #task-page .status-page-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--large-gap);
|
gap: var(--large-gap);
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
|
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
|
||||||
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column {
|
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .property-input-wrapper {
|
#chat-page > div > #main > #task-page .property-input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
|
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
|
||||||
@@ -55,72 +55,72 @@
|
|||||||
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column,
|
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column,
|
||||||
#chat-page > div > #main > #task-page .status-name-row > *,
|
#chat-page > div > #main > #task-page .status-name-row > *,
|
||||||
#chat-page > div > #main > #task-page .status-name-row::before {
|
#chat-page > div > #main > #task-page .status-name-row::before {
|
||||||
width: 18rem;
|
width: 18rem;
|
||||||
min-width: 18rem;
|
min-width: 18rem;
|
||||||
max-width: 18rem;
|
max-width: 18rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .status-name-row,
|
#chat-page > div > #main > #task-page .status-name-row,
|
||||||
#chat-page > div > #main > #task-page .status-grid-wrapper {
|
#chat-page > div > #main > #task-page .status-grid-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--large-gap);
|
gap: var(--large-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .status-page-content,
|
#chat-page > div > #main > #task-page .status-page-content,
|
||||||
#chat-page > div > #main > #task-page .status-grid-wrapper {
|
#chat-page > div > #main > #task-page .status-grid-wrapper {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .status-name-row::before {
|
#chat-page > div > #main > #task-page .status-name-row::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .board-close-button {
|
#chat-page > div > #main > #task-page .board-close-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .board-toggle-button {
|
#chat-page > div > #main > #task-page .board-toggle-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> #task-page[isshowingboard][showingboardlist="false"]
|
> #task-page[isshowingboard][showingboardlist="false"]
|
||||||
#board-list {
|
#board-list {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
#chat-page > div > #main > #task-page .task-grid {
|
#chat-page > div > #main > #task-page .task-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
#chat-page > div > #main > #task-page #board-list {
|
#chat-page > div > #main > #task-page #board-list {
|
||||||
display: false;
|
display: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page #board-content {
|
#chat-page > div > #main > #task-page #board-content {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
|
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
|
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .board-close-button {
|
#chat-page > div > #main > #task-page .board-close-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > #task-page .board-toggle-button {
|
#chat-page > div > #main > #task-page .board-toggle-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,87 +8,97 @@ import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function TaskPage(taskPageViewModel: TaskPageViewModel) {
|
export function TaskPage(taskPageViewModel: TaskPageViewModel) {
|
||||||
taskPageViewModel.loadData();
|
taskPageViewModel.loadData();
|
||||||
|
|
||||||
const isShowingBoard = React.createProxyState(
|
const isShowingBoard = React.createProxyState(
|
||||||
[taskPageViewModel.selectedBoardId],
|
[taskPageViewModel.selectedBoardId],
|
||||||
() => taskPageViewModel.selectedBoardId.value != undefined
|
() => taskPageViewModel.selectedBoardId.value != undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const paneContent = React.createProxyState(
|
const paneContent = React.createProxyState(
|
||||||
[taskPageViewModel.selectedBoardId],
|
[taskPageViewModel.selectedBoardId],
|
||||||
() => {
|
() => {
|
||||||
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
|
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
|
||||||
if (selectedBoardId == undefined) {
|
if (selectedBoardId == undefined) {
|
||||||
return (
|
return (
|
||||||
<div class="pane align-center justify-center">
|
<div class="pane align-center justify-center">
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.chatPage.task.noBoardSelected}
|
{translations.chatPage.task.noBoardSelected}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const selectedBoard =
|
const selectedBoard =
|
||||||
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
|
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
|
||||||
if (selectedBoard == undefined) {
|
if (selectedBoard == undefined) {
|
||||||
return (
|
return (
|
||||||
<div class="pane align-center justify-center">
|
<div class="pane align-center justify-center">
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.chatPage.task.boardNotFound}
|
{translations.chatPage.task.boardNotFound}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BoardPage(selectedBoard);
|
return BoardPage(selectedBoard);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="task-page" toggle:isshowingboard={isShowingBoard} set:showingboardlist={taskPageViewModel.isShowingBoadList}>
|
<div
|
||||||
<div
|
id="task-page"
|
||||||
id="board-list"
|
toggle:isshowingboard={isShowingBoard}
|
||||||
class="pane-wrapper side background"
|
set:showingboardlist={taskPageViewModel.isShowingBoadList}
|
||||||
set:color={taskPageViewModel.chatViewModel.displayedColor}
|
>
|
||||||
>
|
|
||||||
<div class="pane">
|
|
||||||
<div class="toolbar">
|
|
||||||
<div class="flex-row width-input">
|
|
||||||
<input
|
|
||||||
class="no-outline"
|
|
||||||
bind:value={taskPageViewModel.newBoardNameInput}
|
|
||||||
on:enter={taskPageViewModel.createBoard}
|
|
||||||
placeholder={translations.chatPage.task.newBoardNamePlaceholder}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
aria-label={
|
|
||||||
translations.chatPage.task.createBoardButtonAudioLabel
|
|
||||||
}
|
|
||||||
on:click={taskPageViewModel.createBoard}
|
|
||||||
toggle:disabled={taskPageViewModel.cannotCreateBoard}
|
|
||||||
>
|
|
||||||
<span class="icon">add</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div
|
<div
|
||||||
class="grid gap"
|
id="board-list"
|
||||||
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
|
class="pane-wrapper side background"
|
||||||
children:append={[
|
set:color={taskPageViewModel.chatViewModel.displayedColor}
|
||||||
taskPageViewModel.boardViewModels,
|
>
|
||||||
BoardViewModelToEntry,
|
<div class="pane">
|
||||||
]}
|
<div class="toolbar">
|
||||||
|
<div class="flex-row width-input">
|
||||||
|
<input
|
||||||
|
class="no-outline"
|
||||||
|
bind:value={taskPageViewModel.newBoardNameInput}
|
||||||
|
on:enter={taskPageViewModel.createBoard}
|
||||||
|
placeholder={
|
||||||
|
translations.chatPage.task
|
||||||
|
.newBoardNamePlaceholder
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.task
|
||||||
|
.createBoardButtonAudioLabel
|
||||||
|
}
|
||||||
|
on:click={taskPageViewModel.createBoard}
|
||||||
|
toggle:disabled={
|
||||||
|
taskPageViewModel.cannotCreateBoard
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div
|
||||||
|
class="grid gap"
|
||||||
|
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
|
||||||
|
children:append={[
|
||||||
|
taskPageViewModel.boardViewModels,
|
||||||
|
BoardViewModelToEntry,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="board-content"
|
||||||
|
class="pane-wrapper"
|
||||||
|
children:set={paneContent}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
<div
|
|
||||||
id="board-content"
|
|
||||||
class="pane-wrapper"
|
|
||||||
children:set={paneContent}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,32 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
|
|||||||
import { allowDrop } from "../utility";
|
import { allowDrop } from "../utility";
|
||||||
|
|
||||||
export function BoardEntry(boardViewModel: BoardViewModel) {
|
export function BoardEntry(boardViewModel: BoardViewModel) {
|
||||||
const view = (
|
const view = (
|
||||||
<button
|
<button
|
||||||
set:color={boardViewModel.color}
|
set:color={boardViewModel.color}
|
||||||
class="tile colored-tile"
|
class="tile colored-tile"
|
||||||
toggle:selected={boardViewModel.isSelected}
|
toggle:selected={boardViewModel.isSelected}
|
||||||
on:click={boardViewModel.select}
|
on:click={boardViewModel.select}
|
||||||
on:dragover={allowDrop}
|
on:dragover={allowDrop}
|
||||||
on:drop={boardViewModel.handleDropBetweenBoards}
|
on:drop={boardViewModel.handleDropBetweenBoards}
|
||||||
>
|
>
|
||||||
<span class="shadow" subscribe:innerText={boardViewModel.name}></span>
|
<span
|
||||||
<b subscribe:innerText={boardViewModel.name}></b>
|
class="shadow"
|
||||||
</button>
|
subscribe:innerText={boardViewModel.name}
|
||||||
);
|
></span>
|
||||||
|
<b subscribe:innerText={boardViewModel.name}></b>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
boardViewModel.index.subscribe((newIndex) => {
|
boardViewModel.index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BoardViewModelToEntry: React.StateItemConverter<BoardViewModel> = (
|
export const BoardViewModelToEntry: React.StateItemConverter<BoardViewModel> = (
|
||||||
boardViewModel: BoardViewModel
|
boardViewModel: BoardViewModel,
|
||||||
) => {
|
) => {
|
||||||
return BoardEntry(boardViewModel);
|
return BoardEntry(boardViewModel);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardViewModel, {
|
import BoardViewModel, {
|
||||||
BoardPageType,
|
BoardPageType,
|
||||||
} from "../../ViewModel/Pages/boardViewModel";
|
} from "../../ViewModel/Pages/boardViewModel";
|
||||||
|
|
||||||
import { RibbonButton } from "./ribbonButton";
|
import { RibbonButton } from "./ribbonButton";
|
||||||
|
|
||||||
export function BoardViewToggleButton(
|
export function BoardViewToggleButton(
|
||||||
label: string,
|
label: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
page: BoardPageType,
|
page: BoardPageType,
|
||||||
boardViewModel: BoardViewModel
|
boardViewModel: BoardViewModel,
|
||||||
) {
|
) {
|
||||||
function select() {
|
function select() {
|
||||||
boardViewModel.selectedPage.value = page;
|
boardViewModel.selectedPage.value = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = React.createProxyState(
|
const isSelected = React.createProxyState(
|
||||||
[boardViewModel.selectedPage],
|
[boardViewModel.selectedPage],
|
||||||
() => boardViewModel.selectedPage.value == page
|
() => boardViewModel.selectedPage.value == page,
|
||||||
);
|
);
|
||||||
|
|
||||||
return RibbonButton(label, icon, isSelected, select);
|
return RibbonButton(label, icon, isSelected, select);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,37 @@ import * as React from "bloatless-react";
|
|||||||
import ChatViewModel from "../../ViewModel/Chat/chatViewModel";
|
import ChatViewModel from "../../ViewModel/Chat/chatViewModel";
|
||||||
|
|
||||||
export function ChatEntry(chatViewModel: ChatViewModel) {
|
export function ChatEntry(chatViewModel: ChatViewModel) {
|
||||||
const view = (
|
const view = (
|
||||||
<button
|
<button
|
||||||
class="tile colored-tile chat-entry"
|
class="tile colored-tile chat-entry"
|
||||||
set:color={chatViewModel.settingsPageViewModel.color}
|
set:color={chatViewModel.settingsPageViewModel.color}
|
||||||
style="height: 8rem"
|
style="height: 8rem"
|
||||||
on:click={chatViewModel.open}
|
on:click={chatViewModel.open}
|
||||||
toggle:unread={chatViewModel.hasUnreadMessages}
|
toggle:unread={chatViewModel.hasUnreadMessages}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="shadow"
|
class="shadow"
|
||||||
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
|
subscribe:innerText={
|
||||||
></span>
|
chatViewModel.settingsPageViewModel.primaryChannel
|
||||||
<h2
|
}
|
||||||
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
|
></span>
|
||||||
></h2>
|
<h2
|
||||||
</button>
|
subscribe:innerText={
|
||||||
);
|
chatViewModel.settingsPageViewModel.primaryChannel
|
||||||
|
}
|
||||||
|
></h2>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
chatViewModel.index.subscribe((newIndex) => {
|
chatViewModel.index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatViewModelToChatEntry: React.StateItemConverter<
|
export const ChatViewModelToChatEntry: React.StateItemConverter<
|
||||||
ChatViewModel
|
ChatViewModel
|
||||||
> = (chatViewModel: ChatViewModel) => {
|
> = (chatViewModel: ChatViewModel) => {
|
||||||
return ChatEntry(chatViewModel);
|
return ChatEntry(chatViewModel);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,61 +6,65 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
|
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
|
||||||
const statusIcon = React.createProxyState(
|
const statusIcon = React.createProxyState(
|
||||||
[chatMessageViewModel.status],
|
[chatMessageViewModel.status],
|
||||||
() => {
|
() => {
|
||||||
switch (chatMessageViewModel.status.value) {
|
switch (chatMessageViewModel.status.value) {
|
||||||
case ChatMessageStatus.Outbox:
|
case ChatMessageStatus.Outbox:
|
||||||
return "hourglass_top";
|
return "hourglass_top";
|
||||||
case ChatMessageStatus.Sent:
|
case ChatMessageStatus.Sent:
|
||||||
return "check";
|
return "check";
|
||||||
case ChatMessageStatus.Received:
|
case ChatMessageStatus.Received:
|
||||||
return "done_all";
|
return "done_all";
|
||||||
default:
|
default:
|
||||||
return "warning";
|
return "warning";
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="message-bubble"
|
|
||||||
toggle:sentbyuser={chatMessageViewModel.sentByUser}
|
|
||||||
>
|
|
||||||
<div class="main tile">
|
|
||||||
<div class="text-container">
|
|
||||||
<span class="sender-name ellipsis">
|
|
||||||
{chatMessageViewModel.sender}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="body"
|
|
||||||
subscribe:innerText={chatMessageViewModel.body}
|
|
||||||
></span>
|
|
||||||
<span class="timestamp ellipsis">
|
|
||||||
<span class="icon" subscribe:innerText={statusIcon}></span>
|
|
||||||
{chatMessageViewModel.dateSent}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-container">
|
|
||||||
<button
|
|
||||||
on:click={chatMessageViewModel.showInfoModal}
|
|
||||||
aria-label={
|
|
||||||
translations.chatPage.message.showMessageInfoButtonAudioLabel
|
|
||||||
}
|
}
|
||||||
>
|
},
|
||||||
<span class="icon">info</span>
|
);
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ChatMessageInfoModal(chatMessageViewModel)}
|
return (
|
||||||
</div>
|
<div
|
||||||
);
|
class="message-bubble"
|
||||||
|
toggle:sentbyuser={chatMessageViewModel.sentByUser}
|
||||||
|
>
|
||||||
|
<div class="main tile">
|
||||||
|
<div class="text-container">
|
||||||
|
<span class="sender-name ellipsis">
|
||||||
|
{chatMessageViewModel.sender}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="body"
|
||||||
|
subscribe:innerText={chatMessageViewModel.body}
|
||||||
|
></span>
|
||||||
|
<span class="timestamp ellipsis">
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
subscribe:innerText={statusIcon}
|
||||||
|
></span>
|
||||||
|
{chatMessageViewModel.dateSent}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button
|
||||||
|
on:click={chatMessageViewModel.showInfoModal}
|
||||||
|
aria-label={
|
||||||
|
translations.chatPage.message
|
||||||
|
.showMessageInfoButtonAudioLabel
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">info</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ChatMessageInfoModal(chatMessageViewModel)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessageViewModelToView: React.StateItemConverter<
|
export const ChatMessageViewModelToView: React.StateItemConverter<
|
||||||
ChatMessageViewModel
|
ChatMessageViewModel
|
||||||
> = (chatMessageViewModel: ChatMessageViewModel) => {
|
> = (chatMessageViewModel: ChatMessageViewModel) => {
|
||||||
return ChatMessage(chatMessageViewModel);
|
return ChatMessage(chatMessageViewModel);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import ChatViewModel, {
|
import ChatViewModel, {
|
||||||
ChatPageType,
|
ChatPageType,
|
||||||
} from "../../ViewModel/Chat/chatViewModel";
|
} from "../../ViewModel/Chat/chatViewModel";
|
||||||
|
|
||||||
import { RibbonButton } from "./ribbonButton";
|
import { RibbonButton } from "./ribbonButton";
|
||||||
|
|
||||||
export function ChatViewToggleButton(
|
export function ChatViewToggleButton(
|
||||||
label: string,
|
label: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
page: ChatPageType,
|
page: ChatPageType,
|
||||||
chatViewModel: ChatViewModel
|
chatViewModel: ChatViewModel,
|
||||||
) {
|
) {
|
||||||
function select() {
|
function select() {
|
||||||
chatViewModel.selectedPage.value = page;
|
chatViewModel.selectedPage.value = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = React.createProxyState(
|
const isSelected = React.createProxyState(
|
||||||
[chatViewModel.selectedPage],
|
[chatViewModel.selectedPage],
|
||||||
() => chatViewModel.selectedPage.value == page
|
() => chatViewModel.selectedPage.value == page,
|
||||||
);
|
);
|
||||||
|
|
||||||
return RibbonButton(label, icon, isSelected, select);
|
return RibbonButton(label, icon, isSelected, select);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,28 +3,28 @@ import * as React from "bloatless-react";
|
|||||||
import { Color } from "../../colors";
|
import { Color } from "../../colors";
|
||||||
|
|
||||||
export function ColorPicker(selectedColor: React.State<Color>) {
|
export function ColorPicker(selectedColor: React.State<Color>) {
|
||||||
return (
|
return (
|
||||||
<div class="flex-row gap width-input">
|
<div class="flex-row gap width-input">
|
||||||
{...Object.values(Color).map((color) => {
|
{...Object.values(Color).map((color) => {
|
||||||
const isSelected = React.createProxyState(
|
const isSelected = React.createProxyState(
|
||||||
[selectedColor],
|
[selectedColor],
|
||||||
() => selectedColor.value == color
|
() => selectedColor.value == color,
|
||||||
);
|
);
|
||||||
|
|
||||||
function setColor() {
|
function setColor() {
|
||||||
selectedColor.value = color;
|
selectedColor.value = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
color={color}
|
color={color}
|
||||||
class="fill-color width-100 flex"
|
class="fill-color width-100 flex"
|
||||||
style="height: 2rem"
|
style="height: 2rem"
|
||||||
toggle:selected={isSelected}
|
toggle:selected={isSelected}
|
||||||
on:click={setColor}
|
on:click={setColor}
|
||||||
></button>
|
></button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,45 @@ import * as React from "bloatless-react";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function DangerousActionButton(
|
export function DangerousActionButton(
|
||||||
label: string,
|
label: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
action: () => void
|
action: () => void,
|
||||||
) {
|
) {
|
||||||
const isActionRequested = new React.State(false);
|
const isActionRequested = new React.State(false);
|
||||||
const cannotConfirm = React.createProxyState(
|
const cannotConfirm = React.createProxyState(
|
||||||
[isActionRequested],
|
[isActionRequested],
|
||||||
() => isActionRequested.value == false
|
() => isActionRequested.value == false,
|
||||||
);
|
);
|
||||||
|
|
||||||
function requestAction() {
|
function requestAction() {
|
||||||
isActionRequested.value = true;
|
isActionRequested.value = true;
|
||||||
}
|
}
|
||||||
function abort() {
|
function abort() {
|
||||||
isActionRequested.value = false;
|
isActionRequested.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
|
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
|
||||||
{translations.general.abortButton}
|
{translations.general.abortButton}
|
||||||
<span class="icon">undo</span>
|
<span class="icon">undo</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="danger flex"
|
class="danger flex"
|
||||||
on:click={requestAction}
|
on:click={requestAction}
|
||||||
toggle:hidden={isActionRequested}
|
toggle:hidden={isActionRequested}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
<span class="icon">{icon}</span>
|
<span class="icon">{icon}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="danger flex"
|
class="danger flex"
|
||||||
on:click={action}
|
on:click={action}
|
||||||
toggle:hidden={cannotConfirm}
|
toggle:hidden={cannotConfirm}
|
||||||
>
|
>
|
||||||
{translations.general.confirmButton}
|
{translations.general.confirmButton}
|
||||||
<span class="icon">warning</span>
|
<span class="icon">warning</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,25 @@ import * as React from "bloatless-react";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function DeletableListItem(
|
export function DeletableListItem(
|
||||||
text: string,
|
text: string,
|
||||||
primaryButton: HTMLElement,
|
primaryButton: HTMLElement,
|
||||||
ondelete: () => void
|
ondelete: () => void,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div class="tile flex-row justify-apart align-center padding-0">
|
<div class="tile flex-row justify-apart align-center padding-0">
|
||||||
<span class="padding-h ellipsis">{text}</span>
|
<span class="padding-h ellipsis">{text}</span>
|
||||||
|
|
||||||
<div class="flex-row justify-end">
|
<div class="flex-row justify-end">
|
||||||
{primaryButton}
|
{primaryButton}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="danger"
|
class="danger"
|
||||||
aria-label={translations.general.deleteItemButtonAudioLabel}
|
aria-label={translations.general.deleteItemButtonAudioLabel}
|
||||||
on:click={ondelete}
|
on:click={ondelete}
|
||||||
>
|
>
|
||||||
<span class="icon">delete</span>
|
<span class="icon">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,79 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
PATH_COMPONENT_SEPARATOR,
|
PATH_COMPONENT_SEPARATOR,
|
||||||
} from "../../Model/Global/storageModel";
|
} from "../../Model/Global/storageModel";
|
||||||
|
|
||||||
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
||||||
|
|
||||||
export function DirectoryItemList(
|
export function DirectoryItemList(
|
||||||
storageViewModel: StorageViewModel,
|
storageViewModel: StorageViewModel,
|
||||||
pathString: string = PATH_COMPONENT_SEPARATOR
|
pathString: string = PATH_COMPONENT_SEPARATOR,
|
||||||
) {
|
) {
|
||||||
// converter
|
// converter
|
||||||
const StringToDirectoryItemList: React.StateItemConverter<string> = (
|
const StringToDirectoryItemList: React.StateItemConverter<string> = (
|
||||||
pathString: string
|
pathString: string,
|
||||||
) => DirectoryItemList(storageViewModel, pathString);
|
) => DirectoryItemList(storageViewModel, pathString);
|
||||||
|
|
||||||
// data
|
// data
|
||||||
const path = StorageModel.stringToPathComponents(pathString);
|
const path = StorageModel.stringToPathComponents(pathString);
|
||||||
const fileName = StorageModel.getFileName(path);
|
const fileName = StorageModel.getFileName(path);
|
||||||
const items = new React.ListState<string>();
|
const items = new React.ListState<string>();
|
||||||
|
|
||||||
const style = `text-indent: ${path.length}rem`;
|
const style = `text-indent: ${path.length}rem`;
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
function loadItems() {
|
function loadItems() {
|
||||||
items.clear();
|
items.clear();
|
||||||
|
|
||||||
const directoryItems = storageViewModel.storageModel.list(path);
|
const directoryItems = storageViewModel.storageModel.list(path);
|
||||||
|
|
||||||
for (const directoryItem of directoryItems) {
|
for (const directoryItem of directoryItems) {
|
||||||
const itemPath = [...path, directoryItem];
|
const itemPath = [...path, directoryItem];
|
||||||
const pathString = StorageModel.pathComponentsToString(...itemPath);
|
const pathString = StorageModel.pathComponentsToString(...itemPath);
|
||||||
items.add(pathString);
|
items.add(pathString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
storageViewModel.selectedPath.value = pathString;
|
storageViewModel.selectedPath.value = pathString;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
|
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
|
||||||
if (!items.value.has(lastDeletedItemPath)) return;
|
if (!items.value.has(lastDeletedItemPath)) return;
|
||||||
|
|
||||||
select();
|
select();
|
||||||
setTimeout(() => loadItems(), 50);
|
setTimeout(() => loadItems(), 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// state
|
// state
|
||||||
const isSelected = React.createProxyState(
|
const isSelected = React.createProxyState(
|
||||||
[storageViewModel.selectedPath],
|
[storageViewModel.selectedPath],
|
||||||
() => storageViewModel.selectedPath.value == pathString
|
() => storageViewModel.selectedPath.value == pathString,
|
||||||
);
|
);
|
||||||
|
|
||||||
isSelected.subscribe(() => {
|
isSelected.subscribe(() => {
|
||||||
if (isSelected.value == false) return;
|
if (isSelected.value == false) return;
|
||||||
loadItems();
|
loadItems();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-column">
|
<div class="flex-column">
|
||||||
<button
|
<button
|
||||||
class="width-100 flex-1 clip"
|
class="width-100 flex-1 clip"
|
||||||
toggle:selected={isSelected}
|
toggle:selected={isSelected}
|
||||||
on:click={select}
|
on:click={select}
|
||||||
>
|
>
|
||||||
<span class="ellipsis width-100 flex-1" style={style}>
|
<span class="ellipsis width-100 flex-1" style={style}>
|
||||||
{fileName}
|
{fileName}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex-column"
|
class="flex-column"
|
||||||
children:append={[items, StringToDirectoryItemList]}
|
children:append={[items, StringToDirectoryItemList]}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,62 @@
|
|||||||
.file-browser {
|
.file-browser {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser > div {
|
.file-browser > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
flex: 0;
|
flex: 0;
|
||||||
|
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser .scroll-area {
|
.file-browser .scroll-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser .detail-button-wrapper {
|
.file-browser .detail-button-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser .detail-button-wrapper button {
|
.file-browser .detail-button-wrapper button {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser code {
|
.file-browser code {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
font-size: .9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.file-browser > div {
|
.file-browser > div {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser .detail-button-wrapper {
|
.file-browser .detail-button-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,75 +9,85 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function FileBrowser(storageViewModel: StorageViewModel) {
|
export function FileBrowser(storageViewModel: StorageViewModel) {
|
||||||
const detailView = React.createProxyState(
|
const detailView = React.createProxyState(
|
||||||
[storageViewModel.selectedPath],
|
[storageViewModel.selectedPath],
|
||||||
() => {
|
() => {
|
||||||
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
|
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span class="secondary">{translations.storage.noItemSelected}</span>
|
<span class="secondary">
|
||||||
|
{translations.storage.noItemSelected}
|
||||||
|
</span>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
{DangerousActionButton(
|
{DangerousActionButton(
|
||||||
translations.storage.removeJunkButton,
|
translations.storage.removeJunkButton,
|
||||||
"delete_forever",
|
"delete_forever",
|
||||||
storageViewModel.removeJunk
|
storageViewModel.removeJunk,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-column gap">
|
<div class="flex-column gap">
|
||||||
<div class="tile flex-no">
|
<div class="tile flex-no">
|
||||||
|
<div>
|
||||||
|
<b>{translations.storage.path}</b>
|
||||||
|
<span
|
||||||
|
class="break-all"
|
||||||
|
subscribe:innerText={
|
||||||
|
storageViewModel.selectedPath
|
||||||
|
}
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile flex-no">
|
||||||
|
<div>
|
||||||
|
<b>{translations.storage.content}</b>
|
||||||
|
<code
|
||||||
|
subscribe:innerText={
|
||||||
|
storageViewModel.selectedFileContent
|
||||||
|
}
|
||||||
|
></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{DangerousActionButton(
|
||||||
|
translations.storage.deleteItem,
|
||||||
|
"delete_forever",
|
||||||
|
storageViewModel.deleteSelectedItem,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const view = (
|
||||||
|
<div class="file-browser">
|
||||||
<div>
|
<div>
|
||||||
<b>{translations.storage.path}</b>
|
<div class="scroll-area">
|
||||||
<span
|
{DirectoryItemList(storageViewModel)}
|
||||||
class="break-all"
|
</div>
|
||||||
subscribe:innerText={storageViewModel.selectedPath}
|
<div class="detail-button-wrapper">
|
||||||
></span>
|
<button class="ghost" on:click={scrollToDetails}>
|
||||||
|
<span
|
||||||
|
class="ellipsis"
|
||||||
|
subscribe:innerText={
|
||||||
|
storageViewModel.selectedFileName
|
||||||
|
}
|
||||||
|
></span>
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="scroll-area" children:set={detailView}></div>
|
||||||
<div class="tile flex-no">
|
|
||||||
<div>
|
|
||||||
<b>{translations.storage.content}</b>
|
|
||||||
<code
|
|
||||||
subscribe:innerText={storageViewModel.selectedFileContent}
|
|
||||||
></code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{DangerousActionButton(
|
|
||||||
translations.storage.deleteItem,
|
|
||||||
"delete_forever",
|
|
||||||
storageViewModel.deleteSelectedItem
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function scrollToDetails() {
|
||||||
|
view.scrollLeft = view.scrollWidth;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const view = (
|
return view;
|
||||||
<div class="file-browser">
|
|
||||||
<div>
|
|
||||||
<div class="scroll-area">{DirectoryItemList(storageViewModel)}</div>
|
|
||||||
<div class="detail-button-wrapper">
|
|
||||||
<button class="ghost" on:click={scrollToDetails}>
|
|
||||||
<span
|
|
||||||
class="ellipsis"
|
|
||||||
subscribe:innerText={storageViewModel.selectedFileName}
|
|
||||||
></span>
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="scroll-area" children:set={detailView}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function scrollToDetails() {
|
|
||||||
view.scrollLeft = view.scrollWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StringEntryObject,
|
StringEntryObject,
|
||||||
checkDoesObjectMatchReference,
|
checkDoesObjectMatchReference,
|
||||||
} from "../../Model/Utility/utility";
|
} from "../../Model/Utility/utility";
|
||||||
|
|
||||||
export function FilteredList<T>(
|
export function FilteredList<T>(
|
||||||
reference: StringEntryObject,
|
reference: StringEntryObject,
|
||||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||||
objects: React.ListState<T> | React.MapState<T>,
|
objects: React.ListState<T> | React.MapState<T>,
|
||||||
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement
|
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement,
|
||||||
) {
|
) {
|
||||||
const matchingObjects: React.ListState<T> = new React.ListState();
|
const matchingObjects: React.ListState<T> = new React.ListState();
|
||||||
|
|
||||||
objects.handleAddition((newObject) => {
|
objects.handleAddition((newObject) => {
|
||||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||||
reference,
|
reference,
|
||||||
stringEntryObjectConverter(newObject)
|
stringEntryObjectConverter(newObject),
|
||||||
);
|
);
|
||||||
if (doesMatch == false) return;
|
if (doesMatch == false) return;
|
||||||
|
|
||||||
matchingObjects.add(newObject);
|
matchingObjects.add(newObject);
|
||||||
objects.handleRemoval(newObject, () => {
|
objects.handleRemoval(newObject, () => {
|
||||||
matchingObjects.remove(newObject);
|
matchingObjects.remove(newObject);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return viewBuilder(matchingObjects);
|
return viewBuilder(matchingObjects);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +1,106 @@
|
|||||||
.month-grid-wrapper {
|
.month-grid-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid-wrapper .day-labels {
|
.month-grid-wrapper .day-labels {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
|
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid-wrapper > div {
|
.month-grid-wrapper > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
grid-template-columns: repeat(7, minmax(0px, 1fr));
|
grid-template-columns: repeat(7, minmax(0px, 1fr));
|
||||||
|
|
||||||
animation: slide-up var(--animation-duration);
|
animation: slide-up var(--animation-duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid {
|
.month-grid {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
grid-auto-rows: 1fr;
|
grid-auto-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile {
|
.month-grid .tile {
|
||||||
animation: slide-up var(--animation-duration);
|
animation: slide-up var(--animation-duration);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile[today] b {
|
.month-grid .tile[today] b {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile > div {
|
.month-grid .tile > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile .event-count {
|
.month-grid .tile .event-count {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
background-color: var(--text-normal);
|
background-color: var(--text-normal);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
|
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile .event-count[has-events] {
|
.month-grid .tile .event-count[has-events] {
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile .event-list {
|
.month-grid .tile .event-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
}
|
}
|
||||||
.month-grid .tile .event-list span {
|
.month-grid .tile .event-list span {
|
||||||
animation: slide-up var(--animation-duration);
|
animation: slide-up var(--animation-duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) and (max-height: 900px) {
|
@media (max-width: 640px) and (max-height: 900px) {
|
||||||
.month-grid .tile {
|
.month-grid .tile {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile > div {
|
.month-grid .tile > div {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile .event-count {
|
.month-grid .tile .event-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-grid .tile .event-list {
|
.month-grid .tile .event-list {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,103 +8,105 @@ import { allowDrop } from "../utility";
|
|||||||
import { localeCompare } from "../../Model/Utility/utility";
|
import { localeCompare } from "../../Model/Utility/utility";
|
||||||
|
|
||||||
export function MonthGrid<T>(
|
export function MonthGrid<T>(
|
||||||
monthGrid: MonthGrid<React.MapState<T>>,
|
monthGrid: MonthGrid<React.MapState<T>>,
|
||||||
selectedDate: React.State<number>,
|
selectedDate: React.State<number>,
|
||||||
handleDrop: (date: string) => void
|
handleDrop: (date: string) => void,
|
||||||
) {
|
) {
|
||||||
const dayLabels: HTMLElement[] = [];
|
const dayLabels: HTMLElement[] = [];
|
||||||
let currentWeekday = monthGrid.firstDayOfWeek;
|
let currentWeekday = monthGrid.firstDayOfWeek;
|
||||||
while (dayLabels.length < 7) {
|
while (dayLabels.length < 7) {
|
||||||
dayLabels.push(
|
dayLabels.push(
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.regional.weekdays.abbreviated[currentWeekday]}
|
{translations.regional.weekdays.abbreviated[currentWeekday]}
|
||||||
</span>
|
</span>,
|
||||||
|
);
|
||||||
|
currentWeekday++;
|
||||||
|
if (currentWeekday == 7) currentWeekday = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetElements: HTMLElement[] = [];
|
||||||
|
for (let i = 0; i < monthGrid.offset; i++) {
|
||||||
|
offsetElements.push(<div></div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const converter: React.StateItemConverter<TaskViewModel> = (
|
||||||
|
taskViewModel,
|
||||||
|
) => {
|
||||||
|
const view = (
|
||||||
|
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
taskViewModel.index.subscribe((newIndex) => {
|
||||||
|
view.style.order = newIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="month-grid-wrapper">
|
||||||
|
<div class="day-labels">{...dayLabels}</div>
|
||||||
|
<div class="month-grid">
|
||||||
|
{...offsetElements}
|
||||||
|
{...Object.entries(monthGrid.days)
|
||||||
|
.sort((a, b) => localeCompare(a[0], b[0]))
|
||||||
|
.map((entry) => {
|
||||||
|
const [date, mapState] = entry;
|
||||||
|
|
||||||
|
const isSelected = React.createProxyState(
|
||||||
|
[selectedDate],
|
||||||
|
() => selectedDate.value == parseInt(date),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isToday: boolean =
|
||||||
|
monthGrid.isCurrentMonth == true &&
|
||||||
|
parseInt(date) == new Date().getDate();
|
||||||
|
|
||||||
|
const eventCount: React.State<number> =
|
||||||
|
React.createProxyState(
|
||||||
|
[mapState],
|
||||||
|
() => mapState.value.size,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasEvents: React.State<boolean> =
|
||||||
|
React.createProxyState(
|
||||||
|
[eventCount],
|
||||||
|
() => eventCount.value != 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
function select() {
|
||||||
|
selectedDate.value = parseInt(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drop() {
|
||||||
|
handleDrop(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
class="tile"
|
||||||
|
on:click={select}
|
||||||
|
toggle:selected={isSelected}
|
||||||
|
toggle:today={isToday}
|
||||||
|
on:dragover={allowDrop}
|
||||||
|
on:drop={drop}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<b>{date}</b>
|
||||||
|
<span
|
||||||
|
class="event-count"
|
||||||
|
toggle:has-events={hasEvents}
|
||||||
|
subscribe:innerText={eventCount}
|
||||||
|
></span>
|
||||||
|
<div
|
||||||
|
class="event-list"
|
||||||
|
children:append={[mapState, converter]}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
currentWeekday++;
|
|
||||||
if (currentWeekday == 7) currentWeekday = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetElements: HTMLElement[] = [];
|
|
||||||
for (let i = 0; i < monthGrid.offset; i++) {
|
|
||||||
offsetElements.push(<div></div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const converter: React.StateItemConverter<TaskViewModel> = (
|
|
||||||
taskViewModel
|
|
||||||
) => {
|
|
||||||
const view = (
|
|
||||||
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
taskViewModel.index.subscribe((newIndex) => {
|
|
||||||
view.style.order = newIndex;
|
|
||||||
});
|
|
||||||
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="month-grid-wrapper">
|
|
||||||
<div class="day-labels">{...dayLabels}</div>
|
|
||||||
<div class="month-grid">
|
|
||||||
{...offsetElements}
|
|
||||||
{...Object.entries(monthGrid.days)
|
|
||||||
.sort((a, b) => localeCompare(a[0], b[0]))
|
|
||||||
.map((entry) => {
|
|
||||||
const [date, mapState] = entry;
|
|
||||||
|
|
||||||
const isSelected = React.createProxyState(
|
|
||||||
[selectedDate],
|
|
||||||
() => selectedDate.value == parseInt(date)
|
|
||||||
);
|
|
||||||
|
|
||||||
const isToday: boolean =
|
|
||||||
monthGrid.isCurrentMonth == true &&
|
|
||||||
parseInt(date) == new Date().getDate();
|
|
||||||
|
|
||||||
const eventCount: React.State<number> = React.createProxyState(
|
|
||||||
[mapState],
|
|
||||||
() => mapState.value.size
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasEvents: React.State<boolean> = React.createProxyState(
|
|
||||||
[eventCount],
|
|
||||||
() => eventCount.value != 0
|
|
||||||
);
|
|
||||||
|
|
||||||
function select() {
|
|
||||||
selectedDate.value = parseInt(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drop() {
|
|
||||||
handleDrop(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
class="tile"
|
|
||||||
on:click={select}
|
|
||||||
toggle:selected={isSelected}
|
|
||||||
toggle:today={isToday}
|
|
||||||
on:dragover={allowDrop}
|
|
||||||
on:drop={drop}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<b>{date}</b>
|
|
||||||
<span
|
|
||||||
class="event-count"
|
|
||||||
toggle:has-events={hasEvents}
|
|
||||||
subscribe:innerText={eventCount}
|
|
||||||
></span>
|
|
||||||
<div
|
|
||||||
class="event-list"
|
|
||||||
children:append={[mapState, converter]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
export function Option(text: string, value: string, selectedOnCreate: boolean) {
|
export function Option(text: string, value: string, selectedOnCreate: boolean) {
|
||||||
return (
|
return (
|
||||||
<option value={value} toggle:selected={selectedOnCreate}>
|
<option value={value} toggle:selected={selectedOnCreate}>
|
||||||
{text}
|
{text}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StringToOption: React.StateItemConverter<string> = (
|
export const StringToOption: React.StateItemConverter<string> = (
|
||||||
string: string
|
string: string,
|
||||||
) => {
|
) => {
|
||||||
return Option(string, string, false);
|
return Option(string, string, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EntryToOption: React.StateItemConverter<Entry> = (
|
export const EntryToOption: React.StateItemConverter<Entry> = (
|
||||||
entry: Entry
|
entry: Entry,
|
||||||
) => {
|
) => {
|
||||||
return Option(entry[1], entry[0], false);
|
return Option(entry[1], entry[0], false);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VersionIdToOption: React.StateItemConverter<string> = (
|
export const VersionIdToOption: React.StateItemConverter<string> = (
|
||||||
versionId: string
|
versionId: string,
|
||||||
) => {
|
) => {
|
||||||
const [date, rest] = versionId.split("T");
|
const [date, rest] = versionId.split("T");
|
||||||
const [time] = rest.split(".");
|
const [time] = rest.split(".");
|
||||||
const readableName = `${date} ${time}`;
|
const readableName = `${date} ${time}`;
|
||||||
return Option(readableName, versionId, false);
|
return Option(readableName, versionId, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Entry = [string, string];
|
export type Entry = [string, string];
|
||||||
|
|||||||
@@ -1,70 +1,71 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StringEntryObject,
|
StringEntryObject,
|
||||||
collectObjectValuesForKey,
|
collectObjectValuesForKey,
|
||||||
localeCompare,
|
localeCompare,
|
||||||
} from "../../Model/Utility/utility";
|
} from "../../Model/Utility/utility";
|
||||||
|
|
||||||
export function PropertyValueList<T>(
|
export function PropertyValueList<T>(
|
||||||
propertyKey: string,
|
propertyKey: string,
|
||||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||||
objects: React.ListState<T> | React.MapState<T>,
|
objects: React.ListState<T> | React.MapState<T>,
|
||||||
viewBuilder: (
|
viewBuilder: (
|
||||||
keys: React.ListState<string>,
|
keys: React.ListState<string>,
|
||||||
sortedKeys: React.State<string[]>
|
sortedKeys: React.State<string[]>,
|
||||||
) => HTMLElement
|
) => HTMLElement,
|
||||||
) {
|
) {
|
||||||
const propertyValues: React.ListState<string> = new React.ListState();
|
const propertyValues: React.ListState<string> = new React.ListState();
|
||||||
const sortedPropertyValues: React.State<string[]> = createSortedPropertyValueState(propertyValues)
|
const sortedPropertyValues: React.State<string[]> =
|
||||||
objects.subscribe(() => {
|
createSortedPropertyValueState(propertyValues);
|
||||||
collectPropertyValuesToState(
|
objects.subscribe(() => {
|
||||||
propertyKey,
|
collectPropertyValuesToState(
|
||||||
stringEntryObjectConverter,
|
propertyKey,
|
||||||
objects,
|
stringEntryObjectConverter,
|
||||||
propertyValues
|
objects,
|
||||||
);
|
propertyValues,
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return viewBuilder(propertyValues, sortedPropertyValues);
|
return viewBuilder(propertyValues, sortedPropertyValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectPropertyValuesToState<T>(
|
export function collectPropertyValuesToState<T>(
|
||||||
propertyKey: string,
|
propertyKey: string,
|
||||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||||
objects: React.ListState<T> | React.MapState<T>,
|
objects: React.ListState<T> | React.MapState<T>,
|
||||||
propertyValues: React.ListState<string>
|
propertyValues: React.ListState<string>,
|
||||||
) {
|
) {
|
||||||
const values: string[] = collectObjectValuesForKey(
|
const values: string[] = collectObjectValuesForKey(
|
||||||
propertyKey,
|
propertyKey,
|
||||||
stringEntryObjectConverter,
|
stringEntryObjectConverter,
|
||||||
[...objects.value.values()]
|
[...objects.value.values()],
|
||||||
);
|
);
|
||||||
for (const existingValue of values) {
|
for (const existingValue of values) {
|
||||||
if (propertyValues.value.has(existingValue)) continue;
|
if (propertyValues.value.has(existingValue)) continue;
|
||||||
propertyValues.add(existingValue);
|
propertyValues.add(existingValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const displayedValue of propertyValues.value.values()) {
|
for (const displayedValue of propertyValues.value.values()) {
|
||||||
if (values.includes(displayedValue) == false) {
|
if (values.includes(displayedValue) == false) {
|
||||||
propertyValues.remove(displayedValue);
|
propertyValues.remove(displayedValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSortedPropertyValueState(
|
export function createSortedPropertyValueState(
|
||||||
propertyValues: React.ListState<string>
|
propertyValues: React.ListState<string>,
|
||||||
): React.State<string[]> {
|
): React.State<string[]> {
|
||||||
return React.createProxyState([propertyValues], () =>
|
return React.createProxyState([propertyValues], () =>
|
||||||
[...propertyValues.value.values()].sort(localeCompare)
|
[...propertyValues.value.values()].sort(localeCompare),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPropertyValueIndexState(
|
export function createPropertyValueIndexState(
|
||||||
sortedKeys: React.State<string[]>,
|
sortedKeys: React.State<string[]>,
|
||||||
key: string
|
key: string,
|
||||||
): React.State<number> {
|
): React.State<number> {
|
||||||
return React.createProxyState([sortedKeys], () =>
|
return React.createProxyState([sortedKeys], () =>
|
||||||
sortedKeys.value.indexOf(key)
|
sortedKeys.value.indexOf(key),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardViewModel, {
|
import BoardViewModel, {
|
||||||
BoardPageType,
|
BoardPageType,
|
||||||
} from "../../ViewModel/Pages/boardViewModel";
|
} from "../../ViewModel/Pages/boardViewModel";
|
||||||
|
|
||||||
export function RibbonButton(
|
export function RibbonButton(
|
||||||
label: string,
|
label: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
isSelected: React.State<boolean>,
|
isSelected: React.State<boolean>,
|
||||||
select: () => void
|
select: () => void,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class="ribbon-button"
|
class="ribbon-button"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
toggle:selected={isSelected}
|
toggle:selected={isSelected}
|
||||||
on:click={select}
|
on:click={select}
|
||||||
>
|
>
|
||||||
<span class="icon">{icon}</span>
|
<span class="icon">{icon}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,55 +3,58 @@ import * as React from "bloatless-react";
|
|||||||
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
|
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
|
||||||
|
|
||||||
export function TaskEntry(taskViewModel: TaskViewModel) {
|
export function TaskEntry(taskViewModel: TaskViewModel) {
|
||||||
const details: { [icon: string]: string } = {
|
const details: { [icon: string]: string } = {
|
||||||
description: taskViewModel.description.value || "---",
|
description: taskViewModel.description.value || "---",
|
||||||
priority_high: taskViewModel.priority.value || "---",
|
priority_high: taskViewModel.priority.value || "---",
|
||||||
category: taskViewModel.category.value || "---",
|
category: taskViewModel.category.value || "---",
|
||||||
clock_loader_40: taskViewModel.status.value || "---",
|
clock_loader_40: taskViewModel.status.value || "---",
|
||||||
calendar_month: taskViewModel.date.value || "---",
|
calendar_month: taskViewModel.date.value || "---",
|
||||||
schedule: taskViewModel.time.value || "---",
|
schedule: taskViewModel.time.value || "---",
|
||||||
};
|
};
|
||||||
|
|
||||||
const view = (
|
const view = (
|
||||||
<button
|
<button
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="tile flex-no"
|
class="tile flex-no"
|
||||||
style="user-select: none; -webkit-user-select: none"
|
style="user-select: none; -webkit-user-select: none"
|
||||||
on:click={taskViewModel.open}
|
on:click={taskViewModel.open}
|
||||||
on:dragstart={taskViewModel.dragStart}
|
on:dragstart={taskViewModel.dragStart}
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<b class="ellipsis" subscribe:innerText={taskViewModel.name}></b>
|
|
||||||
<hr></hr>
|
|
||||||
<div
|
|
||||||
class="grid secondary"
|
|
||||||
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
|
|
||||||
>
|
>
|
||||||
{...Object.entries(details).map((entry) => (
|
<div>
|
||||||
<span
|
<b
|
||||||
class="flex-row align-center width-100 flex-no clip"
|
class="ellipsis"
|
||||||
style="gap: 1rem"
|
subscribe:innerText={taskViewModel.name}
|
||||||
>
|
></b>
|
||||||
<span class="icon" style="font-size: 1.1rem">
|
<hr></hr>
|
||||||
{entry[0]}
|
<div
|
||||||
</span>
|
class="grid secondary"
|
||||||
<span class="ellipsis">{entry[1]}</span>
|
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
|
||||||
</span>
|
>
|
||||||
))}
|
{...Object.entries(details).map((entry) => (
|
||||||
</div>
|
<span
|
||||||
</div>
|
class="flex-row align-center width-100 flex-no clip"
|
||||||
</button>
|
style="gap: 1rem"
|
||||||
);
|
>
|
||||||
|
<span class="icon" style="font-size: 1.1rem">
|
||||||
|
{entry[0]}
|
||||||
|
</span>
|
||||||
|
<span class="ellipsis">{entry[1]}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
taskViewModel.index.subscribe((newIndex) => {
|
taskViewModel.index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
|
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
|
||||||
taskViewModel: TaskViewModel
|
taskViewModel: TaskViewModel,
|
||||||
) => {
|
) => {
|
||||||
return TaskEntry(taskViewModel);
|
return TaskEntry(taskViewModel);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
export const StringToTextSpan: React.StateItemConverter<string> = (
|
export const StringToTextSpan: React.StateItemConverter<string> = (
|
||||||
string: string
|
string: string,
|
||||||
) => {
|
) => {
|
||||||
return <span class="ellipsis">{string}</span>
|
return <span class="ellipsis">{string}</span>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,42 +6,47 @@ import { DangerousActionButton } from "../Components/dangerousActionButton";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function BoardSettingsModal(boardViewModel: BoardViewModel) {
|
export function BoardSettingsModal(boardViewModel: BoardViewModel) {
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={boardViewModel.isPresentingSettingsModal}>
|
<div
|
||||||
<div>
|
class="modal"
|
||||||
<main>
|
toggle:open={boardViewModel.isPresentingSettingsModal}
|
||||||
<h2>{translations.chatPage.task.boardSettingsHeadline}</h2>
|
>
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">label</span>
|
|
||||||
<div>
|
<div>
|
||||||
<span>{translations.chatPage.task.boardNameInputLabel}</span>
|
<main>
|
||||||
<input
|
<h2>{translations.chatPage.task.boardSettingsHeadline}</h2>
|
||||||
on:enter={boardViewModel.saveSettings}
|
|
||||||
bind:value={boardViewModel.name}
|
<label class="tile flex-no">
|
||||||
></input>
|
<span class="icon">label</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.boardNameInputLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
on:enter={boardViewModel.saveSettings}
|
||||||
|
bind:value={boardViewModel.name}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
{ColorPicker(boardViewModel.color)}
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<div class="width-input">
|
||||||
|
{DangerousActionButton(
|
||||||
|
translations.chatPage.task.deleteBoardButton,
|
||||||
|
"delete_forever",
|
||||||
|
boardViewModel.deleteBoard,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<button on:click={boardViewModel.hideSettings}>
|
||||||
|
{translations.general.closeButton}
|
||||||
|
<span class="icon">close</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
|
);
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
{ColorPicker(boardViewModel.color)}
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="width-input">
|
|
||||||
{DangerousActionButton(
|
|
||||||
translations.chatPage.task.deleteBoardButton,
|
|
||||||
"delete_forever",
|
|
||||||
boardViewModel.deleteBoard
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<button on:click={boardViewModel.hideSettings}>
|
|
||||||
{translations.general.closeButton}
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,73 +4,95 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function ChatMessageInfoModal(
|
export function ChatMessageInfoModal(
|
||||||
chatMessageViewModel: ChatMessageViewModel
|
chatMessageViewModel: ChatMessageViewModel,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={chatMessageViewModel.isPresentingInfoModal}>
|
<div
|
||||||
<div>
|
class="modal"
|
||||||
<main>
|
toggle:open={chatMessageViewModel.isPresentingInfoModal}
|
||||||
<h2>{translations.chatPage.message.messageInfoHeadline}</h2>
|
>
|
||||||
|
<div>
|
||||||
|
<main>
|
||||||
|
<h2>{translations.chatPage.message.messageInfoHeadline}</h2>
|
||||||
|
|
||||||
<div class="flex-column gap">
|
<div class="flex-column gap">
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
<span class="icon">account_circle</span>
|
<span class="icon">account_circle</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{translations.chatPage.message.sentBy}</span>
|
<span>
|
||||||
<b class="break-word">{chatMessageViewModel.sender}</b>
|
{translations.chatPage.message.sentBy}
|
||||||
</div>
|
</span>
|
||||||
|
<b class="break-word">
|
||||||
|
{chatMessageViewModel.sender}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tile">
|
||||||
|
<span class="icon">schedule</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.message.timeSent}
|
||||||
|
</span>
|
||||||
|
<b class="break-word">
|
||||||
|
{chatMessageViewModel.dateSent}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tile">
|
||||||
|
<span class="icon">forum</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.message.channel}
|
||||||
|
</span>
|
||||||
|
<b class="break-word">
|
||||||
|
{chatMessageViewModel.channel}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tile">
|
||||||
|
<span class="icon">description</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
translations.chatPage.message
|
||||||
|
.messageContent
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<b
|
||||||
|
class="break-word"
|
||||||
|
subscribe:innerText={
|
||||||
|
chatMessageViewModel.body
|
||||||
|
}
|
||||||
|
></b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<div class="flex-column gap">
|
||||||
|
<button on:click={chatMessageViewModel.copyMessage}>
|
||||||
|
{translations.chatPage.message.copyMessageButton}
|
||||||
|
<span class="icon">content_copy</span>
|
||||||
|
</button>
|
||||||
|
<button on:click={chatMessageViewModel.resendMessage}>
|
||||||
|
{translations.chatPage.message.resendMessageButton}
|
||||||
|
<span class="icon">redo</span>
|
||||||
|
</button>
|
||||||
|
<button on:click={chatMessageViewModel.decryptMessage}>
|
||||||
|
{translations.chatPage.message.decryptMessageButton}
|
||||||
|
<span class="icon">key</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<button on:click={chatMessageViewModel.hideInfoModal}>
|
||||||
|
{translations.general.closeButton}
|
||||||
|
<span class="icon">close</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tile">
|
);
|
||||||
<span class="icon">schedule</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.message.timeSent}</span>
|
|
||||||
<b class="break-word">{chatMessageViewModel.dateSent}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tile">
|
|
||||||
<span class="icon">forum</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.message.channel}</span>
|
|
||||||
<b class="break-word">{chatMessageViewModel.channel}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tile">
|
|
||||||
<span class="icon">description</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.message.messageContent}</span>
|
|
||||||
<b
|
|
||||||
class="break-word"
|
|
||||||
subscribe:innerText={chatMessageViewModel.body}
|
|
||||||
></b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-column gap">
|
|
||||||
<button on:click={chatMessageViewModel.copyMessage}>
|
|
||||||
{translations.chatPage.message.copyMessageButton}
|
|
||||||
<span class="icon">content_copy</span>
|
|
||||||
</button>
|
|
||||||
<button on:click={chatMessageViewModel.resendMessage}>
|
|
||||||
{translations.chatPage.message.resendMessageButton}
|
|
||||||
<span class="icon">redo</span>
|
|
||||||
</button>
|
|
||||||
<button on:click={chatMessageViewModel.decryptMessage}>
|
|
||||||
{translations.chatPage.message.decryptMessageButton}
|
|
||||||
<span class="icon">key</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<button on:click={chatMessageViewModel.hideInfoModal}>
|
|
||||||
{translations.general.closeButton}
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,58 +5,62 @@ import { DeletableListItem } from "../Components/deletableListItem";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function ConnectionModal(connectionViewModel: ConnectionViewModel) {
|
export function ConnectionModal(connectionViewModel: ConnectionViewModel) {
|
||||||
const previousAddressConverter: React.StateItemConverter<string> = (
|
const previousAddressConverter: React.StateItemConverter<string> = (
|
||||||
address: string
|
address: string,
|
||||||
) => {
|
) => {
|
||||||
function connnect() {
|
function connnect() {
|
||||||
connectionViewModel.connectToAddress(address);
|
connectionViewModel.connectToAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cannotConnect = React.createProxyState(
|
const cannotConnect = React.createProxyState(
|
||||||
[connectionViewModel.isConnected],
|
[connectionViewModel.isConnected],
|
||||||
() =>
|
() =>
|
||||||
connectionViewModel.isConnected.value == true &&
|
connectionViewModel.isConnected.value == true &&
|
||||||
connectionViewModel.connectionModel.address == address
|
connectionViewModel.connectionModel.address == address,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DeletableListItem(
|
||||||
|
address,
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
on:click={connnect}
|
||||||
|
toggle:disabled={cannotConnect}
|
||||||
|
aria-label={
|
||||||
|
translations.connectionModal.connectButtonAudioLabel
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">link</span>
|
||||||
|
</button>,
|
||||||
|
() => {
|
||||||
|
connectionViewModel.removePreviousAddress(address);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="modal"
|
||||||
|
toggle:open={connectionViewModel.isShowingConnectionModal}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<main>
|
||||||
|
<h2>
|
||||||
|
{translations.connectionModal.connectionModalHeadline}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex-column gap"
|
||||||
|
children:append={[
|
||||||
|
connectionViewModel.previousAddresses,
|
||||||
|
previousAddressConverter,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
</main>
|
||||||
|
<button on:click={connectionViewModel.hideConnectionModal}>
|
||||||
|
{translations.general.closeButton}
|
||||||
|
<span class="icon">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return DeletableListItem(
|
|
||||||
address,
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
on:click={connnect}
|
|
||||||
toggle:disabled={cannotConnect}
|
|
||||||
aria-label={translations.connectionModal.connectButtonAudioLabel}
|
|
||||||
>
|
|
||||||
<span class="icon">link</span>
|
|
||||||
</button>,
|
|
||||||
() => {
|
|
||||||
connectionViewModel.removePreviousAddress(address);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="modal"
|
|
||||||
toggle:open={connectionViewModel.isShowingConnectionModal}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<main>
|
|
||||||
<h2>{translations.connectionModal.connectionModalHeadline}</h2>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:append={[
|
|
||||||
connectionViewModel.previousAddresses,
|
|
||||||
previousAddressConverter,
|
|
||||||
]}
|
|
||||||
></div>
|
|
||||||
</main>
|
|
||||||
<button on:click={connectionViewModel.hideConnectionModal}>
|
|
||||||
{translations.general.closeButton}
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import FileTransferViewModel, {
|
import FileTransferViewModel, {
|
||||||
FileTransferModal,
|
FileTransferModal,
|
||||||
FileTransferOption,
|
FileTransferOption,
|
||||||
} from "../../ViewModel/Global/fileTransferViewModel";
|
} from "../../ViewModel/Global/fileTransferViewModel";
|
||||||
|
|
||||||
import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel";
|
import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel";
|
||||||
@@ -12,406 +12,484 @@ import { reload } from "../utility";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function DataTransferModalWrapper(
|
export function DataTransferModalWrapper(
|
||||||
connectionViewModel: ConnectionViewModel,
|
connectionViewModel: ConnectionViewModel,
|
||||||
fileTransferViewModel: FileTransferViewModel
|
fileTransferViewModel: FileTransferViewModel,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{DirectionSelectionModal(connectionViewModel, fileTransferViewModel)}
|
{DirectionSelectionModal(
|
||||||
{FileSelectionModal(fileTransferViewModel)}
|
connectionViewModel,
|
||||||
{TransferDataDisplayModal(fileTransferViewModel)}
|
fileTransferViewModel,
|
||||||
{TransferDisplayModal(fileTransferViewModel)}
|
)}
|
||||||
{TransferDataInputModal(fileTransferViewModel)}
|
{FileSelectionModal(fileTransferViewModel)}
|
||||||
{DataReceptionModal(fileTransferViewModel)}
|
{TransferDataDisplayModal(fileTransferViewModel)}
|
||||||
</div>
|
{TransferDisplayModal(fileTransferViewModel)}
|
||||||
);
|
{TransferDataInputModal(fileTransferViewModel)}
|
||||||
|
{DataReceptionModal(fileTransferViewModel)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirectionSelectionModal(
|
function DirectionSelectionModal(
|
||||||
connectionViewModel: ConnectionViewModel,
|
connectionViewModel: ConnectionViewModel,
|
||||||
fileTransferViewModel: FileTransferViewModel
|
fileTransferViewModel: FileTransferViewModel,
|
||||||
) {
|
) {
|
||||||
const isPresented = React.createProxyState(
|
const isPresented = React.createProxyState(
|
||||||
[fileTransferViewModel.presentedModal],
|
[fileTransferViewModel.presentedModal],
|
||||||
() =>
|
() =>
|
||||||
fileTransferViewModel.presentedModal.value ==
|
fileTransferViewModel.presentedModal.value ==
|
||||||
FileTransferModal.DirectionSelection
|
FileTransferModal.DirectionSelection,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDisconnected = React.createProxyState(
|
const isDisconnected = React.createProxyState(
|
||||||
[connectionViewModel.isConnected],
|
[connectionViewModel.isConnected],
|
||||||
() => connectionViewModel.isConnected.value == false
|
() => connectionViewModel.isConnected.value == false,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isPresented}>
|
<div class="modal" toggle:open={isPresented}>
|
||||||
<div>
|
<div>
|
||||||
<main>
|
<main>
|
||||||
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
|
<h2>
|
||||||
|
{translations.dataTransferModal.transferDataHeadline}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p class="error" toggle:hidden={connectionViewModel.isConnected}>
|
<p
|
||||||
{translations.dataTransferModal.notConnectedError}
|
class="error"
|
||||||
</p>
|
toggle:hidden={connectionViewModel.isConnected}
|
||||||
|
>
|
||||||
|
{translations.dataTransferModal.notConnectedError}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex-column gap content-margin-bottom"
|
class="flex-column gap content-margin-bottom"
|
||||||
toggle:hidden={isDisconnected}
|
toggle:hidden={isDisconnected}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="tile"
|
class="tile"
|
||||||
on:click={fileTransferViewModel.showFileSelectionModal}
|
on:click={
|
||||||
>
|
fileTransferViewModel.showFileSelectionModal
|
||||||
<span class="icon">upload</span>
|
}
|
||||||
<div>
|
>
|
||||||
<b>{translations.dataTransferModal.fromThisDeviceButton}</b>
|
<span class="icon">upload</span>
|
||||||
</div>
|
<div>
|
||||||
<span class="icon">arrow_forward</span>
|
<b>
|
||||||
</button>
|
{
|
||||||
<button
|
translations.dataTransferModal
|
||||||
class="tile"
|
.fromThisDeviceButton
|
||||||
on:click={fileTransferViewModel.showTransferDataInputModal}
|
}
|
||||||
>
|
</b>
|
||||||
<span class="icon">download</span>
|
</div>
|
||||||
<div>
|
<span class="icon">arrow_forward</span>
|
||||||
<b>{translations.dataTransferModal.toThisDeviceButton}</b>
|
</button>
|
||||||
</div>
|
<button
|
||||||
<span class="icon">arrow_forward</span>
|
class="tile"
|
||||||
</button>
|
on:click={
|
||||||
</div>
|
fileTransferViewModel.showTransferDataInputModal
|
||||||
</main>
|
}
|
||||||
<button on:click={fileTransferViewModel.hideModal}>
|
>
|
||||||
{translations.general.closeButton}
|
<span class="icon">download</span>
|
||||||
<span class="icon">close</span>
|
<div>
|
||||||
</button>
|
<b>
|
||||||
</div>
|
{
|
||||||
</div>
|
translations.dataTransferModal
|
||||||
);
|
.toThisDeviceButton
|
||||||
|
}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<button on:click={fileTransferViewModel.hideModal}>
|
||||||
|
{translations.general.closeButton}
|
||||||
|
<span class="icon">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileSelectionModal(fileTransferViewModel: FileTransferViewModel) {
|
function FileSelectionModal(fileTransferViewModel: FileTransferViewModel) {
|
||||||
// option
|
// option
|
||||||
const OptionConverter: React.StateItemConverter<FileTransferOption> = (
|
const OptionConverter: React.StateItemConverter<FileTransferOption> = (
|
||||||
fileOption: FileTransferOption
|
fileOption: FileTransferOption,
|
||||||
) => {
|
) => {
|
||||||
return OptionEntry(fileOption, fileTransferViewModel);
|
return OptionEntry(fileOption, fileTransferViewModel);
|
||||||
};
|
};
|
||||||
|
|
||||||
function OptionEntry(
|
function OptionEntry(
|
||||||
fileOption: FileTransferOption,
|
fileOption: FileTransferOption,
|
||||||
fileTransferViewModel: FileTransferViewModel
|
fileTransferViewModel: FileTransferViewModel,
|
||||||
) {
|
) {
|
||||||
const isSelected = new React.State(false);
|
const isSelected = new React.State(false);
|
||||||
if (fileTransferViewModel.selectedPaths.value.has(fileOption.path)) {
|
if (fileTransferViewModel.selectedPaths.value.has(fileOption.path)) {
|
||||||
isSelected.value = true;
|
isSelected.value = true;
|
||||||
}
|
}
|
||||||
isSelected.subscribeSilent((isSelected) => {
|
isSelected.subscribeSilent((isSelected) => {
|
||||||
if (isSelected == true) {
|
if (isSelected == true) {
|
||||||
fileTransferViewModel.selectedPaths.add(fileOption.path);
|
fileTransferViewModel.selectedPaths.add(fileOption.path);
|
||||||
} else {
|
} else {
|
||||||
fileTransferViewModel.selectedPaths.remove(fileOption.path);
|
fileTransferViewModel.selectedPaths.remove(fileOption.path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
function toggle() {
|
function toggle() {
|
||||||
isSelected.value = !isSelected.value;
|
isSelected.value = !isSelected.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button class="tile" toggle:selected={isSelected} on:click={toggle}>
|
||||||
|
<div>
|
||||||
|
<b class="ellipsis">{fileOption.label}</b>
|
||||||
|
<span class="secondary ellipsis">
|
||||||
|
{StorageModel.pathComponentsToString(
|
||||||
|
...fileOption.path,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// state
|
||||||
|
const isPresented = React.createProxyState(
|
||||||
|
[fileTransferViewModel.presentedModal],
|
||||||
|
() =>
|
||||||
|
fileTransferViewModel.presentedModal.value ==
|
||||||
|
FileTransferModal.FileSelection,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button class="tile" toggle:selected={isSelected} on:click={toggle}>
|
<div class="modal" toggle:open={isPresented}>
|
||||||
<div>
|
<div>
|
||||||
<b class="ellipsis">{fileOption.label}</b>
|
<main>
|
||||||
<span class="secondary ellipsis">
|
<h2>
|
||||||
{StorageModel.pathComponentsToString(...fileOption.path)}
|
{translations.dataTransferModal.transferDataHeadline}
|
||||||
</span>
|
</h2>
|
||||||
|
<span class="secondary">
|
||||||
|
{translations.dataTransferModal.selectionDescription}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<h3>{translations.dataTransferModal.generalHeadline}</h3>
|
||||||
|
<div
|
||||||
|
class="flex-column gap content-margin-bottom"
|
||||||
|
children:append={[
|
||||||
|
fileTransferViewModel.generalFileOptions,
|
||||||
|
OptionConverter,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<h3>{translations.dataTransferModal.chatsHeadline}</h3>
|
||||||
|
<div
|
||||||
|
class="flex-column gap"
|
||||||
|
children:append={[
|
||||||
|
fileTransferViewModel.chatFileOptions,
|
||||||
|
OptionConverter,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
</main>
|
||||||
|
<div class="flex-row width-100">
|
||||||
|
<button
|
||||||
|
class="flex"
|
||||||
|
on:click={
|
||||||
|
fileTransferViewModel.showDirectionSelectionModal
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translations.general.backButton}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary flex"
|
||||||
|
on:click={fileTransferViewModel.showTransferDataModal}
|
||||||
|
toggle:disabled={
|
||||||
|
fileTransferViewModel.hasNoPathsSelected
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translations.general.continueButton}
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// state
|
|
||||||
const isPresented = React.createProxyState(
|
|
||||||
[fileTransferViewModel.presentedModal],
|
|
||||||
() =>
|
|
||||||
fileTransferViewModel.presentedModal.value ==
|
|
||||||
FileTransferModal.FileSelection
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="modal" toggle:open={isPresented}>
|
|
||||||
<div>
|
|
||||||
<main>
|
|
||||||
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
|
|
||||||
<span class="secondary">
|
|
||||||
{translations.dataTransferModal.selectionDescription}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<h3>{translations.dataTransferModal.generalHeadline}</h3>
|
|
||||||
<div
|
|
||||||
class="flex-column gap content-margin-bottom"
|
|
||||||
children:append={[
|
|
||||||
fileTransferViewModel.generalFileOptions,
|
|
||||||
OptionConverter,
|
|
||||||
]}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<h3>{translations.dataTransferModal.chatsHeadline}</h3>
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:append={[
|
|
||||||
fileTransferViewModel.chatFileOptions,
|
|
||||||
OptionConverter,
|
|
||||||
]}
|
|
||||||
></div>
|
|
||||||
</main>
|
|
||||||
<div class="flex-row width-100">
|
|
||||||
<button
|
|
||||||
class="flex"
|
|
||||||
on:click={fileTransferViewModel.showDirectionSelectionModal}
|
|
||||||
>
|
|
||||||
{translations.general.backButton}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="primary flex"
|
|
||||||
on:click={fileTransferViewModel.showTransferDataModal}
|
|
||||||
toggle:disabled={fileTransferViewModel.hasNoPathsSelected}
|
|
||||||
>
|
|
||||||
{translations.general.continueButton}
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransferDataDisplayModal(
|
function TransferDataDisplayModal(
|
||||||
fileTransferViewModel: FileTransferViewModel
|
fileTransferViewModel: FileTransferViewModel,
|
||||||
) {
|
) {
|
||||||
const isPresented = React.createProxyState(
|
const isPresented = React.createProxyState(
|
||||||
[fileTransferViewModel.presentedModal],
|
[fileTransferViewModel.presentedModal],
|
||||||
() =>
|
() =>
|
||||||
fileTransferViewModel.presentedModal.value ==
|
fileTransferViewModel.presentedModal.value ==
|
||||||
FileTransferModal.TransferDataDisplay
|
FileTransferModal.TransferDataDisplay,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isPresented}>
|
<div class="modal" toggle:open={isPresented}>
|
||||||
<div>
|
<div>
|
||||||
<main>
|
<main>
|
||||||
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
|
<h2>
|
||||||
|
{translations.dataTransferModal.transferDataHeadline}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.dataTransferModal.dataEntryDescription}
|
{translations.dataTransferModal.dataEntryDescription}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div class="flex-column gap content-margin-bottom">
|
<div class="flex-column gap content-margin-bottom">
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
<span class="icon">forum</span>
|
<span class="icon">forum</span>
|
||||||
<div>
|
<div>
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.dataTransferModal.transferChannelHeadline}
|
{
|
||||||
</span>
|
translations.dataTransferModal
|
||||||
<b
|
.transferChannelHeadline
|
||||||
subscribe:innerText={fileTransferViewModel.transferChannel}
|
}
|
||||||
></b>
|
</span>
|
||||||
</div>
|
<b
|
||||||
|
subscribe:innerText={
|
||||||
|
fileTransferViewModel.transferChannel
|
||||||
|
}
|
||||||
|
></b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<span class="icon">key</span>
|
||||||
|
<div>
|
||||||
|
<span class="secondary">
|
||||||
|
{
|
||||||
|
translations.dataTransferModal
|
||||||
|
.transferKeyHeadline
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<b
|
||||||
|
subscribe:innerText={
|
||||||
|
fileTransferViewModel.transferKey
|
||||||
|
}
|
||||||
|
></b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<div class="flex-row width-100">
|
||||||
|
<button
|
||||||
|
class="flex"
|
||||||
|
on:click={fileTransferViewModel.showFileSelectionModal}
|
||||||
|
>
|
||||||
|
{translations.general.backButton}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary flex"
|
||||||
|
on:click={fileTransferViewModel.initiateTransfer}
|
||||||
|
>
|
||||||
|
{translations.dataTransferModal.sendButton}
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile">
|
|
||||||
<span class="icon">key</span>
|
|
||||||
<div>
|
|
||||||
<span class="secondary">
|
|
||||||
{translations.dataTransferModal.transferKeyHeadline}
|
|
||||||
</span>
|
|
||||||
<b subscribe:innerText={fileTransferViewModel.transferKey}></b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<div class="flex-row width-100">
|
|
||||||
<button
|
|
||||||
class="flex"
|
|
||||||
on:click={fileTransferViewModel.showFileSelectionModal}
|
|
||||||
>
|
|
||||||
{translations.general.backButton}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="primary flex"
|
|
||||||
on:click={fileTransferViewModel.initiateTransfer}
|
|
||||||
>
|
|
||||||
{translations.dataTransferModal.sendButton}
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransferDisplayModal(fileTransferViewModel: FileTransferViewModel) {
|
function TransferDisplayModal(fileTransferViewModel: FileTransferViewModel) {
|
||||||
const isPresented = React.createProxyState(
|
const isPresented = React.createProxyState(
|
||||||
[fileTransferViewModel.presentedModal],
|
[fileTransferViewModel.presentedModal],
|
||||||
() =>
|
() =>
|
||||||
fileTransferViewModel.presentedModal.value ==
|
fileTransferViewModel.presentedModal.value ==
|
||||||
FileTransferModal.TransferDisplay
|
FileTransferModal.TransferDisplay,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isPresented}>
|
<div class="modal" toggle:open={isPresented}>
|
||||||
<div>
|
<div>
|
||||||
<main>
|
<main>
|
||||||
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
|
<h2>
|
||||||
|
{translations.dataTransferModal.transferDataHeadline}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="secondary"
|
class="secondary"
|
||||||
subscribe:innerText={fileTransferViewModel.filesSentText}
|
subscribe:innerText={
|
||||||
></p>
|
fileTransferViewModel.filesSentText
|
||||||
|
}
|
||||||
|
></p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="secondary"
|
class="secondary"
|
||||||
toggle:hidden={fileTransferViewModel.didNotFinishSending}
|
toggle:hidden={
|
||||||
>
|
fileTransferViewModel.didNotFinishSending
|
||||||
{translations.dataTransferModal.allFilesSent}
|
}
|
||||||
</p>
|
>
|
||||||
|
{translations.dataTransferModal.allFilesSent}
|
||||||
|
</p>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="tile flex-column align-start"
|
class="tile flex-column align-start"
|
||||||
children:append={[
|
children:append={[
|
||||||
fileTransferViewModel.filePathsSent,
|
fileTransferViewModel.filePathsSent,
|
||||||
StringToTextSpan,
|
StringToTextSpan,
|
||||||
]}
|
]}
|
||||||
></div>
|
></div>
|
||||||
</main>
|
</main>
|
||||||
<div class="flex-row width-100">
|
<div class="flex-row width-100">
|
||||||
<button
|
<button
|
||||||
class="flex"
|
class="flex"
|
||||||
on:click={fileTransferViewModel.initiateTransfer}
|
on:click={fileTransferViewModel.initiateTransfer}
|
||||||
>
|
>
|
||||||
{translations.dataTransferModal.sendAgainButton}
|
{translations.dataTransferModal.sendAgainButton}
|
||||||
<span class="icon">restart_alt</span>
|
<span class="icon">restart_alt</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex"
|
class="flex"
|
||||||
on:click={fileTransferViewModel.hideModal}
|
on:click={fileTransferViewModel.hideModal}
|
||||||
toggle:disabled={fileTransferViewModel.didNotFinishSending}
|
toggle:disabled={
|
||||||
>
|
fileTransferViewModel.didNotFinishSending
|
||||||
{translations.general.closeButton}
|
}
|
||||||
<span class="icon">close</span>
|
>
|
||||||
</button>
|
{translations.general.closeButton}
|
||||||
|
<span class="icon">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransferDataInputModal(fileTransferViewModel: FileTransferViewModel) {
|
function TransferDataInputModal(fileTransferViewModel: FileTransferViewModel) {
|
||||||
const isPresented = React.createProxyState(
|
const isPresented = React.createProxyState(
|
||||||
[fileTransferViewModel.presentedModal],
|
[fileTransferViewModel.presentedModal],
|
||||||
() =>
|
() =>
|
||||||
fileTransferViewModel.presentedModal.value ==
|
fileTransferViewModel.presentedModal.value ==
|
||||||
FileTransferModal.TransferDataInput
|
FileTransferModal.TransferDataInput,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isPresented}>
|
<div class="modal" toggle:open={isPresented}>
|
||||||
<div>
|
<div>
|
||||||
<main>
|
<main>
|
||||||
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
|
<h2>
|
||||||
|
{translations.dataTransferModal.transferDataHeadline}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.dataTransferModal.dataEntryInputDescription}
|
{
|
||||||
</span>
|
translations.dataTransferModal
|
||||||
|
.dataEntryInputDescription
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div class="flex-column gap content-margin-bottom">
|
<div class="flex-column gap content-margin-bottom">
|
||||||
<label class="tile">
|
<label class="tile">
|
||||||
<span class="icon">forum</span>
|
<span class="icon">forum</span>
|
||||||
<div>
|
<div>
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{translations.dataTransferModal.transferChannelHeadline}
|
{
|
||||||
</span>
|
translations.dataTransferModal
|
||||||
<input
|
.transferChannelHeadline
|
||||||
bind:value={fileTransferViewModel.receivingTransferChannel}
|
}
|
||||||
></input>
|
</span>
|
||||||
</div>
|
<input
|
||||||
</label>
|
bind:value={
|
||||||
<label class="tile">
|
fileTransferViewModel.receivingTransferChannel
|
||||||
<span class="icon">key</span>
|
}
|
||||||
<div>
|
></input>
|
||||||
<span class="secondary">
|
</div>
|
||||||
{translations.dataTransferModal.transferKeyHeadline}
|
</label>
|
||||||
</span>
|
<label class="tile">
|
||||||
<input
|
<span class="icon">key</span>
|
||||||
bind:value={fileTransferViewModel.receivingTransferKey}
|
<div>
|
||||||
></input>
|
<span class="secondary">
|
||||||
</div>
|
{
|
||||||
</label>
|
translations.dataTransferModal
|
||||||
</div>
|
.transferKeyHeadline
|
||||||
</main>
|
}
|
||||||
<div class="flex-row width-100">
|
</span>
|
||||||
<button
|
<input
|
||||||
class="flex"
|
bind:value={
|
||||||
on:click={fileTransferViewModel.showDirectionSelectionModal}
|
fileTransferViewModel.receivingTransferKey
|
||||||
>
|
}
|
||||||
{translations.general.backButton}
|
></input>
|
||||||
</button>
|
</div>
|
||||||
<button
|
</label>
|
||||||
class="primary flex"
|
</div>
|
||||||
on:click={fileTransferViewModel.prepareReceivingData}
|
</main>
|
||||||
toggle:disabled={fileTransferViewModel.cannotPrepareToReceive}
|
<div class="flex-row width-100">
|
||||||
>
|
<button
|
||||||
{translations.general.continueButton}
|
class="flex"
|
||||||
<span class="icon">arrow_forward</span>
|
on:click={
|
||||||
</button>
|
fileTransferViewModel.showDirectionSelectionModal
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translations.general.backButton}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary flex"
|
||||||
|
on:click={fileTransferViewModel.prepareReceivingData}
|
||||||
|
toggle:disabled={
|
||||||
|
fileTransferViewModel.cannotPrepareToReceive
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translations.general.continueButton}
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataReceptionModal(fileTransferViewModel: FileTransferViewModel) {
|
function DataReceptionModal(fileTransferViewModel: FileTransferViewModel) {
|
||||||
const isPresented = React.createProxyState(
|
const isPresented = React.createProxyState(
|
||||||
[fileTransferViewModel.presentedModal],
|
[fileTransferViewModel.presentedModal],
|
||||||
() =>
|
() =>
|
||||||
fileTransferViewModel.presentedModal.value ==
|
fileTransferViewModel.presentedModal.value ==
|
||||||
FileTransferModal.ReceptionDisplay
|
FileTransferModal.ReceptionDisplay,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isPresented}>
|
<div class="modal" toggle:open={isPresented}>
|
||||||
<div>
|
<div>
|
||||||
<main>
|
<main>
|
||||||
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
|
<h2>
|
||||||
|
{translations.dataTransferModal.transferDataHeadline}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
{translations.dataTransferModal.readyToReceiveDescription}
|
{
|
||||||
</p>
|
translations.dataTransferModal
|
||||||
|
.readyToReceiveDescription
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="secondary"
|
class="secondary"
|
||||||
subscribe:innerText={fileTransferViewModel.filesReceivedText}
|
subscribe:innerText={
|
||||||
></p>
|
fileTransferViewModel.filesReceivedText
|
||||||
|
}
|
||||||
|
></p>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="tile flex-column align-start"
|
class="tile flex-column align-start"
|
||||||
children:append={[
|
children:append={[
|
||||||
fileTransferViewModel.filePathsReceived,
|
fileTransferViewModel.filePathsReceived,
|
||||||
StringToTextSpan,
|
StringToTextSpan,
|
||||||
]}
|
]}
|
||||||
></div>
|
></div>
|
||||||
</main>
|
</main>
|
||||||
<button on:click={reload}>
|
<button on:click={reload}>
|
||||||
{translations.general.reloadAppButton}
|
{translations.general.reloadAppButton}
|
||||||
<span class="icon">refresh</span>
|
<span class="icon">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,57 +6,65 @@ import { translations } from "../translations";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export function SearchModal<T>(
|
export function SearchModal<T>(
|
||||||
searchViewModel: SearchViewModel<T>,
|
searchViewModel: SearchViewModel<T>,
|
||||||
headline: string,
|
headline: string,
|
||||||
converter: React.StateItemConverter<T>,
|
converter: React.StateItemConverter<T>,
|
||||||
isOpen: React.State<boolean>
|
isOpen: React.State<boolean>,
|
||||||
) {
|
) {
|
||||||
function close() {
|
function close() {
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestionId = v4();
|
const suggestionId = v4();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isOpen}>
|
<div class="modal" toggle:open={isOpen}>
|
||||||
<div style="max-width: 64rem">
|
<div style="max-width: 64rem">
|
||||||
<main>
|
<main>
|
||||||
<h2>{headline}</h2>
|
<h2>{headline}</h2>
|
||||||
<div class="flex-row width-input">
|
<div class="flex-row width-input">
|
||||||
<input
|
<input
|
||||||
placeholder={translations.general.searchLabel}
|
placeholder={translations.general.searchLabel}
|
||||||
bind:value={searchViewModel.searchInput}
|
bind:value={searchViewModel.searchInput}
|
||||||
on:enter={searchViewModel.applySearch}
|
on:enter={searchViewModel.applySearch}
|
||||||
list={suggestionId}
|
list={suggestionId}
|
||||||
></input>
|
></input>
|
||||||
<datalist
|
<datalist
|
||||||
hidden
|
hidden
|
||||||
id={suggestionId}
|
id={suggestionId}
|
||||||
children:append={[searchViewModel.suggestions, StringToOption]}
|
children:append={[
|
||||||
></datalist>
|
searchViewModel.suggestions,
|
||||||
<button
|
StringToOption,
|
||||||
class="primary"
|
]}
|
||||||
aria-label={translations.general.searchButtonAudioLabel}
|
></datalist>
|
||||||
on:click={searchViewModel.applySearch}
|
<button
|
||||||
toggle:disabled={searchViewModel.cannotApplySearch}
|
class="primary"
|
||||||
>
|
aria-label={
|
||||||
<span class="icon">search</span>
|
translations.general.searchButtonAudioLabel
|
||||||
</button>
|
}
|
||||||
</div>
|
on:click={searchViewModel.applySearch}
|
||||||
|
toggle:disabled={searchViewModel.cannotApplySearch}
|
||||||
|
>
|
||||||
|
<span class="icon">search</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="grid gap"
|
class="grid gap"
|
||||||
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
|
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
|
||||||
children:append={[searchViewModel.matchingObjects, converter]}
|
children:append={[
|
||||||
></div>
|
searchViewModel.matchingObjects,
|
||||||
</main>
|
converter,
|
||||||
<button on:click={close}>
|
]}
|
||||||
{translations.general.closeButton}
|
></div>
|
||||||
<span class="icon">close</span>
|
</main>
|
||||||
</button>
|
<button on:click={close}>
|
||||||
</div>
|
{translations.general.closeButton}
|
||||||
</div>
|
<span class="icon">close</span>
|
||||||
);
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
|||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function StorageModal(storageViewModel: StorageViewModel) {
|
export function StorageModal(storageViewModel: StorageViewModel) {
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
|
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
|
||||||
<div style="max-width: 64rem">
|
<div style="max-width: 64rem">
|
||||||
<main class="padding-0">{FileBrowser(storageViewModel)}</main>
|
<main class="padding-0">{FileBrowser(storageViewModel)}</main>
|
||||||
<button on:click={storageViewModel.hideStorageModal}>
|
<button on:click={storageViewModel.hideStorageModal}>
|
||||||
{translations.general.closeButton}
|
{translations.general.closeButton}
|
||||||
<span class="icon">close</span>
|
<span class="icon">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Entry,
|
Entry,
|
||||||
EntryToOption,
|
EntryToOption,
|
||||||
Option,
|
Option,
|
||||||
StringToOption,
|
StringToOption,
|
||||||
VersionIdToOption,
|
VersionIdToOption,
|
||||||
} from "../Components/option";
|
} from "../Components/option";
|
||||||
|
|
||||||
import { DangerousActionButton } from "../Components/dangerousActionButton";
|
import { DangerousActionButton } from "../Components/dangerousActionButton";
|
||||||
@@ -14,156 +14,194 @@ import { translations } from "../translations";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export function TaskSettingsModal(taskViewModel: TaskViewModel) {
|
export function TaskSettingsModal(taskViewModel: TaskViewModel) {
|
||||||
const categorySuggestionId = v4();
|
const categorySuggestionId = v4();
|
||||||
const statusSuggestionId = v4();
|
const statusSuggestionId = v4();
|
||||||
|
|
||||||
const BoardOptionConverter: React.StateItemConverter<Entry> = (
|
const BoardOptionConverter: React.StateItemConverter<Entry> = (
|
||||||
entry: Entry
|
entry: Entry,
|
||||||
) => {
|
) => {
|
||||||
const isSelected = entry[0] == taskViewModel.task.boardId;
|
const isSelected = entry[0] == taskViewModel.task.boardId;
|
||||||
return Option(entry[1], entry[0], isSelected);
|
return Option(entry[1], entry[0], isSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" open>
|
<div class="modal" open>
|
||||||
<div>
|
|
||||||
<main>
|
|
||||||
<h2>{translations.chatPage.task.taskSettingsHeadline}</h2>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">history</span>
|
|
||||||
<div>
|
<div>
|
||||||
<span>{translations.general.fileVersionLabel}</span>
|
<main>
|
||||||
<select
|
<h2>{translations.chatPage.task.taskSettingsHeadline}</h2>
|
||||||
bind:value={taskViewModel.selectedVersionId}
|
|
||||||
children:append={[taskViewModel.versionIds, VersionIdToOption]}
|
<label class="tile flex-no">
|
||||||
></select>
|
<span class="icon">history</span>
|
||||||
<span class="icon">arrow_drop_down</span>
|
<div>
|
||||||
|
<span>{translations.general.fileVersionLabel}</span>
|
||||||
|
<select
|
||||||
|
bind:value={taskViewModel.selectedVersionId}
|
||||||
|
children:append={[
|
||||||
|
taskViewModel.versionIds,
|
||||||
|
VersionIdToOption,
|
||||||
|
]}
|
||||||
|
></select>
|
||||||
|
<span class="icon">arrow_drop_down</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">label</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskNameLabel}
|
||||||
|
</span>
|
||||||
|
<input bind:value={taskViewModel.name}></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">category</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskBoardLabel}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
bind:value={taskViewModel.boardId}
|
||||||
|
children:append={[
|
||||||
|
taskViewModel.chatViewModel
|
||||||
|
.taskBoardSuggestions,
|
||||||
|
BoardOptionConverter,
|
||||||
|
]}
|
||||||
|
></select>
|
||||||
|
<span class="icon">arrow_drop_down</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">description</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
translations.chatPage.task
|
||||||
|
.taskDescriptionLabel
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
rows="10"
|
||||||
|
bind:value={taskViewModel.description}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">category</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskCategoryLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
bind:value={taskViewModel.category}
|
||||||
|
list={categorySuggestionId}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<datalist
|
||||||
|
hidden
|
||||||
|
id={categorySuggestionId}
|
||||||
|
children:append={[
|
||||||
|
taskViewModel.coreViewModel.taskCategorySuggestions,
|
||||||
|
StringToOption,
|
||||||
|
]}
|
||||||
|
></datalist>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">clock_loader_40</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskStatusLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
bind:value={taskViewModel.status}
|
||||||
|
list={statusSuggestionId}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<datalist
|
||||||
|
hidden
|
||||||
|
id={statusSuggestionId}
|
||||||
|
children:append={[
|
||||||
|
taskViewModel.coreViewModel.taskStatusSuggestions,
|
||||||
|
StringToOption,
|
||||||
|
]}
|
||||||
|
></datalist>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">priority_high</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskPriorityLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
bind:value={taskViewModel.priority}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">calendar_month</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskDateLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
bind:value={taskViewModel.date}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">schedule</span>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{translations.chatPage.task.taskTimeLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
bind:value={taskViewModel.time}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<div class="width-input">
|
||||||
|
{DangerousActionButton(
|
||||||
|
translations.chatPage.task.deleteTaskButton,
|
||||||
|
"delete_forever",
|
||||||
|
taskViewModel.deleteTask,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<div class="flex-row width-100">
|
||||||
|
<button
|
||||||
|
class="flex"
|
||||||
|
on:click={taskViewModel.closeAndDiscard}
|
||||||
|
>
|
||||||
|
{translations.general.closeButton}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex primary"
|
||||||
|
on:click={taskViewModel.closeAndSave}
|
||||||
|
>
|
||||||
|
{translations.general.saveButton}
|
||||||
|
<span class="icon">save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">label</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskNameLabel}</span>
|
|
||||||
<input bind:value={taskViewModel.name}></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">category</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskBoardLabel}</span>
|
|
||||||
<select
|
|
||||||
bind:value={taskViewModel.boardId}
|
|
||||||
children:append={[
|
|
||||||
taskViewModel.chatViewModel.taskBoardSuggestions,
|
|
||||||
BoardOptionConverter,
|
|
||||||
]}
|
|
||||||
></select>
|
|
||||||
<span class="icon">arrow_drop_down</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">description</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskDescriptionLabel}</span>
|
|
||||||
<textarea
|
|
||||||
rows="10"
|
|
||||||
bind:value={taskViewModel.description}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">category</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskCategoryLabel}</span>
|
|
||||||
<input
|
|
||||||
bind:value={taskViewModel.category}
|
|
||||||
list={categorySuggestionId}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<datalist
|
|
||||||
hidden
|
|
||||||
id={categorySuggestionId}
|
|
||||||
children:append={[
|
|
||||||
taskViewModel.coreViewModel.taskCategorySuggestions,
|
|
||||||
StringToOption,
|
|
||||||
]}
|
|
||||||
></datalist>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">clock_loader_40</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskStatusLabel}</span>
|
|
||||||
<input
|
|
||||||
bind:value={taskViewModel.status}
|
|
||||||
list={statusSuggestionId}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<datalist
|
|
||||||
hidden
|
|
||||||
id={statusSuggestionId}
|
|
||||||
children:append={[
|
|
||||||
taskViewModel.coreViewModel.taskStatusSuggestions,
|
|
||||||
StringToOption,
|
|
||||||
]}
|
|
||||||
></datalist>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">priority_high</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskPriorityLabel}</span>
|
|
||||||
<input type="number" bind:value={taskViewModel.priority}></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">calendar_month</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskDateLabel}</span>
|
|
||||||
<input type="date" bind:value={taskViewModel.date}></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="tile flex-no">
|
|
||||||
<span class="icon">schedule</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.chatPage.task.taskTimeLabel}</span>
|
|
||||||
<input type="time" bind:value={taskViewModel.time}></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="width-input">
|
|
||||||
{DangerousActionButton(
|
|
||||||
translations.chatPage.task.deleteTaskButton,
|
|
||||||
"delete_forever",
|
|
||||||
taskViewModel.deleteTask
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<div class="flex-row width-100">
|
|
||||||
<button class="flex" on:click={taskViewModel.closeAndDiscard}>
|
|
||||||
{translations.general.closeButton}
|
|
||||||
</button>
|
|
||||||
<button class="flex primary" on:click={taskViewModel.closeAndSave}>
|
|
||||||
{translations.general.saveButton}
|
|
||||||
<span class="icon">save</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-100
@@ -1,29 +1,29 @@
|
|||||||
/* show/hide */
|
/* show/hide */
|
||||||
#chat-page-wrapper {
|
#chat-page-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[showing-chat] #home-page {
|
body[showing-chat] #home-page {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[showing-chat] #chat-page-wrapper {
|
body[showing-chat] #chat-page-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
|
||||||
background-color: var(--background-glass);
|
background-color: var(--background-glass);
|
||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* layout */
|
/* layout */
|
||||||
#chat-page {
|
#chat-page {
|
||||||
--ribbon-size: var(--control-height);
|
--ribbon-size: var(--control-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div,
|
#chat-page > div,
|
||||||
@@ -35,179 +35,179 @@ body[showing-chat] #chat-page-wrapper {
|
|||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar,
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span,
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div {
|
#chat-page > div {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main {
|
#chat-page > div > #main {
|
||||||
width: calc(100% - var(--ribbon-size) - 1px);
|
width: calc(100% - var(--ribbon-size) - 1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page #main > div,
|
#chat-page #main > div,
|
||||||
#chat-page > div > #main > div > .pane-wrapper,
|
#chat-page > div > #main > div > .pane-wrapper,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane,
|
#chat-page > div > #main > div > .pane-wrapper > .pane,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper,
|
#chat-page > div > #main > div > .pane-wrapper,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane {
|
#chat-page > div > #main > div > .pane-wrapper > .pane {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
|
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side),
|
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side),
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side) .content {
|
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side) .content {
|
||||||
animation: slide-up var(--animation-duration);
|
animation: slide-up var(--animation-duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
#chat-page > div > #main > div > .pane-wrapper {
|
#chat-page > div > #main > div > .pane-wrapper {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper.side {
|
#chat-page > div > #main > div > .pane-wrapper.side {
|
||||||
width: 18rem;
|
width: 18rem;
|
||||||
min-width: 18rem;
|
min-width: 18rem;
|
||||||
max-width: 18rem;
|
max-width: 18rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page #ribbon,
|
#chat-page #ribbon,
|
||||||
#chat-page #main,
|
#chat-page #main,
|
||||||
#chat-page #main .content {
|
#chat-page #main .content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ribbon, toolbar */
|
/* ribbon, toolbar */
|
||||||
#chat-page > div > #ribbon,
|
#chat-page > div > #ribbon,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon {
|
#chat-page > div > #ribbon {
|
||||||
width: calc(var(--ribbon-size) + 1px);
|
width: calc(var(--ribbon-size) + 1px);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
padding-bottom: var(--safe-area-bottom);
|
padding-bottom: var(--safe-area-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon > span,
|
#chat-page > div > #ribbon > span,
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> div
|
> div
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
> span.ribbon {
|
> span.ribbon {
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
padding: var(--gap-between-tiles);
|
padding: var(--gap-between-tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon > span {
|
#chat-page > div > #ribbon > span {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon button,
|
#chat-page > div > #ribbon button,
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
||||||
width: var(--control-height);
|
width: var(--control-height);
|
||||||
max-width: var(--control-height);
|
max-width: var(--control-height);
|
||||||
height: var(--control-height);
|
height: var(--control-height);
|
||||||
min-height: var(--control-height);
|
min-height: var(--control-height);
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon button.ribbon-button,
|
#chat-page > div > #ribbon button.ribbon-button,
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> div
|
> div
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
button.ribbon-button {
|
button.ribbon-button {
|
||||||
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
||||||
width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
||||||
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
||||||
|
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
|
||||||
|
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon button > .icon,
|
#chat-page > div > #ribbon button > .icon,
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> div
|
> div
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
button
|
button
|
||||||
> .icon {
|
> .icon {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #ribbon > span {
|
#chat-page > div > #ribbon > span {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
|
||||||
height: var(--ribbon-size);
|
height: var(--ribbon-size);
|
||||||
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span.title {
|
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span.title {
|
||||||
padding-left: var(--content-padding);
|
padding-left: var(--content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-page
|
#chat-page
|
||||||
> div
|
> div
|
||||||
> #main
|
> #main
|
||||||
> div
|
> div
|
||||||
> .pane-wrapper
|
> .pane-wrapper
|
||||||
> .pane
|
> .pane
|
||||||
> .toolbar
|
> .toolbar
|
||||||
:is(input, select, button) {
|
:is(input, select, button) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-66
@@ -12,73 +12,73 @@ import { TaskPage } from "./ChatPages/taskPage";
|
|||||||
import { translations } from "./translations";
|
import { translations } from "./translations";
|
||||||
|
|
||||||
export function ChatPage(chatViewModel: ChatViewModel) {
|
export function ChatPage(chatViewModel: ChatViewModel) {
|
||||||
const mainContent = React.createProxyState(
|
const mainContent = React.createProxyState(
|
||||||
[chatViewModel.selectedPage],
|
[chatViewModel.selectedPage],
|
||||||
() => {
|
() => {
|
||||||
chatViewModel.closeSubPages();
|
chatViewModel.closeSubPages();
|
||||||
|
|
||||||
switch (chatViewModel.selectedPage.value) {
|
switch (chatViewModel.selectedPage.value) {
|
||||||
case ChatPageType.Settings: {
|
case ChatPageType.Settings: {
|
||||||
return SettingsPage(chatViewModel.settingsPageViewModel);
|
return SettingsPage(chatViewModel.settingsPageViewModel);
|
||||||
}
|
}
|
||||||
case ChatPageType.Tasks: {
|
case ChatPageType.Tasks: {
|
||||||
return TaskPage(chatViewModel.taskPageViewModel);
|
return TaskPage(chatViewModel.taskPageViewModel);
|
||||||
}
|
}
|
||||||
case ChatPageType.Calendar: {
|
case ChatPageType.Calendar: {
|
||||||
return CalendarPage(chatViewModel.calendarViewModel)
|
return CalendarPage(chatViewModel.calendarViewModel);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return MessagePage(chatViewModel.messagePageViewModel);
|
return MessagePage(chatViewModel.messagePageViewModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
id="chat-page"
|
id="chat-page"
|
||||||
set:color={chatViewModel.displayedColor}
|
set:color={chatViewModel.displayedColor}
|
||||||
class="subtle-background"
|
class="subtle-background"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div id="ribbon">
|
<div id="ribbon">
|
||||||
<button
|
<button
|
||||||
class="ghost"
|
class="ghost"
|
||||||
aria-label={translations.chatPage.closeChatAudioLabe}
|
aria-label={translations.chatPage.closeChatAudioLabe}
|
||||||
on:click={chatViewModel.close}
|
on:click={chatViewModel.close}
|
||||||
>
|
>
|
||||||
<span class="icon">close</span>
|
<span class="icon">close</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{ChatViewToggleButton(
|
{ChatViewToggleButton(
|
||||||
translations.chatPage.pages.calendar,
|
translations.chatPage.pages.calendar,
|
||||||
"calendar_month",
|
"calendar_month",
|
||||||
ChatPageType.Calendar,
|
ChatPageType.Calendar,
|
||||||
chatViewModel
|
chatViewModel,
|
||||||
)}
|
)}
|
||||||
{ChatViewToggleButton(
|
{ChatViewToggleButton(
|
||||||
translations.chatPage.pages.tasks,
|
translations.chatPage.pages.tasks,
|
||||||
"task_alt",
|
"task_alt",
|
||||||
ChatPageType.Tasks,
|
ChatPageType.Tasks,
|
||||||
chatViewModel
|
chatViewModel,
|
||||||
)}
|
)}
|
||||||
{ChatViewToggleButton(
|
{ChatViewToggleButton(
|
||||||
translations.chatPage.pages.messages,
|
translations.chatPage.pages.messages,
|
||||||
"forum",
|
"forum",
|
||||||
ChatPageType.Messages,
|
ChatPageType.Messages,
|
||||||
chatViewModel
|
chatViewModel,
|
||||||
)}
|
)}
|
||||||
{ChatViewToggleButton(
|
{ChatViewToggleButton(
|
||||||
translations.chatPage.pages.settings,
|
translations.chatPage.pages.settings,
|
||||||
"settings",
|
"settings",
|
||||||
ChatPageType.Settings,
|
ChatPageType.Settings,
|
||||||
chatViewModel
|
chatViewModel,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="main" children:set={mainContent}></div>
|
<div id="main" children:set={mainContent}></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,16 @@ import ChatListViewModel from "../ViewModel/Chat/chatListViewModel";
|
|||||||
import { ChatPage } from "./chatPage";
|
import { ChatPage } from "./chatPage";
|
||||||
|
|
||||||
export function ChatPageWrapper(chatListViewModel: ChatListViewModel) {
|
export function ChatPageWrapper(chatListViewModel: ChatListViewModel) {
|
||||||
const chatPageContent = React.createProxyState(
|
const chatPageContent = React.createProxyState(
|
||||||
[chatListViewModel.selectedChat],
|
[chatListViewModel.selectedChat],
|
||||||
() => {
|
() => {
|
||||||
if (chatListViewModel.selectedChat.value == undefined) {
|
if (chatListViewModel.selectedChat.value == undefined) {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
} else {
|
} else {
|
||||||
return ChatPage(chatListViewModel.selectedChat.value);
|
return ChatPage(chatListViewModel.selectedChat.value);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return <div id="chat-page-wrapper" children:set={chatPageContent}></div>;
|
||||||
<div
|
|
||||||
id="chat-page-wrapper"
|
|
||||||
children:set={chatPageContent}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-52
@@ -1,88 +1,88 @@
|
|||||||
#home-page > div {
|
#home-page > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
gap: var(--large-gap);
|
gap: var(--large-gap);
|
||||||
|
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page > div > div {
|
#home-page > div > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
padding-top: calc(var(--header-height) + var(--content-margin));
|
padding-top: calc(var(--header-height) + var(--content-margin));
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page #overview-section,
|
#home-page #overview-section,
|
||||||
#home-page #chat-section {
|
#home-page #chat-section {
|
||||||
padding-bottom: calc(
|
padding-bottom: calc(
|
||||||
var(--content-padding) + var(--header-height) + var(--safe-area-bottom)
|
var(--content-padding) + var(--header-height) + var(--safe-area-bottom)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page #chat-grid {
|
#home-page #chat-grid {
|
||||||
margin-top: var(--content-padding);
|
margin-top: var(--content-padding);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--gap-between-tiles);
|
gap: var(--gap-between-tiles);
|
||||||
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-entry[unread] {
|
.chat-entry[unread] {
|
||||||
border: var(--outline-width) solid black;
|
border: var(--outline-width) solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-entry[unread]:not(:hover):not(:active) {
|
.chat-entry[unread]:not(:hover):not(:active) {
|
||||||
background: rgb(255, 217, 0) !important;
|
background: rgb(255, 217, 0) !important;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
#home-page > div {
|
#home-page > div {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page #overview-section {
|
#home-page #overview-section {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
min-width: 20rem;
|
min-width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page #chat-section {
|
#home-page #chat-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
#home-page #overview-section,
|
#home-page #overview-section,
|
||||||
#home-page #chat-section {
|
#home-page #chat-section {
|
||||||
padding-left: 3rem;
|
padding-left: 3rem;
|
||||||
padding-right: 3rem;
|
padding-right: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page #chat-grid {
|
#home-page #chat-grid {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-page #overview-section {
|
#home-page #overview-section {
|
||||||
width: var(--max-input-width);
|
width: var(--max-input-width);
|
||||||
min-width: var(--max-input-width);
|
min-width: var(--max-input-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+183
-171
@@ -13,184 +13,196 @@ import StorageViewModel from "../ViewModel/Global/storageViewModel";
|
|||||||
import { translations } from "./translations";
|
import { translations } from "./translations";
|
||||||
|
|
||||||
export function HomePage(
|
export function HomePage(
|
||||||
storageViewModel: StorageViewModel,
|
storageViewModel: StorageViewModel,
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
connectionViewModel: ConnectionViewModel,
|
connectionViewModel: ConnectionViewModel,
|
||||||
fileTransferViewModel: FileTransferViewModel,
|
fileTransferViewModel: FileTransferViewModel,
|
||||||
chatListViewModel: ChatListViewModel
|
chatListViewModel: ChatListViewModel,
|
||||||
) {
|
) {
|
||||||
// sections
|
// sections
|
||||||
const overviewSection = (
|
const overviewSection = (
|
||||||
<div id="overview-section">
|
<div id="overview-section">
|
||||||
<h2>{translations.homePage.overviewHeadline}</h2>
|
<h2>{translations.homePage.overviewHeadline}</h2>
|
||||||
|
|
||||||
<label class="tile flex-no">
|
<label class="tile flex-no">
|
||||||
<span class="icon">cell_tower</span>
|
<span class="icon">cell_tower</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{translations.homePage.serverAddress}</span>
|
<span>{translations.homePage.serverAddress}</span>
|
||||||
<input
|
<input
|
||||||
list="previous-connection-list"
|
list="previous-connection-list"
|
||||||
placeholder={translations.homePage.serverAddressPlaceholder}
|
placeholder={
|
||||||
bind:value={connectionViewModel.serverAddressInput}
|
translations.homePage.serverAddressPlaceholder
|
||||||
on:enter={connectionViewModel.connect}
|
}
|
||||||
></input>
|
bind:value={connectionViewModel.serverAddressInput}
|
||||||
<datalist
|
on:enter={connectionViewModel.connect}
|
||||||
hidden
|
></input>
|
||||||
id="previous-connection-list"
|
<datalist
|
||||||
children:append={[
|
hidden
|
||||||
connectionViewModel.previousAddresses,
|
id="previous-connection-list"
|
||||||
StringToOption,
|
children:append={[
|
||||||
]}
|
connectionViewModel.previousAddresses,
|
||||||
></datalist>
|
StringToOption,
|
||||||
|
]}
|
||||||
|
></datalist>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="flex-row">
|
||||||
|
<button
|
||||||
|
class="danger flex justify-center"
|
||||||
|
aria-label={translations.homePage.disconnectAudioLabel}
|
||||||
|
on:click={connectionViewModel.disconnect}
|
||||||
|
toggle:disabled={connectionViewModel.cannotDisonnect}
|
||||||
|
>
|
||||||
|
<span class="icon">link_off</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex justify-center"
|
||||||
|
aria-label={
|
||||||
|
translations.homePage.manageConnectionsAudioLabel
|
||||||
|
}
|
||||||
|
on:click={connectionViewModel.showConnectionModal}
|
||||||
|
toggle:disabled={
|
||||||
|
connectionViewModel.hasNoPreviousConnections
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">history</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary flex justify-center"
|
||||||
|
aria-label={translations.homePage.connectAudioLabel}
|
||||||
|
on:click={connectionViewModel.connect}
|
||||||
|
toggle:disabled={connectionViewModel.cannotConnect}
|
||||||
|
>
|
||||||
|
<span class="icon">link</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">account_circle</span>
|
||||||
|
<div>
|
||||||
|
<span>{translations.homePage.yourNameLabel}</span>
|
||||||
|
<input
|
||||||
|
placeholder={translations.homePage.yourNamePlaceholder}
|
||||||
|
bind:value={settingsViewModel.usernameInput}
|
||||||
|
on:enter={settingsViewModel.setName}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="flex-row justify-end">
|
||||||
|
<button
|
||||||
|
class="width-50"
|
||||||
|
on:click={settingsViewModel.setName}
|
||||||
|
toggle:disabled={settingsViewModel.cannotSetName}
|
||||||
|
aria-label={translations.homePage.setNameButtonAudioLabel}
|
||||||
|
>
|
||||||
|
{translations.general.setButton}
|
||||||
|
<span class="icon">check</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<label class="tile flex-no">
|
||||||
|
<span class="icon">calendar_month</span>
|
||||||
|
<div>
|
||||||
|
<span>{translations.homePage.firstDayOfWeekLabel}</span>
|
||||||
|
<select bind:value={settingsViewModel.firstDayOfWeekInput}>
|
||||||
|
{...translations.regional.weekdays.full.map(
|
||||||
|
(weekdayName, i) =>
|
||||||
|
Option(
|
||||||
|
weekdayName,
|
||||||
|
i.toString(),
|
||||||
|
i.toString() ==
|
||||||
|
settingsViewModel.firstDayOfWeekInput
|
||||||
|
.value,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<span class="icon">arrow_drop_down</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tile flex-no"
|
||||||
|
on:click={fileTransferViewModel.showDirectionSelectionModal}
|
||||||
|
>
|
||||||
|
<span class="icon">sync_alt</span>
|
||||||
|
<div>
|
||||||
|
<span>{translations.homePage.transferDataButton}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tile flex-no"
|
||||||
|
on:click={storageViewModel.showStorageModal}
|
||||||
|
>
|
||||||
|
<span class="icon">hard_drive_2</span>
|
||||||
|
<div>
|
||||||
|
<span>{translations.homePage.manageStorageButton}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mobile-only">
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
|
<div class="flex-row justify-end">
|
||||||
|
<button class="ghost width-50" on:click={scrollToChat}>
|
||||||
|
{translations.homePage.scrollToChatButton}
|
||||||
|
<span class="icon">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
);
|
||||||
|
|
||||||
<div class="flex-row">
|
const chatSection = (
|
||||||
<button
|
<div id="chat-section">
|
||||||
class="danger flex justify-center"
|
<h2>{translations.homePage.chatsHeadline}</h2>
|
||||||
aria-label={translations.homePage.disconnectAudioLabel}
|
|
||||||
on:click={connectionViewModel.disconnect}
|
|
||||||
toggle:disabled={connectionViewModel.cannotDisonnect}
|
|
||||||
>
|
|
||||||
<span class="icon">link_off</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="flex justify-center"
|
|
||||||
aria-label={translations.homePage.manageConnectionsAudioLabel}
|
|
||||||
on:click={connectionViewModel.showConnectionModal}
|
|
||||||
toggle:disabled={connectionViewModel.hasNoPreviousConnections}
|
|
||||||
>
|
|
||||||
<span class="icon">history</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="primary flex justify-center"
|
|
||||||
aria-label={translations.homePage.connectAudioLabel}
|
|
||||||
on:click={connectionViewModel.connect}
|
|
||||||
toggle:disabled={connectionViewModel.cannotConnect}
|
|
||||||
>
|
|
||||||
<span class="icon">link</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
<div class="flex-row width-input">
|
||||||
|
<input
|
||||||
|
placeholder={translations.homePage.addChatPlaceholder}
|
||||||
|
aria-label={translations.homePage.addChatAudioLabel}
|
||||||
|
bind:value={chatListViewModel.newChatPrimaryChannel}
|
||||||
|
on:enter={chatListViewModel.createChat}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
aria-label={translations.homePage.addChatButton}
|
||||||
|
on:click={chatListViewModel.createChat}
|
||||||
|
toggle:disabled={chatListViewModel.cannotCreateChat}
|
||||||
|
>
|
||||||
|
<span class="icon">add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="tile flex-no">
|
<div
|
||||||
<span class="icon">account_circle</span>
|
id="chat-grid"
|
||||||
<div>
|
children:append={[
|
||||||
<span>{translations.homePage.yourNameLabel}</span>
|
chatListViewModel.chatViewModels,
|
||||||
<input
|
ChatViewModelToChatEntry,
|
||||||
placeholder={translations.homePage.yourNamePlaceholder}
|
]}
|
||||||
bind:value={settingsViewModel.usernameInput}
|
></div>
|
||||||
on:enter={settingsViewModel.setName}
|
|
||||||
></input>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
);
|
||||||
<div class="flex-row justify-end">
|
|
||||||
<button
|
|
||||||
class="width-50"
|
|
||||||
on:click={settingsViewModel.setName}
|
|
||||||
toggle:disabled={settingsViewModel.cannotSetName}
|
|
||||||
aria-label={translations.homePage.setNameButtonAudioLabel}
|
|
||||||
>
|
|
||||||
{translations.general.setButton}
|
|
||||||
<span class="icon">check</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
// methods
|
||||||
|
function scrollToChat() {
|
||||||
|
chatSection.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
<label class="tile flex-no">
|
// final
|
||||||
<span class="icon">calendar_month</span>
|
return (
|
||||||
<div>
|
<article id="home-page">
|
||||||
<span>{translations.homePage.firstDayOfWeekLabel}</span>
|
<div>
|
||||||
<select bind:value={settingsViewModel.firstDayOfWeekInput}>
|
{overviewSection}
|
||||||
{...translations.regional.weekdays.full.map((weekdayName, i) =>
|
{chatSection}
|
||||||
Option(
|
</div>
|
||||||
weekdayName,
|
</article>
|
||||||
i.toString(),
|
);
|
||||||
i.toString() == settingsViewModel.firstDayOfWeekInput.value
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<span class="icon">arrow_drop_down</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="tile flex-no"
|
|
||||||
on:click={fileTransferViewModel.showDirectionSelectionModal}
|
|
||||||
>
|
|
||||||
<span class="icon">sync_alt</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.homePage.transferDataButton}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="tile flex-no" on:click={storageViewModel.showStorageModal}>
|
|
||||||
<span class="icon">hard_drive_2</span>
|
|
||||||
<div>
|
|
||||||
<span>{translations.homePage.manageStorageButton}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mobile-only">
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-row justify-end">
|
|
||||||
<button class="ghost width-50" on:click={scrollToChat}>
|
|
||||||
{translations.homePage.scrollToChatButton}
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const chatSection = (
|
|
||||||
<div id="chat-section">
|
|
||||||
<h2>{translations.homePage.chatsHeadline}</h2>
|
|
||||||
|
|
||||||
<div class="flex-row width-input">
|
|
||||||
<input
|
|
||||||
placeholder={translations.homePage.addChatPlaceholder}
|
|
||||||
aria-label={translations.homePage.addChatAudioLabel}
|
|
||||||
bind:value={chatListViewModel.newChatPrimaryChannel}
|
|
||||||
on:enter={chatListViewModel.createChat}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
aria-label={translations.homePage.addChatButton}
|
|
||||||
on:click={chatListViewModel.createChat}
|
|
||||||
toggle:disabled={chatListViewModel.cannotCreateChat}
|
|
||||||
>
|
|
||||||
<span class="icon">add</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="chat-grid"
|
|
||||||
children:append={[
|
|
||||||
chatListViewModel.chatViewModels,
|
|
||||||
ChatViewModelToChatEntry,
|
|
||||||
]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
function scrollToChat() {
|
|
||||||
chatSection.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
// final
|
|
||||||
return (
|
|
||||||
<article id="home-page">
|
|
||||||
<div>
|
|
||||||
{overviewSection}
|
|
||||||
{chatSection}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import * as React from "bloatless-react";
|
|||||||
import { translations } from "./translations";
|
import { translations } from "./translations";
|
||||||
|
|
||||||
export function MainPage() {
|
export function MainPage() {
|
||||||
return (
|
return (
|
||||||
<article id="home-page">
|
<article id="home-page">
|
||||||
<header>
|
<header>
|
||||||
<span>{translations.homePage.appName}</span>
|
<span>{translations.homePage.appName}</span>
|
||||||
</header>
|
</header>
|
||||||
<div></div>
|
<div></div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+722
-708
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
export function allowDrop(event: DragEvent) {
|
export function allowDrop(event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allowDrag(event: DragEvent) {
|
export function allowDrag(event: DragEvent) {
|
||||||
event.dataTransfer?.setData("text", "")
|
event.dataTransfer?.setData("text", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reload() {
|
export function reload() {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,98 +9,99 @@ import SettingsViewModel from "../Global/settingsViewModel";
|
|||||||
import StorageModel from "../../Model/Global/storageModel";
|
import StorageModel from "../../Model/Global/storageModel";
|
||||||
|
|
||||||
export default class ChatListViewModel {
|
export default class ChatListViewModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
chatListModel: ChatListModel;
|
chatListModel: ChatListModel;
|
||||||
settingsViewModel: SettingsViewModel;
|
settingsViewModel: SettingsViewModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
chatIndexManager: IndexManager<ChatViewModel> = new IndexManager(
|
chatIndexManager: IndexManager<ChatViewModel> = new IndexManager(
|
||||||
(chatViewModel: ChatViewModel) =>
|
(chatViewModel: ChatViewModel) =>
|
||||||
chatViewModel.settingsPageViewModel.primaryChannel.value
|
chatViewModel.settingsPageViewModel.primaryChannel.value,
|
||||||
);
|
|
||||||
|
|
||||||
// state
|
|
||||||
newChatPrimaryChannel: React.State<string> = new React.State("");
|
|
||||||
chatViewModels: React.ListState<ChatViewModel> = new React.ListState();
|
|
||||||
|
|
||||||
selectedChat: React.State<ChatViewModel | undefined> = new React.State<any>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// guards
|
|
||||||
cannotCreateChat = React.createProxyState(
|
|
||||||
[this.newChatPrimaryChannel],
|
|
||||||
() => this.newChatPrimaryChannel.value == ""
|
|
||||||
);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
createChat = (): void => {
|
|
||||||
const chatModel: ChatModel = this.chatListModel.createChat(
|
|
||||||
this.newChatPrimaryChannel.value
|
|
||||||
);
|
);
|
||||||
this.newChatPrimaryChannel.value = "";
|
|
||||||
|
|
||||||
const chatViewModel: ChatViewModel = this.createChatViewModel(chatModel);
|
// state
|
||||||
this.trackChat(chatViewModel);
|
newChatPrimaryChannel: React.State<string> = new React.State("");
|
||||||
this.updateIndices();
|
chatViewModels: React.ListState<ChatViewModel> = new React.ListState();
|
||||||
};
|
|
||||||
|
|
||||||
trackChat = (chatViewModel: ChatViewModel): void => {
|
selectedChat: React.State<ChatViewModel | undefined> = new React.State<any>(
|
||||||
this.chatViewModels.add(chatViewModel);
|
undefined,
|
||||||
};
|
|
||||||
|
|
||||||
untrackChat = (chatViewModel: ChatViewModel): void => {
|
|
||||||
this.chatListModel.untrackChat(chatViewModel.chatModel);
|
|
||||||
this.chatViewModels.remove(chatViewModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
createChatViewModel = (chatModel: ChatModel): ChatViewModel => {
|
|
||||||
return new ChatViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this.storageModel,
|
|
||||||
chatModel,
|
|
||||||
this.settingsViewModel,
|
|
||||||
this
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
updateIndices = (): void => {
|
// guards
|
||||||
this.chatIndexManager.update([...this.chatViewModels.value.values()]);
|
cannotCreateChat = React.createProxyState(
|
||||||
for (const chatViewModel of this.chatViewModels.value) {
|
[this.newChatPrimaryChannel],
|
||||||
chatViewModel.updateIndex();
|
() => this.newChatPrimaryChannel.value == "",
|
||||||
|
);
|
||||||
|
|
||||||
|
// methods
|
||||||
|
createChat = (): void => {
|
||||||
|
const chatModel: ChatModel = this.chatListModel.createChat(
|
||||||
|
this.newChatPrimaryChannel.value,
|
||||||
|
);
|
||||||
|
this.newChatPrimaryChannel.value = "";
|
||||||
|
|
||||||
|
const chatViewModel: ChatViewModel =
|
||||||
|
this.createChatViewModel(chatModel);
|
||||||
|
this.trackChat(chatViewModel);
|
||||||
|
this.updateIndices();
|
||||||
|
};
|
||||||
|
|
||||||
|
trackChat = (chatViewModel: ChatViewModel): void => {
|
||||||
|
this.chatViewModels.add(chatViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
untrackChat = (chatViewModel: ChatViewModel): void => {
|
||||||
|
this.chatListModel.untrackChat(chatViewModel.chatModel);
|
||||||
|
this.chatViewModels.remove(chatViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
createChatViewModel = (chatModel: ChatModel): ChatViewModel => {
|
||||||
|
return new ChatViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this.storageModel,
|
||||||
|
chatModel,
|
||||||
|
this.settingsViewModel,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateIndices = (): void => {
|
||||||
|
this.chatIndexManager.update([...this.chatViewModels.value.values()]);
|
||||||
|
for (const chatViewModel of this.chatViewModels.value) {
|
||||||
|
chatViewModel.updateIndex();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// view
|
||||||
|
openChat = (chatViewModel: ChatViewModel): void => {
|
||||||
|
this.selectedChat.value = chatViewModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
closeChat = (): void => {
|
||||||
|
this.selectedChat.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// load
|
||||||
|
loadChats = (): void => {
|
||||||
|
this.chatViewModels.clear();
|
||||||
|
for (const chatModel of this.chatListModel.chatModels.values()) {
|
||||||
|
const chatViewModel = this.createChatViewModel(chatModel);
|
||||||
|
this.trackChat(chatViewModel);
|
||||||
|
}
|
||||||
|
this.updateIndices();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
storageModel: StorageModel,
|
||||||
|
chatListModel: ChatListModel,
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
|
) {
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.chatListModel = chatListModel;
|
||||||
|
this.settingsViewModel = settingsViewModel;
|
||||||
|
|
||||||
|
this.loadChats();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
|
||||||
openChat = (chatViewModel: ChatViewModel): void => {
|
|
||||||
this.selectedChat.value = chatViewModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
closeChat = (): void => {
|
|
||||||
this.selectedChat.value = undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
|
||||||
loadChats = (): void => {
|
|
||||||
this.chatViewModels.clear();
|
|
||||||
for (const chatModel of this.chatListModel.chatModels.values()) {
|
|
||||||
const chatViewModel = this.createChatViewModel(chatModel);
|
|
||||||
this.trackChat(chatViewModel);
|
|
||||||
}
|
|
||||||
this.updateIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
storageModel: StorageModel,
|
|
||||||
chatListModel: ChatListModel,
|
|
||||||
settingsViewModel: SettingsViewModel
|
|
||||||
) {
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.chatListModel = chatListModel;
|
|
||||||
this.settingsViewModel = settingsViewModel;
|
|
||||||
|
|
||||||
this.loadChats();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,62 +6,62 @@ import CoreViewModel from "../Global/coreViewModel";
|
|||||||
import MessagePageViewModel from "../Pages/messagePageViewModel";
|
import MessagePageViewModel from "../Pages/messagePageViewModel";
|
||||||
|
|
||||||
export default class ChatMessageViewModel {
|
export default class ChatMessageViewModel {
|
||||||
messagePageViewModel: MessagePageViewModel;
|
messagePageViewModel: MessagePageViewModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
chatMessage: ChatMessage;
|
chatMessage: ChatMessage;
|
||||||
channel: string;
|
channel: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
dateSent: string;
|
dateSent: string;
|
||||||
body: React.State<string> = new React.State("");
|
body: React.State<string> = new React.State("");
|
||||||
status: React.State<ChatMessageStatus | any> = new React.State<any>(
|
status: React.State<ChatMessageStatus | any> = new React.State<any>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
sentByUser: boolean;
|
sentByUser: boolean;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
isPresentingInfoModal: React.State<boolean> = new React.State(false);
|
isPresentingInfoModal: React.State<boolean> = new React.State(false);
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
copyMessage = (): void => {
|
copyMessage = (): void => {
|
||||||
navigator.clipboard.writeText(this.body.value);
|
navigator.clipboard.writeText(this.body.value);
|
||||||
};
|
};
|
||||||
resendMessage = (): void => {
|
resendMessage = (): void => {
|
||||||
this.messagePageViewModel.sendMessageFromBody(this.body.value);
|
this.messagePageViewModel.sendMessageFromBody(this.body.value);
|
||||||
};
|
};
|
||||||
decryptMessage = (): void => {
|
decryptMessage = (): void => {
|
||||||
this.messagePageViewModel.decryptMessage(this);
|
this.messagePageViewModel.decryptMessage(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
// view
|
// view
|
||||||
showInfoModal = (): void => {
|
showInfoModal = (): void => {
|
||||||
this.isPresentingInfoModal.value = true;
|
this.isPresentingInfoModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
hideInfoModal = (): void => {
|
hideInfoModal = (): void => {
|
||||||
this.isPresentingInfoModal.value = false;
|
this.isPresentingInfoModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
loadData = (): void => {
|
loadData = (): void => {
|
||||||
this.channel = this.chatMessage.channel;
|
this.channel = this.chatMessage.channel;
|
||||||
this.sender = this.chatMessage.sender;
|
this.sender = this.chatMessage.sender;
|
||||||
this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString();
|
this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString();
|
||||||
this.body.value = this.chatMessage.body;
|
this.body.value = this.chatMessage.body;
|
||||||
this.status.value = this.chatMessage.status;
|
this.status.value = this.chatMessage.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
public coreViewModel: CoreViewModel,
|
public coreViewModel: CoreViewModel,
|
||||||
messagePageViewModel: MessagePageViewModel,
|
messagePageViewModel: MessagePageViewModel,
|
||||||
chatMessage: ChatMessage,
|
chatMessage: ChatMessage,
|
||||||
sentByUser: boolean
|
sentByUser: boolean,
|
||||||
) {
|
) {
|
||||||
this.messagePageViewModel = messagePageViewModel;
|
this.messagePageViewModel = messagePageViewModel;
|
||||||
|
|
||||||
this.chatMessage = chatMessage;
|
this.chatMessage = chatMessage;
|
||||||
this.sentByUser = sentByUser;
|
this.sentByUser = sentByUser;
|
||||||
this.loadData();
|
this.loadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-133
@@ -1,12 +1,12 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import CalendarPageViewModel, {
|
import CalendarPageViewModel, {
|
||||||
CALENDAR_EVENT_BOARD_ID,
|
CALENDAR_EVENT_BOARD_ID,
|
||||||
} from "../Pages/calendarPageViewModel";
|
} from "../Pages/calendarPageViewModel";
|
||||||
import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel";
|
import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel";
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
StorageModelSubPath,
|
StorageModelSubPath,
|
||||||
filePaths,
|
filePaths,
|
||||||
} from "../../Model/Global/storageModel";
|
} from "../../Model/Global/storageModel";
|
||||||
|
|
||||||
import ChatListViewModel from "./chatListViewModel";
|
import ChatListViewModel from "./chatListViewModel";
|
||||||
@@ -20,145 +20,145 @@ import TaskPageViewModel from "../Pages/taskPageViewModel";
|
|||||||
import { translations } from "../../View/translations";
|
import { translations } from "../../View/translations";
|
||||||
|
|
||||||
export default class ChatViewModel {
|
export default class ChatViewModel {
|
||||||
chatModel: ChatModel;
|
chatModel: ChatModel;
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
settingsViewModel: SettingsViewModel;
|
settingsViewModel: SettingsViewModel;
|
||||||
chatListViewModel: ChatListViewModel;
|
chatListViewModel: ChatListViewModel;
|
||||||
|
|
||||||
calendarViewModel: CalendarPageViewModel;
|
calendarViewModel: CalendarPageViewModel;
|
||||||
taskPageViewModel: TaskPageViewModel;
|
taskPageViewModel: TaskPageViewModel;
|
||||||
messagePageViewModel: MessagePageViewModel;
|
messagePageViewModel: MessagePageViewModel;
|
||||||
settingsPageViewModel: SettingsPageViewModel;
|
settingsPageViewModel: SettingsPageViewModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
displayedColor: React.State<Color> = new React.State<any>(Color.Standard);
|
displayedColor: React.State<Color> = new React.State<any>(Color.Standard);
|
||||||
selectedPage: React.State<ChatPageType> = new React.State<any>(
|
selectedPage: React.State<ChatPageType> = new React.State<any>(
|
||||||
ChatPageType.Messages
|
ChatPageType.Messages,
|
||||||
);
|
|
||||||
|
|
||||||
index: React.State<number> = new React.State(0);
|
|
||||||
hasUnreadMessages: React.State<boolean> = new React.State(false);
|
|
||||||
|
|
||||||
taskBoardSuggestions: React.MapState<Entry> = new React.MapState();
|
|
||||||
|
|
||||||
// view
|
|
||||||
open = (): void => {
|
|
||||||
this.chatListViewModel.openChat(this);
|
|
||||||
this.markRead();
|
|
||||||
};
|
|
||||||
|
|
||||||
close = (): void => {
|
|
||||||
this.chatListViewModel.closeChat();
|
|
||||||
};
|
|
||||||
|
|
||||||
closeSubPages = (): void => {};
|
|
||||||
|
|
||||||
setColor = (color: Color): void => {
|
|
||||||
this.setDisplayedColor(color);
|
|
||||||
this.chatModel.setColor(color);
|
|
||||||
};
|
|
||||||
|
|
||||||
setDisplayedColor = (color: Color): void => {
|
|
||||||
this.displayedColor.value = color;
|
|
||||||
};
|
|
||||||
|
|
||||||
resetColor = (): void => {
|
|
||||||
this.displayedColor.value = this.settingsPageViewModel.color.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
updateIndex = (): void => {
|
|
||||||
const index: number =
|
|
||||||
this.chatListViewModel.chatIndexManager.getIndex(this);
|
|
||||||
this.index.value = index;
|
|
||||||
};
|
|
||||||
|
|
||||||
markRead = (): void => {
|
|
||||||
this.hasUnreadMessages.value = false;
|
|
||||||
this.chatModel.markRead();
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
|
||||||
loadPageSelection = (): void => {
|
|
||||||
const path: string[] = StorageModel.getPath(
|
|
||||||
StorageModelSubPath.Chat,
|
|
||||||
filePaths.chat.lastUsedPage(this.chatModel.id)
|
|
||||||
);
|
|
||||||
const lastUsedPage: string | null = this.storageModel.read(path);
|
|
||||||
if (lastUsedPage != null) {
|
|
||||||
this.selectedPage.value = lastUsedPage as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedPage.subscribeSilent((newPage) => {
|
|
||||||
this.storageModel.write(path, newPage);
|
|
||||||
this.resetColor();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
loadInfo = (): void => {
|
|
||||||
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
|
|
||||||
|
|
||||||
this.taskBoardSuggestions.set(CALENDAR_EVENT_BOARD_ID, [
|
|
||||||
CALENDAR_EVENT_BOARD_ID,
|
|
||||||
translations.chatPage.calendar.eventsBoard,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
storageModel: StorageModel,
|
|
||||||
chatModel: ChatModel,
|
|
||||||
settingsViewModel: SettingsViewModel,
|
|
||||||
chatListViewModel: ChatListViewModel
|
|
||||||
) {
|
|
||||||
// models
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.chatModel = chatModel;
|
|
||||||
this.settingsViewModel = settingsViewModel;
|
|
||||||
this.chatListViewModel = chatListViewModel;
|
|
||||||
|
|
||||||
// page viewModels
|
|
||||||
this.calendarViewModel = new CalendarPageViewModel(
|
|
||||||
coreViewModel,
|
|
||||||
this,
|
|
||||||
this.storageModel,
|
|
||||||
this.chatModel.fileModel.boardsAndTasksModel.calendarModel,
|
|
||||||
this.chatModel.fileModel.boardsAndTasksModel
|
|
||||||
);
|
|
||||||
this.taskPageViewModel = new TaskPageViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this,
|
|
||||||
this.storageModel,
|
|
||||||
this.chatModel.fileModel.boardsAndTasksModel
|
|
||||||
);
|
|
||||||
this.messagePageViewModel = new MessagePageViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this
|
|
||||||
);
|
|
||||||
this.settingsPageViewModel = new SettingsPageViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// handlers
|
index: React.State<number> = new React.State(0);
|
||||||
chatModel.chatMessageHandlerManager.addHandler(
|
hasUnreadMessages: React.State<boolean> = new React.State(false);
|
||||||
(chatMessage: ChatMessage) => {
|
|
||||||
this.messagePageViewModel.showChatMessage(chatMessage);
|
taskBoardSuggestions: React.MapState<Entry> = new React.MapState();
|
||||||
|
|
||||||
|
// view
|
||||||
|
open = (): void => {
|
||||||
|
this.chatListViewModel.openChat(this);
|
||||||
this.markRead();
|
this.markRead();
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
close = (): void => {
|
||||||
|
this.chatListViewModel.closeChat();
|
||||||
|
};
|
||||||
|
|
||||||
|
closeSubPages = (): void => {};
|
||||||
|
|
||||||
|
setColor = (color: Color): void => {
|
||||||
|
this.setDisplayedColor(color);
|
||||||
|
this.chatModel.setColor(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
setDisplayedColor = (color: Color): void => {
|
||||||
|
this.displayedColor.value = color;
|
||||||
|
};
|
||||||
|
|
||||||
|
resetColor = (): void => {
|
||||||
|
this.displayedColor.value = this.settingsPageViewModel.color.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateIndex = (): void => {
|
||||||
|
const index: number =
|
||||||
|
this.chatListViewModel.chatIndexManager.getIndex(this);
|
||||||
|
this.index.value = index;
|
||||||
|
};
|
||||||
|
|
||||||
|
markRead = (): void => {
|
||||||
|
this.hasUnreadMessages.value = false;
|
||||||
|
this.chatModel.markRead();
|
||||||
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
this.loadPageSelection();
|
loadPageSelection = (): void => {
|
||||||
this.resetColor();
|
const path: string[] = StorageModel.getPath(
|
||||||
this.loadInfo();
|
StorageModelSubPath.Chat,
|
||||||
}
|
filePaths.chat.lastUsedPage(this.chatModel.id),
|
||||||
|
);
|
||||||
|
const lastUsedPage: string | null = this.storageModel.read(path);
|
||||||
|
if (lastUsedPage != null) {
|
||||||
|
this.selectedPage.value = lastUsedPage as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedPage.subscribeSilent((newPage) => {
|
||||||
|
this.storageModel.write(path, newPage);
|
||||||
|
this.resetColor();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInfo = (): void => {
|
||||||
|
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
|
||||||
|
|
||||||
|
this.taskBoardSuggestions.set(CALENDAR_EVENT_BOARD_ID, [
|
||||||
|
CALENDAR_EVENT_BOARD_ID,
|
||||||
|
translations.chatPage.calendar.eventsBoard,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
storageModel: StorageModel,
|
||||||
|
chatModel: ChatModel,
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
|
chatListViewModel: ChatListViewModel,
|
||||||
|
) {
|
||||||
|
// models
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.chatModel = chatModel;
|
||||||
|
this.settingsViewModel = settingsViewModel;
|
||||||
|
this.chatListViewModel = chatListViewModel;
|
||||||
|
|
||||||
|
// page viewModels
|
||||||
|
this.calendarViewModel = new CalendarPageViewModel(
|
||||||
|
coreViewModel,
|
||||||
|
this,
|
||||||
|
this.storageModel,
|
||||||
|
this.chatModel.fileModel.boardsAndTasksModel.calendarModel,
|
||||||
|
this.chatModel.fileModel.boardsAndTasksModel,
|
||||||
|
);
|
||||||
|
this.taskPageViewModel = new TaskPageViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this,
|
||||||
|
this.storageModel,
|
||||||
|
this.chatModel.fileModel.boardsAndTasksModel,
|
||||||
|
);
|
||||||
|
this.messagePageViewModel = new MessagePageViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
this.settingsPageViewModel = new SettingsPageViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
chatModel.chatMessageHandlerManager.addHandler(
|
||||||
|
(chatMessage: ChatMessage) => {
|
||||||
|
this.messagePageViewModel.showChatMessage(chatMessage);
|
||||||
|
this.markRead();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// load
|
||||||
|
this.loadPageSelection();
|
||||||
|
this.resetColor();
|
||||||
|
this.loadInfo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export enum ChatPageType {
|
export enum ChatPageType {
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
Messages = "messages",
|
Messages = "messages",
|
||||||
Tasks = "tasks",
|
Tasks = "tasks",
|
||||||
Calendar = "calendar",
|
Calendar = "calendar",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,89 +4,90 @@ import ConnectionModel from "../../Model/Global/connectionModel";
|
|||||||
import CoreViewModel from "./coreViewModel";
|
import CoreViewModel from "./coreViewModel";
|
||||||
|
|
||||||
export default class ConnectionViewModel {
|
export default class ConnectionViewModel {
|
||||||
connectionModel: ConnectionModel;
|
connectionModel: ConnectionModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
serverAddressInput: React.State<string> = new React.State("");
|
serverAddressInput: React.State<string> = new React.State("");
|
||||||
isConnected: React.State<boolean> = new React.State(false);
|
isConnected: React.State<boolean> = new React.State(false);
|
||||||
|
|
||||||
isShowingConnectionModal: React.State<boolean> = new React.State(false);
|
isShowingConnectionModal: React.State<boolean> = new React.State(false);
|
||||||
|
|
||||||
previousAddresses: React.ListState<string> = new React.ListState();
|
previousAddresses: React.ListState<string> = new React.ListState();
|
||||||
|
|
||||||
// guards
|
// guards
|
||||||
cannotConnect: React.State<boolean> = React.createProxyState(
|
cannotConnect: React.State<boolean> = React.createProxyState(
|
||||||
[this.serverAddressInput, this.isConnected],
|
[this.serverAddressInput, this.isConnected],
|
||||||
() =>
|
() =>
|
||||||
(this.isConnected.value == true &&
|
(this.isConnected.value == true &&
|
||||||
this.serverAddressInput.value == this.connectionModel.address) ||
|
this.serverAddressInput.value ==
|
||||||
this.serverAddressInput.value == ""
|
this.connectionModel.address) ||
|
||||||
);
|
this.serverAddressInput.value == "",
|
||||||
cannotDisonnect: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.isConnected],
|
|
||||||
() => this.isConnected.value == false
|
|
||||||
);
|
|
||||||
hasNoPreviousConnections: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.previousAddresses],
|
|
||||||
() => this.previousAddresses.value.size == 0
|
|
||||||
);
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
connectionChangeHandler = (): void => {
|
|
||||||
this.isConnected.value = this.connectionModel.isConnected;
|
|
||||||
|
|
||||||
if (this.connectionModel.isConnected == false) return;
|
|
||||||
if (this.connectionModel.address == undefined) return;
|
|
||||||
|
|
||||||
this.serverAddressInput.value = this.connectionModel.address;
|
|
||||||
|
|
||||||
if (!this.previousAddresses.value.has(this.connectionModel.address)) {
|
|
||||||
this.previousAddresses.add(this.connectionModel.address);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// methods
|
|
||||||
connect = (): void => {
|
|
||||||
this.connectToAddress(this.serverAddressInput.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
connectToAddress = (address: string): void => {
|
|
||||||
this.connectionModel.connect(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
disconnect = (): void => {
|
|
||||||
this.connectionModel.disconnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
removePreviousAddress = (address: string): void => {
|
|
||||||
this.connectionModel.removeAddress(address);
|
|
||||||
this.updatePreviousAddresses();
|
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
|
||||||
showConnectionModal = (): void => {
|
|
||||||
this.isShowingConnectionModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
hideConnectionModal = (): void => {
|
|
||||||
this.isShowingConnectionModal.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
updatePreviousAddresses = (): void => {
|
|
||||||
this.previousAddresses.clear();
|
|
||||||
this.previousAddresses.add(...this.connectionModel.addresses);
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
connectionModel: ConnectionModel
|
|
||||||
) {
|
|
||||||
this.connectionModel = connectionModel;
|
|
||||||
this.updatePreviousAddresses();
|
|
||||||
|
|
||||||
connectionModel.connectionChangeHandlerManager.addHandler(
|
|
||||||
this.connectionChangeHandler
|
|
||||||
);
|
);
|
||||||
}
|
cannotDisonnect: React.State<boolean> = React.createProxyState(
|
||||||
|
[this.isConnected],
|
||||||
|
() => this.isConnected.value == false,
|
||||||
|
);
|
||||||
|
hasNoPreviousConnections: React.State<boolean> = React.createProxyState(
|
||||||
|
[this.previousAddresses],
|
||||||
|
() => this.previousAddresses.value.size == 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
connectionChangeHandler = (): void => {
|
||||||
|
this.isConnected.value = this.connectionModel.isConnected;
|
||||||
|
|
||||||
|
if (this.connectionModel.isConnected == false) return;
|
||||||
|
if (this.connectionModel.address == undefined) return;
|
||||||
|
|
||||||
|
this.serverAddressInput.value = this.connectionModel.address;
|
||||||
|
|
||||||
|
if (!this.previousAddresses.value.has(this.connectionModel.address)) {
|
||||||
|
this.previousAddresses.add(this.connectionModel.address);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// methods
|
||||||
|
connect = (): void => {
|
||||||
|
this.connectToAddress(this.serverAddressInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
connectToAddress = (address: string): void => {
|
||||||
|
this.connectionModel.connect(address);
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnect = (): void => {
|
||||||
|
this.connectionModel.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
removePreviousAddress = (address: string): void => {
|
||||||
|
this.connectionModel.removeAddress(address);
|
||||||
|
this.updatePreviousAddresses();
|
||||||
|
};
|
||||||
|
|
||||||
|
// view
|
||||||
|
showConnectionModal = (): void => {
|
||||||
|
this.isShowingConnectionModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
hideConnectionModal = (): void => {
|
||||||
|
this.isShowingConnectionModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePreviousAddresses = (): void => {
|
||||||
|
this.previousAddresses.clear();
|
||||||
|
this.previousAddresses.add(...this.connectionModel.addresses);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
connectionModel: ConnectionModel,
|
||||||
|
) {
|
||||||
|
this.connectionModel = connectionModel;
|
||||||
|
this.updatePreviousAddresses();
|
||||||
|
|
||||||
|
connectionModel.connectionChangeHandlerManager.addHandler(
|
||||||
|
this.connectionChangeHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
export default class CoreViewModel {
|
export default class CoreViewModel {
|
||||||
// DRAG & DROP
|
// DRAG & DROP
|
||||||
draggedObject: React.State<any> = new React.State<any>(undefined);
|
draggedObject: React.State<any> = new React.State<any>(undefined);
|
||||||
|
|
||||||
// SUGGESTIONS
|
// SUGGESTIONS
|
||||||
// boards & tasks
|
// boards & tasks
|
||||||
boardSearchSuggestions: React.ListState<string> = new React.ListState();
|
boardSearchSuggestions: React.ListState<string> = new React.ListState();
|
||||||
|
|
||||||
taskCategorySuggestions: React.ListState<string> = new React.ListState();
|
taskCategorySuggestions: React.ListState<string> = new React.ListState();
|
||||||
taskStatusSuggestions: React.ListState<string> = new React.ListState();
|
taskStatusSuggestions: React.ListState<string> = new React.ListState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import FileTransferModel, {
|
import FileTransferModel, {
|
||||||
TransferData,
|
TransferData,
|
||||||
} from "../../Model/Global/fileTransferModel";
|
} from "../../Model/Global/fileTransferModel";
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
StorageModelSubPath,
|
StorageModelSubPath,
|
||||||
filePaths,
|
filePaths,
|
||||||
} from "../../Model/Global/storageModel";
|
} from "../../Model/Global/storageModel";
|
||||||
|
|
||||||
import ChatListModel from "../../Model/Chat/chatListModel";
|
import ChatListModel from "../../Model/Chat/chatListModel";
|
||||||
@@ -13,180 +13,186 @@ import ChatModel from "../../Model/Chat/chatModel";
|
|||||||
import { translations } from "../../View/translations";
|
import { translations } from "../../View/translations";
|
||||||
|
|
||||||
export default class FileTransferViewModel {
|
export default class FileTransferViewModel {
|
||||||
fileTransferModel: FileTransferModel;
|
fileTransferModel: FileTransferModel;
|
||||||
chatListModel: ChatListModel;
|
chatListModel: ChatListModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
presentedModal: React.State<FileTransferModal | undefined> =
|
presentedModal: React.State<FileTransferModal | undefined> =
|
||||||
new React.State<any>(undefined);
|
new React.State<any>(undefined);
|
||||||
|
|
||||||
generalFileOptions: React.ListState<FileTransferOption> =
|
generalFileOptions: React.ListState<FileTransferOption> =
|
||||||
new React.ListState();
|
new React.ListState();
|
||||||
chatFileOptions: React.ListState<FileTransferOption> = new React.ListState();
|
chatFileOptions: React.ListState<FileTransferOption> =
|
||||||
selectedPaths: React.ListState<string[]> = new React.ListState();
|
new React.ListState();
|
||||||
|
selectedPaths: React.ListState<string[]> = new React.ListState();
|
||||||
|
|
||||||
transferChannel: React.State<string> = new React.State("");
|
transferChannel: React.State<string> = new React.State("");
|
||||||
transferKey: React.State<string> = new React.State("");
|
transferKey: React.State<string> = new React.State("");
|
||||||
|
|
||||||
receivingTransferChannel: React.State<string> = new React.State("");
|
receivingTransferChannel: React.State<string> = new React.State("");
|
||||||
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(
|
filesSentCount: React.State<number> = React.createProxyState(
|
||||||
[this.filePathsSent],
|
[this.filePathsSent],
|
||||||
() => this.filePathsSent.value.size
|
() => this.filePathsSent.value.size,
|
||||||
);
|
);
|
||||||
filesSentText: React.State<string> = React.createProxyState(
|
filesSentText: React.State<string> = React.createProxyState(
|
||||||
[this.filesSentCount],
|
[this.filesSentCount],
|
||||||
() =>
|
() =>
|
||||||
translations.dataTransferModal.filesSentCount(this.filesSentCount.value)
|
translations.dataTransferModal.filesSentCount(
|
||||||
);
|
this.filesSentCount.value,
|
||||||
|
),
|
||||||
filePathsReceived: React.ListState<string> = new React.ListState();
|
|
||||||
filesReceivedCount: React.State<number> = React.createProxyState(
|
|
||||||
[this.filePathsReceived],
|
|
||||||
() => this.filePathsReceived.value.size
|
|
||||||
);
|
|
||||||
filesReceivedText: React.State<string> = React.createProxyState(
|
|
||||||
[this.filesReceivedCount],
|
|
||||||
() =>
|
|
||||||
translations.dataTransferModal.filesReceivedCount(
|
|
||||||
this.filesReceivedCount.value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// guards
|
|
||||||
hasNoPathsSelected: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.selectedPaths],
|
|
||||||
() => this.selectedPaths.value.size == 0
|
|
||||||
);
|
|
||||||
didNotFinishSending: React.State<boolean> = new React.State(true);
|
|
||||||
cannotPrepareToReceive: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.receivingTransferChannel, this.receivingTransferKey],
|
|
||||||
() =>
|
|
||||||
this.receivingTransferChannel.value == "" ||
|
|
||||||
this.receivingTransferKey.value == ""
|
|
||||||
);
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
handleReceivedFile = (path: string): void => {
|
|
||||||
this.filePathsReceived.add(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
// methods
|
|
||||||
getOptions = (): void => {
|
|
||||||
this.generalFileOptions.clear();
|
|
||||||
this.chatFileOptions.clear();
|
|
||||||
this.selectedPaths.clear();
|
|
||||||
|
|
||||||
this.generalFileOptions.add(
|
|
||||||
{
|
|
||||||
label: translations.dataTransferModal.connectionData,
|
|
||||||
path: StorageModel.getPath(
|
|
||||||
StorageModelSubPath.ConnectionModel,
|
|
||||||
filePaths.connectionModel.previousAddresses
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: translations.dataTransferModal.settingsData,
|
|
||||||
path: StorageModel.getPath(StorageModelSubPath.SettingsModel, []),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const chatModels: Set<ChatModel> = this.chatListModel.chatModels;
|
filePathsReceived: React.ListState<string> = new React.ListState();
|
||||||
for (const chatModel of chatModels) {
|
filesReceivedCount: React.State<number> = React.createProxyState(
|
||||||
this.chatFileOptions.add({
|
[this.filePathsReceived],
|
||||||
label: chatModel.info.primaryChannel,
|
() => this.filePathsReceived.value.size,
|
||||||
path: chatModel.getBasePath(),
|
);
|
||||||
});
|
filesReceivedText: React.State<string> = React.createProxyState(
|
||||||
}
|
[this.filesReceivedCount],
|
||||||
};
|
() =>
|
||||||
|
translations.dataTransferModal.filesReceivedCount(
|
||||||
getTransferData = (): void => {
|
this.filesReceivedCount.value,
|
||||||
const transferData: TransferData =
|
),
|
||||||
this.fileTransferModel.generateTransferData();
|
|
||||||
this.transferChannel.value = transferData.channel;
|
|
||||||
this.transferKey.value = transferData.key;
|
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
|
||||||
showDirectionSelectionModal = (): void => {
|
|
||||||
this.presentedModal.value = FileTransferModal.DirectionSelection;
|
|
||||||
this.getOptions();
|
|
||||||
};
|
|
||||||
|
|
||||||
showFileSelectionModal = (): void => {
|
|
||||||
this.presentedModal.value = FileTransferModal.FileSelection;
|
|
||||||
};
|
|
||||||
|
|
||||||
showTransferDataModal = (): void => {
|
|
||||||
this.presentedModal.value = FileTransferModal.TransferDataDisplay;
|
|
||||||
this.getTransferData();
|
|
||||||
};
|
|
||||||
|
|
||||||
initiateTransfer = (): void => {
|
|
||||||
this.presentedModal.value = FileTransferModal.TransferDisplay;
|
|
||||||
this.didNotFinishSending.value = true;
|
|
||||||
this.filePathsSent.clear();
|
|
||||||
|
|
||||||
this.fileTransferModel.sendFiles(
|
|
||||||
this.selectedPaths.value.values(),
|
|
||||||
(path: string) => {
|
|
||||||
console.log(path);
|
|
||||||
this.filePathsSent.add(path);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.didNotFinishSending.value = false;
|
// guards
|
||||||
};
|
hasNoPathsSelected: React.State<boolean> = React.createProxyState(
|
||||||
|
[this.selectedPaths],
|
||||||
|
() => this.selectedPaths.value.size == 0,
|
||||||
|
);
|
||||||
|
didNotFinishSending: React.State<boolean> = new React.State(true);
|
||||||
|
cannotPrepareToReceive: React.State<boolean> = React.createProxyState(
|
||||||
|
[this.receivingTransferChannel, this.receivingTransferKey],
|
||||||
|
() =>
|
||||||
|
this.receivingTransferChannel.value == "" ||
|
||||||
|
this.receivingTransferKey.value == "",
|
||||||
|
);
|
||||||
|
|
||||||
showTransferDataInputModal = (): void => {
|
// handlers
|
||||||
this.presentedModal.value = FileTransferModal.TransferDataInput;
|
handleReceivedFile = (path: string): void => {
|
||||||
};
|
this.filePathsReceived.add(path);
|
||||||
|
|
||||||
prepareReceivingData = (): void => {
|
|
||||||
this.presentedModal.value = FileTransferModal.ReceptionDisplay;
|
|
||||||
this.filePathsReceived.clear();
|
|
||||||
|
|
||||||
const transferData: TransferData = {
|
|
||||||
channel: this.receivingTransferChannel.value,
|
|
||||||
key: this.receivingTransferKey.value,
|
|
||||||
};
|
};
|
||||||
this.fileTransferModel.prepareToReceive(transferData);
|
|
||||||
};
|
|
||||||
|
|
||||||
hideModal = (): void => {
|
// methods
|
||||||
this.presentedModal.value = undefined;
|
getOptions = (): void => {
|
||||||
};
|
this.generalFileOptions.clear();
|
||||||
|
this.chatFileOptions.clear();
|
||||||
|
this.selectedPaths.clear();
|
||||||
|
|
||||||
// init
|
this.generalFileOptions.add(
|
||||||
constructor(
|
{
|
||||||
fileTransferModel: FileTransferModel,
|
label: translations.dataTransferModal.connectionData,
|
||||||
chatListModel: ChatListModel
|
path: StorageModel.getPath(
|
||||||
) {
|
StorageModelSubPath.ConnectionModel,
|
||||||
this.fileTransferModel = fileTransferModel;
|
filePaths.connectionModel.previousAddresses,
|
||||||
this.chatListModel = chatListModel;
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: translations.dataTransferModal.settingsData,
|
||||||
|
path: StorageModel.getPath(
|
||||||
|
StorageModelSubPath.SettingsModel,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.fileTransferModel.fileHandlerManager.addHandler(
|
const chatModels: Set<ChatModel> = this.chatListModel.chatModels;
|
||||||
this.handleReceivedFile
|
for (const chatModel of chatModels) {
|
||||||
);
|
this.chatFileOptions.add({
|
||||||
}
|
label: chatModel.info.primaryChannel,
|
||||||
|
path: chatModel.getBasePath(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getTransferData = (): void => {
|
||||||
|
const transferData: TransferData =
|
||||||
|
this.fileTransferModel.generateTransferData();
|
||||||
|
this.transferChannel.value = transferData.channel;
|
||||||
|
this.transferKey.value = transferData.key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// view
|
||||||
|
showDirectionSelectionModal = (): void => {
|
||||||
|
this.presentedModal.value = FileTransferModal.DirectionSelection;
|
||||||
|
this.getOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
showFileSelectionModal = (): void => {
|
||||||
|
this.presentedModal.value = FileTransferModal.FileSelection;
|
||||||
|
};
|
||||||
|
|
||||||
|
showTransferDataModal = (): void => {
|
||||||
|
this.presentedModal.value = FileTransferModal.TransferDataDisplay;
|
||||||
|
this.getTransferData();
|
||||||
|
};
|
||||||
|
|
||||||
|
initiateTransfer = (): void => {
|
||||||
|
this.presentedModal.value = FileTransferModal.TransferDisplay;
|
||||||
|
this.didNotFinishSending.value = true;
|
||||||
|
this.filePathsSent.clear();
|
||||||
|
|
||||||
|
this.fileTransferModel.sendFiles(
|
||||||
|
this.selectedPaths.value.values(),
|
||||||
|
(path: string) => {
|
||||||
|
console.log(path);
|
||||||
|
this.filePathsSent.add(path);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.didNotFinishSending.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
showTransferDataInputModal = (): void => {
|
||||||
|
this.presentedModal.value = FileTransferModal.TransferDataInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
prepareReceivingData = (): void => {
|
||||||
|
this.presentedModal.value = FileTransferModal.ReceptionDisplay;
|
||||||
|
this.filePathsReceived.clear();
|
||||||
|
|
||||||
|
const transferData: TransferData = {
|
||||||
|
channel: this.receivingTransferChannel.value,
|
||||||
|
key: this.receivingTransferKey.value,
|
||||||
|
};
|
||||||
|
this.fileTransferModel.prepareToReceive(transferData);
|
||||||
|
};
|
||||||
|
|
||||||
|
hideModal = (): void => {
|
||||||
|
this.presentedModal.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
fileTransferModel: FileTransferModel,
|
||||||
|
chatListModel: ChatListModel,
|
||||||
|
) {
|
||||||
|
this.fileTransferModel = fileTransferModel;
|
||||||
|
this.chatListModel = chatListModel;
|
||||||
|
|
||||||
|
this.fileTransferModel.fileHandlerManager.addHandler(
|
||||||
|
this.handleReceivedFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileTransferOption {
|
export interface FileTransferOption {
|
||||||
label: string;
|
label: string;
|
||||||
path: string[];
|
path: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FileTransferModal {
|
export enum FileTransferModal {
|
||||||
DirectionSelection,
|
DirectionSelection,
|
||||||
|
|
||||||
// sending
|
// sending
|
||||||
FileSelection,
|
FileSelection,
|
||||||
TransferDataDisplay,
|
TransferDataDisplay,
|
||||||
TransferDisplay,
|
TransferDisplay,
|
||||||
|
|
||||||
// receiving
|
// receiving
|
||||||
TransferDataInput,
|
TransferDataInput,
|
||||||
ReceptionDisplay,
|
ReceptionDisplay,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,44 +4,44 @@ import CoreViewModel from "./coreViewModel";
|
|||||||
import SettingsModel from "../../Model/Global/settingsModel";
|
import SettingsModel from "../../Model/Global/settingsModel";
|
||||||
|
|
||||||
export default class SettingsViewModel {
|
export default class SettingsViewModel {
|
||||||
settingsModel: SettingsModel;
|
settingsModel: SettingsModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
username: React.State<string> = new React.State("");
|
username: React.State<string> = new React.State("");
|
||||||
usernameInput: React.State<string> = new React.State("");
|
usernameInput: React.State<string> = new React.State("");
|
||||||
firstDayOfWeekInput: React.State<string> = new React.State("0");
|
firstDayOfWeekInput: React.State<string> = new React.State("0");
|
||||||
|
|
||||||
// guards
|
// guards
|
||||||
cannotSetName: React.State<boolean> = React.createProxyState(
|
cannotSetName: React.State<boolean> = React.createProxyState(
|
||||||
[this.usernameInput],
|
[this.usernameInput],
|
||||||
() =>
|
() =>
|
||||||
this.usernameInput.value == "" ||
|
this.usernameInput.value == "" ||
|
||||||
this.usernameInput.value == this.settingsModel.username
|
this.usernameInput.value == this.settingsModel.username,
|
||||||
);
|
);
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
setName = (): void => {
|
setName = (): void => {
|
||||||
this.settingsModel.setName(this.usernameInput.value);
|
this.settingsModel.setName(this.usernameInput.value);
|
||||||
this.username.value = this.settingsModel.username;
|
this.username.value = this.settingsModel.username;
|
||||||
this.usernameInput.callSubscriptions();
|
this.usernameInput.callSubscriptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
setFirstDayofWeek = (): void => {
|
setFirstDayofWeek = (): void => {
|
||||||
this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value);
|
this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
public coreViewModel: CoreViewModel,
|
public coreViewModel: CoreViewModel,
|
||||||
settingsModel: SettingsModel
|
settingsModel: SettingsModel,
|
||||||
) {
|
) {
|
||||||
this.settingsModel = settingsModel;
|
this.settingsModel = settingsModel;
|
||||||
|
|
||||||
this.username.value = settingsModel.username;
|
this.username.value = settingsModel.username;
|
||||||
this.usernameInput.value = settingsModel.username;
|
this.usernameInput.value = settingsModel.username;
|
||||||
this.firstDayOfWeekInput.value = settingsModel.firstDayOfWeek;
|
this.firstDayOfWeekInput.value = settingsModel.firstDayOfWeek;
|
||||||
|
|
||||||
// subscriptions
|
// subscriptions
|
||||||
this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek);
|
this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,81 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
PATH_COMPONENT_SEPARATOR,
|
PATH_COMPONENT_SEPARATOR,
|
||||||
} from "../../Model/Global/storageModel";
|
} from "../../Model/Global/storageModel";
|
||||||
|
|
||||||
import CoreViewModel from "./coreViewModel";
|
import CoreViewModel from "./coreViewModel";
|
||||||
import { translations } from "../../View/translations";
|
import { translations } from "../../View/translations";
|
||||||
|
|
||||||
export default class StorageViewModel {
|
export default class StorageViewModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
isShowingStorageModal: React.State<boolean> = new React.State(false);
|
isShowingStorageModal: React.State<boolean> = new React.State(false);
|
||||||
selectedPath: React.State<string> = new React.State(PATH_COMPONENT_SEPARATOR);
|
selectedPath: React.State<string> = new React.State(
|
||||||
didMakeChanges: React.State<boolean> = new React.State(false);
|
PATH_COMPONENT_SEPARATOR,
|
||||||
|
|
||||||
selectedFileName: React.State<string>;
|
|
||||||
selectedFileContent: React.State<string>;
|
|
||||||
|
|
||||||
lastDeletedItemPath: React.State<string> = new React.State("");
|
|
||||||
|
|
||||||
// methods
|
|
||||||
getSelectedItemContent = (): string => {
|
|
||||||
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
|
|
||||||
const content = this.storageModel.read(path);
|
|
||||||
return (
|
|
||||||
(content ?? translations.storage.notAFile) ||
|
|
||||||
translations.storage.contentEmpty
|
|
||||||
);
|
);
|
||||||
};
|
didMakeChanges: React.State<boolean> = new React.State(false);
|
||||||
|
|
||||||
deleteSelectedItem = (): void => {
|
selectedFileName: React.State<string>;
|
||||||
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
|
selectedFileContent: React.State<string>;
|
||||||
this.lastDeletedItemPath.value = this.selectedPath.value;
|
|
||||||
|
|
||||||
this.storageModel.removeRecursively(path);
|
lastDeletedItemPath: React.State<string> = new React.State("");
|
||||||
this.didMakeChanges.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
removeJunk = (): void => {
|
// methods
|
||||||
this.storageModel.removeJunk();
|
getSelectedItemContent = (): string => {
|
||||||
this.selectedPath.value = PATH_COMPONENT_SEPARATOR;
|
const path = StorageModel.stringToPathComponents(
|
||||||
};
|
this.selectedPath.value,
|
||||||
|
);
|
||||||
|
const content = this.storageModel.read(path);
|
||||||
|
return (
|
||||||
|
(content ?? translations.storage.notAFile) ||
|
||||||
|
translations.storage.contentEmpty
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// view
|
deleteSelectedItem = (): void => {
|
||||||
showStorageModal = (): void => {
|
const path = StorageModel.stringToPathComponents(
|
||||||
this.isShowingStorageModal.value = true;
|
this.selectedPath.value,
|
||||||
};
|
);
|
||||||
|
this.lastDeletedItemPath.value = this.selectedPath.value;
|
||||||
|
|
||||||
hideStorageModal = (): void => {
|
this.storageModel.removeRecursively(path);
|
||||||
if (this.didMakeChanges.value == true) {
|
this.didMakeChanges.value = true;
|
||||||
window.location.reload();
|
};
|
||||||
return;
|
|
||||||
|
removeJunk = (): void => {
|
||||||
|
this.storageModel.removeJunk();
|
||||||
|
this.selectedPath.value = PATH_COMPONENT_SEPARATOR;
|
||||||
|
};
|
||||||
|
|
||||||
|
// view
|
||||||
|
showStorageModal = (): void => {
|
||||||
|
this.isShowingStorageModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
hideStorageModal = (): void => {
|
||||||
|
if (this.didMakeChanges.value == true) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isShowingStorageModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
storageModel: StorageModel,
|
||||||
|
) {
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
|
||||||
|
this.selectedFileName = React.createProxyState(
|
||||||
|
[this.selectedPath],
|
||||||
|
() => StorageModel.getFileNameFromString(this.selectedPath.value),
|
||||||
|
);
|
||||||
|
this.selectedFileContent = React.createProxyState(
|
||||||
|
[this.selectedPath],
|
||||||
|
() => this.getSelectedItemContent(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.isShowingStorageModal.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(public coreViewModel: CoreViewModel, storageModel: StorageModel) {
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
|
|
||||||
this.selectedFileName = React.createProxyState([this.selectedPath], () =>
|
|
||||||
StorageModel.getFileNameFromString(this.selectedPath.value)
|
|
||||||
);
|
|
||||||
this.selectedFileContent = React.createProxyState([this.selectedPath], () =>
|
|
||||||
this.getSelectedItemContent()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
BoardInfoFileContent,
|
BoardInfoFileContent,
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
|
|
||||||
import ChatViewModel from "../Chat/chatViewModel";
|
import ChatViewModel from "../Chat/chatViewModel";
|
||||||
@@ -15,293 +15,299 @@ import TaskPageViewModel from "./taskPageViewModel";
|
|||||||
import TaskViewModel from "./taskViewModel";
|
import TaskViewModel from "./taskViewModel";
|
||||||
|
|
||||||
export default class BoardViewModel extends TaskContainingPageViewModel {
|
export default class BoardViewModel extends TaskContainingPageViewModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
boardsAndTasksModel: BoardsAndTasksModel;
|
boardsAndTasksModel: BoardsAndTasksModel;
|
||||||
|
|
||||||
taskPageViewModel: TaskPageViewModel;
|
taskPageViewModel: TaskPageViewModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
boardInfo: BoardInfoFileContent;
|
boardInfo: BoardInfoFileContent;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
name: React.State<string> = new React.State("");
|
name: React.State<string> = new React.State("");
|
||||||
color: React.State<Color> = new React.State<any>(Color.Standard);
|
color: React.State<Color> = new React.State<any>(Color.Standard);
|
||||||
|
|
||||||
index: React.State<number> = new React.State(0);
|
index: React.State<number> = new React.State(0);
|
||||||
|
|
||||||
selectedPage: React.State<BoardPageType> = new React.State<any>(
|
selectedPage: React.State<BoardPageType> = new React.State<any>(
|
||||||
BoardPageType.List
|
BoardPageType.List,
|
||||||
);
|
|
||||||
|
|
||||||
isSelected: React.State<boolean>;
|
|
||||||
isPresentingSettingsModal: React.State<boolean> = new React.State(false);
|
|
||||||
isPresentingFilterModal: React.State<boolean> = new React.State(false);
|
|
||||||
|
|
||||||
searchViewModel: SearchViewModel<TaskViewModel>;
|
|
||||||
filteredTaskViewModels: React.ListState<TaskViewModel> =
|
|
||||||
new React.ListState();
|
|
||||||
|
|
||||||
// paths
|
|
||||||
getBasePath = (): string[] => {
|
|
||||||
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)];
|
|
||||||
};
|
|
||||||
|
|
||||||
getLastUsedBoardPath = (): string[] => {
|
|
||||||
return [...this.getBasePath(), BoardViewModelSubPath.LastUsedView];
|
|
||||||
};
|
|
||||||
|
|
||||||
getPreviousSearchesPath = (): string[] => {
|
|
||||||
return [...this.getBasePath(), BoardViewModelSubPath.PreviousSearches];
|
|
||||||
};
|
|
||||||
|
|
||||||
getLastSearchPath = (): string[] => {
|
|
||||||
return [...this.getBasePath(), BoardViewModelSubPath.LastSearch];
|
|
||||||
};
|
|
||||||
|
|
||||||
// settings
|
|
||||||
saveSettings = (): void => {
|
|
||||||
const newBoardInfoFileContent: BoardInfoFileContent =
|
|
||||||
BoardsAndTasksModel.createBoardInfoFileContent(
|
|
||||||
this.boardInfo.fileId,
|
|
||||||
this.name.value,
|
|
||||||
this.color.value
|
|
||||||
);
|
|
||||||
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
applyColor = (): void => {
|
|
||||||
this.taskPageViewModel.chatViewModel.setDisplayedColor(this.color.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteBoard = (): void => {
|
|
||||||
this.taskPageViewModel.deleteBoard(this.boardInfo);
|
|
||||||
this.chatViewModel.taskBoardSuggestions.remove(this.boardInfo.fileId);
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
// methods
|
|
||||||
createTask = (): void => {
|
|
||||||
this.createTaskFromBoardId(this.boardInfo.fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDropWithinBoard = (category?: string, status?: string): void => {
|
|
||||||
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
|
||||||
if (draggedObject instanceof TaskViewModel == false) return;
|
|
||||||
draggedObject.setCategoryAndStatus(category, status);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDropBetweenBoards = (): void => {
|
|
||||||
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
|
||||||
if (draggedObject instanceof TaskViewModel == false) return;
|
|
||||||
draggedObject.setBoardId(this.boardInfo.fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// storage
|
|
||||||
storeLastUsedView = (): void => {
|
|
||||||
const path: string[] = this.getLastUsedBoardPath();
|
|
||||||
const lastUsedView: string = this.selectedPage.value;
|
|
||||||
this.storageModel.write(path, lastUsedView);
|
|
||||||
};
|
|
||||||
|
|
||||||
restoreLastUsedView = (): void => {
|
|
||||||
const path: string[] = this.getLastUsedBoardPath();
|
|
||||||
const lastUsedView: string | null = this.storageModel.read(path);
|
|
||||||
if (lastUsedView == null) return;
|
|
||||||
|
|
||||||
this.selectedPage.value = lastUsedView as BoardPageType;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNewSearch = (searchTerm: string): void => {
|
|
||||||
const suggestionPath: string[] = [
|
|
||||||
...this.getPreviousSearchesPath(),
|
|
||||||
searchTerm,
|
|
||||||
];
|
|
||||||
this.storageModel.write(suggestionPath, "");
|
|
||||||
if (!this.coreViewModel.boardSearchSuggestions.value.has(searchTerm)) {
|
|
||||||
this.coreViewModel.boardSearchSuggestions.add(searchTerm);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSearchPath: string[] = this.getLastSearchPath();
|
|
||||||
this.storageModel.write(lastSearchPath, searchTerm);
|
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
|
||||||
showTask = (taskFileContent: TaskFileContent): void => {
|
|
||||||
if (taskFileContent.boardId != this.boardInfo.fileId) {
|
|
||||||
// remove task that was moved to different board
|
|
||||||
this.boardsAndTasksModel.deleteTaskReference(
|
|
||||||
this.boardInfo.fileId,
|
|
||||||
taskFileContent.fileId
|
|
||||||
);
|
|
||||||
this.removeTaskFromView(taskFileContent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskViewModel: TaskViewModel = new TaskViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this.chatViewModel,
|
|
||||||
this.boardsAndTasksModel,
|
|
||||||
this,
|
|
||||||
taskFileContent
|
|
||||||
);
|
);
|
||||||
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
|
isSelected: React.State<boolean>;
|
||||||
this.taskViewModels.remove(taskFileContent.fileId);
|
isPresentingSettingsModal: React.State<boolean> = new React.State(false);
|
||||||
this.updateIndex();
|
isPresentingFilterModal: React.State<boolean> = new React.State(false);
|
||||||
};
|
|
||||||
|
|
||||||
select = (): void => {
|
searchViewModel: SearchViewModel<TaskViewModel>;
|
||||||
this.taskPageViewModel.selectBoard(this);
|
filteredTaskViewModels: React.ListState<TaskViewModel> =
|
||||||
};
|
new React.ListState();
|
||||||
|
|
||||||
close = (): void => {
|
// paths
|
||||||
this.taskPageViewModel.closeBoard();
|
getBasePath = (): string[] => {
|
||||||
this.taskViewModels.clear();
|
return [
|
||||||
};
|
...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
showSettings = (): void => {
|
getLastUsedBoardPath = (): string[] => {
|
||||||
this.isPresentingSettingsModal.value = true;
|
return [...this.getBasePath(), BoardViewModelSubPath.LastUsedView];
|
||||||
};
|
};
|
||||||
|
|
||||||
hideSettings = (): void => {
|
getPreviousSearchesPath = (): string[] => {
|
||||||
this.saveSettings();
|
return [...this.getBasePath(), BoardViewModelSubPath.PreviousSearches];
|
||||||
this.isPresentingSettingsModal.value = false;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
showFilterModal = (): void => {
|
getLastSearchPath = (): string[] => {
|
||||||
this.isPresentingFilterModal.value = true;
|
return [...this.getBasePath(), BoardViewModelSubPath.LastSearch];
|
||||||
};
|
};
|
||||||
|
|
||||||
hideFilterModal = (): void => {
|
// settings
|
||||||
this.isPresentingFilterModal.value = false;
|
saveSettings = (): void => {
|
||||||
};
|
const newBoardInfoFileContent: BoardInfoFileContent =
|
||||||
|
BoardsAndTasksModel.createBoardInfoFileContent(
|
||||||
|
this.boardInfo.fileId,
|
||||||
|
this.name.value,
|
||||||
|
this.color.value,
|
||||||
|
);
|
||||||
|
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
|
||||||
|
};
|
||||||
|
|
||||||
updateIndex = (): void => {
|
applyColor = (): void => {
|
||||||
const index: number =
|
this.taskPageViewModel.chatViewModel.setDisplayedColor(
|
||||||
this.taskPageViewModel.boardIndexManager.getIndex(this);
|
this.color.value,
|
||||||
this.index.value = index;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// load
|
deleteBoard = (): void => {
|
||||||
loadListRelevantData = (): void => {
|
this.taskPageViewModel.deleteBoard(this.boardInfo);
|
||||||
this.name.value = this.boardInfo.name;
|
this.chatViewModel.taskBoardSuggestions.remove(this.boardInfo.fileId);
|
||||||
this.color.value = this.boardInfo.color;
|
this.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
loadTasks = (): void => {
|
// methods
|
||||||
const taskIds: string[] = this.boardsAndTasksModel.listTaskIds(
|
createTask = (): void => {
|
||||||
this.boardInfo.fileId
|
this.createTaskFromBoardId(this.boardInfo.fileId);
|
||||||
);
|
};
|
||||||
for (const taskId of taskIds) {
|
|
||||||
if (this.taskViewModels.value.has(taskId)) return;
|
|
||||||
|
|
||||||
const taskFileContent: TaskFileContent | null =
|
handleDropWithinBoard = (category?: string, status?: string): void => {
|
||||||
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
|
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
||||||
if (taskFileContent == null) continue;
|
if (draggedObject instanceof TaskViewModel == false) return;
|
||||||
|
draggedObject.setCategoryAndStatus(category, status);
|
||||||
|
};
|
||||||
|
|
||||||
const taskViewModel: TaskViewModel = new TaskViewModel(
|
handleDropBetweenBoards = (): void => {
|
||||||
this.coreViewModel,
|
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
||||||
this.chatViewModel,
|
if (draggedObject instanceof TaskViewModel == false) return;
|
||||||
this.boardsAndTasksModel,
|
draggedObject.setBoardId(this.boardInfo.fileId);
|
||||||
this,
|
};
|
||||||
taskFileContent
|
|
||||||
);
|
|
||||||
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTaskIndices();
|
// storage
|
||||||
};
|
storeLastUsedView = (): void => {
|
||||||
|
const path: string[] = this.getLastUsedBoardPath();
|
||||||
|
const lastUsedView: string = this.selectedPage.value;
|
||||||
|
this.storageModel.write(path, lastUsedView);
|
||||||
|
};
|
||||||
|
|
||||||
loadSearchSuggestions = (): void => {
|
restoreLastUsedView = (): void => {
|
||||||
const dirPath: string[] = this.getPreviousSearchesPath();
|
const path: string[] = this.getLastUsedBoardPath();
|
||||||
const searches: string[] = this.storageModel.list(dirPath);
|
const lastUsedView: string | null = this.storageModel.read(path);
|
||||||
this.coreViewModel.boardSearchSuggestions.add(...searches);
|
if (lastUsedView == null) return;
|
||||||
};
|
|
||||||
|
|
||||||
restoreSearch = (): void => {
|
this.selectedPage.value = lastUsedView as BoardPageType;
|
||||||
const lastSearchPath: string[] = this.getLastSearchPath();
|
};
|
||||||
const lastSearch: string | null = this.storageModel.read(lastSearchPath);
|
|
||||||
if (lastSearch != null) {
|
|
||||||
this.searchViewModel.search(lastSearch);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadData = (): void => {
|
handleNewSearch = (searchTerm: string): void => {
|
||||||
this.restoreLastUsedView();
|
const suggestionPath: string[] = [
|
||||||
this.loadTasks();
|
...this.getPreviousSearchesPath(),
|
||||||
this.loadSearchSuggestions();
|
searchTerm,
|
||||||
};
|
];
|
||||||
|
this.storageModel.write(suggestionPath, "");
|
||||||
|
if (!this.coreViewModel.boardSearchSuggestions.value.has(searchTerm)) {
|
||||||
|
this.coreViewModel.boardSearchSuggestions.add(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
// init
|
const lastSearchPath: string[] = this.getLastSearchPath();
|
||||||
constructor(
|
this.storageModel.write(lastSearchPath, searchTerm);
|
||||||
public coreViewModel: CoreViewModel,
|
};
|
||||||
public chatViewModel: ChatViewModel,
|
|
||||||
storageModel: StorageModel,
|
|
||||||
boardsAndTasksModel: BoardsAndTasksModel,
|
|
||||||
taskPageViewModel: TaskPageViewModel,
|
|
||||||
boardInfo: BoardInfoFileContent
|
|
||||||
) {
|
|
||||||
super(coreViewModel, chatViewModel, boardsAndTasksModel);
|
|
||||||
|
|
||||||
// set
|
// view
|
||||||
this.storageModel = storageModel;
|
showTask = (taskFileContent: TaskFileContent): void => {
|
||||||
this.boardsAndTasksModel = boardsAndTasksModel;
|
if (taskFileContent.boardId != this.boardInfo.fileId) {
|
||||||
this.taskPageViewModel = taskPageViewModel;
|
// remove task that was moved to different board
|
||||||
this.boardInfo = boardInfo;
|
this.boardsAndTasksModel.deleteTaskReference(
|
||||||
|
this.boardInfo.fileId,
|
||||||
|
taskFileContent.fileId,
|
||||||
|
);
|
||||||
|
this.removeTaskFromView(taskFileContent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskViewModel: TaskViewModel = new TaskViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this.chatViewModel,
|
||||||
|
this.boardsAndTasksModel,
|
||||||
|
this,
|
||||||
|
taskFileContent,
|
||||||
|
);
|
||||||
|
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
|
||||||
|
this.taskViewModels.remove(taskFileContent.fileId);
|
||||||
|
this.updateIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
select = (): void => {
|
||||||
|
this.taskPageViewModel.selectBoard(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
close = (): void => {
|
||||||
|
this.taskPageViewModel.closeBoard();
|
||||||
|
this.taskViewModels.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
showSettings = (): void => {
|
||||||
|
this.isPresentingSettingsModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
hideSettings = (): void => {
|
||||||
|
this.saveSettings();
|
||||||
|
this.isPresentingSettingsModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
showFilterModal = (): void => {
|
||||||
|
this.isPresentingFilterModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
hideFilterModal = (): void => {
|
||||||
|
this.isPresentingFilterModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateIndex = (): void => {
|
||||||
|
const index: number =
|
||||||
|
this.taskPageViewModel.boardIndexManager.getIndex(this);
|
||||||
|
this.index.value = index;
|
||||||
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
this.loadListRelevantData();
|
loadListRelevantData = (): void => {
|
||||||
|
this.name.value = this.boardInfo.name;
|
||||||
|
this.color.value = this.boardInfo.color;
|
||||||
|
};
|
||||||
|
|
||||||
// subscriptions
|
loadTasks = (): void => {
|
||||||
this.isSelected = React.createProxyState(
|
const taskIds: string[] = this.boardsAndTasksModel.listTaskIds(
|
||||||
[this.taskPageViewModel.selectedBoardId],
|
this.boardInfo.fileId,
|
||||||
() =>
|
);
|
||||||
this.taskPageViewModel.selectedBoardId.value == this.boardInfo.fileId
|
for (const taskId of taskIds) {
|
||||||
);
|
if (this.taskViewModels.value.has(taskId)) return;
|
||||||
|
|
||||||
this.color.subscribe(() => {
|
const taskFileContent: TaskFileContent | null =
|
||||||
if (this.isSelected.value == false) return;
|
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
|
||||||
this.applyColor();
|
if (taskFileContent == null) continue;
|
||||||
});
|
|
||||||
|
|
||||||
this.selectedPage.subscribeSilent(() => {
|
const taskViewModel: TaskViewModel = new TaskViewModel(
|
||||||
this.storeLastUsedView();
|
this.coreViewModel,
|
||||||
});
|
this.chatViewModel,
|
||||||
|
this.boardsAndTasksModel,
|
||||||
|
this,
|
||||||
|
taskFileContent,
|
||||||
|
);
|
||||||
|
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
// handlers
|
|
||||||
boardsAndTasksModel.taskHandlerManager.addHandler(
|
|
||||||
(taskFileContent: TaskFileContent) => {
|
|
||||||
if (taskFileContent.boardId != this.boardInfo.fileId) return;
|
|
||||||
this.showTask(taskFileContent);
|
|
||||||
this.updateTaskIndices();
|
this.updateTaskIndices();
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
// search
|
loadSearchSuggestions = (): void => {
|
||||||
this.searchViewModel = new SearchViewModel(
|
const dirPath: string[] = this.getPreviousSearchesPath();
|
||||||
this.taskViewModels,
|
const searches: string[] = this.storageModel.list(dirPath);
|
||||||
this.filteredTaskViewModels,
|
this.coreViewModel.boardSearchSuggestions.add(...searches);
|
||||||
TaskViewModel.getStringsForFilter,
|
};
|
||||||
this.coreViewModel.boardSearchSuggestions
|
|
||||||
);
|
restoreSearch = (): void => {
|
||||||
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
|
const lastSearchPath: string[] = this.getLastSearchPath();
|
||||||
this.handleNewSearch(newQuery);
|
const lastSearch: string | null =
|
||||||
});
|
this.storageModel.read(lastSearchPath);
|
||||||
this.restoreSearch();
|
if (lastSearch != null) {
|
||||||
}
|
this.searchViewModel.search(lastSearch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData = (): void => {
|
||||||
|
this.restoreLastUsedView();
|
||||||
|
this.loadTasks();
|
||||||
|
this.loadSearchSuggestions();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
public chatViewModel: ChatViewModel,
|
||||||
|
storageModel: StorageModel,
|
||||||
|
boardsAndTasksModel: BoardsAndTasksModel,
|
||||||
|
taskPageViewModel: TaskPageViewModel,
|
||||||
|
boardInfo: BoardInfoFileContent,
|
||||||
|
) {
|
||||||
|
super(coreViewModel, chatViewModel, boardsAndTasksModel);
|
||||||
|
|
||||||
|
// set
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.boardsAndTasksModel = boardsAndTasksModel;
|
||||||
|
this.taskPageViewModel = taskPageViewModel;
|
||||||
|
this.boardInfo = boardInfo;
|
||||||
|
|
||||||
|
// load
|
||||||
|
this.loadListRelevantData();
|
||||||
|
|
||||||
|
// subscriptions
|
||||||
|
this.isSelected = React.createProxyState(
|
||||||
|
[this.taskPageViewModel.selectedBoardId],
|
||||||
|
() =>
|
||||||
|
this.taskPageViewModel.selectedBoardId.value ==
|
||||||
|
this.boardInfo.fileId,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.color.subscribe(() => {
|
||||||
|
if (this.isSelected.value == false) return;
|
||||||
|
this.applyColor();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectedPage.subscribeSilent(() => {
|
||||||
|
this.storeLastUsedView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
boardsAndTasksModel.taskHandlerManager.addHandler(
|
||||||
|
(taskFileContent: TaskFileContent) => {
|
||||||
|
if (taskFileContent.boardId != this.boardInfo.fileId) return;
|
||||||
|
this.showTask(taskFileContent);
|
||||||
|
this.updateTaskIndices();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// search
|
||||||
|
this.searchViewModel = new SearchViewModel(
|
||||||
|
this.taskViewModels,
|
||||||
|
this.filteredTaskViewModels,
|
||||||
|
TaskViewModel.getStringsForFilter,
|
||||||
|
this.coreViewModel.boardSearchSuggestions,
|
||||||
|
);
|
||||||
|
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
|
||||||
|
this.handleNewSearch(newQuery);
|
||||||
|
});
|
||||||
|
this.restoreSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BoardViewModelSubPath {
|
export enum BoardViewModelSubPath {
|
||||||
LastUsedView = "last-used-view",
|
LastUsedView = "last-used-view",
|
||||||
PreviousSearches = "previous-searches",
|
PreviousSearches = "previous-searches",
|
||||||
LastSearch = "last-search",
|
LastSearch = "last-search",
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export enum BoardPageType {
|
export enum BoardPageType {
|
||||||
List = "list",
|
List = "list",
|
||||||
Kanban = "kanban",
|
Kanban = "kanban",
|
||||||
StatusGrid = "status-grid",
|
StatusGrid = "status-grid",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
|
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
|
||||||
|
|
||||||
@@ -14,201 +14,208 @@ import TaskViewModel from "./taskViewModel";
|
|||||||
export const CALENDAR_EVENT_BOARD_ID = "events";
|
export const CALENDAR_EVENT_BOARD_ID = "events";
|
||||||
|
|
||||||
export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
calendarModel: CalendarModel;
|
calendarModel: CalendarModel;
|
||||||
boardsAndTasksModel: BoardsAndTasksModel;
|
boardsAndTasksModel: BoardsAndTasksModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
get monthString(): string {
|
get monthString(): string {
|
||||||
return CalendarModel.getMonthString(
|
return CalendarModel.getMonthString(
|
||||||
this.selectedYear.value.toString(),
|
this.selectedYear.value.toString(),
|
||||||
this.selectedMonth.value.toString()
|
this.selectedMonth.value.toString(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// paths
|
|
||||||
getBasePath = (): string[] => {
|
|
||||||
return [...this.calendarModel.getViewPath()];
|
|
||||||
};
|
|
||||||
|
|
||||||
// state
|
|
||||||
selectedYear: React.State<number> = new React.State(0);
|
|
||||||
selectedMonth: React.State<number> = new React.State(0);
|
|
||||||
selectedDate: React.State<number> = new React.State(0);
|
|
||||||
|
|
||||||
monthGrid: React.State<MonthGrid<React.MapState<TaskViewModel>> | undefined> =
|
|
||||||
new React.State<any>(undefined);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
createEvent = (): void => {
|
|
||||||
const taskFileContent: TaskFileContent =
|
|
||||||
this.boardsAndTasksModel.createTask(CALENDAR_EVENT_BOARD_ID);
|
|
||||||
taskFileContent.date = CalendarModel.getISODateString(
|
|
||||||
this.selectedYear.value.toString(),
|
|
||||||
this.selectedMonth.value.toString(),
|
|
||||||
this.selectedDate.value.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
const taskViewModel: TaskViewModel = new TaskViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this.chatViewModel,
|
|
||||||
this.boardsAndTasksModel,
|
|
||||||
this,
|
|
||||||
taskFileContent
|
|
||||||
);
|
|
||||||
|
|
||||||
this.selectTask(taskViewModel);
|
|
||||||
this.updateTaskIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
|
|
||||||
const paddedDate: string = CalendarModel.padDateOrMonth(
|
|
||||||
this.selectedDate.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.monthGrid.value == undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.monthGrid.value.days[paddedDate];
|
// paths
|
||||||
};
|
getBasePath = (): string[] => {
|
||||||
|
return [...this.calendarModel.getViewPath()];
|
||||||
|
};
|
||||||
|
|
||||||
// view
|
// state
|
||||||
getTaskMapState = (
|
selectedYear: React.State<number> = new React.State(0);
|
||||||
taskFileContent: TaskFileContent
|
selectedMonth: React.State<number> = new React.State(0);
|
||||||
): React.MapState<TaskViewModel> | null => {
|
selectedDate: React.State<number> = new React.State(0);
|
||||||
if (this.monthGrid.value == null) return null;
|
|
||||||
|
|
||||||
const date: string = CalendarModel.isoToDateString(
|
monthGrid: React.State<
|
||||||
taskFileContent.date ?? ""
|
MonthGrid<React.MapState<TaskViewModel>> | undefined
|
||||||
);
|
> = new React.State<any>(undefined);
|
||||||
return this.monthGrid.value.days[date];
|
|
||||||
};
|
|
||||||
|
|
||||||
showTask = (taskFileContent: TaskFileContent): void => {
|
// methods
|
||||||
const monthString: string | undefined = CalendarModel.isoToMonthString(
|
createEvent = (): void => {
|
||||||
taskFileContent.date ?? ""
|
const taskFileContent: TaskFileContent =
|
||||||
);
|
this.boardsAndTasksModel.createTask(CALENDAR_EVENT_BOARD_ID);
|
||||||
if (monthString == undefined || monthString != this.monthString) {
|
taskFileContent.date = CalendarModel.getISODateString(
|
||||||
this.removeTaskFromView(taskFileContent);
|
this.selectedYear.value.toString(),
|
||||||
this.calendarModel.deleteTaskReference(
|
this.selectedMonth.value.toString(),
|
||||||
this.monthString,
|
this.selectedDate.value.toString(),
|
||||||
taskFileContent.fileId
|
);
|
||||||
);
|
|
||||||
return;
|
const taskViewModel: TaskViewModel = new TaskViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this.chatViewModel,
|
||||||
|
this.boardsAndTasksModel,
|
||||||
|
this,
|
||||||
|
taskFileContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.selectTask(taskViewModel);
|
||||||
|
this.updateTaskIndices();
|
||||||
|
};
|
||||||
|
|
||||||
|
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
|
||||||
|
const paddedDate: string = CalendarModel.padDateOrMonth(
|
||||||
|
this.selectedDate.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.monthGrid.value == undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.monthGrid.value.days[paddedDate];
|
||||||
|
};
|
||||||
|
|
||||||
|
// view
|
||||||
|
getTaskMapState = (
|
||||||
|
taskFileContent: TaskFileContent,
|
||||||
|
): React.MapState<TaskViewModel> | null => {
|
||||||
|
if (this.monthGrid.value == null) return null;
|
||||||
|
|
||||||
|
const date: string = CalendarModel.isoToDateString(
|
||||||
|
taskFileContent.date ?? "",
|
||||||
|
);
|
||||||
|
return this.monthGrid.value.days[date];
|
||||||
|
};
|
||||||
|
|
||||||
|
showTask = (taskFileContent: TaskFileContent): void => {
|
||||||
|
const monthString: string | undefined = CalendarModel.isoToMonthString(
|
||||||
|
taskFileContent.date ?? "",
|
||||||
|
);
|
||||||
|
if (monthString == undefined || monthString != this.monthString) {
|
||||||
|
this.removeTaskFromView(taskFileContent);
|
||||||
|
this.calendarModel.deleteTaskReference(
|
||||||
|
this.monthString,
|
||||||
|
taskFileContent.fileId,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskViewModel: TaskViewModel = new TaskViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this.chatViewModel,
|
||||||
|
this.boardsAndTasksModel,
|
||||||
|
this,
|
||||||
|
taskFileContent,
|
||||||
|
);
|
||||||
|
const mapState: React.MapState<TaskViewModel> | null =
|
||||||
|
this.getTaskMapState(taskFileContent);
|
||||||
|
|
||||||
|
this.taskViewModels.handleRemoval(taskViewModel, () => {
|
||||||
|
mapState?.remove(taskFileContent.fileId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.taskViewModels.remove(taskFileContent.fileId);
|
||||||
|
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
|
||||||
|
|
||||||
|
mapState?.set(taskFileContent.fileId, taskViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
|
||||||
|
this.taskViewModels.remove(taskFileContent.fileId);
|
||||||
|
};
|
||||||
|
|
||||||
|
showToday = (): void => {
|
||||||
|
const today: Date = new Date();
|
||||||
|
this.selectedYear.value = today.getFullYear();
|
||||||
|
this.selectedMonth.value = today.getMonth() + 1;
|
||||||
|
this.selectedDate.value = today.getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
showPreviousMonth = (): void => {
|
||||||
|
this.selectedMonth.value -= 1;
|
||||||
|
|
||||||
|
if (this.selectedMonth.value <= 0) {
|
||||||
|
this.selectedYear.value -= 1;
|
||||||
|
this.selectedMonth.value = 12;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
showNextMonth = (): void => {
|
||||||
|
this.selectedMonth.value += 1;
|
||||||
|
|
||||||
|
if (this.selectedMonth.value >= 13) {
|
||||||
|
this.selectedYear.value += 1;
|
||||||
|
this.selectedMonth.value = 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDrop = (year: string, month: string, date: string): void => {
|
||||||
|
const ISOString: string = CalendarModel.getISODateString(
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
date,
|
||||||
|
);
|
||||||
|
|
||||||
|
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
||||||
|
if (draggedObject instanceof TaskViewModel == false) return;
|
||||||
|
|
||||||
|
draggedObject.setDate(ISOString);
|
||||||
|
};
|
||||||
|
|
||||||
|
// load
|
||||||
|
loadMonthTasks = (): void => {
|
||||||
|
this.monthGrid.value = this.calendarModel.generateMonthGrid(
|
||||||
|
this.selectedYear.value,
|
||||||
|
this.selectedMonth.value,
|
||||||
|
() => new React.MapState(),
|
||||||
|
);
|
||||||
|
const taskIds: string[] = this.calendarModel.listTaskIds(
|
||||||
|
this.monthString,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const taskId of taskIds) {
|
||||||
|
const taskFileContent: TaskFileContent | null =
|
||||||
|
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
|
||||||
|
if (taskFileContent == null) continue;
|
||||||
|
this.showTask(taskFileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTaskIndices();
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData = (): void => {
|
||||||
|
this.loadMonthTasks();
|
||||||
|
this.showToday();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
public chatViewModel: ChatViewModel,
|
||||||
|
storageModel: StorageModel,
|
||||||
|
calendarModel: CalendarModel,
|
||||||
|
boardsAndTasksModel: BoardsAndTasksModel,
|
||||||
|
) {
|
||||||
|
super(coreViewModel, chatViewModel, boardsAndTasksModel);
|
||||||
|
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.calendarModel = calendarModel;
|
||||||
|
this.boardsAndTasksModel = boardsAndTasksModel;
|
||||||
|
|
||||||
|
this.chatViewModel = chatViewModel;
|
||||||
|
|
||||||
|
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
|
||||||
|
this.loadMonthTasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
boardsAndTasksModel.taskHandlerManager.addHandler(
|
||||||
|
(taskFileContent: TaskFileContent) => {
|
||||||
|
this.showTask(taskFileContent);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskViewModel: TaskViewModel = new TaskViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this.chatViewModel,
|
|
||||||
this.boardsAndTasksModel,
|
|
||||||
this,
|
|
||||||
taskFileContent
|
|
||||||
);
|
|
||||||
const mapState: React.MapState<TaskViewModel> | null =
|
|
||||||
this.getTaskMapState(taskFileContent);
|
|
||||||
|
|
||||||
this.taskViewModels.handleRemoval(taskViewModel, () => {
|
|
||||||
mapState?.remove(taskFileContent.fileId);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.taskViewModels.remove(taskFileContent.fileId);
|
|
||||||
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
|
|
||||||
|
|
||||||
mapState?.set(taskFileContent.fileId, taskViewModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
|
|
||||||
this.taskViewModels.remove(taskFileContent.fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
showToday = (): void => {
|
|
||||||
const today: Date = new Date();
|
|
||||||
this.selectedYear.value = today.getFullYear();
|
|
||||||
this.selectedMonth.value = today.getMonth() + 1;
|
|
||||||
this.selectedDate.value = today.getDate();
|
|
||||||
};
|
|
||||||
|
|
||||||
showPreviousMonth = (): void => {
|
|
||||||
this.selectedMonth.value -= 1;
|
|
||||||
|
|
||||||
if (this.selectedMonth.value <= 0) {
|
|
||||||
this.selectedYear.value -= 1;
|
|
||||||
this.selectedMonth.value = 12;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
showNextMonth = (): void => {
|
|
||||||
this.selectedMonth.value += 1;
|
|
||||||
|
|
||||||
if (this.selectedMonth.value >= 13) {
|
|
||||||
this.selectedYear.value += 1;
|
|
||||||
this.selectedMonth.value = 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDrop = (year: string, month: string, date: string): void => {
|
|
||||||
const ISOString: string = CalendarModel.getISODateString(year, month, date);
|
|
||||||
|
|
||||||
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
|
||||||
if (draggedObject instanceof TaskViewModel == false) return;
|
|
||||||
|
|
||||||
draggedObject.setDate(ISOString);
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
|
||||||
loadMonthTasks = (): void => {
|
|
||||||
this.monthGrid.value = this.calendarModel.generateMonthGrid(
|
|
||||||
this.selectedYear.value,
|
|
||||||
this.selectedMonth.value,
|
|
||||||
() => new React.MapState()
|
|
||||||
);
|
|
||||||
const taskIds: string[] = this.calendarModel.listTaskIds(this.monthString);
|
|
||||||
|
|
||||||
for (const taskId of taskIds) {
|
|
||||||
const taskFileContent: TaskFileContent | null =
|
|
||||||
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
|
|
||||||
if (taskFileContent == null) continue;
|
|
||||||
this.showTask(taskFileContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTaskIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
loadData = (): void => {
|
|
||||||
this.loadMonthTasks();
|
|
||||||
this.showToday();
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
public chatViewModel: ChatViewModel,
|
|
||||||
storageModel: StorageModel,
|
|
||||||
calendarModel: CalendarModel,
|
|
||||||
boardsAndTasksModel: BoardsAndTasksModel
|
|
||||||
) {
|
|
||||||
super(coreViewModel, chatViewModel, boardsAndTasksModel);
|
|
||||||
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.calendarModel = calendarModel;
|
|
||||||
this.boardsAndTasksModel = boardsAndTasksModel;
|
|
||||||
|
|
||||||
this.chatViewModel = chatViewModel;
|
|
||||||
|
|
||||||
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
|
|
||||||
this.loadMonthTasks();
|
|
||||||
});
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
boardsAndTasksModel.taskHandlerManager.addHandler(
|
|
||||||
(taskFileContent: TaskFileContent) => {
|
|
||||||
this.showTask(taskFileContent);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TaskPageViewModelSubPath {
|
export enum TaskPageViewModelSubPath {
|
||||||
LastUsedBoard = "last-used-board",
|
LastUsedBoard = "last-used-board",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,76 +6,83 @@ import ChatViewModel from "../Chat/chatViewModel";
|
|||||||
import CoreViewModel from "../Global/coreViewModel";
|
import CoreViewModel from "../Global/coreViewModel";
|
||||||
|
|
||||||
export default class MessagePageViewModel {
|
export default class MessagePageViewModel {
|
||||||
chatViewModel: ChatViewModel;
|
chatViewModel: ChatViewModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
chatMessageViewModels: React.MapState<ChatMessageViewModel> =
|
chatMessageViewModels: React.MapState<ChatMessageViewModel> =
|
||||||
new React.MapState();
|
new React.MapState();
|
||||||
composingMessage: React.State<string> = new React.State("");
|
composingMessage: React.State<string> = new React.State("");
|
||||||
|
|
||||||
// guards
|
// guards
|
||||||
cannotSendMessage: React.State<boolean>;
|
cannotSendMessage: React.State<boolean>;
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
sendMessage = (): void => {
|
sendMessage = (): void => {
|
||||||
if (this.cannotSendMessage.value == true) return;
|
if (this.cannotSendMessage.value == true) return;
|
||||||
|
|
||||||
this.sendMessageFromBody(this.composingMessage.value);
|
this.sendMessageFromBody(this.composingMessage.value);
|
||||||
this.composingMessage.value = "";
|
this.composingMessage.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessageFromBody = (body: string): void => {
|
sendMessageFromBody = (body: string): void => {
|
||||||
this.chatViewModel.chatModel.sendMessage(body);
|
this.chatViewModel.chatModel.sendMessage(body);
|
||||||
};
|
};
|
||||||
|
|
||||||
decryptMessage = async (
|
decryptMessage = async (
|
||||||
messageViewModel: ChatMessageViewModel
|
messageViewModel: ChatMessageViewModel,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const chatMessage: ChatMessage = messageViewModel.chatMessage;
|
const chatMessage: ChatMessage = messageViewModel.chatMessage;
|
||||||
await this.chatViewModel.chatModel.decryptMessage(chatMessage);
|
await this.chatViewModel.chatModel.decryptMessage(chatMessage);
|
||||||
this.chatViewModel.chatModel.addMessage(chatMessage);
|
this.chatViewModel.chatModel.addMessage(chatMessage);
|
||||||
messageViewModel.loadData();
|
messageViewModel.loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
// view
|
// view
|
||||||
showChatMessage = (chatMessage: ChatMessage): void => {
|
showChatMessage = (chatMessage: ChatMessage): void => {
|
||||||
const chatMessageViewModel = new ChatMessageViewModel(
|
const chatMessageViewModel = new ChatMessageViewModel(
|
||||||
this.coreViewModel,
|
this.coreViewModel,
|
||||||
this,
|
this,
|
||||||
chatMessage,
|
chatMessage,
|
||||||
chatMessage.sender == this.chatViewModel.settingsViewModel.username.value
|
chatMessage.sender ==
|
||||||
);
|
this.chatViewModel.settingsViewModel.username.value,
|
||||||
|
);
|
||||||
|
|
||||||
const existingChatMessageViewModel: ChatMessageViewModel | undefined =
|
const existingChatMessageViewModel: ChatMessageViewModel | undefined =
|
||||||
this.chatMessageViewModels.value.get(chatMessage.id);
|
this.chatMessageViewModels.value.get(chatMessage.id);
|
||||||
if (existingChatMessageViewModel != undefined) {
|
if (existingChatMessageViewModel != undefined) {
|
||||||
existingChatMessageViewModel.body.value = chatMessage.body;
|
existingChatMessageViewModel.body.value = chatMessage.body;
|
||||||
existingChatMessageViewModel.status.value = chatMessage.status;
|
existingChatMessageViewModel.status.value = chatMessage.status;
|
||||||
} else {
|
} else {
|
||||||
this.chatMessageViewModels.set(chatMessage.id, chatMessageViewModel);
|
this.chatMessageViewModels.set(
|
||||||
|
chatMessage.id,
|
||||||
|
chatMessageViewModel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// load
|
||||||
|
loadData = (): void => {
|
||||||
|
this.chatMessageViewModels.clear();
|
||||||
|
for (const chatMessage of this.chatViewModel.chatModel.messages) {
|
||||||
|
this.showChatMessage(chatMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
chatViewModel: ChatViewModel,
|
||||||
|
) {
|
||||||
|
this.chatViewModel = chatViewModel;
|
||||||
|
|
||||||
|
this.cannotSendMessage = React.createProxyState(
|
||||||
|
[
|
||||||
|
this.chatViewModel.settingsViewModel.username,
|
||||||
|
this.composingMessage,
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
this.chatViewModel.settingsViewModel.username.value == "" ||
|
||||||
|
this.composingMessage.value == "",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
|
||||||
loadData = (): void => {
|
|
||||||
this.chatMessageViewModels.clear();
|
|
||||||
for (const chatMessage of this.chatViewModel.chatModel.messages) {
|
|
||||||
this.showChatMessage(chatMessage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
chatViewModel: ChatViewModel
|
|
||||||
) {
|
|
||||||
this.chatViewModel = chatViewModel;
|
|
||||||
|
|
||||||
this.cannotSendMessage = React.createProxyState(
|
|
||||||
[this.chatViewModel.settingsViewModel.username, this.composingMessage],
|
|
||||||
() =>
|
|
||||||
this.chatViewModel.settingsViewModel.username.value == "" ||
|
|
||||||
this.composingMessage.value == ""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,128 +5,128 @@ import { Color } from "../../colors";
|
|||||||
import CoreViewModel from "../Global/coreViewModel";
|
import CoreViewModel from "../Global/coreViewModel";
|
||||||
|
|
||||||
export default class SettingsPageViewModel {
|
export default class SettingsPageViewModel {
|
||||||
chatViewModel: ChatViewModel;
|
chatViewModel: ChatViewModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
primaryChannel: React.State<string> = new React.State("");
|
primaryChannel: React.State<string> = new React.State("");
|
||||||
primaryChannelInput: React.State<string> = new React.State("");
|
primaryChannelInput: React.State<string> = new React.State("");
|
||||||
|
|
||||||
secondaryChannels: React.ListState<string> = new React.ListState();
|
secondaryChannels: React.ListState<string> = new React.ListState();
|
||||||
newSecondaryChannelInput: React.State<string> = new React.State("");
|
newSecondaryChannelInput: React.State<string> = new React.State("");
|
||||||
|
|
||||||
encryptionKeyInput: React.State<string> = new React.State("");
|
encryptionKeyInput: React.State<string> = new React.State("");
|
||||||
shouldShowEncryptionKey: React.State<boolean> = new React.State(false);
|
shouldShowEncryptionKey: React.State<boolean> = new React.State(false);
|
||||||
encryptionKeyInputType: React.State<"text" | "password"> =
|
encryptionKeyInputType: React.State<"text" | "password"> =
|
||||||
React.createProxyState([this.shouldShowEncryptionKey], () =>
|
React.createProxyState([this.shouldShowEncryptionKey], () =>
|
||||||
this.shouldShowEncryptionKey.value == true ? "text" : "password"
|
this.shouldShowEncryptionKey.value == true ? "text" : "password",
|
||||||
|
);
|
||||||
|
|
||||||
|
color: React.State<Color> = new React.State<any>(Color.Standard);
|
||||||
|
|
||||||
|
// guards
|
||||||
|
cannotSetPrimaryChannel: React.State<boolean> = React.createProxyState(
|
||||||
|
[this.primaryChannel, this.primaryChannelInput],
|
||||||
|
() =>
|
||||||
|
this.primaryChannelInput.value == "" ||
|
||||||
|
this.primaryChannelInput.value == this.primaryChannel.value,
|
||||||
);
|
);
|
||||||
|
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
||||||
color: React.State<Color> = new React.State<any>(Color.Standard);
|
[this.newSecondaryChannelInput],
|
||||||
|
() => this.newSecondaryChannelInput.value == "",
|
||||||
// guards
|
|
||||||
cannotSetPrimaryChannel: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.primaryChannel, this.primaryChannelInput],
|
|
||||||
() =>
|
|
||||||
this.primaryChannelInput.value == "" ||
|
|
||||||
this.primaryChannelInput.value == this.primaryChannel.value
|
|
||||||
);
|
|
||||||
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.newSecondaryChannelInput],
|
|
||||||
() => this.newSecondaryChannelInput.value == ""
|
|
||||||
);
|
|
||||||
cannotSetEncryptionKey: React.State<boolean>;
|
|
||||||
|
|
||||||
// methods
|
|
||||||
setPrimaryChannel = (): void => {
|
|
||||||
this.chatViewModel.chatModel.setPrimaryChannel(
|
|
||||||
this.primaryChannelInput.value
|
|
||||||
);
|
);
|
||||||
this.primaryChannel.value =
|
cannotSetEncryptionKey: React.State<boolean>;
|
||||||
this.chatViewModel.chatModel.info.primaryChannel;
|
|
||||||
|
|
||||||
this.chatViewModel.chatListViewModel.updateIndices();
|
// methods
|
||||||
};
|
setPrimaryChannel = (): void => {
|
||||||
|
this.chatViewModel.chatModel.setPrimaryChannel(
|
||||||
|
this.primaryChannelInput.value,
|
||||||
|
);
|
||||||
|
this.primaryChannel.value =
|
||||||
|
this.chatViewModel.chatModel.info.primaryChannel;
|
||||||
|
|
||||||
addSecondaryChannel = (): void => {
|
this.chatViewModel.chatListViewModel.updateIndices();
|
||||||
this.secondaryChannels.add(this.newSecondaryChannelInput.value);
|
};
|
||||||
this.newSecondaryChannelInput.value = "";
|
|
||||||
this.storeSecondaryChannels();
|
|
||||||
this.loadSecondaryChannels();
|
|
||||||
};
|
|
||||||
|
|
||||||
removeSecondaryChannel = (secondaryChannel: string): void => {
|
addSecondaryChannel = (): void => {
|
||||||
this.secondaryChannels.remove(secondaryChannel);
|
this.secondaryChannels.add(this.newSecondaryChannelInput.value);
|
||||||
this.storeSecondaryChannels();
|
this.newSecondaryChannelInput.value = "";
|
||||||
};
|
this.storeSecondaryChannels();
|
||||||
|
this.loadSecondaryChannels();
|
||||||
|
};
|
||||||
|
|
||||||
storeSecondaryChannels = (): void => {
|
removeSecondaryChannel = (secondaryChannel: string): void => {
|
||||||
this.chatViewModel.chatModel.setSecondaryChannels([
|
this.secondaryChannels.remove(secondaryChannel);
|
||||||
...this.secondaryChannels.value.values(),
|
this.storeSecondaryChannels();
|
||||||
]);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
setEncryptionKey = (): void => {
|
storeSecondaryChannels = (): void => {
|
||||||
this.chatViewModel.chatModel.setEncryptionKey(
|
this.chatViewModel.chatModel.setSecondaryChannels([
|
||||||
this.encryptionKeyInput.value
|
...this.secondaryChannels.value.values(),
|
||||||
);
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// disable button
|
setEncryptionKey = (): void => {
|
||||||
this.encryptionKeyInput.callSubscriptions();
|
this.chatViewModel.chatModel.setEncryptionKey(
|
||||||
};
|
this.encryptionKeyInput.value,
|
||||||
|
);
|
||||||
|
|
||||||
applyColor = (newColor: Color): void => {
|
// disable button
|
||||||
this.chatViewModel.setColor(newColor);
|
this.encryptionKeyInput.callSubscriptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
remove = (): void => {
|
applyColor = (newColor: Color): void => {
|
||||||
this.chatViewModel.close();
|
this.chatViewModel.setColor(newColor);
|
||||||
this.chatViewModel.chatModel.delete();
|
};
|
||||||
this.chatViewModel.chatListViewModel.untrackChat(this.chatViewModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
remove = (): void => {
|
||||||
loadListRelevantData = (): void => {
|
this.chatViewModel.close();
|
||||||
this.primaryChannel.value =
|
this.chatViewModel.chatModel.delete();
|
||||||
this.chatViewModel.chatModel.info.primaryChannel;
|
this.chatViewModel.chatListViewModel.untrackChat(this.chatViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
this.color.value = this.chatViewModel.chatModel.color;
|
// load
|
||||||
};
|
loadListRelevantData = (): void => {
|
||||||
|
this.primaryChannel.value =
|
||||||
|
this.chatViewModel.chatModel.info.primaryChannel;
|
||||||
|
|
||||||
loadData = (): void => {
|
this.color.value = this.chatViewModel.chatModel.color;
|
||||||
this.primaryChannelInput.value =
|
};
|
||||||
this.chatViewModel.chatModel.info.primaryChannel;
|
|
||||||
|
|
||||||
this.loadSecondaryChannels();
|
loadData = (): void => {
|
||||||
|
this.primaryChannelInput.value =
|
||||||
|
this.chatViewModel.chatModel.info.primaryChannel;
|
||||||
|
|
||||||
this.encryptionKeyInput.value =
|
this.loadSecondaryChannels();
|
||||||
this.chatViewModel.chatModel.info.encryptionKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSecondaryChannels = (): void => {
|
this.encryptionKeyInput.value =
|
||||||
this.secondaryChannels.clear();
|
this.chatViewModel.chatModel.info.encryptionKey;
|
||||||
for (const secondaryChannel of this.chatViewModel.chatModel
|
};
|
||||||
.secondaryChannels) {
|
|
||||||
this.secondaryChannels.add(secondaryChannel);
|
loadSecondaryChannels = (): void => {
|
||||||
|
this.secondaryChannels.clear();
|
||||||
|
for (const secondaryChannel of this.chatViewModel.chatModel
|
||||||
|
.secondaryChannels) {
|
||||||
|
this.secondaryChannels.add(secondaryChannel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
chatViewModel: ChatViewModel,
|
||||||
|
) {
|
||||||
|
this.chatViewModel = chatViewModel;
|
||||||
|
this.loadListRelevantData();
|
||||||
|
|
||||||
|
this.cannotSetEncryptionKey = React.createProxyState(
|
||||||
|
[this.encryptionKeyInput],
|
||||||
|
() =>
|
||||||
|
this.encryptionKeyInput.value ==
|
||||||
|
this.chatViewModel.chatModel.info.encryptionKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.color.subscribe((newColor) => {
|
||||||
|
this.applyColor(newColor);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
chatViewModel: ChatViewModel
|
|
||||||
) {
|
|
||||||
this.chatViewModel = chatViewModel;
|
|
||||||
this.loadListRelevantData();
|
|
||||||
|
|
||||||
this.cannotSetEncryptionKey = React.createProxyState(
|
|
||||||
[this.encryptionKeyInput],
|
|
||||||
() =>
|
|
||||||
this.encryptionKeyInput.value ==
|
|
||||||
this.chatViewModel.chatModel.info.encryptionKey
|
|
||||||
);
|
|
||||||
|
|
||||||
this.color.subscribe((newColor) => {
|
|
||||||
this.applyColor(newColor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
|
|
||||||
import ChatViewModel from "../Chat/chatViewModel";
|
import ChatViewModel from "../Chat/chatViewModel";
|
||||||
@@ -10,58 +10,58 @@ import { IndexManager } from "../../Model/Utility/utility";
|
|||||||
import TaskViewModel from "./taskViewModel";
|
import TaskViewModel from "./taskViewModel";
|
||||||
|
|
||||||
export default class TaskContainingPageViewModel {
|
export default class TaskContainingPageViewModel {
|
||||||
boardsAndTasksModel: BoardsAndTasksModel;
|
boardsAndTasksModel: BoardsAndTasksModel;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
taskIndexManager: IndexManager<TaskViewModel> = new IndexManager(
|
taskIndexManager: IndexManager<TaskViewModel> = new IndexManager(
|
||||||
(taskViewModel: TaskViewModel) => taskViewModel.sortingString
|
(taskViewModel: TaskViewModel) => taskViewModel.sortingString,
|
||||||
);
|
|
||||||
|
|
||||||
selectedTaskViewModel: React.State<TaskViewModel | undefined> =
|
|
||||||
new React.State<any>(undefined);
|
|
||||||
|
|
||||||
taskViewModels: React.MapState<TaskViewModel> = new React.MapState();
|
|
||||||
|
|
||||||
// methods
|
|
||||||
createTaskFromBoardId = (boardId: string): void => {
|
|
||||||
const taskFileContent: TaskFileContent =
|
|
||||||
this.boardsAndTasksModel.createTask(boardId);
|
|
||||||
const taskViewModel: TaskViewModel = new TaskViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this.chatViewModel,
|
|
||||||
this.boardsAndTasksModel,
|
|
||||||
this,
|
|
||||||
taskFileContent
|
|
||||||
);
|
);
|
||||||
this.selectTask(taskViewModel);
|
|
||||||
this.updateTaskIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
selectedTaskViewModel: React.State<TaskViewModel | undefined> =
|
||||||
showTask = (taskFileContent: TaskFileContent): void => {};
|
new React.State<any>(undefined);
|
||||||
removeTaskFromView = (taskFileContent: TaskFileContent): void => {};
|
|
||||||
|
|
||||||
selectTask = (selectedTask: TaskViewModel): void => {
|
taskViewModels: React.MapState<TaskViewModel> = new React.MapState();
|
||||||
this.selectedTaskViewModel.value = selectedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
closeTask = () => {
|
// methods
|
||||||
this.selectedTaskViewModel.value = undefined;
|
createTaskFromBoardId = (boardId: string): void => {
|
||||||
};
|
const taskFileContent: TaskFileContent =
|
||||||
|
this.boardsAndTasksModel.createTask(boardId);
|
||||||
|
const taskViewModel: TaskViewModel = new TaskViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this.chatViewModel,
|
||||||
|
this.boardsAndTasksModel,
|
||||||
|
this,
|
||||||
|
taskFileContent,
|
||||||
|
);
|
||||||
|
this.selectTask(taskViewModel);
|
||||||
|
this.updateTaskIndices();
|
||||||
|
};
|
||||||
|
|
||||||
updateTaskIndices = (): void => {
|
// view
|
||||||
this.taskIndexManager.update([...this.taskViewModels.value.values()]);
|
showTask = (taskFileContent: TaskFileContent): void => {};
|
||||||
for (const boardViewModel of this.taskViewModels.value.values()) {
|
removeTaskFromView = (taskFileContent: TaskFileContent): void => {};
|
||||||
boardViewModel.updateIndex();
|
|
||||||
|
selectTask = (selectedTask: TaskViewModel): void => {
|
||||||
|
this.selectedTaskViewModel.value = selectedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
closeTask = () => {
|
||||||
|
this.selectedTaskViewModel.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTaskIndices = (): void => {
|
||||||
|
this.taskIndexManager.update([...this.taskViewModels.value.values()]);
|
||||||
|
for (const boardViewModel of this.taskViewModels.value.values()) {
|
||||||
|
boardViewModel.updateIndex();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
public chatViewModel: ChatViewModel,
|
||||||
|
boardsAndTasksModel: BoardsAndTasksModel,
|
||||||
|
) {
|
||||||
|
this.boardsAndTasksModel = boardsAndTasksModel;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
public chatViewModel: ChatViewModel,
|
|
||||||
boardsAndTasksModel: BoardsAndTasksModel
|
|
||||||
) {
|
|
||||||
this.boardsAndTasksModel = boardsAndTasksModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
BoardInfoFileContent,
|
BoardInfoFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
|
|
||||||
import BoardViewModel from "./boardViewModel";
|
import BoardViewModel from "./boardViewModel";
|
||||||
@@ -11,169 +11,169 @@ import { IndexManager } from "../../Model/Utility/utility";
|
|||||||
import StorageModel from "../../Model/Global/storageModel";
|
import StorageModel from "../../Model/Global/storageModel";
|
||||||
|
|
||||||
export default class TaskPageViewModel {
|
export default class TaskPageViewModel {
|
||||||
storageModel: StorageModel;
|
storageModel: StorageModel;
|
||||||
boardsAndTasksModel: BoardsAndTasksModel;
|
boardsAndTasksModel: BoardsAndTasksModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
boardIndexManager: IndexManager<BoardViewModel> = new IndexManager(
|
boardIndexManager: IndexManager<BoardViewModel> = new IndexManager(
|
||||||
(boardViewModel: BoardViewModel) => boardViewModel.name.value
|
(boardViewModel: BoardViewModel) => boardViewModel.name.value,
|
||||||
);
|
|
||||||
|
|
||||||
// paths
|
|
||||||
getBasePath = (): string[] => {
|
|
||||||
return [...this.boardsAndTasksModel.getViewPath()];
|
|
||||||
};
|
|
||||||
|
|
||||||
getBoardViewPath = (boardId): string[] => {
|
|
||||||
return [...this.getBasePath(), boardId];
|
|
||||||
};
|
|
||||||
|
|
||||||
getLastUsedBoardPath = (): string[] => {
|
|
||||||
return [...this.getBasePath(), TaskPageViewModelSubPath.LastUsedBoard];
|
|
||||||
};
|
|
||||||
|
|
||||||
// state
|
|
||||||
newBoardNameInput: React.State<string> = new React.State("");
|
|
||||||
|
|
||||||
boardViewModels: React.MapState<BoardViewModel> = new React.MapState();
|
|
||||||
|
|
||||||
isShowingBoadList: React.State<boolean> = new React.State(true);
|
|
||||||
|
|
||||||
selectedBoardId: React.State<string | undefined> = new React.State<any>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// guards
|
|
||||||
cannotCreateBoard: React.State<boolean> = React.createProxyState(
|
|
||||||
[this.newBoardNameInput],
|
|
||||||
() => this.newBoardNameInput.value == ""
|
|
||||||
);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
createBoard = (): void => {
|
|
||||||
if (this.cannotCreateBoard.value == true) return;
|
|
||||||
|
|
||||||
const boardInfoFileContent: BoardInfoFileContent =
|
|
||||||
this.boardsAndTasksModel.createBoard(this.newBoardNameInput.value);
|
|
||||||
this.newBoardNameInput.value = "";
|
|
||||||
|
|
||||||
this.showBoardInList(boardInfoFileContent);
|
|
||||||
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
|
|
||||||
this.updateBoardIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
|
||||||
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
|
|
||||||
this.updateBoardIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
|
||||||
this.boardsAndTasksModel.deleteBoard(boardInfoFileContent.fileId);
|
|
||||||
this.boardViewModels.remove(boardInfoFileContent.fileId);
|
|
||||||
this.updateBoardIndices();
|
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
|
||||||
toggleBoardList = (): void => {
|
|
||||||
this.isShowingBoadList.value = !this.isShowingBoadList.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
showBoardInList = (boardInfo: BoardInfoFileContent): void => {
|
|
||||||
const boardViewModel: BoardViewModel = new BoardViewModel(
|
|
||||||
this.coreViewModel,
|
|
||||||
this.chatViewModel,
|
|
||||||
this.storageModel,
|
|
||||||
this.boardsAndTasksModel,
|
|
||||||
this,
|
|
||||||
boardInfo
|
|
||||||
);
|
);
|
||||||
this.boardViewModels.set(boardInfo.fileId, boardViewModel);
|
|
||||||
|
|
||||||
this.chatViewModel.taskBoardSuggestions.set(boardInfo.fileId, [
|
// paths
|
||||||
boardInfo.fileId,
|
getBasePath = (): string[] => {
|
||||||
this.boardsAndTasksModel.getBoardName(boardInfo.fileId),
|
return [...this.boardsAndTasksModel.getViewPath()];
|
||||||
]);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
selectBoard = (boardViewModel: BoardViewModel): void => {
|
getBoardViewPath = (boardId): string[] => {
|
||||||
this.selectedBoardId.value = boardViewModel.boardInfo.fileId;
|
return [...this.getBasePath(), boardId];
|
||||||
this.chatViewModel.displayedColor.value = boardViewModel.color.value;
|
};
|
||||||
|
|
||||||
this.storeLastUsedBoard();
|
getLastUsedBoardPath = (): string[] => {
|
||||||
};
|
return [...this.getBasePath(), TaskPageViewModelSubPath.LastUsedBoard];
|
||||||
|
};
|
||||||
|
|
||||||
closeBoard = (): void => {
|
// state
|
||||||
this.selectedBoardId.value = undefined;
|
newBoardNameInput: React.State<string> = new React.State("");
|
||||||
this.chatViewModel.resetColor();
|
|
||||||
|
|
||||||
this.storeLastUsedBoard();
|
boardViewModels: React.MapState<BoardViewModel> = new React.MapState();
|
||||||
};
|
|
||||||
|
|
||||||
updateBoardIndices = (): void => {
|
isShowingBoadList: React.State<boolean> = new React.State(true);
|
||||||
this.boardIndexManager.update([...this.boardViewModels.value.values()]);
|
|
||||||
for (const boardViewModel of this.boardViewModels.value.values()) {
|
|
||||||
boardViewModel.updateIndex();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// storage
|
selectedBoardId: React.State<string | undefined> = new React.State<any>(
|
||||||
storeLastUsedBoard = (): void => {
|
undefined,
|
||||||
const path: string[] = this.getLastUsedBoardPath();
|
);
|
||||||
const lastUsedBoardId: string = this.selectedBoardId.value ?? "";
|
|
||||||
this.storageModel.write(path, lastUsedBoardId);
|
|
||||||
};
|
|
||||||
|
|
||||||
openLastUsedBoard = (): void => {
|
// guards
|
||||||
const path: string[] = this.getLastUsedBoardPath();
|
cannotCreateBoard: React.State<boolean> = React.createProxyState(
|
||||||
const lastUsedBoardId: string | null = this.storageModel.read(path);
|
[this.newBoardNameInput],
|
||||||
if (lastUsedBoardId == null) return;
|
() => this.newBoardNameInput.value == "",
|
||||||
|
);
|
||||||
|
|
||||||
const boardViewModel: BoardViewModel | undefined =
|
// methods
|
||||||
this.boardViewModels.value.get(lastUsedBoardId);
|
createBoard = (): void => {
|
||||||
if (boardViewModel == undefined) return;
|
if (this.cannotCreateBoard.value == true) return;
|
||||||
|
|
||||||
this.selectBoard(boardViewModel);
|
const boardInfoFileContent: BoardInfoFileContent =
|
||||||
};
|
this.boardsAndTasksModel.createBoard(this.newBoardNameInput.value);
|
||||||
|
this.newBoardNameInput.value = "";
|
||||||
|
|
||||||
// load
|
|
||||||
loadData = (): void => {
|
|
||||||
this.boardViewModels.clear();
|
|
||||||
|
|
||||||
const boardIds: string[] = this.boardsAndTasksModel.listBoardIds();
|
|
||||||
for (const boardId of boardIds) {
|
|
||||||
const boardInfo: BoardInfoFileContent | null =
|
|
||||||
this.boardsAndTasksModel.getBoardInfo(boardId);
|
|
||||||
if (boardInfo == null) continue;
|
|
||||||
|
|
||||||
this.showBoardInList(boardInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateBoardIndices();
|
|
||||||
this.openLastUsedBoard();
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
public coreViewModel: CoreViewModel,
|
|
||||||
public chatViewModel: ChatViewModel,
|
|
||||||
storageModel: StorageModel,
|
|
||||||
boardModel: BoardsAndTasksModel
|
|
||||||
) {
|
|
||||||
this.storageModel = storageModel;
|
|
||||||
this.boardsAndTasksModel = boardModel;
|
|
||||||
|
|
||||||
this.chatViewModel = chatViewModel;
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
boardModel.boardHandlerManager.addHandler(
|
|
||||||
(boardInfoFileContent: BoardInfoFileContent) => {
|
|
||||||
this.showBoardInList(boardInfoFileContent);
|
this.showBoardInList(boardInfoFileContent);
|
||||||
|
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
|
||||||
this.updateBoardIndices();
|
this.updateBoardIndices();
|
||||||
}
|
};
|
||||||
);
|
|
||||||
}
|
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||||
|
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
|
||||||
|
this.updateBoardIndices();
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||||
|
this.boardsAndTasksModel.deleteBoard(boardInfoFileContent.fileId);
|
||||||
|
this.boardViewModels.remove(boardInfoFileContent.fileId);
|
||||||
|
this.updateBoardIndices();
|
||||||
|
};
|
||||||
|
|
||||||
|
// view
|
||||||
|
toggleBoardList = (): void => {
|
||||||
|
this.isShowingBoadList.value = !this.isShowingBoadList.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
showBoardInList = (boardInfo: BoardInfoFileContent): void => {
|
||||||
|
const boardViewModel: BoardViewModel = new BoardViewModel(
|
||||||
|
this.coreViewModel,
|
||||||
|
this.chatViewModel,
|
||||||
|
this.storageModel,
|
||||||
|
this.boardsAndTasksModel,
|
||||||
|
this,
|
||||||
|
boardInfo,
|
||||||
|
);
|
||||||
|
this.boardViewModels.set(boardInfo.fileId, boardViewModel);
|
||||||
|
|
||||||
|
this.chatViewModel.taskBoardSuggestions.set(boardInfo.fileId, [
|
||||||
|
boardInfo.fileId,
|
||||||
|
this.boardsAndTasksModel.getBoardName(boardInfo.fileId),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
selectBoard = (boardViewModel: BoardViewModel): void => {
|
||||||
|
this.selectedBoardId.value = boardViewModel.boardInfo.fileId;
|
||||||
|
this.chatViewModel.displayedColor.value = boardViewModel.color.value;
|
||||||
|
|
||||||
|
this.storeLastUsedBoard();
|
||||||
|
};
|
||||||
|
|
||||||
|
closeBoard = (): void => {
|
||||||
|
this.selectedBoardId.value = undefined;
|
||||||
|
this.chatViewModel.resetColor();
|
||||||
|
|
||||||
|
this.storeLastUsedBoard();
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBoardIndices = (): void => {
|
||||||
|
this.boardIndexManager.update([...this.boardViewModels.value.values()]);
|
||||||
|
for (const boardViewModel of this.boardViewModels.value.values()) {
|
||||||
|
boardViewModel.updateIndex();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// storage
|
||||||
|
storeLastUsedBoard = (): void => {
|
||||||
|
const path: string[] = this.getLastUsedBoardPath();
|
||||||
|
const lastUsedBoardId: string = this.selectedBoardId.value ?? "";
|
||||||
|
this.storageModel.write(path, lastUsedBoardId);
|
||||||
|
};
|
||||||
|
|
||||||
|
openLastUsedBoard = (): void => {
|
||||||
|
const path: string[] = this.getLastUsedBoardPath();
|
||||||
|
const lastUsedBoardId: string | null = this.storageModel.read(path);
|
||||||
|
if (lastUsedBoardId == null) return;
|
||||||
|
|
||||||
|
const boardViewModel: BoardViewModel | undefined =
|
||||||
|
this.boardViewModels.value.get(lastUsedBoardId);
|
||||||
|
if (boardViewModel == undefined) return;
|
||||||
|
|
||||||
|
this.selectBoard(boardViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
// load
|
||||||
|
loadData = (): void => {
|
||||||
|
this.boardViewModels.clear();
|
||||||
|
|
||||||
|
const boardIds: string[] = this.boardsAndTasksModel.listBoardIds();
|
||||||
|
for (const boardId of boardIds) {
|
||||||
|
const boardInfo: BoardInfoFileContent | null =
|
||||||
|
this.boardsAndTasksModel.getBoardInfo(boardId);
|
||||||
|
if (boardInfo == null) continue;
|
||||||
|
|
||||||
|
this.showBoardInList(boardInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateBoardIndices();
|
||||||
|
this.openLastUsedBoard();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
public chatViewModel: ChatViewModel,
|
||||||
|
storageModel: StorageModel,
|
||||||
|
boardModel: BoardsAndTasksModel,
|
||||||
|
) {
|
||||||
|
this.storageModel = storageModel;
|
||||||
|
this.boardsAndTasksModel = boardModel;
|
||||||
|
|
||||||
|
this.chatViewModel = chatViewModel;
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
boardModel.boardHandlerManager.addHandler(
|
||||||
|
(boardInfoFileContent: BoardInfoFileContent) => {
|
||||||
|
this.showBoardInList(boardInfoFileContent);
|
||||||
|
this.updateBoardIndices();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TaskPageViewModelSubPath {
|
export enum TaskPageViewModelSubPath {
|
||||||
LastUsedBoard = "last-used-board",
|
LastUsedBoard = "last-used-board",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
import { localeCompare, padZero } from "../../Model/Utility/utility";
|
import { localeCompare, padZero } from "../../Model/Utility/utility";
|
||||||
|
|
||||||
@@ -11,225 +11,235 @@ import TaskContainingPageViewModel from "./taskContainingPageViewModel";
|
|||||||
import { allowDrag } from "../../View/utility";
|
import { allowDrag } from "../../View/utility";
|
||||||
|
|
||||||
export default class TaskViewModel {
|
export default class TaskViewModel {
|
||||||
boardsAndTasksModel: BoardsAndTasksModel;
|
boardsAndTasksModel: BoardsAndTasksModel;
|
||||||
|
|
||||||
containingModel: TaskContainingPageViewModel;
|
containingModel: TaskContainingPageViewModel;
|
||||||
|
|
||||||
// data
|
// data
|
||||||
task: TaskFileContent;
|
task: TaskFileContent;
|
||||||
|
|
||||||
get sortingString(): string {
|
get sortingString(): string {
|
||||||
const splitDate: string[] = this.date.value.split("-");
|
const splitDate: string[] = this.date.value.split("-");
|
||||||
const year = padZero(splitDate[0], 4);
|
const year = padZero(splitDate[0], 4);
|
||||||
const month = padZero(splitDate[1], 2);
|
const month = padZero(splitDate[1], 2);
|
||||||
const date = padZero(splitDate[2], 2);
|
const date = padZero(splitDate[2], 2);
|
||||||
|
|
||||||
const splitTime: string[] = this.time.value.split(":");
|
const splitTime: string[] = this.time.value.split(":");
|
||||||
const hour = padZero(splitTime[0], 2);
|
const hour = padZero(splitTime[0], 2);
|
||||||
const minute = padZero(splitTime[1], 2);
|
const minute = padZero(splitTime[1], 2);
|
||||||
|
|
||||||
const priorityNumber = parseInt(this.priority.value);
|
const priorityNumber = parseInt(this.priority.value);
|
||||||
const invertedPriority = 100 - priorityNumber;
|
const invertedPriority = 100 - priorityNumber;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
year + month + date + hour + minute + invertedPriority + this.name.value
|
year +
|
||||||
);
|
month +
|
||||||
}
|
date +
|
||||||
|
hour +
|
||||||
// paths
|
minute +
|
||||||
getFilePath = (): string[] => {
|
invertedPriority +
|
||||||
return this.boardsAndTasksModel.getTaskFilePath(this.task.fileId);
|
this.name.value
|
||||||
};
|
);
|
||||||
|
|
||||||
// state
|
|
||||||
index: React.State<number> = new React.State(0);
|
|
||||||
|
|
||||||
boardId: React.State<string> = new React.State("");
|
|
||||||
|
|
||||||
name: React.State<string> = new React.State("");
|
|
||||||
description: React.State<string> = new React.State("");
|
|
||||||
|
|
||||||
category: React.State<string> = new React.State("");
|
|
||||||
status: React.State<string> = new React.State("");
|
|
||||||
priority: React.State<string> = new React.State("");
|
|
||||||
|
|
||||||
date: React.State<string> = new React.State("");
|
|
||||||
time: React.State<string> = new React.State("");
|
|
||||||
|
|
||||||
selectedVersionId: React.State<string> = new React.State("");
|
|
||||||
versionIds: React.ListState<string> = new React.ListState();
|
|
||||||
|
|
||||||
// methods
|
|
||||||
dragStart = (event: DragEvent): void => {
|
|
||||||
allowDrag(event);
|
|
||||||
this.coreViewModel.draggedObject.value = this;
|
|
||||||
};
|
|
||||||
|
|
||||||
setCategoryAndStatus = (category?: string, status?: string): void => {
|
|
||||||
if (category != undefined) this.category.value = category;
|
|
||||||
if (status != undefined) this.status.value = status;
|
|
||||||
this.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
setBoardId = (boardId: string): void => {
|
|
||||||
this.boardId.value = boardId;
|
|
||||||
this.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
setDate = (dateISOString: string): void => {
|
|
||||||
this.date.value = dateISOString;
|
|
||||||
this.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
// view
|
|
||||||
open = (): void => {
|
|
||||||
this.containingModel.selectTask(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
close = (): void => {
|
|
||||||
this.containingModel.closeTask();
|
|
||||||
};
|
|
||||||
|
|
||||||
closeAndDiscard = (): void => {
|
|
||||||
this.close();
|
|
||||||
this.loadTaskData();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAndSave = (): void => {
|
|
||||||
this.close();
|
|
||||||
this.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateIndex = (): void => {
|
|
||||||
const index: number = this.containingModel.taskIndexManager.getIndex(this);
|
|
||||||
this.index.value = index;
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSuggestions = (): void => {
|
|
||||||
if (
|
|
||||||
this.coreViewModel.taskCategorySuggestions.value.has(
|
|
||||||
this.category.value
|
|
||||||
) == false
|
|
||||||
) {
|
|
||||||
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// paths
|
||||||
this.coreViewModel.taskStatusSuggestions.value.has(this.status.value) ==
|
getFilePath = (): string[] => {
|
||||||
false
|
return this.boardsAndTasksModel.getTaskFilePath(this.task.fileId);
|
||||||
) {
|
};
|
||||||
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// settings
|
// state
|
||||||
save = (): void => {
|
index: React.State<number> = new React.State(0);
|
||||||
const newTaskFileContent: TaskFileContent =
|
|
||||||
BoardsAndTasksModel.createTaskFileContent(
|
|
||||||
this.task.fileId,
|
|
||||||
this.name.value,
|
|
||||||
this.task.boardId
|
|
||||||
);
|
|
||||||
|
|
||||||
newTaskFileContent.boardId = this.boardId.value;
|
boardId: React.State<string> = new React.State("");
|
||||||
newTaskFileContent.description = this.description.value;
|
|
||||||
newTaskFileContent.status = this.status.value;
|
|
||||||
newTaskFileContent.category = this.category.value;
|
|
||||||
newTaskFileContent.priority = this.priority.value;
|
|
||||||
newTaskFileContent.date = this.date.value;
|
|
||||||
newTaskFileContent.time = this.time.value;
|
|
||||||
|
|
||||||
this.boardsAndTasksModel.updateTaskAndSend(newTaskFileContent);
|
name: React.State<string> = new React.State("");
|
||||||
|
description: React.State<string> = new React.State("");
|
||||||
|
|
||||||
this.containingModel.showTask(newTaskFileContent);
|
category: React.State<string> = new React.State("");
|
||||||
this.containingModel.updateTaskIndices();
|
status: React.State<string> = new React.State("");
|
||||||
|
priority: React.State<string> = new React.State("");
|
||||||
|
|
||||||
this.updateSuggestions();
|
date: React.State<string> = new React.State("");
|
||||||
};
|
time: React.State<string> = new React.State("");
|
||||||
|
|
||||||
deleteTask = (): void => {
|
selectedVersionId: React.State<string> = new React.State("");
|
||||||
this.close();
|
versionIds: React.ListState<string> = new React.ListState();
|
||||||
this.boardsAndTasksModel.deleteTask(this.task.boardId, this.task.fileId);
|
|
||||||
this.containingModel.removeTaskFromView(this.task);
|
|
||||||
};
|
|
||||||
|
|
||||||
// load
|
// methods
|
||||||
loadVersionIds = (): void => {
|
dragStart = (event: DragEvent): void => {
|
||||||
const versionIds: string[] = this.boardsAndTasksModel.listTaskVersionIds(
|
allowDrag(event);
|
||||||
this.task.fileId
|
this.coreViewModel.draggedObject.value = this;
|
||||||
);
|
};
|
||||||
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
|
|
||||||
this.versionIds.clear();
|
|
||||||
this.versionIds.add(...sortedVersionIds);
|
|
||||||
};
|
|
||||||
|
|
||||||
switchVersion = (versionId: string): void => {
|
setCategoryAndStatus = (category?: string, status?: string): void => {
|
||||||
const taskFileContent: TaskFileContent | null =
|
if (category != undefined) this.category.value = category;
|
||||||
this.boardsAndTasksModel.getSpecificTaskFileContent(
|
if (status != undefined) this.status.value = status;
|
||||||
this.task.fileId,
|
this.save();
|
||||||
versionId
|
};
|
||||||
);
|
|
||||||
if (taskFileContent == null) return;
|
|
||||||
|
|
||||||
this.task = taskFileContent;
|
setBoardId = (boardId: string): void => {
|
||||||
this.loadTaskData();
|
this.boardId.value = boardId;
|
||||||
};
|
this.save();
|
||||||
|
};
|
||||||
|
|
||||||
loadAllData = (): void => {
|
setDate = (dateISOString: string): void => {
|
||||||
this.loadTaskData();
|
this.date.value = dateISOString;
|
||||||
this.loadVersionIds();
|
this.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
loadTaskData = (): void => {
|
// view
|
||||||
this.boardId.value = this.task.boardId;
|
open = (): void => {
|
||||||
|
this.containingModel.selectTask(this);
|
||||||
|
};
|
||||||
|
|
||||||
this.name.value = this.task.name;
|
close = (): void => {
|
||||||
this.description.value = this.task.description ?? "";
|
this.containingModel.closeTask();
|
||||||
|
};
|
||||||
|
|
||||||
this.category.value = this.task.category ?? "";
|
closeAndDiscard = (): void => {
|
||||||
this.status.value = this.task.status ?? "";
|
this.close();
|
||||||
this.priority.value = this.task.priority ?? "";
|
this.loadTaskData();
|
||||||
|
};
|
||||||
|
|
||||||
this.date.value = this.task.date ?? "";
|
closeAndSave = (): void => {
|
||||||
this.time.value = this.task.time ?? "";
|
this.close();
|
||||||
|
this.save();
|
||||||
|
};
|
||||||
|
|
||||||
this.selectedVersionId.value = this.task.fileContentId;
|
updateIndex = (): void => {
|
||||||
|
const index: number =
|
||||||
|
this.containingModel.taskIndexManager.getIndex(this);
|
||||||
|
this.index.value = index;
|
||||||
|
};
|
||||||
|
|
||||||
this.updateSuggestions();
|
updateSuggestions = (): void => {
|
||||||
};
|
if (
|
||||||
|
this.coreViewModel.taskCategorySuggestions.value.has(
|
||||||
|
this.category.value,
|
||||||
|
) == false
|
||||||
|
) {
|
||||||
|
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
|
||||||
|
}
|
||||||
|
|
||||||
// init
|
if (
|
||||||
constructor(
|
this.coreViewModel.taskStatusSuggestions.value.has(
|
||||||
public coreViewModel: CoreViewModel,
|
this.status.value,
|
||||||
public chatViewModel: ChatViewModel,
|
) == false
|
||||||
boardsAndTasksModel: BoardsAndTasksModel,
|
) {
|
||||||
containingModel: TaskContainingPageViewModel,
|
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
|
||||||
taskFileContent: TaskFileContent
|
}
|
||||||
) {
|
};
|
||||||
this.boardsAndTasksModel = boardsAndTasksModel;
|
|
||||||
this.containingModel = containingModel;
|
|
||||||
|
|
||||||
this.task = taskFileContent;
|
// settings
|
||||||
|
save = (): void => {
|
||||||
|
const newTaskFileContent: TaskFileContent =
|
||||||
|
BoardsAndTasksModel.createTaskFileContent(
|
||||||
|
this.task.fileId,
|
||||||
|
this.name.value,
|
||||||
|
this.task.boardId,
|
||||||
|
);
|
||||||
|
|
||||||
|
newTaskFileContent.boardId = this.boardId.value;
|
||||||
|
newTaskFileContent.description = this.description.value;
|
||||||
|
newTaskFileContent.status = this.status.value;
|
||||||
|
newTaskFileContent.category = this.category.value;
|
||||||
|
newTaskFileContent.priority = this.priority.value;
|
||||||
|
newTaskFileContent.date = this.date.value;
|
||||||
|
newTaskFileContent.time = this.time.value;
|
||||||
|
|
||||||
|
this.boardsAndTasksModel.updateTaskAndSend(newTaskFileContent);
|
||||||
|
|
||||||
|
this.containingModel.showTask(newTaskFileContent);
|
||||||
|
this.containingModel.updateTaskIndices();
|
||||||
|
|
||||||
|
this.updateSuggestions();
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTask = (): void => {
|
||||||
|
this.close();
|
||||||
|
this.boardsAndTasksModel.deleteTask(
|
||||||
|
this.task.boardId,
|
||||||
|
this.task.fileId,
|
||||||
|
);
|
||||||
|
this.containingModel.removeTaskFromView(this.task);
|
||||||
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
this.loadAllData();
|
loadVersionIds = (): void => {
|
||||||
|
const versionIds: string[] =
|
||||||
|
this.boardsAndTasksModel.listTaskVersionIds(this.task.fileId);
|
||||||
|
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
|
||||||
|
this.versionIds.clear();
|
||||||
|
this.versionIds.add(...sortedVersionIds);
|
||||||
|
};
|
||||||
|
|
||||||
// subscriptions
|
switchVersion = (versionId: string): void => {
|
||||||
this.selectedVersionId.subscribeSilent((selectedVersionId) => {
|
const taskFileContent: TaskFileContent | null =
|
||||||
this.switchVersion(selectedVersionId);
|
this.boardsAndTasksModel.getSpecificTaskFileContent(
|
||||||
});
|
this.task.fileId,
|
||||||
}
|
versionId,
|
||||||
|
);
|
||||||
|
if (taskFileContent == null) return;
|
||||||
|
|
||||||
// utility
|
this.task = taskFileContent;
|
||||||
static getStringsForFilter = (taskViewModel: TaskViewModel): string[] => {
|
this.loadTaskData();
|
||||||
return [
|
};
|
||||||
taskViewModel.task.name,
|
|
||||||
taskViewModel.task.description ?? "",
|
loadAllData = (): void => {
|
||||||
taskViewModel.task.category ?? "",
|
this.loadTaskData();
|
||||||
taskViewModel.task.status ?? "",
|
this.loadVersionIds();
|
||||||
taskViewModel.task.priority ?? "",
|
};
|
||||||
taskViewModel.task.date ?? "",
|
|
||||||
taskViewModel.task.time ?? "",
|
loadTaskData = (): void => {
|
||||||
];
|
this.boardId.value = this.task.boardId;
|
||||||
};
|
|
||||||
|
this.name.value = this.task.name;
|
||||||
|
this.description.value = this.task.description ?? "";
|
||||||
|
|
||||||
|
this.category.value = this.task.category ?? "";
|
||||||
|
this.status.value = this.task.status ?? "";
|
||||||
|
this.priority.value = this.task.priority ?? "";
|
||||||
|
|
||||||
|
this.date.value = this.task.date ?? "";
|
||||||
|
this.time.value = this.task.time ?? "";
|
||||||
|
|
||||||
|
this.selectedVersionId.value = this.task.fileContentId;
|
||||||
|
|
||||||
|
this.updateSuggestions();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
public coreViewModel: CoreViewModel,
|
||||||
|
public chatViewModel: ChatViewModel,
|
||||||
|
boardsAndTasksModel: BoardsAndTasksModel,
|
||||||
|
containingModel: TaskContainingPageViewModel,
|
||||||
|
taskFileContent: TaskFileContent,
|
||||||
|
) {
|
||||||
|
this.boardsAndTasksModel = boardsAndTasksModel;
|
||||||
|
this.containingModel = containingModel;
|
||||||
|
|
||||||
|
this.task = taskFileContent;
|
||||||
|
|
||||||
|
// load
|
||||||
|
this.loadAllData();
|
||||||
|
|
||||||
|
// subscriptions
|
||||||
|
this.selectedVersionId.subscribeSilent((selectedVersionId) => {
|
||||||
|
this.switchVersion(selectedVersionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility
|
||||||
|
static getStringsForFilter = (taskViewModel: TaskViewModel): string[] => {
|
||||||
|
return [
|
||||||
|
taskViewModel.task.name,
|
||||||
|
taskViewModel.task.description ?? "",
|
||||||
|
taskViewModel.task.category ?? "",
|
||||||
|
taskViewModel.task.status ?? "",
|
||||||
|
taskViewModel.task.priority ?? "",
|
||||||
|
taskViewModel.task.date ?? "",
|
||||||
|
taskViewModel.task.time ?? "",
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,74 +3,74 @@ import * as React from "bloatless-react";
|
|||||||
import { checkDoesObjectMatchSearch } from "../../Model/Utility/utility";
|
import { checkDoesObjectMatchSearch } from "../../Model/Utility/utility";
|
||||||
|
|
||||||
export default class SearchViewModel<T> {
|
export default class SearchViewModel<T> {
|
||||||
// data
|
// data
|
||||||
allObjects: React.ListState<T> | React.MapState<T>;
|
allObjects: React.ListState<T> | React.MapState<T>;
|
||||||
getStringsOfObject: (object: T) => string[];
|
getStringsOfObject: (object: T) => string[];
|
||||||
|
|
||||||
// state
|
// state
|
||||||
appliedQuery: React.State<string> = new React.State("");
|
appliedQuery: React.State<string> = new React.State("");
|
||||||
searchInput: React.State<string> = new React.State("");
|
searchInput: React.State<string> = new React.State("");
|
||||||
matchingObjects: React.ListState<T>;
|
matchingObjects: React.ListState<T>;
|
||||||
|
|
||||||
suggestions: React.ListState<string>;
|
suggestions: React.ListState<string>;
|
||||||
|
|
||||||
// guards
|
// guards
|
||||||
cannotApplySearch: React.State<boolean> = React.createProxyState(
|
cannotApplySearch: React.State<boolean> = React.createProxyState(
|
||||||
[this.searchInput, this.appliedQuery],
|
[this.searchInput, this.appliedQuery],
|
||||||
() => this.searchInput.value == this.appliedQuery.value
|
() => this.searchInput.value == this.appliedQuery.value,
|
||||||
);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
search = (searchTerm: string): void => {
|
|
||||||
this.searchInput.value = searchTerm;
|
|
||||||
this.applySearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
applySearch = (): void => {
|
|
||||||
this.appliedQuery.value = this.searchInput.value;
|
|
||||||
|
|
||||||
this.matchingObjects.clear();
|
|
||||||
for (const object of this.allObjects.value.values()) {
|
|
||||||
const doesMatch: boolean = this.checkDoesMatchSearch(object);
|
|
||||||
if (doesMatch == false) continue;
|
|
||||||
|
|
||||||
this.matchingObjects.add(object);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(
|
|
||||||
allObjects: React.ListState<T> | React.MapState<T>,
|
|
||||||
matchingObjects: React.ListState<T>,
|
|
||||||
getStringsOfObject: (object: T) => string[],
|
|
||||||
suggestions: React.ListState<string>
|
|
||||||
) {
|
|
||||||
this.allObjects = allObjects;
|
|
||||||
this.matchingObjects = matchingObjects;
|
|
||||||
this.getStringsOfObject = getStringsOfObject;
|
|
||||||
this.suggestions = suggestions;
|
|
||||||
|
|
||||||
// handle new objects
|
|
||||||
this.allObjects.handleAddition((newObject: T) => {
|
|
||||||
const doesMatch: boolean = this.checkDoesMatchSearch(newObject);
|
|
||||||
if (doesMatch == false) {
|
|
||||||
this.matchingObjects.remove(newObject);
|
|
||||||
} else {
|
|
||||||
if (this.matchingObjects.value.has(newObject)) return;
|
|
||||||
this.matchingObjects.add(newObject);
|
|
||||||
this.allObjects.handleRemoval(newObject, () => {
|
|
||||||
this.matchingObjects.remove(newObject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility
|
|
||||||
checkDoesMatchSearch = (object: T): boolean => {
|
|
||||||
return checkDoesObjectMatchSearch(
|
|
||||||
this.appliedQuery.value,
|
|
||||||
this.getStringsOfObject,
|
|
||||||
object
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
// methods
|
||||||
|
search = (searchTerm: string): void => {
|
||||||
|
this.searchInput.value = searchTerm;
|
||||||
|
this.applySearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
applySearch = (): void => {
|
||||||
|
this.appliedQuery.value = this.searchInput.value;
|
||||||
|
|
||||||
|
this.matchingObjects.clear();
|
||||||
|
for (const object of this.allObjects.value.values()) {
|
||||||
|
const doesMatch: boolean = this.checkDoesMatchSearch(object);
|
||||||
|
if (doesMatch == false) continue;
|
||||||
|
|
||||||
|
this.matchingObjects.add(object);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init
|
||||||
|
constructor(
|
||||||
|
allObjects: React.ListState<T> | React.MapState<T>,
|
||||||
|
matchingObjects: React.ListState<T>,
|
||||||
|
getStringsOfObject: (object: T) => string[],
|
||||||
|
suggestions: React.ListState<string>,
|
||||||
|
) {
|
||||||
|
this.allObjects = allObjects;
|
||||||
|
this.matchingObjects = matchingObjects;
|
||||||
|
this.getStringsOfObject = getStringsOfObject;
|
||||||
|
this.suggestions = suggestions;
|
||||||
|
|
||||||
|
// handle new objects
|
||||||
|
this.allObjects.handleAddition((newObject: T) => {
|
||||||
|
const doesMatch: boolean = this.checkDoesMatchSearch(newObject);
|
||||||
|
if (doesMatch == false) {
|
||||||
|
this.matchingObjects.remove(newObject);
|
||||||
|
} else {
|
||||||
|
if (this.matchingObjects.value.has(newObject)) return;
|
||||||
|
this.matchingObjects.add(newObject);
|
||||||
|
this.allObjects.handleRemoval(newObject, () => {
|
||||||
|
this.matchingObjects.remove(newObject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility
|
||||||
|
checkDoesMatchSearch = (object: T): boolean => {
|
||||||
|
return checkDoesObjectMatchSearch(
|
||||||
|
this.appliedQuery.value,
|
||||||
|
this.getStringsOfObject,
|
||||||
|
object,
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,70 +3,72 @@ import * as React from "bloatless-react";
|
|||||||
import TaskViewModel from "../Pages/taskViewModel";
|
import TaskViewModel from "../Pages/taskViewModel";
|
||||||
|
|
||||||
export default class TaskPropertyBulkChangeViewModel {
|
export default class TaskPropertyBulkChangeViewModel {
|
||||||
taskViewModels: React.ListState<TaskViewModel>;
|
taskViewModels: React.ListState<TaskViewModel>;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
inputValue: React.State<string> = new React.State("");
|
inputValue: React.State<string> = new React.State("");
|
||||||
|
|
||||||
// guards
|
// guards
|
||||||
cannotSet: React.State<boolean>;
|
cannotSet: React.State<boolean>;
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
set = (): void => {
|
set = (): void => {
|
||||||
if (this.cannotSet.value == true) return;
|
if (this.cannotSet.value == true) return;
|
||||||
this.taskViewModels.value.forEach((taskViewModel: TaskViewModel) => {
|
this.taskViewModels.value.forEach((taskViewModel: TaskViewModel) => {
|
||||||
this.setValue(this.inputValue.value, taskViewModel);
|
this.setValue(this.inputValue.value, taskViewModel);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
setValue: (newValue: string, taskViewModel: TaskViewModel) => void;
|
setValue: (newValue: string, taskViewModel: TaskViewModel) => void;
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
taskViewModels: React.ListState<TaskViewModel>,
|
taskViewModels: React.ListState<TaskViewModel>,
|
||||||
valueSetter: (newValue: string, taskViewModel: TaskViewModel) => void,
|
valueSetter: (newValue: string, taskViewModel: TaskViewModel) => void,
|
||||||
initialValue: string
|
initialValue: string,
|
||||||
) {
|
) {
|
||||||
this.taskViewModels = taskViewModels;
|
this.taskViewModels = taskViewModels;
|
||||||
this.inputValue.value = initialValue;
|
this.inputValue.value = initialValue;
|
||||||
|
|
||||||
this.setValue = valueSetter;
|
this.setValue = valueSetter;
|
||||||
|
|
||||||
this.cannotSet = React.createProxyState(
|
this.cannotSet = React.createProxyState(
|
||||||
[this.inputValue],
|
[this.inputValue],
|
||||||
() => this.inputValue.value == "" || this.inputValue.value == initialValue
|
() =>
|
||||||
);
|
this.inputValue.value == "" ||
|
||||||
}
|
this.inputValue.value == initialValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TaskCategoryBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
|
export class TaskCategoryBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
|
||||||
constructor(
|
constructor(
|
||||||
taskViewModels: React.ListState<TaskViewModel>,
|
taskViewModels: React.ListState<TaskViewModel>,
|
||||||
initialValue: string
|
initialValue: string,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
taskViewModels,
|
taskViewModels,
|
||||||
(newCategory: string, taskViewModel: TaskViewModel) => {
|
(newCategory: string, taskViewModel: TaskViewModel) => {
|
||||||
taskViewModel.category.value = newCategory;
|
taskViewModel.category.value = newCategory;
|
||||||
taskViewModel.save();
|
taskViewModel.save();
|
||||||
},
|
},
|
||||||
initialValue
|
initialValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TaskStatusBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
|
export class TaskStatusBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
|
||||||
constructor(
|
constructor(
|
||||||
taskViewModels: React.ListState<TaskViewModel>,
|
taskViewModels: React.ListState<TaskViewModel>,
|
||||||
initialValue: string
|
initialValue: string,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
taskViewModels,
|
taskViewModels,
|
||||||
(newStatus: string, taskViewModel: TaskViewModel) => {
|
(newStatus: string, taskViewModel: TaskViewModel) => {
|
||||||
taskViewModel.status.value = newStatus;
|
taskViewModel.status.value = newStatus;
|
||||||
taskViewModel.save();
|
taskViewModel.save();
|
||||||
},
|
},
|
||||||
initialValue
|
initialValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-56
@@ -3,96 +3,96 @@
|
|||||||
#home-page #chat-section,
|
#home-page #chat-section,
|
||||||
#chat-page,
|
#chat-page,
|
||||||
#chat-page #main > div {
|
#chat-page #main > div {
|
||||||
animation: slide-up var(--animation-duration) ease;
|
animation: slide-up var(--animation-duration) ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient */
|
/* Gradient */
|
||||||
|
|
||||||
#background-wrapper {
|
#background-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: var(--safe-area-left);
|
left: var(--safe-area-left);
|
||||||
right: var(--safe-area-right);
|
right: var(--safe-area-right);
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper div {
|
#background-wrapper div {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -80px;
|
top: -80px;
|
||||||
left: -80px;
|
left: -80px;
|
||||||
right: -80px;
|
right: -80px;
|
||||||
bottom: -80px;
|
bottom: -80px;
|
||||||
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
filter: blur(40px);
|
filter: blur(40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper #sky {
|
#background-wrapper #sky {
|
||||||
left: -100%;
|
left: -100%;
|
||||||
bottom: -50%;
|
bottom: -50%;
|
||||||
|
|
||||||
--color-1: hsl(188, 100%, 83%);
|
--color-1: hsl(188, 100%, 83%);
|
||||||
--color-2: hsl(194, 72%, 56%);
|
--color-2: hsl(194, 72%, 56%);
|
||||||
background-image: radial-gradient(var(--color-1), var(--color-2));
|
background-image: radial-gradient(var(--color-1), var(--color-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper #grass-1 {
|
#background-wrapper #grass-1 {
|
||||||
--color-1: hsl(150, 100%, 30%);
|
--color-1: hsl(150, 100%, 30%);
|
||||||
--color-2: hsl(150, 100%, 25%);
|
--color-2: hsl(150, 100%, 25%);
|
||||||
|
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
350deg,
|
350deg,
|
||||||
var(--color-1),
|
var(--color-1),
|
||||||
var(--color-2) 30%,
|
var(--color-2) 30%,
|
||||||
transparent 40%
|
transparent 40%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper #grass-2 {
|
#background-wrapper #grass-2 {
|
||||||
--color-1: hsl(110, 100%, 40%);
|
--color-1: hsl(110, 100%, 40%);
|
||||||
--color-2: hsl(110, 100%, 35%);
|
--color-2: hsl(110, 100%, 35%);
|
||||||
|
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
9deg,
|
9deg,
|
||||||
var(--color-1),
|
var(--color-1),
|
||||||
var(--color-2) 30%,
|
var(--color-2) 30%,
|
||||||
transparent 35%
|
transparent 35%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
#background-wrapper {
|
#background-wrapper {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper #sky {
|
#background-wrapper #sky {
|
||||||
--color-1: hsl(188, 100%, 30%);
|
--color-1: hsl(188, 100%, 30%);
|
||||||
--color-2: hsl(194, 72%, 24%);
|
--color-2: hsl(194, 72%, 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper #grass-1 {
|
#background-wrapper #grass-1 {
|
||||||
--color-1: hsl(120, 30%, 30%);
|
--color-1: hsl(120, 30%, 30%);
|
||||||
--color-2: hsl(120, 30%, 24%);
|
--color-2: hsl(120, 30%, 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#background-wrapper #grass-2 {
|
#background-wrapper #grass-2 {
|
||||||
--color-1: hsl(110, 60%, 30%);
|
--color-1: hsl(110, 60%, 30%);
|
||||||
--color-2: hsl(110, 50%, 24%);
|
--color-2: hsl(110, 50%, 24%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -23,4 +23,4 @@
|
|||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-114
@@ -1,202 +1,202 @@
|
|||||||
/* basic */
|
/* basic */
|
||||||
[color]:not([color="standard"]) {
|
[color]:not([color="standard"]) {
|
||||||
--primary: hsl(var(--hue), 100%, var(--accent-luma));
|
--primary: hsl(var(--hue), 100%, var(--accent-luma));
|
||||||
--primary-hover: hsl(var(--hue), 95%, var(--accent-luma));
|
--primary-hover: hsl(var(--hue), 95%, var(--accent-luma));
|
||||||
--primary-active: hsl(var(--hue), 80%, var(--accent-luma));
|
--primary-active: hsl(var(--hue), 80%, var(--accent-luma));
|
||||||
|
|
||||||
--text-on-color: hsl(0, 0%, var(--accent-text-luma));
|
--text-on-color: hsl(0, 0%, var(--accent-text-luma));
|
||||||
--link: hsl(var(--hue), 100%, var(--accent-luma));
|
--link: hsl(var(--hue), 100%, var(--accent-luma));
|
||||||
|
|
||||||
--surface: hsla(var(--hue), 80%, 30%, 5%);
|
--surface: hsla(var(--hue), 80%, 30%, 5%);
|
||||||
--surface-hover: hsla(var(--hue), 100%, 5%, 10%);
|
--surface-hover: hsla(var(--hue), 100%, 5%, 10%);
|
||||||
--surface-active: hsla(var(--hue), 100%, 5%, 15%);
|
--surface-active: hsla(var(--hue), 100%, 5%, 15%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
[color]:not([color="standard"]) {
|
[color]:not([color="standard"]) {
|
||||||
--accent-luma: var(--accent-luma-dark);
|
--accent-luma: var(--accent-luma-dark);
|
||||||
--accent-text-luma: var(--accent-text-luma-dark);
|
--accent-text-luma: var(--accent-text-luma-dark);
|
||||||
|
|
||||||
--surface: hsla(var(--hue), 100%, 95%, 5%);
|
--surface: hsla(var(--hue), 100%, 95%, 5%);
|
||||||
--surface-hover: hsla(var(--hue), 100%, 95%, 10%);
|
--surface-hover: hsla(var(--hue), 100%, 95%, 10%);
|
||||||
--surface-active: hsla(var(--hue), 100%, 95%, 15%);
|
--surface-active: hsla(var(--hue), 100%, 95%, 15%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tile */
|
/* tile */
|
||||||
[color]:not([color="standard"]).tile {
|
[color]:not([color="standard"]).tile {
|
||||||
--gradient-luma-1: 70%;
|
--gradient-luma-1: 70%;
|
||||||
--gradient-luma-2: 90%;
|
--gradient-luma-2: 90%;
|
||||||
|
|
||||||
--hover-luma: 75%;
|
--hover-luma: 75%;
|
||||||
--active-luma: 80%;
|
--active-luma: 80%;
|
||||||
|
|
||||||
--text-luma: 20%;
|
--text-luma: 20%;
|
||||||
|
|
||||||
--surface: linear-gradient(
|
--surface: linear-gradient(
|
||||||
120deg,
|
120deg,
|
||||||
hsl(var(--hue), 100%, var(--gradient-luma-1)),
|
hsl(var(--hue), 100%, var(--gradient-luma-1)),
|
||||||
hsla(var(--hue), 100%, var(--gradient-luma-2), 90%)
|
hsla(var(--hue), 100%, var(--gradient-luma-2), 90%)
|
||||||
);
|
);
|
||||||
--surface-hover: hsl(var(--hue), 100%, var(--hover-luma));
|
--surface-hover: hsl(var(--hue), 100%, var(--hover-luma));
|
||||||
--surface-active: hsl(var(--hue), 100%, var(--active-luma));
|
--surface-active: hsl(var(--hue), 100%, var(--active-luma));
|
||||||
|
|
||||||
--text-normal: hsl(var(--hue), 100%, var(--text-luma));
|
--text-normal: hsl(var(--hue), 100%, var(--text-luma));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
[color]:not([color="standard"]).tile {
|
[color]:not([color="standard"]).tile {
|
||||||
--gradient-luma-1: 20%;
|
--gradient-luma-1: 20%;
|
||||||
--gradient-luma-2: 15%;
|
--gradient-luma-2: 15%;
|
||||||
|
|
||||||
--hover-luma: 25%;
|
--hover-luma: 25%;
|
||||||
--active-luma: 30%;
|
--active-luma: 30%;
|
||||||
|
|
||||||
--text-luma: 90%;
|
--text-luma: 90%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fill */
|
/* fill */
|
||||||
[color]:not([color="standard"]).fill-color {
|
[color]:not([color="standard"]).fill-color {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
[color]:not([color="standard"])[selected].fill-color {
|
[color]:not([color="standard"])[selected].fill-color {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* background */
|
/* background */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
[color]:not([color="standard"]).background {
|
|
||||||
--luma: var(--background-luma);
|
|
||||||
--sat: 80%;
|
|
||||||
background: hsl(var(--hue), var(--sat), var(--luma));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
[color]:not([color="standard"]).background {
|
[color]:not([color="standard"]).background {
|
||||||
--luma: var(--background-luma-dark);
|
--luma: var(--background-luma);
|
||||||
--sat: 40%;
|
--sat: 80%;
|
||||||
|
background: hsl(var(--hue), var(--sat), var(--luma));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
[color="standard"].background {
|
@media (prefers-color-scheme: dark) {
|
||||||
background: var(--surface);
|
[color]:not([color="standard"]).background {
|
||||||
}
|
--luma: var(--background-luma-dark);
|
||||||
|
--sat: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[color="standard"].background {
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* subtle */
|
/* subtle */
|
||||||
[color]:not([color="standard"]).subtle-background {
|
[color]:not([color="standard"]).subtle-background {
|
||||||
--luma: var(--subtle-luma);
|
--luma: var(--subtle-luma);
|
||||||
background: hsl(var(--hue), 100%, var(--luma), 50%);
|
background: hsl(var(--hue), 100%, var(--luma), 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
[color]:not([color="standard"]).subtle-background {
|
|
||||||
--luma: var(--subtle-luma-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (display-mode: standalone) {
|
|
||||||
[color]:not([color="standard"]).subtle-background {
|
[color]:not([color="standard"]).subtle-background {
|
||||||
--luma: 0%;
|
--luma: var(--subtle-luma-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
[color]:not([color="standard"]).subtle-background {
|
||||||
|
--luma: 0%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* definitions */
|
/* definitions */
|
||||||
[color="coral"] {
|
[color="coral"] {
|
||||||
--hue: 20;
|
--hue: 20;
|
||||||
|
|
||||||
--accent-luma: 50%;
|
--accent-luma: 50%;
|
||||||
--accent-luma-dark: 70%;
|
--accent-luma-dark: 70%;
|
||||||
|
|
||||||
--accent-text-luma: 100%;
|
--accent-text-luma: 100%;
|
||||||
--accent-text-luma-dark: 100%;
|
--accent-text-luma-dark: 100%;
|
||||||
|
|
||||||
--background-luma: 90%;
|
--background-luma: 90%;
|
||||||
--background-luma-dark: 11%;
|
--background-luma-dark: 11%;
|
||||||
|
|
||||||
--subtle-luma: 95%;
|
--subtle-luma: 95%;
|
||||||
--subtle-luma-dark: 5%;
|
--subtle-luma-dark: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[color="yellow"] {
|
[color="yellow"] {
|
||||||
--hue: 50;
|
--hue: 50;
|
||||||
|
|
||||||
--accent-luma: 50%;
|
--accent-luma: 50%;
|
||||||
--accent-luma-dark: 60%;
|
--accent-luma-dark: 60%;
|
||||||
|
|
||||||
--accent-text-luma: 0%;
|
--accent-text-luma: 0%;
|
||||||
--accent-text-luma-dark: 0%;
|
--accent-text-luma-dark: 0%;
|
||||||
|
|
||||||
--background-luma: 85%;
|
--background-luma: 85%;
|
||||||
--background-luma-dark: 11%;
|
--background-luma-dark: 11%;
|
||||||
|
|
||||||
--subtle-luma: 95%;
|
--subtle-luma: 95%;
|
||||||
--subtle-luma-dark: 5%;
|
--subtle-luma-dark: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[color="mint"] {
|
[color="mint"] {
|
||||||
--hue: 150;
|
--hue: 150;
|
||||||
|
|
||||||
--accent-luma: 40%;
|
--accent-luma: 40%;
|
||||||
--accent-luma-dark: 60%;
|
--accent-luma-dark: 60%;
|
||||||
|
|
||||||
--accent-text-luma: 100%;
|
--accent-text-luma: 100%;
|
||||||
--accent-text-luma-dark: 0%;
|
--accent-text-luma-dark: 0%;
|
||||||
|
|
||||||
--background-luma: 85%;
|
--background-luma: 85%;
|
||||||
--background-luma-dark: 12%;
|
--background-luma-dark: 12%;
|
||||||
|
|
||||||
--subtle-luma: 95%;
|
--subtle-luma: 95%;
|
||||||
--subtle-luma-dark: 5%;
|
--subtle-luma-dark: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[color="lightblue"] {
|
[color="lightblue"] {
|
||||||
--hue: 180;
|
--hue: 180;
|
||||||
|
|
||||||
--accent-luma: 40%;
|
--accent-luma: 40%;
|
||||||
--accent-luma-dark: 60%;
|
--accent-luma-dark: 60%;
|
||||||
|
|
||||||
--accent-text-luma: 0%;
|
--accent-text-luma: 0%;
|
||||||
--accent-text-luma-dark: 0%;
|
--accent-text-luma-dark: 0%;
|
||||||
|
|
||||||
--background-luma: 85%;
|
--background-luma: 85%;
|
||||||
--background-luma-dark: 12%;
|
--background-luma-dark: 12%;
|
||||||
|
|
||||||
--subtle-luma: 95%;
|
--subtle-luma: 95%;
|
||||||
--subtle-luma-dark: 5%;
|
--subtle-luma-dark: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[color="blue"] {
|
[color="blue"] {
|
||||||
--hue: 220;
|
--hue: 220;
|
||||||
|
|
||||||
--accent-luma: 50%;
|
--accent-luma: 50%;
|
||||||
--accent-luma-dark: 70%;
|
--accent-luma-dark: 70%;
|
||||||
|
|
||||||
--accent-text-luma: 100%;
|
--accent-text-luma: 100%;
|
||||||
--accent-text-luma-dark: 100%;
|
--accent-text-luma-dark: 100%;
|
||||||
|
|
||||||
--background-luma: 90%;
|
--background-luma: 90%;
|
||||||
--background-luma-dark: 12%;
|
--background-luma-dark: 12%;
|
||||||
|
|
||||||
--subtle-luma: 95%;
|
--subtle-luma: 95%;
|
||||||
--subtle-luma-dark: 5%;
|
--subtle-luma-dark: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[color="purple"] {
|
[color="purple"] {
|
||||||
--hue: 280;
|
--hue: 280;
|
||||||
|
|
||||||
--accent-luma: 50%;
|
--accent-luma: 50%;
|
||||||
--accent-luma-dark: 70%;
|
--accent-luma-dark: 70%;
|
||||||
|
|
||||||
--accent-text-luma: 100%;
|
--accent-text-luma: 100%;
|
||||||
--accent-text-luma-dark: 100%;
|
--accent-text-luma-dark: 100%;
|
||||||
|
|
||||||
--background-luma: 90%;
|
--background-luma: 90%;
|
||||||
--background-luma-dark: 11%;
|
--background-luma-dark: 11%;
|
||||||
|
|
||||||
--subtle-luma: 95%;
|
--subtle-luma: 95%;
|
||||||
--subtle-luma-dark: 5%;
|
--subtle-luma-dark: 5%;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -1,9 +1,9 @@
|
|||||||
export enum Color {
|
export enum Color {
|
||||||
Standard = "standard",
|
Standard = "standard",
|
||||||
Coral = "coral",
|
Coral = "coral",
|
||||||
Yellow = "yellow",
|
Yellow = "yellow",
|
||||||
Mint = "mint",
|
Mint = "mint",
|
||||||
LightBlue = "lightblue",
|
LightBlue = "lightblue",
|
||||||
Blue = "blue",
|
Blue = "blue",
|
||||||
purple = "purple",
|
purple = "purple",
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-34
@@ -27,9 +27,9 @@ const storageModel = new StorageModel();
|
|||||||
const settingsModel = new SettingsModel(storageModel);
|
const settingsModel = new SettingsModel(storageModel);
|
||||||
const connectionModel = new ConnectionModel(storageModel);
|
const connectionModel = new ConnectionModel(storageModel);
|
||||||
const chatListModel = new ChatListModel(
|
const chatListModel = new ChatListModel(
|
||||||
storageModel,
|
storageModel,
|
||||||
settingsModel,
|
settingsModel,
|
||||||
connectionModel
|
connectionModel,
|
||||||
);
|
);
|
||||||
const fileTransferModel = new FileTransferModel(storageModel, connectionModel);
|
const fileTransferModel = new FileTransferModel(storageModel, connectionModel);
|
||||||
|
|
||||||
@@ -42,47 +42,47 @@ const coreVieWModel = new CoreViewModel();
|
|||||||
const storageViewModel = new StorageViewModel(coreVieWModel, storageModel);
|
const storageViewModel = new StorageViewModel(coreVieWModel, storageModel);
|
||||||
const settingsViewModel = new SettingsViewModel(coreVieWModel, settingsModel);
|
const settingsViewModel = new SettingsViewModel(coreVieWModel, settingsModel);
|
||||||
const connectionViewModel = new ConnectionViewModel(
|
const connectionViewModel = new ConnectionViewModel(
|
||||||
coreVieWModel,
|
coreVieWModel,
|
||||||
connectionModel
|
connectionModel,
|
||||||
);
|
);
|
||||||
const chatListViewModel = new ChatListViewModel(
|
const chatListViewModel = new ChatListViewModel(
|
||||||
coreVieWModel,
|
coreVieWModel,
|
||||||
storageModel,
|
storageModel,
|
||||||
chatListModel,
|
chatListModel,
|
||||||
settingsViewModel
|
settingsViewModel,
|
||||||
);
|
);
|
||||||
const fileTransferViewModel = new FileTransferViewModel(
|
const fileTransferViewModel = new FileTransferViewModel(
|
||||||
fileTransferModel,
|
fileTransferModel,
|
||||||
chatListModel
|
chatListModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
// view
|
// view
|
||||||
chatListViewModel.selectedChat.subscribe(() => {
|
chatListViewModel.selectedChat.subscribe(() => {
|
||||||
document.body.toggleAttribute(
|
document.body.toggleAttribute(
|
||||||
"showing-chat",
|
"showing-chat",
|
||||||
chatListViewModel.selectedChat.value != undefined
|
chatListViewModel.selectedChat.value != undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.append(
|
document.body.append(
|
||||||
<div id="background-wrapper">
|
<div id="background-wrapper">
|
||||||
<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>,
|
||||||
);
|
);
|
||||||
document
|
document
|
||||||
.querySelector("main")!
|
.querySelector("main")!
|
||||||
.append(
|
.append(
|
||||||
HomePage(
|
HomePage(
|
||||||
storageViewModel,
|
storageViewModel,
|
||||||
settingsViewModel,
|
settingsViewModel,
|
||||||
connectionViewModel,
|
connectionViewModel,
|
||||||
fileTransferViewModel,
|
fileTransferViewModel,
|
||||||
chatListViewModel
|
chatListViewModel,
|
||||||
),
|
),
|
||||||
ChatPageWrapper(chatListViewModel),
|
ChatPageWrapper(chatListViewModel),
|
||||||
ConnectionModal(connectionViewModel),
|
ConnectionModal(connectionViewModel),
|
||||||
DataTransferModalWrapper(connectionViewModel, fileTransferViewModel),
|
DataTransferModalWrapper(connectionViewModel, fileTransferViewModel),
|
||||||
StorageModal(storageViewModel)
|
StorageModal(storageViewModel),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user