add file browser
This commit is contained in:
+23
-17
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string>
|
||||
) {
|
||||
// converter
|
||||
const StringToDirectoryItemList: React.StateItemConverter<string> = (
|
||||
pathString: string
|
||||
) => DirectoryItemList(storageModel, pathString, selectedPath);
|
||||
|
||||
// data
|
||||
const path = StorageModel.stringToPathComponents(pathString);
|
||||
const fileName = path[path.length - 1] || "\\";
|
||||
const items = new React.ListState<string>();
|
||||
|
||||
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 (
|
||||
<div class="flex-column">
|
||||
<button
|
||||
class="width-100 flex-1 clip"
|
||||
toggle:selected={isSelected}
|
||||
on:click={select}
|
||||
>
|
||||
<span class="ellipsis width-100 flex-1" style={style}>
|
||||
{fileName}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="flex-column"
|
||||
children:append={[items, StringToDirectoryItemList]}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string>
|
||||
) {
|
||||
const fileContent = React.createProxyState([selectedPath], () => {
|
||||
const path = StorageModel.stringToPathComponents(selectedPath.value);
|
||||
const content = storageModel.restore(path);
|
||||
return content || "";
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="file-browser">
|
||||
{DirectoryItemList(storageModel, "\\", selectedPath)}
|
||||
<div>
|
||||
<code subscribe:innerText={fileContent}></code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
|
||||
<div style="max-width: 64rem">
|
||||
<main class="padding-0">
|
||||
{FileBrowser(
|
||||
storageViewModel.storageModel,
|
||||
storageViewModel.selectedPath
|
||||
)}
|
||||
</main>
|
||||
<button on:click={storageViewModel.hideStorageModal}>
|
||||
{translations.general.closeButton}
|
||||
<span class="icon">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+15
-1
@@ -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(
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -148,7 +159,10 @@ export function HomePage(
|
||||
|
||||
<div
|
||||
id="chat-grid"
|
||||
children:append={[chatListViewModel.chatViewModels, ChatViewModelToChatEntry]}
|
||||
children:append={[
|
||||
chatListViewModel.chatViewModels,
|
||||
ChatViewModelToChatEntry,
|
||||
]}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<boolean> = new React.State(false);
|
||||
selectedPath: React.State<string> = new React.State("\\");
|
||||
|
||||
// view methods
|
||||
showStorageModal = (): void => {
|
||||
this.isShowingStorageModal.value = true;
|
||||
}
|
||||
|
||||
hideStorageModal = (): void => {
|
||||
this.isShowingStorageModal.value = false;
|
||||
}
|
||||
|
||||
// init
|
||||
constructor(storageModel: StorageModel) {
|
||||
this.storageModel = storageModel;
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -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)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user