diff --git a/.vscode/settings.json b/.vscode/settings.json
index b5b037c..ef292ef 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,7 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"cSpell.words": [
+ "hypertuner",
"kbar",
"pocketbase",
"prefs",
diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx
index 45826d0..41e0639 100644
--- a/src/components/TopBar.tsx
+++ b/src/components/TopBar.tsx
@@ -1,7 +1,11 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+/* eslint-disable jsx-a11y/anchor-has-content */
+
import {
useCallback,
useEffect,
useMemo,
+ useRef,
} from 'react';
import {
useLocation,
@@ -31,10 +35,7 @@ import {
LoginOutlined,
LineChartOutlined,
SlidersOutlined,
- FileExcelOutlined,
- FileTextOutlined,
FileZipOutlined,
- SaveOutlined,
DesktopOutlined,
DownOutlined,
SearchOutlined,
@@ -44,6 +45,8 @@ import {
LogoutOutlined,
InfoCircleOutlined,
CarOutlined,
+ FileTextOutlined,
+ FileExcelOutlined,
} from '@ant-design/icons';
import { useKBar } from 'kbar';
import store from '../store';
@@ -53,10 +56,17 @@ import { Routes } from '../routes';
import { useAuth } from '../contexts/AuthContext';
import { logOutSuccessful } from '../pages/auth/notifications';
import { TuneDataState } from '../types/state';
+import { removeFilenameSuffix } from '../pocketbase';
+import useServerStorage from '../hooks/useServerStorage';
const { Header } = Layout;
const { useBreakpoint } = Grid;
-const { SubMenu } = Menu;
+
+const logsExtensionsIcons: { [key: string]: any } = {
+ 'mlg': ,
+ 'msl': ,
+ 'csv': ,
+};
const TopBar = ({
tuneData,
@@ -77,6 +87,8 @@ const TopBar = ({
const tabMatch = useMatch(`${Routes.TUNE_TAB}/*`);
const uploadMatch = useMatch(Routes.UPLOAD);
const hubMatch = useMatch(Routes.HUB);
+ const { downloadFile } = useServerStorage();
+ const downloadAnchorRef = useRef(null);
const logoutClick = useCallback(() => {
logout();
logOutSuccessful();
@@ -92,6 +104,54 @@ const TopBar = ({
}
}, []);
+ const downloadLogsItems = {
+ label: 'Logs',
+ icon: ,
+ key: 'logs',
+ children: (tuneData?.logFiles || []).map((filename) => ({
+ key: filename,
+ label: removeFilenameSuffix(filename),
+ icon: logsExtensionsIcons[filename.slice(-3)],
+ onClick: () => downloadFile(tuneData!.id, filename, downloadAnchorRef.current!),
+ })),
+ };
+
+ const downloadToothLogsItems = {
+ label: 'Tooth logs',
+ icon: ,
+ key: 'toothLogs',
+ children: (tuneData?.toothLogFiles || []).map((filename) => ({
+ key: filename,
+ label: removeFilenameSuffix(filename),
+ icon: logsExtensionsIcons[filename.slice(-3)],
+ onClick: () => downloadFile(tuneData!.id, filename, downloadAnchorRef.current!),
+ })),
+ };
+
+ const downloadItems = [
+ {
+ label: 'Tune',
+ icon: ,
+ key: 'tune',
+ children: [
+ {
+ label: 'Download',
+ icon: ,
+ key: 'download',
+ onClick: () => downloadFile(tuneData!.id, tuneData!.tuneFile, downloadAnchorRef.current!),
+ },
+ {
+ label: 'Open in app',
+ icon: ,
+ key: 'open',
+ onClick: () => window.open(`hypertuner://hypertuner.cloud/t/${tuneData!.tuneId}`, '_blank'),
+ },
+ ],
+ },
+ (tuneData?.logFiles || []).length > 0 ? { ...downloadLogsItems } : null,
+ (tuneData?.toothLogFiles || []).length > 0 ? { ...downloadToothLogsItems } : null,
+ ];
+
useEffect(() => {
document.addEventListener('keydown', handleGlobalKeyboard);
@@ -212,31 +272,9 @@ const TopBar = ({
{lg && 'Upload'}
-
- }>
- }>
-
- Download
-
-
- }>
- Open in app
-
-
- }>
- }>
- MLG
-
- }>
- MSL
-
- }>
- CSV
-
-
-
+
}
placement="bottom"
trigger={['click']}
@@ -244,7 +282,7 @@ const TopBar = ({
}>
{downloadButton}
-
+ }
}
placement="bottomRight"
@@ -254,6 +292,8 @@ const TopBar = ({
{sm && }
+ {/* dummy anchor for file download */}
+
diff --git a/src/hooks/useServerStorage.ts b/src/hooks/useServerStorage.ts
index 11113be..f9551d7 100644
--- a/src/hooks/useServerStorage.ts
+++ b/src/hooks/useServerStorage.ts
@@ -1,12 +1,16 @@
import Pako from 'pako';
import * as Sentry from '@sentry/browser';
-import { API_URL } from '../pocketbase';
+import {
+ API_URL,
+ removeFilenameSuffix,
+} from '../pocketbase';
import { Collections } from '../@types/pocketbase-types';
import useDb from './useDb';
import {
fetchWithProgress,
OnProgress,
} from '../utils/http';
+import { downloading } from '../pages/auth/notifications';
const useServerStorage = () => {
const { getIni } = useDb();
@@ -43,10 +47,28 @@ const useServerStorage = () => {
signal,
).then((response) => response);
+ const downloadFile = async (recordId: string, filename: string, anchorRef: HTMLAnchorElement) => {
+ downloading();
+
+ const response = await fetch(buildFileUrl(Collections.Tunes, recordId, filename));
+ const data = Pako.inflate(new Uint8Array(await response.arrayBuffer()));
+ const url = window.URL.createObjectURL(new Blob([data]));
+
+ // eslint-disable-next-line no-param-reassign
+ anchorRef.href = url;
+ // eslint-disable-next-line no-param-reassign
+ anchorRef.target = '_blank';
+ // eslint-disable-next-line no-param-reassign
+ anchorRef.download = removeFilenameSuffix(filename);
+ anchorRef.click();
+ window.URL.revokeObjectURL(url);
+ };
+
return {
fetchTuneFile: (recordId: string, filename: string): Promise => fetchTuneFile(recordId, filename),
fetchINIFile: (signature: string): Promise => fetchINIFile(signature),
fetchLogFileWithProgress: (recordId: string, filename: string, onProgress?: OnProgress, signal?: AbortSignal): Promise => fetchLogFileWithProgress(recordId, filename, onProgress, signal),
+ downloadFile: (recordId: string, filename: string, anchorRef: HTMLAnchorElement): Promise => downloadFile(recordId, filename, anchorRef),
};
};
diff --git a/src/pages/auth/notifications.ts b/src/pages/auth/notifications.ts
index 117c487..24547e4 100644
--- a/src/pages/auth/notifications.ts
+++ b/src/pages/auth/notifications.ts
@@ -147,6 +147,12 @@ const signatureNotSupportedWarning = (message: string) => notification.warning({
...baseOptions,
});
+const downloading = () => notification.success({
+ message: 'Downloading...',
+ ...baseOptions,
+ duration: 1,
+});
+
export {
error,
emailNotVerified,
@@ -172,4 +178,5 @@ export {
iniLoadingError,
tuneParsingError,
signatureNotSupportedWarning,
+ downloading,
};