mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-07-28 06:15:55 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
// Copyright 2017-2025 @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<HTMLInputElement>) => void
|
||||
|
||||
const Noop = (): void => undefined;
|
||||
|
||||
export interface Props {
|
||||
className?: string,
|
||||
}
|
||||
|
||||
function CrustFiles ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { queueAction } = useQueue();
|
||||
const [showUpMode, setShowUpMode] = useState(false);
|
||||
const wFiles = useFiles();
|
||||
const [file, setFile] = useState<FileInfo | undefined>(undefined);
|
||||
const inputRef = useRef<HTMLInputElement>(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<FunInputFile>((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<HTMLInputElement>(null);
|
||||
const _clickImport = useCallback(() => {
|
||||
if (!importInputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
importInputRef.current.click();
|
||||
}, [importInputRef]);
|
||||
const _onInputImportFile = useCallback<FunInputFile>((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<string, boolean> = {};
|
||||
|
||||
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 <StyledMain className={className}>
|
||||
<header></header>
|
||||
<input
|
||||
onChange={_onInputFile}
|
||||
ref={inputRef}
|
||||
style={{ display: 'none' }}
|
||||
type={'file'}
|
||||
/>
|
||||
<input
|
||||
onChange={_onInputImportFile}
|
||||
ref={importInputRef}
|
||||
style={{ display: 'none' }}
|
||||
type={'file'}
|
||||
/>
|
||||
{file && showUpMode && (
|
||||
<UploadModal
|
||||
file={file}
|
||||
onClose={_onClose}
|
||||
onSuccess={_onSuccess}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', paddingBottom: '1.5rem' }}>
|
||||
<div className='uploadBtn'>
|
||||
<Button
|
||||
icon={'upload'}
|
||||
label={t('Upload')}
|
||||
onClick={Noop}
|
||||
/>
|
||||
<div className='uploadMenu'>
|
||||
<div
|
||||
className='menuItem'
|
||||
onClick={onClickUpFile}
|
||||
>{t('File')}</div>
|
||||
<div
|
||||
className='menuItem'
|
||||
onClick={onClickUpFolder}
|
||||
>{t('Folder')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Button
|
||||
icon={'file-import'}
|
||||
label={t('Import')}
|
||||
onClick={_clickImport}
|
||||
/>
|
||||
<Button
|
||||
icon={'file-export'}
|
||||
label={t('Export')}
|
||||
onClick={_export}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
empty={t('No files')}
|
||||
emptySpinner={t('Loading')}
|
||||
header={[
|
||||
[t('files'), 'start', 2],
|
||||
[t('file cid'), 'expand', 2],
|
||||
[undefined, 'start'],
|
||||
[t('file size'), 'expand', 2],
|
||||
[t('status'), 'expand'],
|
||||
[t('action'), 'expand'],
|
||||
[]
|
||||
]}
|
||||
>
|
||||
{wFiles.files.map((f, index) =>
|
||||
<ItemFile key={`files_item-${index}`}>
|
||||
<td
|
||||
className=''
|
||||
colSpan={2}
|
||||
>
|
||||
{f.items && (
|
||||
<Icon
|
||||
className='highlight--color'
|
||||
icon='folder'
|
||||
/>
|
||||
)}
|
||||
{shortStr(f.Name)}</td>
|
||||
<td
|
||||
className='end'
|
||||
colSpan={2}
|
||||
>{f.Hash}</td>
|
||||
<td
|
||||
className=''
|
||||
colSpan={1}
|
||||
>
|
||||
<MCopyButton value={f.Hash}>
|
||||
<Badge
|
||||
color='highlight'
|
||||
hover={t('Copy file cid')}
|
||||
icon='copy'
|
||||
/>
|
||||
</MCopyButton>
|
||||
</td>
|
||||
<td
|
||||
className='end'
|
||||
colSpan={2}
|
||||
>{`${f.Size} bytes`}</td>
|
||||
<td
|
||||
className='end'
|
||||
colSpan={1}
|
||||
>
|
||||
<a
|
||||
href={'https://apps.crust.network/?rpc=wss%3A%2F%2Frpc.crust.network#/storage_files/status/' + f.Hash}
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>{t('View status in Crust')}</a>
|
||||
</td>
|
||||
<td
|
||||
className='end'
|
||||
colSpan={1}
|
||||
>
|
||||
<div className='actions'>
|
||||
{!f.items && (
|
||||
<Badge
|
||||
color='highlight'
|
||||
hover={t('Download')}
|
||||
icon='download'
|
||||
onClick={createOnDown(f)}
|
||||
/>
|
||||
)}
|
||||
<MCopyButton value={createUrl(f)}>
|
||||
<Badge
|
||||
color='highlight'
|
||||
hover={t('Copy link')}
|
||||
icon='copy'
|
||||
/>
|
||||
</MCopyButton>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td colSpan={1} />
|
||||
</ItemFile>
|
||||
)}
|
||||
</Table>
|
||||
<div>
|
||||
{t('Note: The file list is cached locally, switching browsers or devices will not keep displaying the original browser information.')}
|
||||
</div>
|
||||
</StyledMain>;
|
||||
}
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user