add DangerousActionButton, add option to delete files thru StorageModal

This commit is contained in:
marlon-erler
2024-07-21 20:21:32 +02:00
parent 3824224e85
commit 331823f2ec
9 changed files with 259 additions and 118 deletions
+103 -41
View File
@@ -1100,6 +1100,8 @@
general: { general: {
closeButton: "Close", closeButton: "Close",
deleteItemButtonAudioLabel: "delete item", deleteItemButtonAudioLabel: "delete item",
abortButton: "Abort",
confirmButton: "Confirm",
setButton: "Set" setButton: "Set"
}, },
regional: { regional: {
@@ -1143,12 +1145,13 @@
/// ///
connectButtonAudioLabel: "connect" connectButtonAudioLabel: "connect"
}, },
fileBrowser: { storage: {
noItemSelected: "No item selected", noItemSelected: "No item selected",
notAFile: "(not a file)", notAFile: "(not a file)",
contentEmpty: "(empty)", contentEmpty: "(empty)",
path: "Path", path: "Path",
content: "Content" content: "Content",
deleteItem: "Delete item"
}, },
chatPage: { chatPage: {
closeChatAudioLabe: "close chat", closeChatAudioLabe: "close chat",
@@ -1306,6 +1309,40 @@
)))))); ))))));
} }
// src/View/Components/dangerousActionButton.tsx
function DangerousActionButton(label, icon, action) {
const isActionRequested = new State(false);
const cannotConfirm = createProxyState(
[isActionRequested],
() => isActionRequested.value == false
);
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
return /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": abort, "toggle:hidden": cannotConfirm }, translations.general.abortButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "undo")), /* @__PURE__ */ createElement(
"button",
{
class: "danger flex",
"on:click": requestAction,
"toggle:hidden": isActionRequested
},
label,
/* @__PURE__ */ createElement("span", { class: "icon" }, icon)
), /* @__PURE__ */ createElement(
"button",
{
class: "danger flex",
"on:click": action,
"toggle:hidden": cannotConfirm
},
translations.general.confirmButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "warning")
));
}
// src/View/Components/deletableListItem.tsx // src/View/Components/deletableListItem.tsx
function DeletableListItem(text, primaryButton, ondelete) { function DeletableListItem(text, primaryButton, ondelete) {
return /* @__PURE__ */ createElement("div", { class: "tile flex-row justify-apart align-center padding-0" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, text), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, primaryButton, /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "tile flex-row justify-apart align-center padding-0" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, text), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, primaryButton, /* @__PURE__ */ createElement(
@@ -1409,15 +1446,11 @@
"on:click": setColor "on:click": setColor
} }
); );
})), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement( })), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
"button",
{
class: "danger width-input flex-no",
"on:click": chatViewModel.remove
},
translations.chatPage.settings.deleteChatButton, translations.chatPage.settings.deleteChatButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "delete_forever") "chat_error",
))); chatViewModel.remove
))));
} }
// src/View/chatPage.tsx // src/View/chatPage.tsx
@@ -2092,15 +2125,15 @@
}; };
// src/View/Components/directoryItemList.tsx // src/View/Components/directoryItemList.tsx
function DirectoryItemList(storageModel2, pathString, selectedPath) { function DirectoryItemList(storageViewModel2, pathString = PATH_COMPONENT_SEPARATOR) {
const StringToDirectoryItemList = (pathString2) => DirectoryItemList(storageModel2, pathString2, selectedPath); const StringToDirectoryItemList = (pathString2) => DirectoryItemList(storageViewModel2, pathString2);
const path = StorageModel.stringToPathComponents(pathString); const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path); const fileName = StorageModel.getFileName(path);
const items = new ListState(); const items = new ListState();
const style = `text-indent: ${path.length * 2}rem`; const style = `text-indent: ${path.length * 2}rem`;
function loadItems() { function loadItems() {
items.clear(); items.clear();
const directoryItems = storageModel2.list(path); const directoryItems = storageViewModel2.storageModel.list(path);
for (const directoryItem of directoryItems) { for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem]; const itemPath = [...path, directoryItem];
const pathString2 = StorageModel.pathComponentsToString(...itemPath); const pathString2 = StorageModel.pathComponentsToString(...itemPath);
@@ -2108,11 +2141,16 @@
} }
} }
function select() { function select() {
selectedPath.value = pathString; storageViewModel2.selectedPath.value = pathString;
} }
storageViewModel2.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
select();
setTimeout(() => loadItems(), 50);
});
const isSelected = createProxyState( const isSelected = createProxyState(
[selectedPath], [storageViewModel2.selectedPath],
() => selectedPath.value == pathString () => storageViewModel2.selectedPath.value == pathString
); );
isSelected.subscribe(() => { isSelected.subscribe(() => {
if (isSelected.value == false) return; if (isSelected.value == false) return;
@@ -2136,30 +2174,35 @@
} }
// src/View/Components/fileBrowser.tsx // src/View/Components/fileBrowser.tsx
function FileBrowser(storageModel2, selectedPath, didMakeChanges) { function FileBrowser(storageViewModel2) {
const fileContentToShow = createProxyState([selectedPath], () => { const detailView = createProxyState(
const path = StorageModel.stringToPathComponents(selectedPath.value); [storageViewModel2.selectedPath],
const content = storageModel2.restore(path); () => {
return (content ?? translations.fileBrowser.notAFile) || translations.fileBrowser.contentEmpty; if (storageViewModel2.selectedPath.value == PATH_COMPONENT_SEPARATOR)
}); return /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.storage.noItemSelected);
const selectedFileName = createProxyState( return /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.storage.path), /* @__PURE__ */ createElement(
[selectedPath], "span",
() => StorageModel.getFileNameFromString(selectedPath.value) ?? PATH_COMPONENT_SEPARATOR {
class: "break-all",
"subscribe:innerText": storageViewModel2.selectedPath
}
))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.storage.content), /* @__PURE__ */ createElement(
"code",
{
"subscribe:innerText": storageViewModel2.selectedFileContent
}
))), DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel2.deleteSelectedItem
));
}
); );
const detailView = createProxyState([selectedPath], () => { const view = /* @__PURE__ */ createElement("div", { class: "file-browser" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "scroll-area" }, DirectoryItemList(storageViewModel2)), /* @__PURE__ */ createElement("div", { class: "detail-button-wrapper" }, /* @__PURE__ */ createElement("button", { class: "ghost", "on:click": scrollToDetails }, /* @__PURE__ */ createElement(
if (selectedPath.value == PATH_COMPONENT_SEPARATOR)
return /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.fileBrowser.noItemSelected);
return /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.fileBrowser.path), /* @__PURE__ */ createElement("span", { class: "break-all", "subscribe:innerText": selectedPath }))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.fileBrowser.content), /* @__PURE__ */ createElement("code", { "subscribe:innerText": fileContentToShow }))));
});
const view = /* @__PURE__ */ createElement("div", { class: "file-browser" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "scroll-area" }, DirectoryItemList(
storageModel2,
PATH_COMPONENT_SEPARATOR,
selectedPath
)), /* @__PURE__ */ createElement("div", { class: "detail-button-wrapper" }, /* @__PURE__ */ createElement("button", { class: "ghost", "on:click": scrollToDetails }, /* @__PURE__ */ createElement(
"span", "span",
{ {
class: "ellipsis", class: "ellipsis",
"subscribe:innerText": selectedFileName "subscribe:innerText": storageViewModel2.selectedFileName
} }
), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))), /* @__PURE__ */ createElement("div", { class: "scroll-area", "children:set": detailView })); ), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))), /* @__PURE__ */ createElement("div", { class: "scroll-area", "children:set": detailView }));
function scrollToDetails() { function scrollToDetails() {
@@ -2170,11 +2213,7 @@
// src/View/Modals/storageModal.tsx // src/View/Modals/storageModal.tsx
function StorageModal(storageViewModel2) { 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( 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)), /* @__PURE__ */ createElement("button", { "on:click": storageViewModel2.hideStorageModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
storageViewModel2.storageModel,
storageViewModel2.selectedPath,
storageViewModel2.didMakeChanges
)), /* @__PURE__ */ createElement("button", { "on:click": storageViewModel2.hideStorageModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
} }
// src/ViewModel/storageViewModel.ts // src/ViewModel/storageViewModel.ts
@@ -2184,6 +2223,21 @@
isShowingStorageModal = new State(false); isShowingStorageModal = new State(false);
selectedPath = new State(PATH_COMPONENT_SEPARATOR); selectedPath = new State(PATH_COMPONENT_SEPARATOR);
didMakeChanges = new State(false); didMakeChanges = new State(false);
selectedFileName;
selectedFileContent;
lastDeletedItemPath = new State("");
// methods
getSelectedItemContent = () => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
const content = this.storageModel.restore(path);
return (content ?? translations.storage.notAFile) || translations.storage.contentEmpty;
};
deleteSelectedItem = () => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
this.lastDeletedItemPath.value = this.selectedPath.value;
this.storageModel.removeRecursive(path);
this.didMakeChanges.value = true;
};
// view methods // view methods
showStorageModal = () => { showStorageModal = () => {
this.isShowingStorageModal.value = true; this.isShowingStorageModal.value = true;
@@ -2197,6 +2251,14 @@
// init // init
constructor(storageModel2) { constructor(storageModel2) {
this.storageModel = storageModel2; this.storageModel = storageModel2;
this.selectedFileName = createProxyState(
[this.selectedPath],
() => StorageModel.getFileNameFromString(this.selectedPath.value)
);
this.selectedFileContent = createProxyState(
[this.selectedPath],
() => this.getSelectedItemContent()
);
} }
}; };
+5 -2
View File
@@ -457,11 +457,14 @@ textarea {
display: inline-flex; display: inline-flex;
} }
.modal, .modal{
[hidden] {
display: none; display: none;
} }
[hidden] {
display: none !important;
}
main, main,
.tile, .tile,
.tile > div { .tile > div {
+8 -7
View File
@@ -2,6 +2,7 @@ import * as React from "bloatless-react";
import ChatViewModel from "../../ViewModel/chatViewModel"; import ChatViewModel from "../../ViewModel/chatViewModel";
import { Color } from "../../ViewModel/colors"; import { Color } from "../../ViewModel/colors";
import { DangerousActionButton } from "../Components/dangerousActionButton";
import { DeletableListItem } from "../Components/deletableListItem"; import { DeletableListItem } from "../Components/deletableListItem";
import { translations } from "../translations"; import { translations } from "../translations";
@@ -139,13 +140,13 @@ export function SettingsPage(chatViewModel: ChatViewModel) {
<hr></hr> <hr></hr>
<button <div class="width-input">
class="danger width-input flex-no" {DangerousActionButton(
on:click={chatViewModel.remove} translations.chatPage.settings.deleteChatButton,
> "chat_error",
{translations.chatPage.settings.deleteChatButton} chatViewModel.remove
<span class="icon">delete_forever</span> )}
</button> </div>
</div> </div>
</div> </div>
); );
@@ -0,0 +1,47 @@
import * as React from "bloatless-react";
import { translations } from "../translations";
export function DangerousActionButton(
label: string,
icon: string,
action: () => void
) {
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false
);
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
}
+20 -10
View File
@@ -1,17 +1,19 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import StorageModel from "../../Model/storageModel"; import StorageModel, {
import { translations } from "../translations"; PATH_COMPONENT_SEPARATOR,
} from "../../Model/storageModel";
import StorageViewModel from "../../ViewModel/storageViewModel";
export function DirectoryItemList( export function DirectoryItemList(
storageModel: StorageModel, storageViewModel: StorageViewModel,
pathString: string, pathString: string = PATH_COMPONENT_SEPARATOR
selectedPath: React.State<string>
) { ) {
// converter // converter
const StringToDirectoryItemList: React.StateItemConverter<string> = ( const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string pathString: string
) => DirectoryItemList(storageModel, pathString, selectedPath); ) => DirectoryItemList(storageViewModel, pathString);
// data // data
const path = StorageModel.stringToPathComponents(pathString); const path = StorageModel.stringToPathComponents(pathString);
@@ -24,7 +26,7 @@ export function DirectoryItemList(
function loadItems() { function loadItems() {
items.clear(); items.clear();
const directoryItems = 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];
@@ -34,13 +36,21 @@ export function DirectoryItemList(
} }
function select() { function select() {
selectedPath.value = pathString; storageViewModel.selectedPath.value = pathString;
} }
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
select();
setTimeout(() => loadItems(), 50)
});
// state // state
const isSelected = React.createProxyState( const isSelected = React.createProxyState(
[selectedPath], [storageViewModel.selectedPath],
() => selectedPath.value == pathString () => storageViewModel.selectedPath.value == pathString
); );
isSelected.subscribe(() => { isSelected.subscribe(() => {
+27 -39
View File
@@ -6,71 +6,59 @@ import StorageModel, {
PATH_COMPONENT_SEPARATOR, PATH_COMPONENT_SEPARATOR,
} from "../../Model/storageModel"; } from "../../Model/storageModel";
import { DangerousActionButton } from "./dangerousActionButton";
import { DirectoryItemList } from "./directoryItemList"; import { DirectoryItemList } from "./directoryItemList";
import StorageViewModel from "../../ViewModel/storageViewModel";
import { translations } from "../translations"; import { translations } from "../translations";
export function FileBrowser( export function FileBrowser(storageViewModel: StorageViewModel) {
storageModel: StorageModel, const detailView = React.createProxyState(
selectedPath: React.State<string>, [storageViewModel.selectedPath],
didMakeChanges: React.State<boolean> () => {
) { if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
// state
const fileContentToShow = React.createProxyState([selectedPath], () => {
const path = StorageModel.stringToPathComponents(selectedPath.value);
const content = storageModel.restore(path);
return ( return (
(content ?? translations.fileBrowser.notAFile) || <span class="secondary">{translations.storage.noItemSelected}</span>
translations.fileBrowser.contentEmpty
);
});
const selectedFileName = React.createProxyState(
[selectedPath],
() =>
StorageModel.getFileNameFromString(selectedPath.value) ??
PATH_COMPONENT_SEPARATOR
);
// view
const detailView = React.createProxyState([selectedPath], () => {
if (selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<span class="secondary">{translations.fileBrowser.noItemSelected}</span>
); );
return ( return (
<div class="flex-column gap"> <div class="flex-column gap">
<div class="tile flex-no"> <div class="tile flex-no">
<div> <div>
<b>{translations.fileBrowser.path}</b> <b>{translations.storage.path}</b>
<span class="break-all" subscribe:innerText={selectedPath}></span> <span
class="break-all"
subscribe:innerText={storageViewModel.selectedPath}
></span>
</div> </div>
</div> </div>
<div class="tile flex-no"> <div class="tile flex-no">
<div> <div>
<b>{translations.fileBrowser.content}</b> <b>{translations.storage.content}</b>
<code subscribe:innerText={fileContentToShow}></code> <code
subscribe:innerText={storageViewModel.selectedFileContent}
></code>
</div> </div>
</div> </div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem
)}
</div> </div>
); );
}); }
);
const view = ( const view = (
<div class="file-browser"> <div class="file-browser">
<div> <div>
<div class="scroll-area"> <div class="scroll-area">{DirectoryItemList(storageViewModel)}</div>
{DirectoryItemList(
storageModel,
PATH_COMPONENT_SEPARATOR,
selectedPath
)}
</div>
<div class="detail-button-wrapper"> <div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}> <button class="ghost" on:click={scrollToDetails}>
<span <span
class="ellipsis" class="ellipsis"
subscribe:innerText={selectedFileName} subscribe:innerText={storageViewModel.selectedFileName}
></span> ></span>
<span class="icon">arrow_forward</span> <span class="icon">arrow_forward</span>
</button> </button>
+1 -7
View File
@@ -8,13 +8,7 @@ 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"> <main class="padding-0">{FileBrowser(storageViewModel)}</main>
{FileBrowser(
storageViewModel.storageModel,
storageViewModel.selectedPath,
storageViewModel.didMakeChanges
)}
</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>
+5 -1
View File
@@ -2,6 +2,8 @@ const englishTranslations = {
general: { general: {
closeButton: "Close", closeButton: "Close",
deleteItemButtonAudioLabel: "delete item", deleteItemButtonAudioLabel: "delete item",
abortButton: "Abort",
confirmButton: "Confirm",
setButton: "Set", setButton: "Set",
}, },
@@ -62,13 +64,15 @@ const englishTranslations = {
connectButtonAudioLabel: "connect", connectButtonAudioLabel: "connect",
}, },
fileBrowser: { storage: {
noItemSelected: "No item selected", noItemSelected: "No item selected",
notAFile: "(not a file)", notAFile: "(not a file)",
contentEmpty: "(empty)", contentEmpty: "(empty)",
path: "Path", path: "Path",
content: "Content", content: "Content",
deleteItem: "Delete item"
}, },
chatPage: { chatPage: {
+32
View File
@@ -2,6 +2,8 @@ import * as React from "bloatless-react";
import StorageModel, { PATH_COMPONENT_SEPARATOR } from "../Model/storageModel"; import StorageModel, { PATH_COMPONENT_SEPARATOR } from "../Model/storageModel";
import { translations } from "../View/translations";
export default class StorageViewModel { export default class StorageViewModel {
storageModel: StorageModel; storageModel: StorageModel;
@@ -10,6 +12,29 @@ export default class StorageViewModel {
selectedPath: React.State<string> = new React.State(PATH_COMPONENT_SEPARATOR); selectedPath: React.State<string> = new React.State(PATH_COMPONENT_SEPARATOR);
didMakeChanges: React.State<boolean> = new React.State(false); didMakeChanges: React.State<boolean> = new React.State(false);
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.restore(path);
return (
(content ?? translations.storage.notAFile) ||
translations.storage.contentEmpty
);
};
deleteSelectedItem = (): void => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
this.lastDeletedItemPath.value = this.selectedPath.value;
this.storageModel.removeRecursive(path);
this.didMakeChanges.value = true;
};
// view methods // view methods
showStorageModal = (): void => { showStorageModal = (): void => {
this.isShowingStorageModal.value = true; this.isShowingStorageModal.value = true;
@@ -25,5 +50,12 @@ export default class StorageViewModel {
// init // init
constructor(storageModel: StorageModel) { constructor(storageModel: StorageModel) {
this.storageModel = storageModel; this.storageModel = storageModel;
this.selectedFileName = React.createProxyState([this.selectedPath], () =>
StorageModel.getFileNameFromString(this.selectedPath.value)
);
this.selectedFileContent = React.createProxyState([this.selectedPath], () =>
this.getSelectedItemContent()
);
} }
} }