Load tune from URL (#369)
This commit is contained in:
parent
89ad4cbc47
commit
0229830b1b
2
.env
2
.env
|
@ -1,4 +1,6 @@
|
|||
NPM_GITHUB_TOKEN=
|
||||
REACT_APP_WEB_URL=http://localhost:3000
|
||||
REACT_APP_SENTRY_DSN=
|
||||
REACT_APP_FIREBASE_APP_SENTRY_DSN=
|
||||
REACT_APP_FIREBASE_API_KEY=
|
||||
REACT_APP_FIREBASE_AUTH_DOMAIN=
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# Development guide
|
||||
|
||||
This guide will help you set up this project.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Node](https://nodejs.org/) 16.x.x (Node Version Manager: [nvm](https://github.com/nvm-sh/nvm))
|
||||
- [Firebase](https://console.firebase.google.com/)
|
||||
- Authentication
|
||||
- Storage
|
||||
- Firestore Database
|
||||
- [Firebase CLI](https://firebase.google.com/docs/cli)
|
||||
- [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) (`brew install --cask google-cloud-sdk`)
|
||||
|
||||
### Setup local environment variables
|
||||
|
||||
```bash
|
||||
cp .env .env.local
|
||||
```
|
||||
|
||||
### Authenticate to GitHub Packages
|
||||
|
||||
Project uses shared packages (`@speedy-tuner/...`).
|
||||
|
||||
They are hosted using `GitHub Packages`, to install them you need to [authenticate to GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-to-github-packages) first.
|
||||
|
||||
#### Personal access token
|
||||
|
||||
Generate GitHub [Personal access token](https://github.com/settings/tokens).
|
||||
|
||||
Private token can be assign to ENV when running `npm install` in the same shell:
|
||||
|
||||
```bash
|
||||
export NPM_GITHUB_TOKEN=YOUR_PRIVATE_GITHUB_TOKEN
|
||||
```
|
||||
|
||||
### Install dependencies and run in development mode
|
||||
|
||||
```bash
|
||||
# install packages
|
||||
npm install
|
||||
|
||||
# run development server
|
||||
npm start
|
||||
```
|
||||
|
||||
## Firebase
|
||||
|
||||
### Storage
|
||||
|
||||
Authenticate:
|
||||
|
||||
```bash
|
||||
gcloud auth login
|
||||
```
|
||||
|
||||
Set up CORS:
|
||||
|
||||
```bash
|
||||
gsutil cors set cors.json gs://<YOUR-BUCKET>
|
||||
```
|
43
README.md
43
README.md
|
@ -13,16 +13,6 @@ Share your [Speeduino](https://speeduino.com/) tune and logs.
|
|||
|
||||
![Screenshot](https://speedytuner.cloud/img/screen.png)
|
||||
|
||||
## Project main goals
|
||||
|
||||
- 🚀 always free and open source (FOSS)
|
||||
- 💻 Cloud based web app with CDN
|
||||
- 🔥 `60 FPS` animations and fast load times
|
||||
- 👍 good user experience
|
||||
- 💎 intuitive, modern and responsive UI
|
||||
- 👶 easy for newcomers with tips, tools and simple diagnostics
|
||||
- 📱 touch screen friendly
|
||||
|
||||
## ECU firmware
|
||||
|
||||
- Documentation: [wiki.speeduino.com](https://wiki.speeduino.com/)
|
||||
|
@ -32,39 +22,12 @@ Share your [Speeduino](https://speeduino.com/) tune and logs.
|
|||
|
||||
There are many ways in which you can participate in the project and every bit of help is greatly appreciated.
|
||||
|
||||
- 👋 Say Hi and start a conversation over at [Discussions](https://github.com/karniv00l/speedy-tuner/discussions)
|
||||
- 🐞 [Submit bugs and feature requests](https://github.com/karniv00l/speedy-tuner/issues)
|
||||
- 👋 Say Hi and start a conversation over at [Discussions](https://github.com/speedy-tuner/speedy-tuner-cloud/discussions)
|
||||
- 🐞 [Submit bugs and feature requests](https://github.com/speedy-tuner/speedy-tuner-cloud/issues)
|
||||
- 🧪 Test on different platforms, hardware and Speeduino firmware
|
||||
- 👓 Review source code
|
||||
- ⌨️ Write tests and refactor code according to best practices
|
||||
|
||||
## Development
|
||||
|
||||
### Recommended dev environment
|
||||
|
||||
- [Node](https://nodejs.org/) 16.x.x
|
||||
|
||||
### Authenticate to GitHub Packages
|
||||
|
||||
Project uses shared packages (`@speedy-tuner/...`).
|
||||
|
||||
They are hosted using `GitHub Packages`, to install them you need to [authenticate to GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-to-github-packages) first.
|
||||
|
||||
Private token can be assing to ENV like so when running `npm install` in the same shell:
|
||||
|
||||
```bash
|
||||
export NPM_GITHUB_TOKEN=YOUR_PRIVATE_GITHUB_TOKEN
|
||||
```
|
||||
|
||||
### Install and run
|
||||
|
||||
```bash
|
||||
# install packages
|
||||
npm install
|
||||
|
||||
# run development server
|
||||
npm start
|
||||
|
||||
# open in browser
|
||||
open http://localhost:3000
|
||||
```
|
||||
Please see [Developer guide](https://github.com/speedy-tuner/speedy-tuner-cloud/blob/master/DEVELOPMENT.md)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"origin": [
|
||||
"*"
|
||||
],
|
||||
"method": [
|
||||
"GET"
|
||||
],
|
||||
"maxAgeSeconds": 3600
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
82
src/App.tsx
82
src/App.tsx
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useLocation,
|
||||
matchPath,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Layout,
|
||||
|
@ -15,21 +16,25 @@ import {
|
|||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import {
|
||||
AppState,
|
||||
UIState,
|
||||
Config as ConfigType,
|
||||
} from '@speedy-tuner/types';
|
||||
import useBrowserStorage from './hooks/useBrowserStorage';
|
||||
import TopBar from './components/TopBar';
|
||||
import StatusBar from './components/StatusBar';
|
||||
import { Routes } from './routes';
|
||||
import useStorage from './hooks/useStorage';
|
||||
import { loadAll } from './utils/api';
|
||||
import { loadTune } from './utils/api';
|
||||
import store from './store';
|
||||
import Log from './pages/Log';
|
||||
|
||||
import 'react-perfect-scrollbar/dist/css/styles.css';
|
||||
import './App.less';
|
||||
import {
|
||||
AppState,
|
||||
NavigationState,
|
||||
UIState,
|
||||
} from './types/state';
|
||||
import useDb from './hooks/useDb';
|
||||
import useServerStorage from './hooks/useServerStorage';
|
||||
|
||||
// TODO: fix this
|
||||
// lazy loading this component causes a weird Curve canvas scaling
|
||||
|
@ -47,17 +52,41 @@ const { Content } = Layout;
|
|||
const mapStateToProps = (state: AppState) => ({
|
||||
ui: state.ui,
|
||||
status: state.status,
|
||||
config: state.config,
|
||||
navigation: state.navigation,
|
||||
});
|
||||
|
||||
const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||
const App = ({ ui, navigation }: { ui: UIState, navigation: NavigationState }) => {
|
||||
const margin = ui.sidebarCollapsed ? 80 : 250;
|
||||
const { getTune } = useDb();
|
||||
const { getFile } = useServerStorage();
|
||||
const { storageSet } = useBrowserStorage();
|
||||
|
||||
// const [lastDialogPath, setLastDialogPath] = useState<string|null>();
|
||||
const { storageGetSync } = useStorage();
|
||||
const lastDialogPath = storageGetSync('lastDialog');
|
||||
// const lastDialogPath = storageGetSync('lastDialog');
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const matchedTunePath = useMemo(() => matchPath(pathname, {
|
||||
path: Routes.TUNE_ROOT,
|
||||
}), [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
const tuneId = (matchedTunePath?.params as any)?.tuneId;
|
||||
if (tuneId) {
|
||||
|
||||
getTune(tuneId).then(async (tuneData) => {
|
||||
const [tuneRaw, iniRaw] = await Promise.all([
|
||||
await getFile(tuneData.tuneFile!),
|
||||
await getFile(tuneData.customIniFile!),
|
||||
]);
|
||||
|
||||
loadTune(tuneRaw, iniRaw);
|
||||
});
|
||||
|
||||
|
||||
storageSet('lastTuneId', tuneId);
|
||||
store.dispatch({ type: 'navigation/tuneId', payload: tuneId });
|
||||
}
|
||||
|
||||
// storageGet('lastDialog')
|
||||
// .then((path) => setLastDialogPath(path));
|
||||
|
||||
|
@ -95,42 +124,51 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
|||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<TopBar />
|
||||
<TopBar tuneId={navigation.tuneId} />
|
||||
<Switch>
|
||||
<Route path={Routes.ROOT} exact>
|
||||
<Redirect to={lastDialogPath || Routes.TUNE} />
|
||||
{/* <Route path={Routes.ROOT} exact>
|
||||
<Redirect to={lastDialogPath || Routes.TUNE_ROOT} />
|
||||
</Route> */}
|
||||
<ContentFor>
|
||||
<Result
|
||||
status="info"
|
||||
title="This page is under construction"
|
||||
style={{ marginTop: 50 }}
|
||||
/>
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.TUNE}>
|
||||
<Route path={Routes.TUNE_TUNE}>
|
||||
<ContentFor marginLeft={margin}>
|
||||
<Tune />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.LOG}>
|
||||
<Route path={Routes.TUNE_LOG} exact>
|
||||
<ContentFor marginLeft={margin}>
|
||||
<Log />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.DIAGNOSE}>
|
||||
<Route path={Routes.TUNE_DIAGNOSE} exact>
|
||||
<ContentFor marginLeft={margin}>
|
||||
<Diagnose />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.LOGIN}>
|
||||
<Route path={Routes.LOGIN} exact>
|
||||
<ContentFor>
|
||||
<Login />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.SIGN_UP}>
|
||||
<Route path={Routes.SIGN_UP} exact>
|
||||
<ContentFor>
|
||||
<SignUp />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.RESET_PASSWORD}>
|
||||
<Route path={Routes.RESET_PASSWORD} exact>
|
||||
<ContentFor>
|
||||
<ResetPassword />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.UPLOAD}>
|
||||
<Route path={Routes.UPLOAD} exact>
|
||||
<ContentFor>
|
||||
<Upload />
|
||||
</ContentFor>
|
||||
|
|
|
@ -17,8 +17,6 @@ import {
|
|||
import {
|
||||
Config as ConfigType,
|
||||
Menus as MenusType,
|
||||
AppState,
|
||||
UIState,
|
||||
Tune as TuneType,
|
||||
} from '@speedy-tuner/types';
|
||||
import store from '../store';
|
||||
|
@ -26,6 +24,11 @@ import Icon from './SideBar/Icon';
|
|||
import { evaluateExpression } from '../utils/tune/expression';
|
||||
import { Routes } from '../routes';
|
||||
import useConfig from '../hooks/useConfig';
|
||||
import {
|
||||
AppState,
|
||||
NavigationState,
|
||||
UIState,
|
||||
} from '../types/state';
|
||||
|
||||
const { Sider } = Layout;
|
||||
const { SubMenu } = Menu;
|
||||
|
@ -42,6 +45,7 @@ const mapStateToProps = (state: AppState) => ({
|
|||
config: state.config,
|
||||
tune: state.tune,
|
||||
ui: state.ui,
|
||||
navigation: state.navigation,
|
||||
});
|
||||
|
||||
const SKIP_MENUS = [
|
||||
|
@ -62,11 +66,13 @@ const SideBar = ({
|
|||
config,
|
||||
tune,
|
||||
ui,
|
||||
navigation,
|
||||
matchedPath,
|
||||
}: {
|
||||
config: ConfigType,
|
||||
tune: TuneType,
|
||||
ui: UIState,
|
||||
navigation: NavigationState,
|
||||
matchedPath: DialogMatchedPathType,
|
||||
}) => {
|
||||
const sidebarWidth = 250;
|
||||
|
@ -79,10 +85,11 @@ const SideBar = ({
|
|||
} as any;
|
||||
const { isConfigReady } = useConfig(config);
|
||||
const checkCondition = useCallback((condition: string) => evaluateExpression(condition, tune.constants, config), [tune.constants, config]);
|
||||
const buildLink = useCallback((main: string, sub: string) => generatePath(Routes.DIALOG, {
|
||||
const buildUrl = useCallback((main: string, sub: string) => generatePath(Routes.TUNE_DIALOG, {
|
||||
tuneId: navigation.tuneId || 'not-ready',
|
||||
category: main,
|
||||
dialog: sub,
|
||||
}), []);
|
||||
}), [navigation.tuneId]);
|
||||
const [menus, setMenus] = useState<any[]>([]);
|
||||
|
||||
const menusList = useCallback((types: MenusType) => (
|
||||
|
@ -99,7 +106,7 @@ const SideBar = ({
|
|||
>
|
||||
{Object.keys(types[menuName].subMenus).map((subMenuName: string) => {
|
||||
if (subMenuName === 'std_separator') {
|
||||
return <Menu.Divider key={buildLink(menuName, subMenuName)} />;
|
||||
return <Menu.Divider key={buildUrl(menuName, subMenuName)} />;
|
||||
}
|
||||
|
||||
if (SKIP_SUB_MENUS.includes(`${menuName}/${subMenuName}`)) {
|
||||
|
@ -114,11 +121,11 @@ const SideBar = ({
|
|||
}
|
||||
|
||||
return (<Menu.Item
|
||||
key={buildLink(menuName, subMenuName)}
|
||||
key={buildUrl(menuName, subMenuName)}
|
||||
icon={<Icon name={subMenuName} />}
|
||||
disabled={!enabled}
|
||||
>
|
||||
<Link to={buildLink(menuName, subMenuName)}>
|
||||
<Link to={buildUrl(menuName, subMenuName)}>
|
||||
{subMenu.title}
|
||||
</Link>
|
||||
</Menu.Item>);
|
||||
|
@ -126,7 +133,7 @@ const SideBar = ({
|
|||
</SubMenu>
|
||||
);
|
||||
})
|
||||
), [buildLink, checkCondition]);
|
||||
), [buildUrl, checkCondition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(tune.constants).length) {
|
||||
|
|
|
@ -3,7 +3,10 @@ import {
|
|||
useLocation,
|
||||
useHistory,
|
||||
} from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Link,
|
||||
generatePath,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Layout,
|
||||
Space,
|
||||
|
@ -60,12 +63,15 @@ const { Header } = Layout;
|
|||
const { useBreakpoint } = Grid;
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
const TopBar = () => {
|
||||
const TopBar = ({ tuneId }: { tuneId: string | null }) => {
|
||||
const { sm, lg } = useBreakpoint();
|
||||
const { pathname } = useLocation();
|
||||
const { currentUser, logout } = useAuth();
|
||||
const history = useHistory();
|
||||
const matchedTabPath = useMemo(() => matchPath(pathname, { path: Routes.TAB }), [pathname]);
|
||||
const buildTuneUrl = (route: string) => tuneId ? generatePath(route, { tuneId }) : null;
|
||||
const matchedTabPath = useMemo(() => matchPath(pathname, {
|
||||
path: Routes.TUNE_TAB,
|
||||
}), [pathname]);
|
||||
const logoutClick = useCallback(async () => {
|
||||
try {
|
||||
await logout();
|
||||
|
@ -96,38 +102,42 @@ const TopBar = () => {
|
|||
return () => document.removeEventListener('keydown', handleGlobalKeyboard);
|
||||
});
|
||||
|
||||
const tabs = (
|
||||
<Col span={10} md={10} sm={16} style={{ textAlign: 'center' }}>
|
||||
<Radio.Group
|
||||
key={pathname}
|
||||
defaultValue={matchedTabPath?.url}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => history.push(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={buildTuneUrl(Routes.TUNE_TUNE)}>
|
||||
<Space>
|
||||
<ToolOutlined />
|
||||
{sm && 'Tune'}
|
||||
</Space>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={buildTuneUrl(Routes.TUNE_LOG)}>
|
||||
<Space>
|
||||
<FundOutlined />
|
||||
{sm && 'Log'}
|
||||
</Space>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={buildTuneUrl(Routes.TUNE_DIAGNOSE)}>
|
||||
<Space>
|
||||
<SettingOutlined />
|
||||
{sm && 'Diagnose'}
|
||||
</Space>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
);
|
||||
|
||||
return (
|
||||
<Header className="app-top-bar">
|
||||
<Row>
|
||||
<Col span={0} md={6} sm={0} />
|
||||
<Col span={10} md={10} sm={16} style={{ textAlign: 'center' }}>
|
||||
<Radio.Group
|
||||
key={pathname}
|
||||
defaultValue={matchedTabPath?.url}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => history.push(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={Routes.TUNE}>
|
||||
<Space>
|
||||
<ToolOutlined />
|
||||
{sm && 'Tune'}
|
||||
</Space>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={Routes.LOG}>
|
||||
<Space>
|
||||
<FundOutlined />
|
||||
{sm && 'Log'}
|
||||
</Space>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={Routes.DIAGNOSE}>
|
||||
<Space>
|
||||
<SettingOutlined />
|
||||
{sm && 'Diagnose'}
|
||||
</Space>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
{tuneId ? tabs : <Col span={10} md={10} sm={16} />}
|
||||
<Col span={12} md={8} sm={8} style={{ textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Tooltip visible={false} title={
|
||||
|
@ -140,7 +150,7 @@ const TopBar = () => {
|
|||
<Button disabled icon={<SearchOutlined />} ref={searchInput} />
|
||||
</Tooltip>
|
||||
<Link to={Routes.UPLOAD}>
|
||||
<Button icon={<CloudUploadOutlined />} type={matchedTabPath?.url === Routes.UPLOAD ? 'primary' : 'default' }>
|
||||
<Button icon={<CloudUploadOutlined />} type={matchedTabPath?.url === Routes.UPLOAD ? 'primary' : 'default'}>
|
||||
{lg && 'Upload'}
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
|
@ -39,7 +39,7 @@ import {
|
|||
} from '../../utils/tune/table';
|
||||
import Map from './Dialog/Map';
|
||||
import { evaluateExpression } from '../../utils/tune/expression';
|
||||
import useStorage from '../../hooks/useStorage';
|
||||
import useBrowserStorage from '../../hooks/useBrowserStorage';
|
||||
import useConfig from '../../hooks/useConfig';
|
||||
|
||||
interface DialogsAndCurves {
|
||||
|
@ -107,7 +107,7 @@ const Dialog = ({
|
|||
url: string,
|
||||
}) => {
|
||||
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
|
||||
const { storageSet } = useStorage();
|
||||
const { storageSet } = useBrowserStorage();
|
||||
const { findConstantOnPage } = useConfig(config);
|
||||
const [panelsComponents, setPanelsComponents] = useState<any[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
uploadBytes,
|
||||
uploadBytesResumable,
|
||||
deleteObject,
|
||||
getBytes,
|
||||
} from 'firebase/storage';
|
||||
import {
|
||||
getFirestore,
|
||||
|
@ -62,6 +63,7 @@ export {
|
|||
uploadBytes,
|
||||
uploadBytesResumable,
|
||||
deleteObject,
|
||||
getBytes,
|
||||
doc as fireStoreDoc,
|
||||
collection as fireStoreCollection,
|
||||
setDoc,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { StorageInterface } from '../StorageInterface';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
class BrowserStorage implements StorageInterface {
|
||||
class BrowserStorage {
|
||||
private storage: Storage;
|
||||
|
||||
public constructor() {
|
||||
|
@ -30,4 +30,15 @@ class BrowserStorage implements StorageInterface {
|
|||
}
|
||||
}
|
||||
|
||||
export default BrowserStorage;
|
||||
const useBrowserStorage = () => {
|
||||
const storage = useMemo(() => new BrowserStorage(), []);
|
||||
|
||||
return {
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
export default useBrowserStorage;
|
|
@ -0,0 +1,48 @@
|
|||
import { notification } from 'antd';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import {
|
||||
fireStoreDoc,
|
||||
getDoc,
|
||||
setDoc,
|
||||
db,
|
||||
} from '../firebase';
|
||||
import { TuneDbData } from '../types/dbData';
|
||||
|
||||
const genericError = (error: Error) => notification.error({ message: 'Database Error', description: error.message });
|
||||
|
||||
const useDb = () => {
|
||||
const getData = async (tuneId: string, collection: string) => {
|
||||
try {
|
||||
const tune = (await getDoc(fireStoreDoc(db, collection, tuneId))).data() as TuneDbData;
|
||||
|
||||
return Promise.resolve(tune);
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error(error);
|
||||
genericError(error as Error);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateData = async (tuneId: string, collection: string, data: TuneDbData) => {
|
||||
try {
|
||||
await setDoc(fireStoreDoc(db, collection, tuneId), data, { merge: true });
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error(error);
|
||||
genericError(error as Error);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getTune: (tuneId: string): Promise<TuneDbData> => getData(tuneId, 'tunes'),
|
||||
updateData: (tuneId: string, data: TuneDbData): Promise<void> => updateData(tuneId, 'tunes', data),
|
||||
};
|
||||
};
|
||||
|
||||
export default useDb;
|
|
@ -0,0 +1,31 @@
|
|||
import { notification } from 'antd';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import {
|
||||
storage,
|
||||
storageRef,
|
||||
getBytes,
|
||||
} from '../firebase';
|
||||
|
||||
const genericError = (error: Error) => notification.error({ message: 'Database Error', description: error.message });
|
||||
|
||||
const useServerStorage = () => {
|
||||
const getFile = async (path: string) => {
|
||||
try {
|
||||
const buffer = await getBytes(storageRef(storage, path));
|
||||
|
||||
return Promise.resolve(buffer);
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error(error);
|
||||
genericError(error as Error);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getFile: (path: string): Promise<ArrayBuffer> => getFile(path),
|
||||
};
|
||||
};
|
||||
|
||||
export default useServerStorage;
|
|
@ -1,15 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import Storage from '../utils/Storage';
|
||||
|
||||
const useStorage = () => {
|
||||
const storage = useMemo(() => new Storage(), []);
|
||||
|
||||
return {
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
export default useStorage;
|
|
@ -19,6 +19,7 @@ import {
|
|||
FileTextOutlined,
|
||||
GlobalOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import pako from 'pako';
|
||||
import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -121,6 +122,7 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
|
|||
setStep(2);
|
||||
} catch (error) {
|
||||
setFetchError(error as Error);
|
||||
Sentry.captureException(error);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,30 +7,30 @@ import {
|
|||
} from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
AppState,
|
||||
UIState,
|
||||
Config as ConfigType,
|
||||
} from '@speedy-tuner/types';
|
||||
import { Config as ConfigType } from '@speedy-tuner/types';
|
||||
import Dialog from '../components/Tune/Dialog';
|
||||
import SideBar, { DialogMatchedPathType } from '../components/SideBar';
|
||||
import { Routes } from '../routes';
|
||||
import useStorage from '../hooks/useStorage';
|
||||
import useBrowserStorage from '../hooks/useBrowserStorage';
|
||||
import useConfig from '../hooks/useConfig';
|
||||
import {
|
||||
AppState,
|
||||
NavigationState,
|
||||
} from '../types/state';
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
ui: state.ui,
|
||||
navigation: state.navigation,
|
||||
status: state.status,
|
||||
config: state.config,
|
||||
});
|
||||
|
||||
const Tune = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||
const Tune = ({ navigation, config }: { navigation: NavigationState, config: ConfigType }) => {
|
||||
const { pathname } = useLocation();
|
||||
const { storageGetSync } = useStorage();
|
||||
const { storageGetSync } = useBrowserStorage();
|
||||
const lastDialogPath = storageGetSync('lastDialog');
|
||||
const { isConfigReady } = useConfig(config);
|
||||
const dialogMatchedPath: DialogMatchedPathType = useMemo(() => matchPath(pathname, {
|
||||
path: Routes.DIALOG,
|
||||
path: Routes.TUNE_DIALOG,
|
||||
exact: true,
|
||||
}) || { url: '', params: { category: '', dialog: '' } }, [pathname]);
|
||||
|
||||
|
@ -41,12 +41,16 @@ const Tune = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
|||
|
||||
const firstCategory = Object.keys(config.menus)[0];
|
||||
const firstDialog = Object.keys(config.menus[firstCategory].subMenus)[0];
|
||||
return generatePath(Routes.DIALOG, { category: firstCategory, dialog: firstDialog });
|
||||
}, [config.menus, isConfigReady]);
|
||||
return generatePath(Routes.TUNE_DIALOG, {
|
||||
tuneId: navigation.tuneId || 'not-ready',
|
||||
category: firstCategory,
|
||||
dialog: firstDialog,
|
||||
});
|
||||
}, [config.menus, isConfigReady, navigation.tuneId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Route path={Routes.TUNE} exact>
|
||||
<Route path={Routes.TUNE_TUNE} exact>
|
||||
{firstDialogPath && <Redirect to={lastDialogPath || firstDialogPath} />}
|
||||
</Route>
|
||||
<SideBar matchedPath={dialogMatchedPath} />
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
ShareAltOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { INI } from '@speedy-tuner/ini';
|
||||
import { UploadRequestOption } from 'rc-upload/lib/interface';
|
||||
import { UploadFile } from 'antd/lib/upload/interface';
|
||||
|
@ -46,19 +47,17 @@ import {
|
|||
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';
|
||||
import useBrowserStorage from '../hooks/useBrowserStorage';
|
||||
import TuneParser from '../utils/tune/TuneParser';
|
||||
import TriggerLogsParser from '../utils/logs/TriggerLogsParser';
|
||||
import LogParser from '../utils/logs/LogParser';
|
||||
import { TuneDbData } from '../types/dbData';
|
||||
import useDb from '../hooks/useDb';
|
||||
|
||||
enum MaxFiles {
|
||||
TUNE_FILES = 1,
|
||||
|
@ -67,36 +66,6 @@ enum MaxFiles {
|
|||
CUSTOM_INI_FILES = 1,
|
||||
}
|
||||
|
||||
interface TuneDataDetails {
|
||||
readme?: string | null;
|
||||
make?: string | null;
|
||||
model?: string | null;
|
||||
displacement?: string | null;
|
||||
year?: number | null;
|
||||
hp?: number | null;
|
||||
stockHp?: number | null;
|
||||
engineCode?: string | null;
|
||||
cylinders?: number | null;
|
||||
aspiration?: string | null;
|
||||
fuel?: string | null;
|
||||
injectors?: string | null;
|
||||
coils?: string | null;
|
||||
}
|
||||
|
||||
interface TuneDbData {
|
||||
userUid?: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
isPublished?: boolean;
|
||||
isListed?: boolean;
|
||||
isPublic?: boolean;
|
||||
tuneFile?: string | null;
|
||||
logFiles?: string[];
|
||||
toothLogFiles?: string[];
|
||||
customIniFile?: string | null;
|
||||
details?: TuneDataDetails;
|
||||
}
|
||||
|
||||
type Path = string;
|
||||
|
||||
interface UploadedFile {
|
||||
|
@ -150,7 +119,8 @@ const UploadPage = () => {
|
|||
const hasNavigatorShare = navigator.share !== undefined;
|
||||
const { currentUser, refreshToken } = useAuth();
|
||||
const history = useHistory();
|
||||
const { storageSet, storageGet, storageDelete } = useStorage();
|
||||
const { storageSet, storageGet, storageDelete } = useBrowserStorage();
|
||||
const { updateData, getTune } = useDb();
|
||||
|
||||
// details
|
||||
const [readme, setReadme] = useState('# My Tune\n\ndescription');
|
||||
|
@ -179,26 +149,6 @@ const UploadPage = () => {
|
|||
|
||||
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 = async (path: string) => {
|
||||
try {
|
||||
return await deleteObject(storageRef(storage, path));
|
||||
|
@ -209,7 +159,7 @@ const UploadPage = () => {
|
|||
|
||||
const publish = async () => {
|
||||
setIsLoading(true);
|
||||
await updateDbData(newTuneId!, {
|
||||
await updateData(newTuneId!, {
|
||||
updatedAt: new Date(),
|
||||
isPublished: true,
|
||||
isPublic,
|
||||
|
@ -272,6 +222,7 @@ const UploadPage = () => {
|
|||
},
|
||||
);
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error('Upload error:', error);
|
||||
notification.error({ message: 'Upload error', description: (error as Error).message });
|
||||
onError!(error as Error);
|
||||
|
@ -301,8 +252,8 @@ const UploadPage = () => {
|
|||
});
|
||||
|
||||
const uploadTune = async (options: UploadRequestOption) => {
|
||||
const found = await getDbData(newTuneId!);
|
||||
if (!found.exists()) {
|
||||
const found = await getTune(newTuneId!);
|
||||
if (found) {
|
||||
const tuneData: TuneDbData = {
|
||||
userUid: currentUser!.uid,
|
||||
createdAt: new Date(),
|
||||
|
@ -316,15 +267,15 @@ const UploadPage = () => {
|
|||
customIniFile: null,
|
||||
details: {},
|
||||
};
|
||||
await updateDbData(newTuneId!, tuneData);
|
||||
await updateData(newTuneId!, tuneData);
|
||||
}
|
||||
setShareUrl(`https://speedytuner.cloud/#/t/${newTuneId}`);
|
||||
setShareUrl(`${process.env.REACT_APP_WEB_URL}/#/t/${newTuneId}`);
|
||||
|
||||
const { path } = (options.data as unknown as UploadFileData);
|
||||
const tune: UploadedFile = {};
|
||||
tune[(options.file as UploadFile).uid] = path;
|
||||
upload(path, options, () => {
|
||||
updateDbData(newTuneId!, { tuneFile: path });
|
||||
updateData(newTuneId!, { tuneFile: path });
|
||||
}, async (file) => {
|
||||
const { result, message } = await validateSize(file);
|
||||
if (!result) {
|
||||
|
@ -353,7 +304,7 @@ const UploadPage = () => {
|
|||
tune[uuid] = path;
|
||||
const newValues = { ...logFiles, ...tune };
|
||||
upload(path, options, () => {
|
||||
updateDbData(newTuneId!, { logFiles: Object.values(newValues) });
|
||||
updateData(newTuneId!, { logFiles: Object.values(newValues) });
|
||||
}, async (file) => {
|
||||
const { result, message } = await validateSize(file);
|
||||
if (!result) {
|
||||
|
@ -394,7 +345,7 @@ const UploadPage = () => {
|
|||
tune[(options.file as UploadFile).uid] = path;
|
||||
const newValues = { ...toothLogFiles, ...tune };
|
||||
upload(path, options, () => {
|
||||
updateDbData(newTuneId!, { toothLogFiles: Object.values(newValues) });
|
||||
updateData(newTuneId!, { toothLogFiles: Object.values(newValues) });
|
||||
}, async (file) => {
|
||||
const { result, message } = await validateSize(file);
|
||||
if (!result) {
|
||||
|
@ -420,7 +371,7 @@ const UploadPage = () => {
|
|||
const tune: UploadedFile = {};
|
||||
tune[(options.file as UploadFile).uid] = path;
|
||||
upload(path, options, () => {
|
||||
updateDbData(newTuneId!, { customIniFile: path });
|
||||
updateData(newTuneId!, { customIniFile: path });
|
||||
}, async (file) => {
|
||||
const { result, message } = await validateSize(file);
|
||||
if (!result) {
|
||||
|
@ -428,6 +379,7 @@ const UploadPage = () => {
|
|||
}
|
||||
|
||||
// TODO: change to common interface, add some validation method
|
||||
// Create INI parser
|
||||
const parser = new INI((new TextDecoder()).decode(await file.arrayBuffer()));
|
||||
const valid = parser.parse().megaTune.signature.length > 0;
|
||||
|
||||
|
@ -447,7 +399,7 @@ const UploadPage = () => {
|
|||
removeFile(tuneFile[file.uid]);
|
||||
}
|
||||
setTuneFile(null);
|
||||
updateDbData(newTuneId!, { tuneFile: null });
|
||||
updateData(newTuneId!, { tuneFile: null });
|
||||
};
|
||||
|
||||
const removeLogFile = async (file: UploadFile) => {
|
||||
|
@ -458,7 +410,7 @@ const UploadPage = () => {
|
|||
const newValues = { ...logFiles };
|
||||
delete newValues[uid];
|
||||
setLogFiles(newValues);
|
||||
updateDbData(newTuneId!, { logFiles: Object.values(newValues) });
|
||||
updateData(newTuneId!, { logFiles: Object.values(newValues) });
|
||||
};
|
||||
|
||||
const removeToothLogFile = async (file: UploadFile) => {
|
||||
|
@ -469,7 +421,7 @@ const UploadPage = () => {
|
|||
const newValues = { ...toothLogFiles };
|
||||
delete newValues[uid];
|
||||
setToothLogFiles(newValues);
|
||||
updateDbData(newTuneId!, { toothLogFiles: Object.values(newValues) });
|
||||
updateData(newTuneId!, { toothLogFiles: Object.values(newValues) });
|
||||
};
|
||||
|
||||
const removeCustomIniFile = async (file: UploadFile) => {
|
||||
|
@ -477,7 +429,7 @@ const UploadPage = () => {
|
|||
removeFile(customIniFile![file.uid]);
|
||||
}
|
||||
setCustomIniFile(null);
|
||||
updateDbData(newTuneId!, { customIniFile: null });
|
||||
updateData(newTuneId!, { customIniFile: null });
|
||||
};
|
||||
|
||||
const prepareData = useCallback(async () => {
|
||||
|
@ -498,6 +450,7 @@ const UploadPage = () => {
|
|||
}
|
||||
setIsUserAuthorized(true);
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
storageDelete(newTuneIdKey);
|
||||
console.error(error);
|
||||
genericError(error as Error);
|
||||
|
@ -524,7 +477,7 @@ const UploadPage = () => {
|
|||
const shareSection = (
|
||||
<>
|
||||
<Divider>Publish & Share</Divider>
|
||||
<Row>
|
||||
{isPublished && <Row>
|
||||
<Input
|
||||
style={{ width: `calc(100% - ${hasNavigatorShare ? 65 : 35}px)` }}
|
||||
value={shareUrl!}
|
||||
|
@ -540,16 +493,24 @@ const UploadPage = () => {
|
|||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Row>
|
||||
</Row>}
|
||||
<Row style={{ marginTop: 10 }}>
|
||||
<Button
|
||||
{!isPublished ? <Button
|
||||
type="primary"
|
||||
block
|
||||
disabled={isPublished || isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={publish}
|
||||
>
|
||||
{isPublished && !isLoading ? 'Published' : 'Publish'}
|
||||
</Button>
|
||||
Publish
|
||||
</Button> : <Button
|
||||
type="primary"
|
||||
block
|
||||
onClick={() => {
|
||||
window.location.href = shareUrl as string;
|
||||
}}
|
||||
>
|
||||
Open
|
||||
</Button>}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -39,30 +39,31 @@ const Login = () => {
|
|||
const { login, googleAuth, githubAuth } = useAuth();
|
||||
const history = useHistory();
|
||||
const isAnythingLoading = isEmailLoading || isGoogleLoading || isGithubLoading;
|
||||
const redirectAfterLogin = useCallback(() => history.push(Routes.UPLOAD), [history]);
|
||||
|
||||
const googleLogin = useCallback(async () => {
|
||||
setIsGoogleLoading(true);
|
||||
try {
|
||||
await googleAuth();
|
||||
logInSuccessful();
|
||||
history.push(Routes.ROOT);
|
||||
redirectAfterLogin();
|
||||
} catch (error) {
|
||||
logInFailed(error as Error);
|
||||
setIsGoogleLoading(false);
|
||||
}
|
||||
}, [googleAuth, history]);
|
||||
}, [googleAuth, redirectAfterLogin]);
|
||||
|
||||
const githubLogin = useCallback(async () => {
|
||||
setIsGithubLoading(true);
|
||||
try {
|
||||
await githubAuth();
|
||||
logInSuccessful();
|
||||
history.push(Routes.ROOT);
|
||||
redirectAfterLogin();
|
||||
} catch (error) {
|
||||
logInFailed(error as Error);
|
||||
setIsGithubLoading(false);
|
||||
}
|
||||
}, [githubAuth, history]);
|
||||
}, [githubAuth, redirectAfterLogin]);
|
||||
|
||||
const emailLogin = async ({ email, password }: { form: any, email: string, password: string }) => {
|
||||
setIsEmailLoading(true);
|
||||
|
@ -74,7 +75,7 @@ const Login = () => {
|
|||
emailNotVerified();
|
||||
}
|
||||
|
||||
history.push(Routes.ROOT);
|
||||
redirectAfterLogin();
|
||||
} catch (error) {
|
||||
form.resetFields();
|
||||
console.warn(error);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export enum Routes {
|
||||
ROOT = '/',
|
||||
TAB = '/:tab',
|
||||
TUNE = '/tune',
|
||||
DIALOG = '/tune/:category/:dialog',
|
||||
LOG = '/log',
|
||||
DIAGNOSE = '/diagnose',
|
||||
|
||||
TUNE_ROOT = '/t/:tuneId',
|
||||
TUNE_TAB = '/t/:tuneId/:tab',
|
||||
TUNE_TUNE = '/t/:tuneId/tune',
|
||||
TUNE_DIALOG = '/t/:tuneId/tune/:category/:dialog',
|
||||
TUNE_LOG = '/t/:tuneId/log',
|
||||
TUNE_DIAGNOSE = '/t/:tuneId/diagnose',
|
||||
|
||||
LOGIN = '/auth/login',
|
||||
LOGOUT = '/auth/logout',
|
||||
SIGN_UP = '/auth/sign-up',
|
||||
|
|
41
src/store.ts
41
src/store.ts
|
@ -5,15 +5,24 @@ import {
|
|||
createAction,
|
||||
createReducer,
|
||||
} from '@reduxjs/toolkit';
|
||||
import * as Types from '@speedy-tuner/types';
|
||||
import {
|
||||
AppState,
|
||||
ConfigState,
|
||||
LogsState,
|
||||
TuneState,
|
||||
UpdateTunePayload,
|
||||
} from './types/state';
|
||||
|
||||
// tune and config
|
||||
const updateTune = createAction<Types.UpdateTunePayload>('tune/update');
|
||||
const loadTune = createAction<Types.TuneState>('tune/load');
|
||||
const loadConfig = createAction<Types.ConfigState>('config/load');
|
||||
const updateTune = createAction<UpdateTunePayload>('tune/update');
|
||||
const loadTune = createAction<TuneState>('tune/load');
|
||||
const loadConfig = createAction<ConfigState>('config/load');
|
||||
|
||||
// navigation
|
||||
const setTuneId = createAction<string>('navigation/tuneId');
|
||||
|
||||
// logs
|
||||
const loadLogs = createAction<Types.LogsState>('logs/load');
|
||||
const loadLogs = createAction<LogsState>('logs/load');
|
||||
|
||||
// status bar
|
||||
const setStatus = createAction<string>('status');
|
||||
|
@ -22,7 +31,7 @@ const setStatus = createAction<string>('status');
|
|||
const setSidebarCollapsed = createAction<boolean>('ui/sidebarCollapsed');
|
||||
const toggleSidebar = createAction('ui/toggleSidebar');
|
||||
|
||||
const initialState: Types.AppState = {
|
||||
const initialState: AppState = {
|
||||
tune: {
|
||||
constants: {},
|
||||
},
|
||||
|
@ -34,30 +43,36 @@ const initialState: Types.AppState = {
|
|||
status: {
|
||||
text: null,
|
||||
},
|
||||
navigation: {
|
||||
tuneId: null,
|
||||
},
|
||||
};
|
||||
|
||||
const rootReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(loadConfig, (state: Types.AppState, action) => {
|
||||
.addCase(loadConfig, (state: AppState, action) => {
|
||||
state.config = action.payload;
|
||||
})
|
||||
.addCase(loadTune, (state: Types.AppState, action) => {
|
||||
.addCase(loadTune, (state: AppState, action) => {
|
||||
state.tune = action.payload;
|
||||
})
|
||||
.addCase(loadLogs, (state: Types.AppState, action) => {
|
||||
.addCase(loadLogs, (state: AppState, action) => {
|
||||
state.logs = action.payload;
|
||||
})
|
||||
.addCase(updateTune, (state: Types.AppState, action) => {
|
||||
.addCase(updateTune, (state: AppState, action) => {
|
||||
state.tune.constants[action.payload.name].value = action.payload.value;
|
||||
})
|
||||
.addCase(setSidebarCollapsed, (state: Types.AppState, action) => {
|
||||
.addCase(setSidebarCollapsed, (state: AppState, action) => {
|
||||
state.ui.sidebarCollapsed = action.payload;
|
||||
})
|
||||
.addCase(toggleSidebar, (state: Types.AppState) => {
|
||||
.addCase(toggleSidebar, (state: AppState) => {
|
||||
state.ui.sidebarCollapsed = !state.ui.sidebarCollapsed;
|
||||
})
|
||||
.addCase(setStatus, (state: Types.AppState, action) => {
|
||||
.addCase(setStatus, (state: AppState, action) => {
|
||||
state.status.text = action.payload;
|
||||
})
|
||||
.addCase(setTuneId, (state: AppState, action) => {
|
||||
state.navigation.tuneId = action.payload;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
export interface TuneDataDetails {
|
||||
readme?: string | null;
|
||||
make?: string | null;
|
||||
model?: string | null;
|
||||
displacement?: string | null;
|
||||
year?: number | null;
|
||||
hp?: number | null;
|
||||
stockHp?: number | null;
|
||||
engineCode?: string | null;
|
||||
cylinders?: number | null;
|
||||
aspiration?: string | null;
|
||||
fuel?: string | null;
|
||||
injectors?: string | null;
|
||||
coils?: string | null;
|
||||
}
|
||||
|
||||
export interface TuneDbData {
|
||||
userUid?: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
isPublished?: boolean;
|
||||
isListed?: boolean;
|
||||
isPublic?: boolean;
|
||||
tuneFile?: string | null;
|
||||
logFiles?: string[];
|
||||
toothLogFiles?: string[];
|
||||
customIniFile?: string | null;
|
||||
details?: TuneDataDetails;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
Config,
|
||||
Logs,
|
||||
Tune,
|
||||
} from '@speedy-tuner/types';
|
||||
|
||||
export interface ConfigState extends Config {}
|
||||
|
||||
export interface TuneState extends Tune {}
|
||||
|
||||
export interface LogsState extends Logs {}
|
||||
|
||||
export interface UIState {
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
export interface StatusState {
|
||||
text: string | null;
|
||||
}
|
||||
|
||||
export interface NavigationState {
|
||||
tuneId: string | null;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
tune: TuneState;
|
||||
config: ConfigState;
|
||||
logs: LogsState,
|
||||
ui: UIState;
|
||||
status: StatusState;
|
||||
navigation: NavigationState;
|
||||
}
|
||||
|
||||
export interface UpdateTunePayload {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import BrowserStorage from './storage/BrowserStorage';
|
||||
import { StorageInterface } from './StorageInterface';
|
||||
|
||||
class Storage {
|
||||
private storage: StorageInterface;
|
||||
|
||||
public constructor() {
|
||||
this.storage = new BrowserStorage();
|
||||
}
|
||||
|
||||
public get(key: string) {
|
||||
return this.storage.get(key);
|
||||
}
|
||||
|
||||
public getSync(key: string) {
|
||||
return this.storage.getSync(key);
|
||||
}
|
||||
|
||||
public set(key: string, value: string) {
|
||||
return this.storage.set(key, value);
|
||||
}
|
||||
|
||||
public delete(key: string) {
|
||||
return this.storage.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export default Storage;
|
|
@ -1,7 +0,0 @@
|
|||
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>;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { Config as ConfigType } from '@speedy-tuner/types';
|
||||
import { INI } from '@speedy-tuner/ini';
|
||||
import pako from 'pako';
|
||||
import store from '../store';
|
||||
import stdDialogs from '../data/standardDialogs';
|
||||
import help from '../data/help';
|
||||
|
@ -9,22 +10,19 @@ import {
|
|||
} from './http';
|
||||
import TuneParser from './tune/TuneParser';
|
||||
|
||||
export const loadAll = async () => {
|
||||
export const loadTune = async (tuneRaw: ArrayBuffer, iniRaw: ArrayBuffer) => {
|
||||
const started = new Date();
|
||||
// const version = 202012;
|
||||
const version = 202103;
|
||||
|
||||
const json: ConfigType = await fetch(`./tunes/${version}.json`)
|
||||
.then((response) => response.json());
|
||||
|
||||
const tuneRaw = await fetch(`./tunes/${version}.msq`);
|
||||
const tuneParser = new TuneParser().parse(await tuneRaw.arrayBuffer());
|
||||
const tuneParser = new TuneParser()
|
||||
.parse(pako.inflate(new Uint8Array(tuneRaw)));
|
||||
|
||||
if (!tuneParser.isValid()) {
|
||||
// TODO: capture exception
|
||||
console.error('Invalid tune');
|
||||
}
|
||||
|
||||
const config = json;
|
||||
const buff = pako.inflate(new Uint8Array(iniRaw));
|
||||
const config = new INI((new TextDecoder()).decode(buff)).parse();
|
||||
|
||||
// override / merge standard dialogs, constants and help
|
||||
config.dialogs = {
|
||||
|
|
Loading…
Reference in New Issue