Working Upload (#356)

This commit is contained in:
Piotr Rogowski 2022-01-02 22:25:52 +01:00 committed by GitHub
parent 4c19e143ea
commit 9248ceb7a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 653 additions and 134 deletions

1
package-lock.json generated
View File

@ -16,6 +16,7 @@
"antd": "^4.18.0",
"firebase": "^9.6.1",
"mlg-converter": "^0.5.1",
"nanoid": "^3.1.30",
"pako": "^2.0.4",
"react": "^17.0.1",
"react-dom": "^17.0.2",

View File

@ -38,6 +38,7 @@
"antd": "^4.18.0",
"firebase": "^9.6.1",
"mlg-converter": "^0.5.1",
"nanoid": "^3.1.30",
"pako": "^2.0.4",
"react": "^17.0.1",
"react-dom": "^17.0.2",

View File

@ -50,6 +50,10 @@ html, body {
// border-top-width: 1px;
// border-top-color: @border-color-split;
// border-top-style: solid;
a {
color: @text;
}
}
.app-content {
@ -68,6 +72,15 @@ html, body {
user-select: none;
}
.ant-upload-list-picture-card
.ant-upload-list-item-actions
.anticon-delete,
.ant-upload-list-picture-card
.ant-upload-list-item-actions
.anticon-eye {
color: @text;
}
.table {
margin: 20px;

View File

@ -26,20 +26,19 @@ import {
import Dialog from './components/Dialog';
import { loadAll } from './utils/api';
import SideBar, { DialogMatchedPathType } from './components/SideBar';
import BurnButton from './components/BurnButton';
import TopBar from './components/TopBar';
import StatusBar from './components/StatusBar';
import { isDesktop } from './utils/env';
import 'react-perfect-scrollbar/dist/css/styles.css';
import './App.less';
import { Routes } from './routes';
import Log from './components/Log';
import Diagnose from './components/Diagnose';
import Log from './pages/Log';
import Diagnose from './pages/Diagnose';
import useStorage from './hooks/useStorage';
import useConfig from './hooks/useConfig';
import Login from './components/Auth/Login';
import SignUp from './components/Auth/SignUp';
import ResetPassword from './components/Auth/ResetPassword';
import Login from './pages/auth/Login';
import SignUp from './pages/auth/SignUp';
import ResetPassword from './pages/auth/ResetPassword';
import Upload from './pages/Upload';
const { Content } = Layout;
@ -117,7 +116,6 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
<Dialog
name={dialogMatchedPath.params.dialog}
url={dialogMatchedPath.url}
burnButton={isDesktop && <BurnButton />}
/>
</Content>
</Layout>
@ -148,6 +146,11 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
<ResetPassword />
</ContentFor>
</Route>
<Route path={Routes.UPLOAD}>
<ContentFor>
<Upload />
</ContentFor>
</Route>
</Switch>
<Result
status="warning"

View File

@ -1,26 +0,0 @@
import {
Button,
Grid,
} from 'antd';
import { FireOutlined } from '@ant-design/icons';
const { useBreakpoint } = Grid;
const BurnButton = () => {
const { md } = useBreakpoint();
return (
<Button
type="primary"
size="large"
danger
htmlType="submit"
icon={<FireOutlined />}
style={{ position: 'fixed', right: 35, bottom: 45 }}
>
{md && 'Burn'}
</Button>
);
};
export default BurnButton;

View File

@ -99,14 +99,12 @@ const Dialog = ({
tune,
url,
name,
burnButton,
}: {
ui: UIState,
config: ConfigType,
tune: TuneType,
name: string,
url: string,
burnButton: any
}) => {
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
const { storageSet } = useStorage();
@ -411,9 +409,6 @@ const Dialog = ({
<Row gutter={20}>
{panelsComponents}
</Row>
<Form.Item>
{burnButton}
</Form.Item>
</Form>
</div>
);

View File

@ -34,7 +34,7 @@ const SmartNumber = ({
if (isSlider(units || '')) {
return (
<Slider
defaultValue={defaultValue}
value={defaultValue}
min={min}
max={max}
step={10**-digits}
@ -49,7 +49,7 @@ const SmartNumber = ({
return (
<InputNumber
defaultValue={defaultValue}
value={defaultValue}
precision={digits}
min={min}
max={max}

View File

@ -35,7 +35,7 @@ const SmartSelect = ({
if (values.length < 3) {
return (
<Radio.Group
defaultValue={values.indexOf(defaultValue)}
value={values.indexOf(defaultValue)}
optionType="button"
buttonStyle="solid"
disabled={disabled}
@ -49,7 +49,7 @@ const SmartSelect = ({
return (
<Select
defaultValue={values.indexOf(defaultValue)}
value={values.indexOf(defaultValue)}
optionFilterProp="label"
disabled={disabled}
style={{ maxWidth: 250 }}

View File

@ -7,7 +7,7 @@ import {
} from 'antd';
import {
InfoCircleOutlined,
FieldTimeOutlined,
GithubOutlined,
} from '@ant-design/icons';
import { connect } from 'react-redux';
import {
@ -37,10 +37,16 @@ const StatusBar = ({ status, config }: { status: StatusState, config: ConfigStat
{config.megaTune && firmware(config.megaTune.signature)}
</Col>
<Col span={12} style={{ textAlign: 'right' }}>
<Space>
<FieldTimeOutlined />
{status.text}
</Space>
<a
href="https://github.com/speedy-tuner/speedy-tuner-cloud"
target="__blank"
rel="noopener noreferrer"
>
<Space>
<GithubOutlined />
GitHub
</Space>
</a>
</Col>
</Row>
</Footer>

View File

@ -8,7 +8,6 @@ import {
Layout,
Space,
Button,
Input,
Row,
Col,
Tooltip,
@ -20,21 +19,17 @@ import {
} from 'antd';
import {
UserOutlined,
ShareAltOutlined,
CloudUploadOutlined,
CloudDownloadOutlined,
SettingOutlined,
LoginOutlined,
LineChartOutlined,
SlidersOutlined,
GithubOutlined,
FileExcelOutlined,
FileTextOutlined,
FileZipOutlined,
SaveOutlined,
DesktopOutlined,
GlobalOutlined,
LinkOutlined,
DownOutlined,
SearchOutlined,
ToolOutlined,
@ -59,14 +54,14 @@ import { useAuth } from '../contexts/AuthContext';
import {
logOutFailed,
logOutSuccessful,
} from './Auth/notifications';
} from '../pages/auth/notifications';
const { Header } = Layout;
const { useBreakpoint } = Grid;
const { SubMenu } = Menu;
const TopBar = () => {
const { sm } = useBreakpoint();
const { sm, lg } = useBreakpoint();
const { pathname } = useLocation();
const { currentUser, logout } = useAuth();
const history = useHistory();
@ -81,76 +76,7 @@ const TopBar = () => {
}
}, [logout]);
const userMenu = (
<Menu>
{currentUser ? (
<Menu.Item key="logout" icon={<LogoutOutlined />} onClick={logoutClick}>
Logout
</Menu.Item>
) : (
<>
<Menu.Item key="login" icon={<LoginOutlined />}>
<Link to={Routes.LOGIN}>Login</Link>
</Menu.Item>
<Menu.Item key="sign-up" icon={<UserAddOutlined />}>
<Link to={Routes.SIGN_UP}>Sign Up</Link>
</Menu.Item>
</>
)}
<Menu.Divider />
<Menu.Item key="gh" icon={<GithubOutlined />}>
<a
href="https://github.com/speedy-tuner/speedy-tuner-cloud"
target="__blank"
rel="noopener noreferrer"
>
GitHub
</a>
</Menu.Item>
<Menu.Item key="preferences" disabled icon={<SettingOutlined />}>
Preferences
</Menu.Item>
</Menu>
);
const shareMenu = (
<Menu>
<Menu.Item key="upload" disabled icon={<CloudUploadOutlined />}>
Upload
</Menu.Item>
<SubMenu key="download-sub" title="Download" icon={<CloudDownloadOutlined />}>
<SubMenu key="tune-sub" title="Tune" icon={<SlidersOutlined />}>
<Menu.Item key="download" icon={<SaveOutlined />}>
<a href="/tunes/202103.msq" target="__blank" rel="noopener noreferrer">
Download
</a>
</Menu.Item>
<Menu.Item key="open" disabled icon={<DesktopOutlined />}>
Open in app
</Menu.Item>
</SubMenu>
<SubMenu key="logs-sub" title="Logs" icon={<LineChartOutlined />}>
<Menu.Item key="mlg" disabled icon={<FileZipOutlined />}>
MLG
</Menu.Item>
<Menu.Item key="msl" disabled icon={<FileTextOutlined />}>
MSL
</Menu.Item>
<Menu.Item key="csv" disabled icon={<FileExcelOutlined />}>
CSV
</Menu.Item>
</SubMenu>
</SubMenu>
<Menu.Item key="link" disabled icon={<LinkOutlined />}>
Create link
</Menu.Item>
<Menu.Item key="publish" disabled icon={<GlobalOutlined />}>
Publish to Hub
</Menu.Item>
</Menu>
);
const searchInput = useRef<Input | null>(null);
const searchInput = useRef<HTMLElement | null>(null);
const handleGlobalKeyboard = (e: KeyboardEvent) => {
if (isCommand(e)) {
if (searchInput) {
@ -204,25 +130,76 @@ const TopBar = () => {
</Col>
<Col span={12} md={8} sm={8} style={{ textAlign: 'right' }}>
<Space>
<Tooltip title={
<Tooltip visible={false} title={
<>
<Typography.Text keyboard>{isMac ? '⌘' : 'CTRL'}</Typography.Text>
<Typography.Text keyboard>SHIFT</Typography.Text>
<Typography.Text keyboard>P</Typography.Text>
</>
}>
<Button icon={<SearchOutlined />} ref={searchInput as any} />
<Button disabled icon={<SearchOutlined />} ref={searchInput} />
</Tooltip>
<Link to={Routes.UPLOAD}>
<Button icon={<CloudUploadOutlined />}>
{lg && 'Upload'}
</Button>
</Link>
<Dropdown
overlay={shareMenu}
overlay={
<Menu>
<SubMenu key="tune-sub" title="Tune" icon={<SlidersOutlined />}>
<Menu.Item key="download" icon={<SaveOutlined />}>
<a href="/tunes/202103.msq" target="__blank" rel="noopener noreferrer">
Download
</a>
</Menu.Item>
<Menu.Item key="open" disabled icon={<DesktopOutlined />}>
Open in app
</Menu.Item>
</SubMenu>
<SubMenu key="logs-sub" title="Logs" icon={<LineChartOutlined />}>
<Menu.Item key="mlg" disabled icon={<FileZipOutlined />}>
MLG
</Menu.Item>
<Menu.Item key="msl" disabled icon={<FileTextOutlined />}>
MSL
</Menu.Item>
<Menu.Item key="csv" disabled icon={<FileExcelOutlined />}>
CSV
</Menu.Item>
</SubMenu>
</Menu>
}
placement="bottomCenter"
trigger={['click']}
>
<Button icon={<ShareAltOutlined />}>
<Button icon={<CloudDownloadOutlined />}>
{lg && 'Download'}
<DownOutlined />
</Button>
</Dropdown>
<Dropdown
overlay={userMenu}
overlay={
<Menu>
{currentUser ? (
<Menu.Item key="logout" icon={<LogoutOutlined />} onClick={logoutClick}>
Logout
</Menu.Item>
) : (
<>
<Menu.Item key="login" icon={<LoginOutlined />}>
<Link to={Routes.LOGIN}>Login</Link>
</Menu.Item>
<Menu.Item key="sign-up" icon={<UserAddOutlined />}>
<Link to={Routes.SIGN_UP}>Sign Up</Link>
</Menu.Item>
</>
)}
<Menu.Item key="preferences" disabled icon={<SettingOutlined />}>
Preferences
</Menu.Item>
</Menu>
}
placement="bottomCenter"
trigger={['click']}
>

View File

@ -30,6 +30,7 @@ interface AuthValue {
resetPassword: (email: string) => Promise<void>,
googleAuth: () => Promise<void>,
githubAuth: () => Promise<void>,
refreshToken: () => Promise<string> | undefined,
}
const AuthContext = createContext<AuthValue | null>(null);
@ -58,6 +59,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
const credentials = await signInWithPopup(auth, provider);
setCurrentUser(credentials.user);
},
refreshToken: () => auth.currentUser?.getIdToken(true),
}), [currentUser]);
useEffect(() => {

View File

@ -12,6 +12,21 @@ import {
signInWithPopup,
} from 'firebase/auth';
import { getAnalytics } from 'firebase/analytics';
import {
getStorage,
ref,
uploadBytes,
uploadBytesResumable,
deleteObject,
} from 'firebase/storage';
import {
getFirestore,
doc,
setDoc,
collection,
addDoc,
getDoc,
} from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
@ -27,6 +42,8 @@ const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
const performance = getPerformance(app);
const auth = getAuth(app);
const db = getFirestore();
const storage = getStorage();
export {
auth,
@ -40,4 +57,15 @@ export {
GoogleAuthProvider,
GithubAuthProvider,
signInWithPopup,
ref as storageRef,
storage,
uploadBytes,
uploadBytesResumable,
deleteObject,
doc as fireStoreDoc,
collection as fireStoreCollection,
setDoc,
addDoc,
getDoc,
db,
};

View File

@ -8,6 +8,7 @@ const useStorage = () => {
storageGet: (key: string) => storage.get(key),
storageGetSync: (key: string) => storage.getSync(key),
storageSet: (key: string, value: string) => storage.set(key, value),
storageDelete: (key: string) => storage.delete(key),
};
};

View File

@ -35,12 +35,12 @@ import {
} from '../utils/api';
import store from '../store';
import { formatBytes } from '../utils/number';
import CompositeCanvas from './TriggerLog/CompositeCanvas';
import CompositeCanvas from '../components/TriggerLog/CompositeCanvas';
import TriggerLogsParser, {
CompositeLogEntry,
ToothLogEntry,
} from '../utils/logs/TriggerLogsParser';
import ToothCanvas from './TriggerLog/ToothCanvas';
import ToothCanvas from '../components/TriggerLog/ToothCanvas';
const { TabPane } = Tabs;
const { Content } = Layout;

View File

@ -38,7 +38,7 @@ import {
Tune as TuneType,
} from '@speedy-tuner/types';
import { loadLogs } from '../utils/api';
import LogCanvas from './Log/LogCanvas';
import LogCanvas from '../components/Log/LogCanvas';
import store from '../store';
import {
formatBytes,

508
src/pages/Upload.tsx Normal file
View File

@ -0,0 +1,508 @@
import {
useCallback,
useEffect,
useState,
} from 'react';
import {
Button,
Divider,
Input,
notification,
Skeleton,
Space,
Switch,
Tooltip,
Typography,
Upload,
} from 'antd';
import {
PlusOutlined,
ToolOutlined,
FundOutlined,
SettingOutlined,
CopyOutlined,
ShareAltOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import { UploadRequestOption } from 'rc-upload/lib/interface';
import { UploadFile } from 'antd/lib/upload/interface';
import { useHistory } from 'react-router-dom';
import pako from 'pako';
import {
customAlphabet,
nanoid,
} from 'nanoid';
import {
emailNotVerified,
restrictedPage,
} from './auth/notifications';
import { useAuth } from '../contexts/AuthContext';
import { Routes } from '../routes';
import {
fireStoreDoc,
setDoc,
getDoc,
storage,
storageRef,
uploadBytesResumable,
deleteObject,
db,
} from '../firebase';
import useStorage from '../hooks/useStorage';
enum MaxFiles {
TUNE_FILES = 1,
LOG_FILES = 5,
TOOTH_LOG_FILES = 5,
CUSTOM_INI_FILES = 1,
}
interface TuneDbData {
userUid?: string;
createdAt?: Date;
updatedAt?: Date;
isPublished?: boolean;
isListed?: boolean;
isPublic?: boolean;
tuneFile?: string | null;
logFiles?: string[];
toothLogFiles?: string[];
customIniFile?: string | null;
}
type Path = string;
interface UploadedFile {
[autoUid: string]: Path;
}
interface UploadFileData {
path: string;
}
const containerStyle = {
padding: 20,
maxWidth: 600,
margin: '0 auto',
};
const NEW_TUNE_ID_KEY = 'newTuneId';
const MAX_FILE_SIZE_MB = 10;
const tuneIcon = () => <ToolOutlined />;
const logIcon = () => <FundOutlined />;
const toothLogIcon = () => <SettingOutlined />;
const iniIcon = () => <FileTextOutlined />;
const nanoidCustom = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
const baseUploadPath = 'public/users';
const UploadPage = () => {
const [newTuneId, setNewTuneId] = useState<string>();
const [isUserAuthorized, setIsUserAuthorized] = useState(false);
const hasNavigatorShare = navigator.share !== undefined;
const [shareUrl, setShareUrl] = useState<string>();
const [copied, setCopied] = useState(false);
const [showOptions, setShowOptions] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isPublished, setIsPublished] = useState(false);
const [isPublic, setIsPublic] = useState(true);
const [isListed, setIsListed] = useState(true);
const { currentUser, refreshToken } = useAuth();
const history = useHistory();
const { storageSet, storageGet, storageDelete } = useStorage();
const [tuneFile, setTuneFile] = useState<UploadedFile | null>(null);
const [logFiles, setLogFiles] = useState<UploadedFile>({});
const [toothLogFiles, setToothLogFiles] = useState<UploadedFile>({});
const [customIniFile, setCustomIniFile] = useState<UploadedFile | null>(null);
const copyToClipboard = async () => {
if (navigator.clipboard) {
await navigator.clipboard.writeText(shareUrl!);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
}
};
const genericError = (error: Error) => notification.error({ message: 'Error', description: error.message });
const updateDbData = (tuneId: string, dbData: TuneDbData) => {
try {
return setDoc(fireStoreDoc(db, 'tunes', tuneId), dbData, { merge: true });
} catch (error) {
console.error(error);
genericError(error as Error);
return Promise.reject(error);
}
};
const getDbData = (tuneId: string) => {
try {
return getDoc(fireStoreDoc(db, 'tunes', tuneId));
} catch (error) {
console.error(error);
genericError(error as Error);
return Promise.reject(error);
}
};
const removeFile = (path: string) => {
try {
return deleteObject(storageRef(storage, path));
} catch (error) {
console.error(error);
genericError(error as Error);
return Promise.reject(error);
}
};
const publish = async () => {
setIsLoading(true);
await updateDbData(newTuneId!, {
updatedAt: new Date(),
isPublished: true,
isPublic,
isListed,
});
setIsPublished(true);
setIsLoading(false);
storageDelete(NEW_TUNE_ID_KEY);
};
const upload = async (path: string, options: UploadRequestOption, done?: Function) => {
const { onError, onSuccess, onProgress, file } = options;
if ((file as File).size / 1024 / 1024 > MAX_FILE_SIZE_MB) {
const errorName = 'File too large';
const errorMessage = `File should not be larger than ${MAX_FILE_SIZE_MB}MB!`;
notification.error({ message: errorName, description: errorMessage });
onError!({ name: errorName, message: errorMessage });
return false;
}
try {
const buffer = await (file as File).arrayBuffer();
const compressed = pako.deflate(new Uint8Array(buffer));
const uploadTask = uploadBytesResumable(storageRef(storage, path), compressed, {
customMetadata: {
name: (file as File).name,
size: `${(file as File).size}`,
},
});
uploadTask.on(
'state_changed',
(snap) => onProgress!({ percent: (snap.bytesTransferred / snap.totalBytes) * 100 }),
(err) => onError!(err),
() => {
onSuccess!(file);
if (done) done();
},
);
} catch (error) {
console.error('Upload error:', error);
notification.error({ message: 'Upload error', description: (error as Error).message });
onError!(error as Error);
}
return true;
};
const tuneFileData = () => ({
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/${nanoid()}.msq.gz`,
});
const logFileData = (file: UploadFile) => {
const { name } = file;
const extension = name.split('.').pop();
return {
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/logs/${nanoid()}.${extension}.gz`,
};
};
const toothLogFilesData = () => ({
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/tooth-logs/${nanoid()}.csv.gz`,
});
const customIniFileData = () => ({
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/${nanoid()}.ini.gz`,
});
const uploadTune = async (options: UploadRequestOption) => {
const found = await getDbData(newTuneId!);
if (!found.exists()) {
const tuneData: TuneDbData = {
userUid: currentUser!.uid,
createdAt: new Date(),
updatedAt: new Date(),
isPublished: false,
isPublic,
isListed,
tuneFile: null,
logFiles: [],
toothLogFiles: [],
customIniFile: null,
};
await updateDbData(newTuneId!, tuneData);
}
setShareUrl(`https://speedytuner.cloud/#/t/${newTuneId}`);
const { path } = (options.data as unknown as UploadFileData);
upload(path, options, () => {
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
setTuneFile(tune);
updateDbData(newTuneId!, { tuneFile: path });
});
};
const uploadLogs = async (options: UploadRequestOption) => {
const { path } = (options.data as unknown as UploadFileData);
upload(path, options, async () => {
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
const newValues = { ...logFiles, ...tune };
await updateDbData(newTuneId!, { logFiles: Object.values(newValues) });
setLogFiles(newValues);
});
};
const uploadToothLogs = async (options: UploadRequestOption) => {
const { path } = (options.data as unknown as UploadFileData);
upload(path, options, async () => {
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
const newValues = { ...toothLogFiles, ...tune };
await updateDbData(newTuneId!, { toothLogFiles: Object.values(newValues) });
setToothLogFiles(newValues);
});
};
const uploadCustomIni = async (options: UploadRequestOption) => {
const { path } = (options.data as unknown as UploadFileData);
upload(path, options, () => {
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
setCustomIniFile(tune);
updateDbData(newTuneId!, { customIniFile: path });
});
};
const removeTuneFile = async (file: UploadFile) => {
removeFile(tuneFile![file.uid]);
updateDbData(newTuneId!, { tuneFile: null });
setTuneFile(null);
};
const removeLogFile = async (file: UploadFile) => {
const { uid } = file;
removeFile(logFiles[file.uid]);
const newValues = { ...logFiles };
delete newValues[uid];
updateDbData(newTuneId!, { logFiles: Object.values(newValues) });
setLogFiles(newValues);
};
const removeToothLogFile = async (file: UploadFile) => {
const { uid } = file;
removeFile(toothLogFiles[file.uid]);
const newValues = { ...toothLogFiles };
delete newValues[uid];
updateDbData(newTuneId!, { toothLogFiles: Object.values(newValues) });
setToothLogFiles(newValues);
};
const removeCustomIniFile = async (file: UploadFile) => {
removeFile(customIniFile![file.uid]);
updateDbData(newTuneId!, { customIniFile: null });
setCustomIniFile(null);
};
const prepareData = useCallback(async () => {
if (!currentUser) {
restrictedPage();
history.push(Routes.LOGIN);
return;
}
try {
await refreshToken();
if (!currentUser.emailVerified) {
emailNotVerified();
history.push(Routes.LOGIN);
return;
}
setIsUserAuthorized(true);
} catch (error) {
storageDelete(NEW_TUNE_ID_KEY);
console.error(error);
genericError(error as Error);
}
let newTuneIdTemp = await storageGet(NEW_TUNE_ID_KEY);
if (!newTuneIdTemp) {
newTuneIdTemp = nanoidCustom();
await storageSet(NEW_TUNE_ID_KEY, newTuneIdTemp);
}
setNewTuneId(newTuneIdTemp);
}, [currentUser, history, refreshToken, storageDelete, storageGet, storageSet]);
useEffect(() => {
prepareData();
}, [currentUser, history, prepareData, refreshToken]);
const uploadButton = (
<Space direction="vertical">
<PlusOutlined />Upload
</Space>
);
const shareSection = (
<>
<Divider>Publish & Share</Divider>
<Input
style={{ width: `calc(100% - ${hasNavigatorShare ? 160 : 128}px)` }}
value={shareUrl!}
/>
<Tooltip title={copied ? 'Copied!' : 'Copy URL'}>
<Button icon={<CopyOutlined />} onClick={copyToClipboard} />
</Tooltip>
{hasNavigatorShare && (
<Tooltip title="Share">
<Button
icon={<ShareAltOutlined />}
onClick={() => navigator.share({ url: shareUrl! })}
/>
</Tooltip>
)}
<Button
type="primary"
style={{ float: 'right' }}
disabled={isPublished || isLoading}
onClick={publish}
>
{isPublished && !isLoading ? 'Published' : 'Publish'}
</Button>
</>
);
const optionalSection = (
<>
<Divider>
<Space>
Upload Logs
<Typography.Text type="secondary">(.mlg, .csv, .msl)</Typography.Text>
</Space>
</Divider>
<Upload
listType="picture-card"
customRequest={uploadLogs}
data={logFileData}
onRemove={removeLogFile}
iconRender={logIcon}
multiple
maxCount={MaxFiles.LOG_FILES}
disabled={isPublished}
accept=".mlg,.csv,.msl"
>
{Object.keys(logFiles).length < MaxFiles.LOG_FILES && uploadButton}
</Upload>
<Divider>
<Space>
Upload Tooth and Composite logs
<Typography.Text type="secondary">(.csv)</Typography.Text>
</Space>
</Divider>
<Upload
listType="picture-card"
customRequest={uploadToothLogs}
data={toothLogFilesData}
onRemove={removeToothLogFile}
iconRender={toothLogIcon}
multiple
maxCount={MaxFiles.TOOTH_LOG_FILES}
accept=".csv"
>
{Object.keys(toothLogFiles).length < MaxFiles.TOOTH_LOG_FILES && uploadButton}
</Upload>
<Space style={{ marginTop: 30 }}>
Show more:
<Switch checked={showOptions} onChange={setShowOptions} />
</Space>
{showOptions && <>
<Divider>
<Space>
Upload Custom INI
<Typography.Text type="secondary">(.ini)</Typography.Text>
</Space>
</Divider>
<Upload
listType="picture-card"
customRequest={uploadCustomIni}
data={customIniFileData}
onRemove={removeCustomIniFile}
iconRender={iniIcon}
disabled={isPublished}
accept=".ini"
>
{!customIniFile && uploadButton}
</Upload>
<Divider>
Visibility
</Divider>
<Space direction="vertical" size="large">
<Space>
Public:<Switch disabled checked={isPublic} onChange={setIsPublic} />
</Space>
<Space>
Listed:<Switch checked={isListed} onChange={setIsListed} />
</Space>
</Space>
</>}
{shareUrl && tuneFile && shareSection}
</>
);
if (!isUserAuthorized) {
return (
<div style={containerStyle}>
<Skeleton active />
</div>
);
}
if (isPublished) {
return (
<div style={containerStyle}>
{shareSection}
</div>
);
}
return (
<div style={containerStyle}>
<Divider>
<Space>
Upload Tune
<Typography.Text type="secondary">(.msq)</Typography.Text>
</Space>
</Divider>
<Upload
listType="picture-card"
customRequest={uploadTune}
data={tuneFileData}
onRemove={removeTuneFile}
iconRender={tuneIcon}
disabled={isPublished}
accept=".msq"
>
{!tuneFile && uploadButton}
</Upload>
{tuneFile && optionalSection}
</div>
);
};
export default UploadPage;

View File

@ -11,4 +11,5 @@ export enum Routes {
SIGN_UP = '/auth/sign-up',
FORGOT_PASSWORD = '/auth/forgot-password',
RESET_PASSWORD = '/auth/reset-password',
UPLOAD = '/upload',
}

View File

@ -8,6 +8,6 @@ enum Keys {
ESCAPE = 'Escape',
}
export const isCommand = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.COMMAND;
export const isCommand = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.shiftKey && e.key === Keys.COMMAND;
export const isToggleSidebar = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.SIDEBAR;
export const isEscape = (e: KeyEvent) => e.key === Keys.ESCAPE;

View File

@ -19,6 +19,10 @@ class Storage {
public set(key: string, value: string) {
return this.storage.set(key, value);
}
public delete(key: string) {
return this.storage.delete(key);
}
}
export default Storage;

View File

@ -21,6 +21,10 @@ class BrowserStorage implements StorageInterface {
this.storage.setItem(key, value);
}
public delete(key: string): void {
return this.storage.removeItem(key);
}
public async isAvailable(): Promise<boolean> {
return !!this.storage;
}

View File

@ -2,5 +2,6 @@ export interface StorageInterface {
get(key: string): Promise<string | null>;
getSync(key: string): string | null;
set(key: string, value: string): Promise<void>;
delete(key: string): void;
isAvailable(): Promise<boolean>;
}