// Copyright 2017-2026 @pezkuwi/app-files authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { ActionStatusBase } from '@pezkuwi/react-components/Status/types'; import type { DirFile, FileInfo, SaveFile } from './types.js'; import FileSaver from 'file-saver'; import React, { useCallback, useRef, useState } from 'react'; import { Badge, Button, CopyButton, Icon, styled, Table } from '@pezkuwi/react-components'; import { useQueue } from '@pezkuwi/react-hooks'; import { useFiles } from './hooks.js'; import { useTranslation } from './translate.js'; import UploadModal from './UploadModal.js'; const MCopyButton = styled(CopyButton)` .copySpan { display: none; } `; const ItemFile = styled.tr` height: 3.5rem; .end { text-align: end; } .actions { display: flex; justify-content: flex-end; align-items: center; } `; const shortStr = (name: string, count = 6): string => { if (name.length > (count * 2)) { return `${name.substring(0, count)}...${name.substring(name.length - count)}`; } return name; }; function createUrl (f: SaveFile) { const endpoint = 'https://cf-ipfs.com'; return `${endpoint}/ipfs/${f.Hash}?filename=${f.Name}`; } const createOnDown = (f: SaveFile) => () => { window.open(createUrl(f), '_blank'); // FileSaver.saveAs(createUrl(f), f.Name); }; type FunInputFile = (e: React.ChangeEvent) => void const Noop = (): void => undefined; export interface Props { className?: string, } function CrustFiles ({ className }: Props): React.ReactElement { const { t } = useTranslation(); const { queueAction } = useQueue(); const [showUpMode, setShowUpMode] = useState(false); const wFiles = useFiles(); const [file, setFile] = useState(undefined); const inputRef = useRef(null); const _clickUploadFile = useCallback((dir = false) => { if (!inputRef.current) { return; } // eslint-disable-next-line // @ts-ignore // eslint-disable-next-line inputRef.current.webkitdirectory = dir; // eslint-disable-next-line inputRef.current.multiple = dir; inputRef.current.click(); }, [inputRef]); const onClickUpFile = useCallback(() => _clickUploadFile(false), [_clickUploadFile]); const onClickUpFolder = useCallback(() => _clickUploadFile(true), [_clickUploadFile]); const _onInputFile = useCallback((e) => { const files = e.target.files; if (!files) { return; } if (files.length > 2000) { queueAction({ action: 'Upload Folder', message: t('Please do not upload more than 2000 files'), status: 'error' }); return; } if (files.length === 0) { queueAction({ action: 'Upload Folder', message: t('Please select non-empty folder'), status: 'error' }); return; } // eslint-disable-next-line // @ts-ignore // eslint-disable-next-line const isDirectory = e.target.webkitdirectory; if (!isDirectory) { setFile({ file: files[0] }); setShowUpMode(true); } else if (files.length >= 1) { // eslint-disable-next-line // @ts-ignore eslint-disable-next-line // eslint-disable-next-line @typescript-eslint/no-unsafe-call const dirFiles: DirFile[] = []; for (let i = 0, count = files.length; i < count; i++) { // console.info('f:', files[index]); dirFiles.push(files[i]); } console.info(dirFiles); const [dir] = dirFiles[0].webkitRelativePath.split('/'); setFile({ dir, files: dirFiles }); setShowUpMode(true); } e.target.value = ''; }, [setFile, setShowUpMode, queueAction, t]); const _onImportResult = useCallback<(m: string, s?: ActionStatusBase['status']) => void>( (message, status = 'queued') => { queueAction && queueAction({ action: t('Import files'), message, status }); }, [queueAction, t] ); const importInputRef = useRef(null); const _clickImport = useCallback(() => { if (!importInputRef.current) { return; } importInputRef.current.click(); }, [importInputRef]); const _onInputImportFile = useCallback((e) => { try { _onImportResult(t('Importing')); const fileReader = new FileReader(); const files = e.target.files; if (!files) { return; } fileReader.readAsText(files[0], 'UTF-8'); if (!(/(.json)$/i.test(e.target.value))) { return _onImportResult(t('file error'), 'error'); } fileReader.onload = (e) => { const _list = JSON.parse(e.target?.result as string) as SaveFile[]; if (!Array.isArray(_list)) { return _onImportResult(t('file content error'), 'error'); } const fitter: SaveFile[] = []; const mapImport: Record = {}; for (const item of _list) { if (item.Hash && item.Name && item.UpEndpoint && item.PinEndpoint) { fitter.push(item); mapImport[item.Hash] = true; } } const filterOld = wFiles.files.filter((item) => !mapImport[item.Hash]); wFiles.setFiles([...fitter, ...filterOld]); _onImportResult(t('Import Success'), 'success'); }; } catch { _onImportResult(t('file content error'), 'error'); } }, [wFiles, _onImportResult, t]); const _onClose = useCallback(() => { setShowUpMode(false); }, []); const _onSuccess = useCallback((res: SaveFile) => { setShowUpMode(false); const filterFiles = wFiles.files.filter((f) => f.Hash !== res.Hash); wFiles.setFiles([res, ...filterFiles]); }, [wFiles]); const _export = useCallback(() => { const blob = new Blob([JSON.stringify(wFiles.files)], { type: 'application/json; charset=utf-8' }); // eslint-disable-next-line deprecation/deprecation FileSaver.saveAs(blob, 'files.json'); }, [wFiles]); return
{file && showUpMode && ( )}
{wFiles.files.map((f, index) =>
{f.items && ( )} {shortStr(f.Name)} {f.Hash} {`${f.Size} bytes`} {t('View status in Crust')}
{!f.items && ( )}
)}
{t('Note: The file list is cached locally, switching browsers or devices will not keep displaying the original browser information.')}
; } const StyledMain = styled.main` h1 { text-transform: unset !important; } .uploadBtn { position: relative; padding: 5px 0; &:hover { .uploadMenu { display: block; } } } .uploadMenu { z-index: 200; display: none; background-color: var(--bg-table); position: absolute; top: 43px; left: 0; box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); border-radius: 4px; overflow: hidden; line-height: 40px; .menuItem { cursor: pointer; padding: 0 2rem; display: flex; align-items: center; white-space: nowrap; &:hover { background-color: var(--bg-page); } } } `; export default React.memo(CrustFiles);