farjs-app
Advanced tools
| /** | ||
| * @import { | ||
| * FileListDirUpdatedAction, | ||
| * FileListDiskSpaceUpdatedAction, | ||
| * } from "@farjs/filelist/FileListActions.mjs" | ||
| */ | ||
| import Task from "@farjs/ui/task/Task.mjs"; | ||
| import TaskAction from "@farjs/ui/task/TaskAction.mjs"; | ||
| import FileListActions from "@farjs/filelist/FileListActions.mjs"; | ||
| import ZipApi from "./ZipApi.mjs"; | ||
| class ZipActions extends FileListActions { | ||
| /** | ||
| * @param {ZipApi} zipApi | ||
| */ | ||
| constructor(zipApi) { | ||
| super(zipApi); | ||
| /** @private @readonly @type {ZipApi} */ | ||
| this.zipApi = zipApi; | ||
| } | ||
| /** @type {FileListActions['updateDir']} */ | ||
| updateDir(dispatch, path) { | ||
| const entriesByParentP = ZipActions.readZip(this.zipApi.zipPath).then( | ||
| (entries) => { | ||
| let totalSize = 0; | ||
| for (const [_, items] of entries) { | ||
| totalSize += items.reduce((total, i) => total + i.size, 0); | ||
| } | ||
| /** @type {FileListDiskSpaceUpdatedAction} */ | ||
| const action = { | ||
| action: "FileListDiskSpaceUpdatedAction", | ||
| diskSpace: totalSize, | ||
| }; | ||
| dispatch(action); | ||
| return entries; | ||
| }, | ||
| ); | ||
| this.api = ZipActions.createApi( | ||
| this.zipApi.zipPath, | ||
| this.zipApi.rootPath, | ||
| entriesByParentP, | ||
| ); | ||
| const updateP = entriesByParentP | ||
| .then(() => this.api.readDir(path)) | ||
| .then((currDir) => { | ||
| /** @type {FileListDirUpdatedAction} */ | ||
| const action = { action: "FileListDirUpdatedAction", currDir }; | ||
| dispatch(action); | ||
| return currDir; | ||
| }); | ||
| return TaskAction(Task("Updating Dir", updateP)); | ||
| } | ||
| static readZip = ZipApi.readZip; | ||
| static createApi = ZipApi.create; | ||
| } | ||
| export default ZipActions; |
| /** | ||
| * @typedef {import("@farjs/blessed").Widgets.Screen} BlessedScreen | ||
| * @typedef {import("@farjs/filelist/api/FileListItem.mjs").FileListItem} FileListItem | ||
| * @import { FileListData } from "@farjs/filelist/FileListData.mjs" | ||
| * @import { FileListPanelProps } from "@farjs/filelist/FileListPanel.mjs" | ||
| * @import PanelStackItem from "@farjs/filelist/stack/PanelStackItem.mjs"; | ||
| * @import { | ||
| * FileListDiskSpaceUpdatedAction, | ||
| * FileListDirChangedAction, | ||
| * } from "@farjs/filelist/FileListActions.mjs" | ||
| */ | ||
| import React, { useLayoutEffect, useState } from "react"; | ||
| import Task from "@farjs/ui/task/Task.mjs"; | ||
| import TaskAction from "@farjs/ui/task/TaskAction.mjs"; | ||
| import MessageBox from "@farjs/ui/popup/MessageBox.mjs"; | ||
| import MessageBoxAction from "@farjs/ui/popup/MessageBoxAction.mjs"; | ||
| import Theme from "@farjs/ui/theme/Theme.mjs"; | ||
| import FileListDir from "@farjs/filelist/api/FileListDir.mjs"; | ||
| import FileListItem from "@farjs/filelist/api/FileListItem.mjs"; | ||
| import WithStacks from "@farjs/filelist/stack/WithStacks.mjs"; | ||
| import WithStacksProps from "@farjs/filelist/stack/WithStacksProps.mjs"; | ||
| import FileListEvent from "@farjs/filelist/FileListEvent.mjs"; | ||
| import FileListState from "@farjs/filelist/FileListState.mjs"; | ||
| import FileListPanel from "@farjs/filelist/FileListPanel.mjs"; | ||
| import ZipApi from "./ZipApi.mjs"; | ||
| import AddToArchController from "../AddToArchController.mjs"; | ||
| const h = React.createElement; | ||
| /** | ||
| * @typedef {{ | ||
| * readonly data: FileListData; | ||
| * readonly items: readonly FileListItem[]; | ||
| * readonly move: boolean; | ||
| * }} ZipPanelState | ||
| */ | ||
| /** | ||
| * @param {string} zipPath | ||
| * @param {string} rootPath | ||
| * @param {Promise<Map<string, readonly FileListItem[]>>} entriesByParentP | ||
| * @param {() => void} onClose | ||
| * @returns | ||
| */ | ||
| const ZipPanel = (zipPath, rootPath, entriesByParentP, onClose) => { | ||
| const { fileListPanelComp, addToArchController, messageBoxComp } = ZipPanel; | ||
| /** | ||
| * @param {FileListPanelProps} props | ||
| */ | ||
| const ZipPanelImpl = (props) => { | ||
| const stacks = WithStacks.useStacks(); | ||
| const [zipData, setZipData] = useState( | ||
| /** @type {ZipPanelState | null} */ (null), | ||
| ); | ||
| const [showWarning, setShowWarning] = useState(false); | ||
| const theme = Theme.useTheme().popup; | ||
| const onClosePanel = () => { | ||
| const data = (() => { | ||
| /** @type {PanelStackItem<FileListState>} */ | ||
| const stackItem = WithStacksProps.active(stacks).stack.peekLast(); | ||
| return stackItem.getData(); | ||
| })(); | ||
| if (data) { | ||
| const { dispatch, actions, state } = data; | ||
| dispatch(actions.updateDir(dispatch, state.currDir.path)); | ||
| } | ||
| onClose(); | ||
| }; | ||
| /** @type {(screen: BlessedScreen, key: string) => boolean} */ | ||
| function onKeypress(_, key) { | ||
| let processed = true; | ||
| switch (key) { | ||
| case "C-pageup": | ||
| if (props.state.currDir.path === rootPath) { | ||
| onClosePanel(); | ||
| } else processed = false; | ||
| break; | ||
| case "enter": | ||
| case "C-pagedown": | ||
| if ( | ||
| FileListState.currentItem( | ||
| props.state, | ||
| (i) => i.isDir && i.name === FileListItem.up.name, | ||
| ) && | ||
| props.state.currDir.path === rootPath | ||
| ) { | ||
| onClosePanel(); | ||
| } else processed = false; | ||
| break; | ||
| case FileListEvent.onFileListCopy: | ||
| case FileListEvent.onFileListMove: | ||
| if (props.state.currDir.path !== rootPath) setShowWarning(true); | ||
| else { | ||
| const data = (() => { | ||
| /** @type {PanelStackItem<FileListState>} */ | ||
| const stackItem = WithStacksProps.active(stacks).stack.peek(); | ||
| return stackItem.getData(); | ||
| })(); | ||
| if (data) { | ||
| const { actions, state } = data; | ||
| const items = (() => { | ||
| if (state.selectedNames.size > 0) { | ||
| return FileListState.selectedItems(state); | ||
| } | ||
| const currItem = FileListState.currentItem( | ||
| state, | ||
| (_) => _ != FileListItem.up, | ||
| ); | ||
| return currItem ? [currItem] : []; | ||
| })(); | ||
| if (actions.api.isLocal && items.length > 0) { | ||
| setZipData({ | ||
| data, | ||
| items, | ||
| move: key === FileListEvent.onFileListMove, | ||
| }); | ||
| } else processed = false; | ||
| } | ||
| } | ||
| break; | ||
| default: | ||
| processed = false; | ||
| break; | ||
| } | ||
| return processed; | ||
| } | ||
| useLayoutEffect(() => { | ||
| if (props.state.currDir.items.length === 0) { | ||
| const zipP = entriesByParentP | ||
| .then((entriesByParent) => { | ||
| let total = 0; | ||
| entriesByParent.forEach((entries) => { | ||
| total += entries.reduce((res, i) => res + i.size, 0.0); | ||
| }); | ||
| /** @type {FileListDiskSpaceUpdatedAction} */ | ||
| const diskSpaceUpdatedAction = { | ||
| action: "FileListDiskSpaceUpdatedAction", | ||
| diskSpace: total, | ||
| }; | ||
| props.dispatch(diskSpaceUpdatedAction); | ||
| /** @type {FileListDirChangedAction} */ | ||
| const dirChangedAction = { | ||
| action: "FileListDirChangedAction", | ||
| dir: FileListItem.currDir.name, | ||
| currDir: FileListDir( | ||
| rootPath, | ||
| false, | ||
| entriesByParent.get("") ?? [], | ||
| ), | ||
| }; | ||
| props.dispatch(dirChangedAction); | ||
| }) | ||
| .catch((error) => { | ||
| /** @type {FileListDirChangedAction} */ | ||
| const dirChangedAction = { | ||
| action: "FileListDirChangedAction", | ||
| dir: FileListItem.currDir.name, | ||
| currDir: FileListDir(rootPath, false, []), | ||
| }; | ||
| props.dispatch(dirChangedAction); | ||
| return Promise.reject(error); | ||
| }); | ||
| props.dispatch(TaskAction(Task("Reading zip archive", zipP))); | ||
| } | ||
| }, []); | ||
| return h( | ||
| React.Fragment, | ||
| null, | ||
| h(fileListPanelComp, { ...props, onKeypress }), | ||
| showWarning | ||
| ? h(messageBoxComp, { | ||
| title: "Warning", | ||
| message: "Items can only be added to zip root.", | ||
| actions: [ | ||
| MessageBoxAction.OK(() => { | ||
| setShowWarning(false); | ||
| }), | ||
| ], | ||
| style: theme.regular, | ||
| }) | ||
| : null, | ||
| zipData !== null | ||
| ? (() => { | ||
| const { items, move } = zipData; | ||
| const { dispatch, actions, state } = zipData.data; | ||
| return h(addToArchController, { | ||
| dispatch, | ||
| actions, | ||
| state, | ||
| archName: zipPath, | ||
| archType: "zip", | ||
| archAction: move ? "Move" : "Copy", | ||
| addToArchApi: ZipApi.addToZip, | ||
| items, | ||
| onComplete: () => { | ||
| setZipData(null); | ||
| const updateAction = props.actions.updateDir( | ||
| props.dispatch, | ||
| props.state.currDir.path, | ||
| ); | ||
| props.dispatch(updateAction); | ||
| if (move) { | ||
| updateAction.task.result.then(() => { | ||
| dispatch( | ||
| actions.deleteItems(dispatch, state.currDir.path, items), | ||
| ); | ||
| }); | ||
| } | ||
| }, | ||
| onCancel: () => { | ||
| setZipData(null); | ||
| }, | ||
| }); | ||
| })() | ||
| : null, | ||
| ); | ||
| }; | ||
| ZipPanelImpl.displayName = "ZipPanel"; | ||
| return ZipPanelImpl; | ||
| }; | ||
| ZipPanel.fileListPanelComp = FileListPanel; | ||
| ZipPanel.addToArchController = AddToArchController; | ||
| ZipPanel.messageBoxComp = MessageBox; | ||
| export default ZipPanel; |
@@ -24,6 +24,6 @@ /** | ||
| /** @private @readonly @type {string} */ | ||
| /** @readonly @type {string} */ | ||
| this.zipPath = zipPath; | ||
| /** @private @readonly @type {string} */ | ||
| /** @readonly @type {string} */ | ||
| this.rootPath = rootPath; | ||
@@ -56,2 +56,43 @@ | ||
| /** @type {FileListApi['readFile']} */ | ||
| async readFile(parent, item) { | ||
| const filePath = stripPrefix( | ||
| stripPrefix(`${parent}/${item.name}`, this.rootPath), | ||
| "/", | ||
| ); | ||
| const { stdout, exitP } = await this.extract(this.zipPath, filePath); | ||
| let pos = 0; | ||
| return { | ||
| file: filePath, | ||
| readNextBytes: async (buff) => { | ||
| const content = await stdout.readNextBytes(buff.length); | ||
| if (content !== undefined) { | ||
| const bytesRead = content.length; | ||
| content.copy(buff, 0, 0, bytesRead); | ||
| pos += bytesRead; | ||
| return bytesRead; | ||
| } | ||
| if (pos !== item.size) { | ||
| const error = await exitP; | ||
| if (error) { | ||
| throw error; | ||
| } | ||
| } | ||
| return 0; | ||
| }, | ||
| close: async () => { | ||
| if (pos !== item.size) { | ||
| stdout.readable.destroy(); | ||
| } | ||
| await exitP; | ||
| }, | ||
| }; | ||
| } | ||
| /** @type {FileListApi['delete']} */ | ||
@@ -186,4 +227,11 @@ delete(parent, items) { | ||
| } | ||
| /** | ||
| * @type {(zipPath: string, rootPath: string, entriesByParentP: Promise<Map<string, readonly FileListItem[]>>) => ZipApi} | ||
| */ | ||
| static create = (zipPath, rootPath, entriesByParentP) => { | ||
| return new ZipApi(zipPath, rootPath, entriesByParentP); | ||
| }; | ||
| } | ||
| export default ZipApi; |
+2
-2
| { | ||
| "author": "viktor-podzigun", | ||
| "name": "farjs-app", | ||
| "version": "0.19.7", | ||
| "version": "0.19.8", | ||
| "repository": { | ||
@@ -77,3 +77,3 @@ "type": "git", | ||
| "@farjs/better-sqlite3-migrate": "^3.0.0", | ||
| "@farjs/filelist": "0.8.2", | ||
| "@farjs/filelist": "0.8.3", | ||
| "@farjs/ui": "0.9.1", | ||
@@ -80,0 +80,0 @@ "iconv-lite": "0.6.3" |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
89
2.3%682886
-12.18%21815
-10.22%+ Added
- Removed
Updated