diff --git a/dist/index.css b/dist/index.css index ac144663..abe5469d 100644 --- a/dist/index.css +++ b/dist/index.css @@ -487,3 +487,35 @@ body[showing-chat] #chat-page-wrapper { opacity: 0.1; white-space: nowrap; } + +/* src/View/Components/fileBrowser.css */ +.file-browser { + width: 100%; + height: 100%; + display: flex; + overflow-x: scroll; + overflow-y: hidden; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; +} +.file-browser > div { + width: 100%; + min-width: 100%; + flex: 0; + display: flex; + flex-direction: column; + padding: var(--content-padding); + scroll-snap-align: center; + overflow: auto; +} +.file-browser code { + width: 100%; + height: 100%; +} +@media (min-width: 640px) { + .file-browser > div { + width: 50%; + min-width: 50%; + flex: 1; + } +} diff --git a/dist/index.js b/dist/index.js index da7317ff..7c68789b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -302,24 +302,30 @@ storageEntryTree = {}; // basic store = (pathComponents, value) => { - const key = _StorageModel.pathComponentsToKey(...pathComponents); - localStorage.setItem(key, value); + const pathString = _StorageModel.pathComponentsToString( + ...pathComponents + ); + localStorage.setItem(pathString, value); this.updateTree(...pathComponents); }; restore = (pathComponents) => { - const key = _StorageModel.pathComponentsToKey(...pathComponents); - return localStorage.getItem(key); + const pathString = _StorageModel.pathComponentsToString( + ...pathComponents + ); + return localStorage.getItem(pathString); }; remove = (pathComponents, shouldInitialize = true) => { - const key = _StorageModel.pathComponentsToKey(...pathComponents); - localStorage.removeItem(key); + const pathString = _StorageModel.pathComponentsToString( + ...pathComponents + ); + localStorage.removeItem(pathString); if (shouldInitialize == true) { this.initializeTree(); } }; removeRecursive = (pathComponentsOfEntityToDelete) => { a: for (const key of Object.keys(localStorage)) { - const pathComponentsOfCurrentEntity = _StorageModel.keyToPathComponents(key); + const pathComponentsOfCurrentEntity = _StorageModel.stringToPathComponents(key); for (let i = 0; i < pathComponentsOfEntityToDelete.length; i++) { if (!pathComponentsOfCurrentEntity[i]) continue a; if (pathComponentsOfCurrentEntity[i] != pathComponentsOfEntityToDelete[i]) @@ -336,7 +342,7 @@ if (nextParent == void 0) return []; currentParent = nextParent; } - return [...Object.keys(currentParent)]; + return [...Object.keys(currentParent).sort(localeCompare)]; }; // stringifiable storeStringifiable = (pathComponents, value) => { @@ -357,7 +363,7 @@ console.log("initializing tree"); this.storageEntryTree = {}; for (const key of Object.keys(localStorage)) { - const components = _StorageModel.keyToPathComponents(key); + const components = _StorageModel.stringToPathComponents(key); this.updateTree(...components); } }; @@ -370,22 +376,22 @@ currentParent = currentParent[pathPart]; } }; - print = () => { - console.log(stringify(this.storageEntryTree)); + printTree = () => { + return stringify(this.storageEntryTree); }; - static pathComponentsToKey = (...pathComponents) => { + static pathComponentsToString = (...pathComponents) => { return pathComponents.join(PATH_COMPONENT_SEPARATOR); }; - static keyToPathComponents = (key) => { - return key.split(PATH_COMPONENT_SEPARATOR); + static stringToPathComponents = (string) => { + return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != ""); }; static join = (...items) => { let allComponents = []; for (const item of items) { - const parts = this.keyToPathComponents(item); + const parts = this.stringToPathComponents(item); allComponents.push(...parts); } - return _StorageModel.pathComponentsToKey(...allComponents); + return _StorageModel.pathComponentsToString(...allComponents); }; }; var storageKeys = { @@ -1116,6 +1122,7 @@ yourNamePlaceholder: "Jane Doe", setNameButtonAudioLabel: "set name", firstDayOfWeekLabel: "First day of week", + manageStorageButton: "Manage storage", scrollToChatButton: "Chats", /// backToOverviewAudioLabel: "go back to overview", @@ -1901,7 +1908,7 @@ }; // src/View/homePage.tsx - function HomePage(settingsViewModel2, connectionViewModel2, chatListViewModel2) { + function HomePage(storageViewModel2, settingsViewModel2, connectionViewModel2, chatListViewModel2) { const overviewSection = /* @__PURE__ */ createElement("div", { id: "overview-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.overviewHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.serverAddress), /* @__PURE__ */ createElement( "input", { @@ -1970,7 +1977,7 @@ i.toString(), i == settingsViewModel2.firstDayOfWeekInput.value ) - )), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__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("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("hr", null), /* @__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( "input", { @@ -1992,7 +1999,10 @@ "div", { id: "chat-grid", - "children:append": [chatListViewModel2.chatViewModels, ChatViewModelToChatEntry] + "children:append": [ + chatListViewModel2.chatViewModels, + ChatViewModelToChatEntry + ] } )); function scrollToChat() { @@ -2067,6 +2077,87 @@ } }; + // src/View/Components/directoryItemList.tsx + function DirectoryItemList(storageModel2, pathString, selectedPath) { + const StringToDirectoryItemList = (pathString2) => DirectoryItemList(storageModel2, pathString2, selectedPath); + const path = StorageModel.stringToPathComponents(pathString); + const fileName = path[path.length - 1] || "\\"; + const items = new ListState(); + const style = `text-indent: ${path.length * 2}rem`; + function loadItems() { + items.clear(); + const directoryItems = storageModel2.list(path); + for (const directoryItem of directoryItems) { + const itemPath = [...path, directoryItem]; + const pathString2 = StorageModel.pathComponentsToString(...itemPath); + items.add(pathString2); + } + } + function select() { + selectedPath.value = pathString; + } + const isSelected = createProxyState( + [selectedPath], + () => selectedPath.value == pathString + ); + isSelected.subscribe(() => { + if (isSelected.value == false) return; + loadItems(); + }); + return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement( + "button", + { + class: "width-100 flex-1 clip", + "toggle:selected": isSelected, + "on:click": select + }, + /* @__PURE__ */ createElement("span", { class: "ellipsis width-100 flex-1", style }, fileName) + ), /* @__PURE__ */ createElement( + "div", + { + class: "flex-column", + "children:append": [items, StringToDirectoryItemList] + } + )); + } + + // src/View/Components/fileBrowser.tsx + function FileBrowser(storageModel2, selectedPath) { + const fileContent = createProxyState([selectedPath], () => { + const path = StorageModel.stringToPathComponents(selectedPath.value); + const content = storageModel2.restore(path); + return content || ""; + }); + return /* @__PURE__ */ createElement("div", { class: "file-browser" }, DirectoryItemList(storageModel2, "\\", selectedPath), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("code", { "subscribe:innerText": fileContent }))); + } + + // src/View/Modals/storageModal.tsx + function StorageModal(storageViewModel2) { + return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": storageViewModel2.isShowingStorageModal }, /* @__PURE__ */ createElement("div", { style: "max-width: 64rem" }, /* @__PURE__ */ createElement("main", { class: "padding-0" }, FileBrowser( + storageViewModel2.storageModel, + storageViewModel2.selectedPath + )), /* @__PURE__ */ createElement("button", { "on:click": storageViewModel2.hideStorageModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + } + + // src/ViewModel/storageViewModel.ts + var StorageViewModel = class { + storageModel; + // state + isShowingStorageModal = new State(false); + selectedPath = new State("\\"); + // view methods + showStorageModal = () => { + this.isShowingStorageModal.value = true; + }; + hideStorageModal = () => { + this.isShowingStorageModal.value = false; + }; + // init + constructor(storageModel2) { + this.storageModel = storageModel2; + } + }; + // src/index.tsx var storageModel = new StorageModel(); var settingsModel = new SettingsModel(storageModel); @@ -2076,7 +2167,7 @@ settingsModel, connectionModel ); - storageModel.print(); + var storageViewModel = new StorageViewModel(storageModel); var settingsViewModel = new SettingsViewModel(settingsModel); var connectionViewModel = new ConnectionViewModel(connectionModel); var chatListViewModel = new ChatListViewModel( @@ -2094,8 +2185,14 @@ /* @__PURE__ */ createElement("div", { id: "background-wrapper" }, /* @__PURE__ */ createElement("div", { id: "sky" }), /* @__PURE__ */ createElement("div", { id: "grass-1" }), /* @__PURE__ */ createElement("div", { id: "grass-2" })) ); document.querySelector("main").append( - HomePage(settingsViewModel, connectionViewModel, chatListViewModel), + HomePage( + storageViewModel, + settingsViewModel, + connectionViewModel, + chatListViewModel + ), ChatPageWrapper(chatListViewModel), - ConnectionModal(connectionViewModel) + ConnectionModal(connectionViewModel), + StorageModal(storageViewModel) ); })(); diff --git a/dist/styles.css b/dist/styles.css index 29423f2f..1b249856 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -1035,6 +1035,7 @@ menu { article, body, main, +.clip, .ellipsis { overflow: hidden; } @@ -1080,6 +1081,11 @@ main { white-space: nowrap; } +code { + white-space: pre-wrap; + word-break: break-all; +} + /* scrolling */ main { scrollbar-width: none; diff --git a/src/Model/storageModel.ts b/src/Model/storageModel.ts index 2b802bc8..e441738a 100644 --- a/src/Model/storageModel.ts +++ b/src/Model/storageModel.ts @@ -1,6 +1,6 @@ // this file is responsible for reading and writing persistent data; all storage shall be handled by this file. -import { parse, stringify } from "./Utility/utility"; +import { localeCompare, parse, stringify } from "./Utility/utility"; import { DATA_VERSION } from "./Utility/typeSafety"; @@ -11,22 +11,28 @@ export default class StorageModel { // basic store = (pathComponents: string[], value: string): void => { - const key: string = StorageModel.pathComponentsToKey(...pathComponents); - localStorage.setItem(key, value); + const pathString: string = StorageModel.pathComponentsToString( + ...pathComponents + ); + localStorage.setItem(pathString, value); this.updateTree(...pathComponents); }; restore = (pathComponents: string[]): string | null => { - const key: string = StorageModel.pathComponentsToKey(...pathComponents); - return localStorage.getItem(key); + const pathString: string = StorageModel.pathComponentsToString( + ...pathComponents + ); + return localStorage.getItem(pathString); }; remove = ( pathComponents: string[], shouldInitialize: boolean = true ): void => { - const key: string = StorageModel.pathComponentsToKey(...pathComponents); - localStorage.removeItem(key); + const pathString: string = StorageModel.pathComponentsToString( + ...pathComponents + ); + localStorage.removeItem(pathString); if (shouldInitialize == true) { this.initializeTree(); @@ -36,7 +42,7 @@ export default class StorageModel { removeRecursive = (pathComponentsOfEntityToDelete: string[]): void => { a: for (const key of Object.keys(localStorage)) { const pathComponentsOfCurrentEntity: string[] = - StorageModel.keyToPathComponents(key); + StorageModel.stringToPathComponents(key); for (let i = 0; i < pathComponentsOfEntityToDelete.length; i++) { if (!pathComponentsOfCurrentEntity[i]) continue a; @@ -59,7 +65,7 @@ export default class StorageModel { currentParent = nextParent; } - return [...Object.keys(currentParent)]; + return [...Object.keys(currentParent).sort(localeCompare)]; }; // stringifiable @@ -86,7 +92,7 @@ export default class StorageModel { this.storageEntryTree = {}; for (const key of Object.keys(localStorage)) { - const components: string[] = StorageModel.keyToPathComponents(key); + const components: string[] = StorageModel.stringToPathComponents(key); this.updateTree(...components); } }; @@ -102,25 +108,25 @@ export default class StorageModel { } }; - print = () => { - console.log(stringify(this.storageEntryTree)); + printTree = (): string => { + return stringify(this.storageEntryTree); }; - static pathComponentsToKey = (...pathComponents: string[]): string => { + static pathComponentsToString = (...pathComponents: string[]): string => { return pathComponents.join(PATH_COMPONENT_SEPARATOR); }; - static keyToPathComponents = (key: string): string[] => { - return key.split(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.keyToPathComponents(item); + const parts = this.stringToPathComponents(item); allComponents.push(...parts); } - return StorageModel.pathComponentsToKey(...allComponents); + return StorageModel.pathComponentsToString(...allComponents); }; } diff --git a/src/View/Components/directoryItemList.tsx b/src/View/Components/directoryItemList.tsx new file mode 100644 index 00000000..8d01a360 --- /dev/null +++ b/src/View/Components/directoryItemList.tsx @@ -0,0 +1,69 @@ +import * as React from "bloatless-react"; + +import StorageModel from "../../Model/storageModel"; +import { translations } from "../translations"; + +export function DirectoryItemList( + storageModel: StorageModel, + pathString: string, + selectedPath: React.State +) { + // converter + const StringToDirectoryItemList: React.StateItemConverter = ( + pathString: string + ) => DirectoryItemList(storageModel, pathString, selectedPath); + + // data + const path = StorageModel.stringToPathComponents(pathString); + const fileName = path[path.length - 1] || "\\"; + const items = new React.ListState(); + + const style = `text-indent: ${path.length * 2}rem`; + + // methods + function loadItems() { + items.clear(); + + const directoryItems = storageModel.list(path); + + for (const directoryItem of directoryItems) { + const itemPath = [...path, directoryItem]; + const pathString = StorageModel.pathComponentsToString(...itemPath); + items.add(pathString); + } + } + + function select() { + selectedPath.value = pathString; + } + + // state + const isSelected = React.createProxyState( + [selectedPath], + () => selectedPath.value == pathString + ); + + isSelected.subscribe(() => { + if (isSelected.value == false) return; + loadItems(); + }); + + return ( +
+ + +
+
+ ); +} diff --git a/src/View/Components/fileBrowser.css b/src/View/Components/fileBrowser.css new file mode 100644 index 00000000..c458753e --- /dev/null +++ b/src/View/Components/fileBrowser.css @@ -0,0 +1,39 @@ +.file-browser { + width: 100%; + height: 100%; + + display: flex; + + overflow-x: scroll; + overflow-y: hidden; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; +} + +.file-browser > div { + width: 100%; + min-width: 100%; + flex: 0; + + display: flex; + flex-direction: column; + + padding: var(--content-padding); + + scroll-snap-align: center; + overflow: auto; +} + +.file-browser code { + width: 100%; + height: 100%; +} + +@media (min-width: 640px) { + .file-browser > div { + width: 50%; + min-width: 50%; + + flex: 1; + } +} diff --git a/src/View/Components/fileBrowser.tsx b/src/View/Components/fileBrowser.tsx new file mode 100644 index 00000000..70ec4ca7 --- /dev/null +++ b/src/View/Components/fileBrowser.tsx @@ -0,0 +1,26 @@ +import "./fileBrowser.css"; + +import * as React from "bloatless-react"; + +import { DirectoryItemList } from "./directoryItemList"; +import StorageModel from "../../Model/storageModel"; + +export function FileBrowser( + storageModel: StorageModel, + selectedPath: React.State +) { + const fileContent = React.createProxyState([selectedPath], () => { + const path = StorageModel.stringToPathComponents(selectedPath.value); + const content = storageModel.restore(path); + return content || ""; + }); + + return ( +
+ {DirectoryItemList(storageModel, "\\", selectedPath)} +
+ +
+
+ ); +} diff --git a/src/View/Modals/storageModal.tsx b/src/View/Modals/storageModal.tsx new file mode 100644 index 00000000..8ebc7b3f --- /dev/null +++ b/src/View/Modals/storageModal.tsx @@ -0,0 +1,26 @@ +import * as React from "bloatless-react"; + +import { DirectoryItemList } from "../Components/directoryItemList"; +import { FileBrowser } from "../Components/fileBrowser"; +import StorageModel from "../../Model/storageModel"; +import StorageViewModel from "../../ViewModel/storageViewModel"; +import { translations } from "../translations"; + +export function StorageModal(storageViewModel: StorageViewModel) { + return ( + + ); +} diff --git a/src/View/homePage.tsx b/src/View/homePage.tsx index f35d8bea..aa744f9a 100644 --- a/src/View/homePage.tsx +++ b/src/View/homePage.tsx @@ -8,9 +8,11 @@ import ChatListViewModel from "../ViewModel/chatListViewModel"; import { ChatViewModelToChatEntry } from "./Components/chatEntry"; import ConnectionViewModel from "../ViewModel/connectionViewModel"; import SettingsViewModel from "../ViewModel/settingsViewModel"; +import StorageViewModel from "../ViewModel/storageViewModel"; import { translations } from "./translations"; export function HomePage( + storageViewModel: StorageViewModel, settingsViewModel: SettingsViewModel, connectionViewModel: ConnectionViewModel, chatListViewModel: ChatListViewModel @@ -112,6 +114,15 @@ export function HomePage( +
+ + +

@@ -148,7 +159,10 @@ export function HomePage(
); diff --git a/src/View/translations.ts b/src/View/translations.ts index 145e757c..8262547a 100644 --- a/src/View/translations.ts +++ b/src/View/translations.ts @@ -40,6 +40,8 @@ const englishTranslations = { firstDayOfWeekLabel: "First day of week", + manageStorageButton: "Manage storage", + scrollToChatButton: "Chats", /// @@ -106,7 +108,7 @@ const englishTranslations = { resendMessageButton: "Resend message", decryptMessageButton: "Decrypt message", deleteMessageButton: "Delete message", - } + }, }, }; diff --git a/src/ViewModel/connectionViewModel.ts b/src/ViewModel/connectionViewModel.ts index 4b7868b5..6465a1df 100644 --- a/src/ViewModel/connectionViewModel.ts +++ b/src/ViewModel/connectionViewModel.ts @@ -1,7 +1,6 @@ import * as React from "bloatless-react"; import ConnectionModel from "../Model/connectionModel"; -import { Message } from "udn-frontend"; export default class ConnectionViewModel { connectionModel: ConnectionModel; diff --git a/src/ViewModel/storageViewModel.ts b/src/ViewModel/storageViewModel.ts new file mode 100644 index 00000000..1663503a --- /dev/null +++ b/src/ViewModel/storageViewModel.ts @@ -0,0 +1,25 @@ +import * as React from "bloatless-react"; + +import StorageModel from "../Model/storageModel"; + +export default class StorageViewModel { + storageModel: StorageModel; + + // state + isShowingStorageModal: React.State = new React.State(false); + selectedPath: React.State = new React.State("\\"); + + // view methods + showStorageModal = (): void => { + this.isShowingStorageModal.value = true; + } + + hideStorageModal = (): void => { + this.isShowingStorageModal.value = false; + } + + // init + constructor(storageModel: StorageModel) { + this.storageModel = storageModel; + } +} diff --git a/src/index.tsx b/src/index.tsx index bcba86a6..218b3162 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,9 @@ import ConnectionViewModel from "./ViewModel/connectionViewModel"; import { HomePage } from "./View/homePage"; import SettingsModel from "./Model/settingsModel"; import SettingsViewModel from "./ViewModel/settingsViewModel"; +import { StorageModal } from "./View/Modals/storageModal"; import StorageModel from "./Model/storageModel"; +import StorageViewModel from "./ViewModel/storageViewModel"; // models const storageModel = new StorageModel(); @@ -24,9 +26,8 @@ const chatListModel = new ChatListModel( connectionModel ); -storageModel.print(); - // viewModels +const storageViewModel = new StorageViewModel(storageModel); const settingsViewModel = new SettingsViewModel(settingsModel); const connectionViewModel = new ConnectionViewModel(connectionModel); const chatListViewModel = new ChatListViewModel( @@ -53,7 +54,13 @@ document.body.append( document .querySelector("main")! .append( - HomePage(settingsViewModel, connectionViewModel, chatListViewModel), + HomePage( + storageViewModel, + settingsViewModel, + connectionViewModel, + chatListViewModel + ), ChatPageWrapper(chatListViewModel), - ConnectionModal(connectionViewModel) + ConnectionModal(connectionViewModel), + StorageModal(storageViewModel) );