Rewrite using typescript, integrate the latest changes from @mwarning/openwrt-firmware-selector

This commit is contained in:
Sudhanshu Gautam 2021-01-10 19:56:13 +05:30
parent d5c4ea592a
commit ce4c36622b
47 changed files with 5269 additions and 5282 deletions

1
.env.development Normal file
View file

@ -0,0 +1 @@
REACT_APP_I18N_DEBUG=1

View file

@ -1 +1,2 @@
src/config.ts
src/serviceWorker.js src/serviceWorker.js

42
.eslintrc.js Normal file
View file

@ -0,0 +1,42 @@
module.exports = {
extends: [
'airbnb-typescript',
'airbnb/hooks',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'prettier',
'prettier/react',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
plugins: ['react', '@typescript-eslint', 'jest'],
env: {
browser: true,
es6: true,
jest: true,
},
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
project: './tsconfig.json',
},
rules: {
'linebreak-style': 'off',
'react/prop-types': 'off',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
};

View file

@ -1,38 +0,0 @@
env:
browser: true
node: true
es6: true
jest: true
extends:
- 'eslint:recommended'
- 'plugin:react/recommended'
globals:
Atomics: readonly
SharedArrayBuffer: readonly
parser: 'babel-eslint'
parserOptions:
ecmaFeatures:
jsx: true
ecmaVersion: 2018
sourceType: module
plugins:
- react
- prettier
settings:
react:
version: 'detect'
rules:
prettier/prettier:
- error
indent:
- error
- 2
linebreak-style:
- error
- unix
quotes:
- error
- single
semi:
- error
- always

4
.gitignore vendored
View file

@ -18,9 +18,13 @@
.env.test.local .env.test.local
.env.production.local .env.production.local
public/data
.eslintcache
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# local editor configs # local editor configs
.idea .idea
.vscode

View file

@ -1,4 +1,6 @@
{ {
"singleQuote": true, "trailingComma": "es5",
"trailingComma": "es5" "printWidth": 100,
"semi": true,
"singleQuote": true
} }

View file

@ -4,19 +4,23 @@
"private": true, "private": true,
"homepage": "https://sudhanshu16.github.io/openwrt-firmware-selector/", "homepage": "https://sudhanshu16.github.io/openwrt-firmware-selector/",
"dependencies": { "dependencies": {
"@material-ui/core": "^4.1.2", "@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.2.1", "@material-ui/icons": "^4.2.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"@material-ui/styles": "^4.11.2",
"axios": "^0.19.0", "axios": "^0.19.0",
"fuzzyset.js": "^0.0.8",
"gh-pages": "^2.0.1", "gh-pages": "^2.0.1",
"i18next": "^17.0.4", "i18next": "^17.0.4",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
"node-sass": "^4.12.0", "lodash": "^4.17.20",
"match-sorter": "^6.1.0",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-i18next": "^10.11.2", "react-i18next": "^10.11.2",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.2.0",
"react-scripts": "3.0.1" "react-scripts": "4.0.1",
"react-use": "^15.3.8",
"web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
@ -24,7 +28,9 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"predeploy": "yarn run build", "predeploy": "yarn run build",
"deploy": "gh-pages -d build" "deploy": "gh-pages -d build",
"format": "prettier --write src/**/*.ts{,x}",
"lint": "tsc --noEmit && eslint src/**/*.ts{,x}"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -39,16 +45,27 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.0.2", "@types/lodash": "^4.14.167",
"eslint": "^6.1.0", "@types/react": "^17.0.0",
"eslint-plugin-prettier": "^3.1.0", "@types/react-dom": "^17.0.0",
"eslint-plugin-react": "^7.14.2", "@types/react-router-dom": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"eslint": "7.2.0",
"eslint-config-airbnb": "18.2.1",
"eslint-config-airbnb-typescript": "^12.0.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.3",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "4.0.0",
"husky": "^3.0.5", "husky": "^3.0.5",
"prettier": "^1.18.2", "prettier": "^2.2.1",
"pretty-quick": "^1.11.1", "pretty-quick": "^1.11.1",
"prop-types": "latest", "sass": "^1.32.2",
"stylelint-prettier": "^1.1.1", "typescript": "^4.1.3"
"typescript": "^3.5.3"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

264
scripts/collect.py Executable file
View file

@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""
Tool to create overview.json files and update the config.ts.
"""
from pathlib import Path
import tempfile
import datetime
import argparse
import time
import json
import glob
import sys
import os
import re
from distutils.version import StrictVersion
SUPPORTED_METADATA_VERSION = 1
BUILD_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
assert sys.version_info >= (3, 5), "Python version too old. Python >=3.5.0 needed."
def write_json(path, content, formatted):
print("write: {}".format(path))
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as file:
if formatted:
json.dump(content, file, indent=" ", sort_keys=True)
else:
json.dump(content, file, sort_keys=True)
# generate an overview of all models of a build
def assemble_overview_json(release, profiles):
overview = {"profiles": [], "release": release}
for profile in profiles:
obj = profile["file_content"]
for model_id, model_obj in obj["profiles"].items():
overview["profiles"].append(
{"target": obj["target"], "titles": model_obj["titles"], "id": model_id}
)
return overview
def update_config(config_path, versions):
config_path = os.path.join(config_path, "config.ts")
if os.path.isfile(config_path):
content = ""
with open(str(config_path), "r", encoding="utf-8") as file:
content = file.read()
latest_version = "0.0.0"
for version in versions.keys():
try:
if StrictVersion(version) > StrictVersion(latest_version):
latest_version = version
except ValueError:
print("Warning: Non numeric version: {}".format(version))
continue
content = re.sub(
"versions:[\\s]*{[^}]*}", "versions: {}".format(versions), content
)
content = re.sub(
"default_version:.*,",
'default_version: "{}",'.format(latest_version),
content,
)
with open(str(config_path), "w+") as file:
print("write: {}".format(config_path))
file.write(content)
else:
sys.stderr.write("Warning: File not found: {}\n".format(config_path))
"""
Replace {base} variable in download URL with the intersection
of all profile.json paths. E.g.:
../tmp/releases/18.06.8/targets => base is releases/18.06.8/targets
../tmp/snapshots/targets => base in snapshots/targets
"""
def replace_base(releases, profiles, url):
def get_common_path(profiles):
paths = [profile["file_path"] for profile in profiles]
return os.path.commonpath(paths)
def get_common_base(releases):
paths = []
for release, profiles in releases.items():
paths.append(get_common_path(profiles))
return os.path.commonpath(paths)
if "{base}" in url:
common = get_common_path(profiles)
base = get_common_base(releases)
return url.replace("{base}", common[len(base) + 1 :])
else:
return url
def add_profile(releases, profile):
release = profile["file_content"]["version_number"]
releases.setdefault(release, []).append(profile)
def write_data(releases, args):
versions = {}
for release, profiles in releases.items():
overview_json = assemble_overview_json(release, profiles)
if args.image_url:
image_url = replace_base(releases, profiles, args.image_url)
overview_json["image_url"] = image_url
if args.info_url:
info_url = replace_base(releases, profiles, args.info_url)
overview_json["info_url"] = info_url
write_json(
os.path.join(args.dump_path, "data", release, "overview.json"),
overview_json,
args.formatted,
)
# write <device-id>.json files
for profile in profiles:
obj = profile["file_content"]
for model_id, model_obj in obj["profiles"].items():
combined = {**obj, **model_obj}
combined["build_at"] = profile["last_modified"]
combined["id"] = model_id
del combined["profiles"]
profiles_path = os.path.join(
args.dump_path,
"data",
release,
obj["target"],
"{}.json".format(model_id),
)
write_json(profiles_path, combined, args.formatted)
versions[release] = "data/{}".format(release)
update_config(args.config_path, versions)
"""
Scrape profiles.json using wget (slower but more generic).
Merge into overview.json files.
Update config.json.
"""
def scrape(args):
releases = {}
with tempfile.TemporaryDirectory() as tmp_dir:
# download all profiles.json files
os.system(
"wget -c -r -P {} -A 'profiles.json' --reject-regex 'kmods|packages' --no-parent {}".format(
tmp_dir, args.release_src
)
)
# delete empty folders
os.system("find {}/* -type d -empty -delete".format(tmp_dir))
# create overview.json files
for path in glob.glob("{}".format(tmp_dir)):
for ppath in Path(path).rglob("profiles.json"):
with open(str(ppath), "r", encoding="utf-8") as file:
# we assume local timezone is UTC/GMT
last_modified = datetime.datetime.fromtimestamp(
os.path.getmtime(str(ppath))
).strftime(BUILD_DATE_FORMAT)
add_profile(
releases,
{
"file_path": str(ppath),
"file_content": json.loads(file.read()),
"last_modified": last_modified,
},
)
write_data(releases, args)
"""
Scan a local directory for releases with profiles.json.
Merge into overview.json files.
Update config.json.
"""
def scan(args):
releases = {}
for path in Path(args.release_src).rglob("profiles.json"):
with open(str(path), "r", encoding="utf-8") as file:
content = file.read()
last_modified = time.strftime(
BUILD_DATE_FORMAT, time.gmtime(os.path.getmtime(str(path)))
)
add_profile(
releases,
{
"file_path": str(path),
"file_content": json.loads(content),
"last_modified": last_modified,
},
)
write_data(releases, args)
def main():
parser = argparse.ArgumentParser(
description="""
Scan for JSON files generated by OpenWrt. Create JSON files in <dump_path>/data/ and update <config_path>/config.ts.
By default dump_path = config_path
Usage Examples:
./misc/collect.py --image-url 'https://downloads.openwrt.org/{base}/{target}' ~/openwrt/bin www/
or
./misc/collect.py --image-url 'https://downloads.openwrt.org/{base}/{target}' https://downloads.openwrt.org www/
""",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--formatted", action="store_true", help="Output formatted JSON data."
)
parser.add_argument("--info-url", help="Info URL template.")
parser.add_argument("--image-url", help="URL template to download images.")
parser.add_argument(
"release_src",
help="Local folder to scan or website URL to scrape for profiles.json files.",
)
parser.add_argument("config_path", help="Path of the config.ts.")
parser.add_argument("dump_path", help="Path to dump the scraped JSONs to.")
args = parser.parse_args()
if args.dump_path is None:
args.dump_path = args.config_path
if not os.path.isfile("{}/config.ts".format(args.config_path)):
print("Error: {}/config.ts does not exits!".format(args.config_path))
exit(1)
if args.release_src.startswith("http"):
scrape(args)
else:
scan(args)
if __name__ == "__main__":
main()

View file

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -1,57 +0,0 @@
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.scss';
import { createMuiTheme } from '@material-ui/core/styles';
import { ThemeProvider } from '@material-ui/styles';
import Header from './components/header.js';
import Home from './containers/home/home';
import NotFound from './containers/not-found/not-found';
import LinearProgress from '@material-ui/core/LinearProgress';
import { Paper } from '@material-ui/core';
const theme = createMuiTheme({
palette: {
primary: {
main: '#3F51B5',
},
secondary: {
main: '#009688',
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<Suspense fallback={<LinearProgress />}>
<div className="App">
<Header />
<Router>
<Switch>
<Route path="" component={Home} />
<Route default component={NotFound} />
</Switch>
</Router>
<Paper elevation={4} className="report-problem-container">
<span>
If you come across any issue, feel free to report{' '}
<a href="https://github.com/aparcar/attendedsysupgrade-server/issues">
here
</a>
.
</span>
<span className="report-link">
For contributions, go to{' '}
<a href="https://github.com/sudhanshu16/openwrt-firmware-selector/">
Github
</a>
</span>
</Paper>
</div>
</Suspense>
</ThemeProvider>
);
}
export default App;

View file

@ -1,7 +1,3 @@
.App {
text-align: center;
}
.App-logo { .App-logo {
animation: App-logo-spin infinite 20s linear; animation: App-logo-spin infinite 20s linear;
height: 40vmin; height: 40vmin;

View file

@ -1,9 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View file

@ -1,26 +1,53 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import logo from './logo.svg'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css'; import './App.scss';
function App() { import { createMuiTheme } from '@material-ui/core/styles';
import { ThemeProvider } from '@material-ui/styles';
import LinearProgress from '@material-ui/core/LinearProgress';
import { Paper, Toolbar } from '@material-ui/core';
import Header from './components/Header';
import Home from './containers/home/home';
import NotFound from './containers/not-found/not-found';
const theme = createMuiTheme({
palette: {
primary: {
main: '#3F51B5',
},
secondary: {
main: '#009688',
},
},
});
const App: FunctionComponent = () => {
return ( return (
<ThemeProvider theme={theme}>
<React.Suspense fallback={<LinearProgress />}>
<div className="App"> <div className="App">
<header className="App-header"> <Header />
<img src={logo} className="App-logo" alt="logo" /> <Router>
<p> <Switch>
Edit <code>src/App.tsx</code> and save to reload. <Route path="" component={Home} />
</p> <Route default component={NotFound} />
<a </Switch>
className="App-link" </Router>
href="https://reactjs.org" <Toolbar hidden />
target="_blank" <Paper elevation={4} className="report-problem-container">
rel="noopener noreferrer" <span>
> If you come across any issue, feel free to report{' '}
Learn React <a href="https://github.com/aparcar/attendedsysupgrade-server/issues">here</a>.
</a> </span>
</header> <span className="report-link">
For contributions, go to{' '}
<a href="https://github.com/sudhanshu16/openwrt-firmware-selector/">Github</a>
</span>
</Paper>
</div> </div>
</React.Suspense>
</ThemeProvider>
); );
} };
export default App; export default App;

View file

@ -1,3 +1,4 @@
import React, { FunctionComponent } from 'react';
import { import {
Button, Button,
Dialog, Dialog,
@ -6,10 +7,18 @@ import {
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
} from '@material-ui/core'; } from '@material-ui/core';
import React from 'react';
import PropTypes from 'prop-types';
function AlertDialog({ type Props = {
open: boolean;
cancelHandler: () => void;
acceptHandler: () => void;
body: React.ReactElement;
title: React.ReactElement;
cancelComponent: React.ReactElement;
acceptComponent: React.ReactElement;
};
const AlertDialog: FunctionComponent<Props> = ({
open, open,
cancelHandler, cancelHandler,
acceptHandler, acceptHandler,
@ -17,7 +26,7 @@ function AlertDialog({
title, title,
cancelComponent, cancelComponent,
acceptComponent, acceptComponent,
}) { }) => {
return ( return (
<Dialog <Dialog
open={open} open={open}
@ -27,9 +36,7 @@ function AlertDialog({
> >
<DialogTitle id="alert-dialog-title">{title}</DialogTitle> <DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">{body}</DialogContentText>
{body}
</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{acceptHandler && ( {acceptHandler && (
@ -38,28 +45,13 @@ function AlertDialog({
</Button> </Button>
)} )}
{cancelHandler && ( {cancelHandler && (
<Button <Button onClick={cancelHandler} color="secondary" variant="contained" autoFocus>
onClick={cancelHandler}
color="secondary"
variant="contained"
autoFocus
>
{cancelComponent} {cancelComponent}
</Button> </Button>
)} )}
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
}
AlertDialog.propTypes = {
open: PropTypes.bool,
cancelHandler: PropTypes.func,
acceptHandler: PropTypes.func,
body: PropTypes.object,
title: PropTypes.string,
cancelComponent: PropTypes.elementType,
acceptComponent: PropTypes.object,
}; };
export default AlertDialog; export default AlertDialog;

View file

@ -1,15 +1,9 @@
import { import React, { FunctionComponent } from 'react';
IconButton, import { IconButton, makeStyles, Snackbar, SnackbarContent } from '@material-ui/core';
makeStyles,
Snackbar,
SnackbarContent,
} from '@material-ui/core';
import ErrorIcon from '@material-ui/icons/Error'; import ErrorIcon from '@material-ui/icons/Error';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import React from 'react';
import PropTypes from 'prop-types';
const SnackBarStyles = makeStyles(theme => ({ const SnackBarStyles = makeStyles((theme) => ({
error: { error: {
backgroundColor: theme.palette.error.dark, backgroundColor: theme.palette.error.dark,
}, },
@ -23,7 +17,13 @@ const SnackBarStyles = makeStyles(theme => ({
}, },
})); }));
function ErrorSnackBar({ open, closeHandle, errorMessage }) { type Props = {
open: boolean;
closeHandle: () => void;
errorMessage: string;
};
const ErrorSnackbar: FunctionComponent<Props> = ({ open, closeHandle, errorMessage }) => {
const classes = SnackBarStyles(); const classes = SnackBarStyles();
return ( return (
<Snackbar <Snackbar
@ -48,24 +48,13 @@ function ErrorSnackBar({ open, closeHandle, errorMessage }) {
</span> </span>
} }
action={[ action={[
<IconButton <IconButton key="close" aria-label="Close" color="inherit" onClick={closeHandle}>
key="close"
aria-label="Close"
color="inherit"
onClick={closeHandle}
>
<CloseIcon /> <CloseIcon />
</IconButton>, </IconButton>,
]} ]}
/> />
</Snackbar> </Snackbar>
); );
}
ErrorSnackBar.propTypes = {
open: PropTypes.bool,
closeHandle: PropTypes.func,
errorMessage: PropTypes.string,
}; };
export default ErrorSnackBar; export default ErrorSnackbar;

70
src/components/Header.tsx Normal file
View file

@ -0,0 +1,70 @@
import React, { FunctionComponent, useRef } from 'react';
import TranslateIcon from '@material-ui/icons/Translate';
import {
AppBar,
Box,
Button,
Menu,
MenuItem,
Checkbox,
Toolbar,
Typography,
} from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import locales from '../locales';
import '../App.scss';
const Header: FunctionComponent = () => {
const { t, i18n } = useTranslation();
const languageSwitchAnchorEl = useRef<null | HTMLButtonElement>(null);
const [showLanguageSwitch, toggleLanguageSwitch] = React.useState(false);
const handleLanguageChange = (l: string) => {
i18n.changeLanguage(l);
};
const openChangeLanguagePopper = () => {
toggleLanguageSwitch(!showLanguageSwitch);
};
return (
<AppBar position="sticky" className="header">
<Toolbar>
<Typography variant="h6">{t('tr-title')}</Typography>
<span className="title-mobile">{t('tr-title')}</span>
<div style={{ flexGrow: 1 }} />
<Box position="relative">
<Button
aria-controls="language-menu"
aria-haspopup="true"
color="secondary"
variant="contained"
onClick={openChangeLanguagePopper}
ref={languageSwitchAnchorEl}
>
<span className="language-toggle-text">
{i18n.language ? locales[i18n.language] : 'change language'} &nbsp;
</span>
<TranslateIcon />
</Button>
<Menu
id="language-menu"
open={showLanguageSwitch}
anchorEl={languageSwitchAnchorEl.current}
onClose={() => toggleLanguageSwitch(false)}
>
{Object.keys(locales).map((l) => (
<MenuItem key={l} value={l} onClick={() => handleLanguageChange(l)}>
<Checkbox size="small" checked={i18n.language === l} /> {t(locales[l])}
</MenuItem>
))}
</Menu>
</Box>
</Toolbar>
</AppBar>
);
};
export default Header;

View file

@ -1,52 +0,0 @@
import { InputAdornment, makeStyles, TextField } from '@material-ui/core';
import { fade } from '@material-ui/core/styles';
import SearchIcon from '@material-ui/icons/Search';
import React from 'react';
import PropTypes from 'prop-types';
const useStylesSearch = makeStyles(theme => ({
root: {
borderColor: '#e2e2e1',
overflow: 'hidden',
margin: 0,
borderRadius: 4,
transition: theme.transitions.create(['border-color', 'box-shadow']),
'&:hover': {
borderColor: fade(theme.palette.primary.main, 0.25),
},
'&$focused': {
backgroundColor: '#fff',
boxShadow: `${fade(theme.palette.primary.main, 0.25)} 0 0 0 2px`,
borderColor: theme.palette.primary.main,
},
},
focused: {},
}));
function SearchTextField(props) {
const classes = useStylesSearch();
return (
<TextField
variant="outlined"
label={<div className="search-label">{props.labeltext}</div>}
disabled={props.disabled}
InputProps={{
classes,
endAdornment: (
<InputAdornment position="start">
<SearchIcon className={classes.label} />
</InputAdornment>
),
}}
{...props}
/>
);
}
SearchTextField.propTypes = {
labeltext: PropTypes.string,
disabled: PropTypes.bool,
};
export default SearchTextField;

View file

@ -1,120 +0,0 @@
import React from 'react';
import TranslateIcon from '@material-ui/icons/Translate';
import {
AppBar,
Button,
Fade,
FormControl,
FormControlLabel,
FormLabel,
Paper,
Popper,
Radio,
RadioGroup,
Toolbar,
Typography,
} from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import i18next from 'i18next';
import '../App.scss';
export default function Header() {
const { t, i18n } = useTranslation();
const [value, setValue] = React.useState('en');
const [anchorEl, setAnchorEl] = React.useState(null);
const changeLanguage = event => {
var val = event.target.value;
i18n.changeLanguage(val);
setAnchorEl(null);
};
const openChangeLanguagePopper = event => {
setValue(i18next.language.substring(0, 2));
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popper' : undefined;
return (
<AppBar position="static" className="header">
<Toolbar>
<Typography edge="start" variant="h6" className="title-desktop">
{t('OpenWrt Firmware Selector')}
</Typography>
<span className="title-mobile">{t('OpenWrt Firmware Selector')}</span>
<div style={{ flexGrow: 1 }} />
<Button
aria-describedby={id}
color="secondary"
variant="contained"
onClick={openChangeLanguagePopper}
href="#"
>
<span className="language-toggle-text">
{t('Change Language')} &nbsp;
</span>
<TranslateIcon />
</Button>
<Popper
id={id}
open={open}
anchorEl={anchorEl}
transition
disablePortal={true}
className="language-selector-popper-container"
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper className="language-selector-popper">
<FormControl component="fieldset">
<FormLabel component="legend">Change Language</FormLabel>
<br />
<RadioGroup
aria-label="Language"
name="language"
value={value}
onChange={changeLanguage}
>
<FormControlLabel
value="en"
control={<Radio />}
label={t('English')}
/>
<FormControlLabel
value="de"
control={<Radio />}
label={t('German')}
/>
<FormControlLabel
value="ru"
control={<Radio />}
label={t('Russian')}
/>
<FormControlLabel
value="pt_br"
control={<Radio />}
label={t('Brazilian Portuguese')}
/>
<FormControlLabel
value="tr"
control={<Radio />}
label={t('Turkish')}
/>
<FormControlLabel
value="es"
control={<Radio />}
label={t('Spanish')}
/>
</RadioGroup>
</FormControl>
</Paper>
</Fade>
)}
</Popper>
</Toolbar>
</AppBar>
);
}

View file

@ -1,17 +0,0 @@
const asu_base = 'https://aparcar.stephen304.com';
const defaults = {
CORSbyPass: 'https://cors-anywhere.herokuapp.com/',
base_api: asu_base + '/api/',
asu: asu_base,
asu_vanilla: asu_base + '/download/json-demo/openwrt/',
};
const prod = {
i18nDebug: false,
};
const dev = {
i18nDebug: true,
};
export default {
...defaults,
settings: process.env.NODE_ENV === 'development' ? dev : prod,
};

24
src/config.ts Normal file
View file

@ -0,0 +1,24 @@
/* exported config */
const config = {
// Show help text for images
show_help: true,
// Path to overview.json file or URL to the ASU API
versions: { '19.07.5': 'data/19.07.5', SNAPSHOT: 'data/SNAPSHOT' },
// Pre-selected version (optional)
default_version: '19.07.5',
// Image download URL (optional)
image_url: 'https://downloads.openwrt.org/releases/{version}/{target}',
// Info link URL (optional)
info_url: 'https://openwrt.org/start?do=search&id=toh&q={title}',
// Build custom images (optional)
// See https://github.com/aparcar/asu
// asu_url: 'https://chef.libremesh.org'
};
export default config;

View file

@ -0,0 +1,197 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import {
Box,
Button,
CircularProgress,
Link,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@material-ui/core';
import { Launch, CloudDownload } from '@material-ui/icons';
import Axios from 'axios';
import { isEqual } from 'lodash';
import { useTranslation } from 'react-i18next';
import { ProfilesEntity } from '../../../types/overview';
import { Profile, TitlesEntity } from '../../../types/profile';
import config from '../../../config';
type Props = {
selectedVersion: string;
selectedProfile: ProfilesEntity;
};
const getTitle = (title: TitlesEntity) => {
return title.title || `${title.vendor} ${title.model}`;
};
const profilesData: { [key: string]: Profile } = {};
const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedProfile }) => {
const [profile, setProfileData] = useState<Profile>();
const [working, toggleWorking] = useState<boolean>(true);
const { t } = useTranslation();
const getHelpKey = (type: string) => {
const lc = type.toLowerCase();
if (lc.includes('sysupgrade')) {
return 'sysupgrade-help';
}
if (lc.includes('factory') || lc === 'trx' || lc === 'chk') {
return 'factory-help';
}
if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
return 'kernel-help';
}
if (lc.includes('root')) {
return 'rootfs-help';
}
if (lc.includes('sdcard')) {
return 'sdcard-help';
}
if (lc.includes('tftp')) {
return 'tftp-help';
}
return 'other-help';
};
const getProfileData = useCallback(async () => {
let profileData = profilesData[selectedProfile.id];
toggleWorking(true);
if (!profileData) {
const response = await Axios.get<Profile>(
`${process.env.PUBLIC_URL}/data/${selectedVersion}/${selectedProfile.target}/${selectedProfile.id}.json`
);
profileData = response.data;
profilesData[selectedProfile.id] = profileData;
}
toggleWorking(false);
return profileData;
}, [selectedVersion, selectedProfile]);
useEffect(() => {
if (selectedVersion && selectedProfile) {
getProfileData().then((_profileData) => {
if (!isEqual(profile, _profileData)) setProfileData(_profileData);
});
}
}, [selectedVersion, selectedProfile, getProfileData, profile]);
if (working || !profile) return <CircularProgress />;
const buildAt = new Date(profile.build_at);
return (
<>
<Box paddingTop={3} paddingBottom={2}>
<Typography variant="h6" component="h1" align="left">
{t('tr-version-build')}
</Typography>
</Box>
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>{t('tr-model')}</TableCell>
<TableCell>{profile.titles?.map((title) => getTitle(title)).join(', ')}</TableCell>
</TableRow>
<TableRow>
<TableCell>{t('tr-target')}</TableCell>
<TableCell>{profile.target}</TableCell>
</TableRow>
<TableRow>
<TableCell>{t('tr-version')}</TableCell>
<TableCell>
{profile.version_number} ({profile.version_code})
</TableCell>
</TableRow>
<TableRow>
<TableCell>{t('tr-date')}</TableCell>
<TableCell>{buildAt.toLocaleString()}</TableCell>
</TableRow>
<TableRow>
<TableCell>Info</TableCell>
{profile.titles && (
<TableCell>
{profile.titles
.map<React.ReactNode>((title) => {
const titleString = getTitle(title);
const infoUrl = config.info_url
.replace('{title}', encodeURI(titleString))
.replace('{target}', profile.target)
.replace('{id}', profile.id)
.replace('{version}', profile.version_number);
return (
<Link href={infoUrl}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{profile.titles!.length > 1 && (
<Typography component="span">{titleString}</Typography>
)}
<Launch
style={{
marginLeft: 10,
verticalAlign: 'middle',
}}
/>
</Link>
);
})
.reduce((prev, curr) => [
prev,
<Box display="inline-block" marginRight={2} />,
curr,
])}
</TableCell>
)}
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Box paddingTop={3} paddingBottom={2}>
<Typography variant="h6" component="h1" align="left">
{t('tr-downloads')}
</Typography>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Download link</TableCell>
<TableCell>Help Text</TableCell>
</TableRow>
</TableHead>
<TableBody>
{profile.images?.map((i) => (
<TableRow>
<TableCell>
<Button endIcon={<CloudDownload />} variant="contained" color="primary">
{i.type}
</Button>
</TableCell>
<TableCell>
<Box p={1}>
<Typography>{t(`tr-${getHelpKey(i.type)}`)}</Typography>
<Typography variant="caption">sha256sum: {i.sha256}</Typography>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
export default ProfileDetails;

View file

@ -0,0 +1,97 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { CircularProgress, TextField } from '@material-ui/core';
import { Autocomplete, AutocompleteRenderInputParams, FilterOptionsState } from '@material-ui/lab';
import Axios from 'axios';
import { isEqual, throttle } from 'lodash';
import { matchSorter } from 'match-sorter';
import { useTranslation } from 'react-i18next';
import { Overview, ProfilesEntity } from '../../../types/overview';
type Props = {
selectedVersion: string;
onProfileChange: (profile: ProfilesEntity) => void;
};
type SearchData = { value: ProfilesEntity; search: string; title: string };
const overviewData: { [key: string]: Overview } = {};
const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChange }) => {
const [searchData, setSearchData] = useState<SearchData[]>([]);
const [working, toggleWorking] = useState<boolean>(true);
const { t } = useTranslation();
const getSearchData = useCallback(async () => {
let overview = overviewData[selectedVersion];
const searchDataArray: SearchData[] = [];
toggleWorking(true);
if (!overview) {
const response = await Axios.get<Overview>(
`${process.env.PUBLIC_URL}/data/${selectedVersion}/overview.json`
);
overview = response.data;
overviewData[selectedVersion] = overview;
}
toggleWorking(false);
overview.profiles?.forEach((profile) => {
profile.titles?.forEach((title) => {
searchDataArray.push({
value: profile,
search: profile.id + title.title,
title: title.title || `${title.vendor} ${title.model}`,
});
});
});
return searchDataArray;
}, [selectedVersion]);
useEffect(() => {
getSearchData().then((_searchData) => {
if (!isEqual(_searchData, searchData)) setSearchData(_searchData);
});
}, [getSearchData, searchData, selectedVersion]);
const handleProfileSelect = (_: unknown, searchDataRow: SearchData | null) => {
if (!searchDataRow) return;
onProfileChange(searchDataRow.value);
};
const getOptionLabel = (option: SearchData) => option.title;
const renderInput = (params: AutocompleteRenderInputParams) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<TextField {...params} fullWidth variant="outlined" label={t('tr-model')} />
);
const filterOptions: (
options: SearchData[],
state: FilterOptionsState<SearchData>
) => SearchData[] = (options, { inputValue }) =>
throttle(
() =>
matchSorter(options, inputValue.replaceAll(' ', ''), {
keys: ['search'],
}).slice(0, 10),
1000
)() || [];
if (working) return <CircularProgress />;
return (
<Autocomplete
options={searchData}
getOptionLabel={getOptionLabel}
renderInput={renderInput}
filterOptions={filterOptions}
onChange={handleProfileSelect}
/>
);
};
export default SearchField;

View file

@ -0,0 +1,40 @@
import React, { FunctionComponent } from 'react';
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import config from '../../../config';
type Props = {
selectedVersion: string;
onVersionChange: (version: string) => void;
};
const VersionSelector: FunctionComponent<Props> = ({ selectedVersion, onVersionChange }) => {
const { versions } = config;
const { t } = useTranslation();
const handleVersionChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const version: string = event.target.value as string;
onVersionChange(version);
};
return (
<FormControl fullWidth variant="outlined">
<InputLabel id="version-select-label">{t('tr-version')}</InputLabel>
<Select
labelWidth={60}
labelId="version-select-label"
value={selectedVersion}
onChange={handleVersionChange}
>
{Object.keys(versions).map((version) => (
<MenuItem value={version} key={version}>
{version}
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default VersionSelector;

View file

@ -1,895 +0,0 @@
import React from 'react';
import {
AppBar,
Button,
Chip,
CircularProgress,
ClickAwayListener,
Container,
FormControl,
Grid,
Input,
InputLabel,
List,
ListItem,
ListItemText,
MenuItem,
OutlinedInput,
Paper,
Select,
Tab,
Tabs,
TextField,
Tooltip,
Typography,
ExpansionPanel,
ExpansionPanelSummary,
ExpansionPanelDetails,
} from '@material-ui/core';
import CloudDownloadIcon from '@material-ui/icons/CloudDownload';
import WarningIcon from '@material-ui/icons/Warning';
import BuildIcon from '@material-ui/icons/Build';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import './home.scss';
import { withTranslation } from 'react-i18next';
import FuzzySet from 'fuzzyset.js';
import config from '../../config';
import DataService from '../../services/data';
import AlertDialog from '../../components/alert-dialog';
import ErrorSnackBar from '../../components/error-snackbar';
import SearchTextField from '../../components/device-search';
import PropTypes from 'prop-types';
const buildStatusCheckInterval = 5000;
const confirmationPopupOnBuildResquest = false;
function TabContainer({ children, dir }) {
return (
<Typography component="div" dir={dir} style={{ padding: '20px 0 0' }}>
{children}
</Typography>
);
}
TabContainer.propTypes = {
children: PropTypes.any,
dir: PropTypes.any,
};
const sleep = m => new Promise(r => setTimeout(r, m));
const { CORSbyPass, asu, asu_vanilla } = config;
class Home extends React.Component {
state = {
selection: {
version: null,
device: null,
},
showDeviceData: false,
deviceLoaded: false,
data: [],
devicesLoaded: false,
searchResults: [],
showSearch: false,
selectedSearchIndex: 0,
query: '',
downloading: false,
packages: this.packages,
configChanged: true,
packageName: '',
builtDeviceManifest: [],
builtImages: [],
isBuilding: false,
queuePosition: -1,
showUnexpectedErrorBar: false,
errorMessage: '',
fuzzySet: null,
showAdvanced: true,
basicInterface: 0,
errorDialogMessage: <></>,
openErrorDialog: false,
uciDefaults: '',
};
confirmingBuild = false;
dataService = new DataService();
async componentDidMount() {
try {
const versionsResponse = await this.dataService.getVersions(
CORSbyPass + asu_vanilla + 'versions.json'
);
let data = versionsResponse.data.versions;
for (var i = 0; i < data.length; i++) {
const overviewResponse = await this.dataService.getOverview(
CORSbyPass + asu_vanilla + data[i].path + '/overview.json'
);
data[i].devices = overviewResponse.data.devices;
}
this.generateFuzzySet(data[0].devices);
this.setState({
data,
selection: {
version: 0,
},
devicesLoaded: true,
});
} catch (err) {
this.setState({
showUnexpectedErrorBar: true,
});
console.log(err);
}
}
closeUnexpectedErrorBar = () => {
this.setState({
showUnexpectedErrorBar: false,
});
};
generateFuzzySet = data => {
let deviceNames = [];
Object.keys(data).forEach(deviceName => {
deviceNames.push(deviceName);
});
this.setState({
fuzzySet: FuzzySet(deviceNames),
});
};
setRelease = event => {
this.generateFuzzySet(this.state.data[event.target.value].devices);
this.setState({
selection: {
version: event.target.value,
},
deviceLoaded: false,
showDeviceData: false,
query: '',
});
};
selectDevice = async device_name => {
const version = this.state.data[this.state.selection.version];
let selection;
try {
let deviceSubPath = version.devices[device_name];
if (deviceSubPath.indexOf('//') > 0) {
deviceSubPath = deviceSubPath.replace('//', '/generic/');
}
const devicePath = version.path + '/targets/' + deviceSubPath;
this.setState({
showDeviceData: true,
showSearch: false,
query: device_name,
basicInterface: 0,
deviceLoaded: false,
showAdvanced: false,
configChanged: true,
});
const deviceResponse = await this.dataService.getDeviceData(
CORSbyPass + asu_vanilla + devicePath
);
selection = this.state.selection;
selection.device = deviceResponse.data;
if (selection.device.target[selection.device.target.length - 1] === '/') {
selection.device.target += 'generic';
}
selection.device.deviceManifest = await this.dataService.getDeviceManifest(
CORSbyPass +
asu_vanilla +
version.path +
'/targets/' +
selection.device.target +
'/openwrt-' +
selection.device.target.split('/')[0] +
'-' +
selection.device.target.split('/')[1] +
'-default.manifest'
);
} catch (err) {
this.setState({
showUnexpectedErrorBar: true,
});
console.log(err);
return;
}
const noPackageFoundError = { error: 'no-packages-found' };
try {
let devicePackagesResponse = await this.dataService.getDevicePackages(
version.name,
selection.device.target,
selection.device.id
);
if (devicePackagesResponse.data.length === 0) {
throw noPackageFoundError;
}
var packages = devicePackagesResponse.data;
packages.sort();
this.setState({
packages,
showAdvanced: true,
});
} catch (err) {
console.log(err);
this.setState({
showAdvanced: false,
});
}
this.setState({
selection,
deviceLoaded: true,
});
};
search = event => {
const query = event.target.value;
this.setState({
query,
searchResults: [],
showSearch: false,
});
const deviceNames = this.state.fuzzySet.get(query, undefined, 0);
let searchResults = [];
if (deviceNames != null) {
for (let i = 0; i < deviceNames.length && i < 6; i++) {
searchResults.push(deviceNames[i][1]);
}
}
this.setState({
searchResults,
showSearch: query.length > 0,
});
};
hideSearchResults = () => {
this.setState({
showSearch: false,
});
};
changeInterface = (e, val) => {
this.setState({
basicInterface: val,
});
};
downloadingImageIndicatorShow = () => {
this.setState({
downloading: true,
});
setTimeout(() => {
this.setState({
downloading: false,
});
}, 2000);
};
changeAddPackageInput = event => {
this.setState({
packageName: event.target.value,
});
};
deletePackage = i => {
let packages = this.state.packages;
packages.splice(i, 1);
this.setState({
packages,
configChanged: true,
});
};
addPackage = event => {
if ((event.which || event.keyCode) === 13 && !event.shiftKey) {
let packages = this.state.packages;
const packageArray = this.state.packageName.split(/[,\n]+/);
packageArray.forEach(package_name => {
package_name = package_name.replace(' ', '');
if (package_name !== '' && packages.indexOf(package_name) === -1) {
packages.push(package_name);
}
});
this.setState({
packages,
packageName: '',
configChanged: true,
});
}
};
uciDefaultsEdit = event => {
this.setState({
uciDefaults: event.target.value,
configChanged: true,
});
};
closeConfirmBuildDialog = () => {
this.confirmingBuild = false;
};
openConfirmBuildDialog = () => {
this.confirmingBuild = true;
};
displayBuiltImageData = async buildStatusResponse => {
const builtDeviceManifest = await this.dataService.getDeviceManifest(
CORSbyPass +
asu +
buildStatusResponse.data.image_folder +
'/' +
buildStatusResponse.data.image_prefix +
'.manifest'
);
let builtImages = [];
buildStatusResponse.data.images.forEach(image => {
builtImages.push({
url: asu + buildStatusResponse.data.image_folder + '/' + image.name,
type: image.type,
});
});
if (this.state.isBuilding) {
this.setState({
builtDeviceManifest,
builtImages,
configChanged: false,
isBuilding: false,
});
}
};
buildImageCheck = async request_hash => {
try {
if (!this.state.isBuilding) {
return;
}
const buildStatusResponse = await this.dataService.buildStatusCheck(
request_hash
);
if (buildStatusResponse.status === 202) {
if (
buildStatusResponse.headers['X-Build-Queue-Position'] !== undefined
) {
this.setState({
queuePosition:
buildStatusResponse.headers['X-Build-Queue-Position'],
});
}
await sleep(buildStatusCheckInterval);
await this.buildImageCheck(request_hash);
} else if (buildStatusResponse.status === 200) {
await this.displayBuiltImageData(buildStatusResponse);
} else if (buildStatusResponse.status === 409) {
this.setState({
openErrorDialog: true,
errorDialogMessage: (
<>
{buildStatusResponse.data.error} <br />
<a href={buildStatusResponse.data.error}>Build logs</a>
</>
),
});
} else {
throw buildStatusResponse.data;
}
} catch (e) {
if (e.response.status === 409) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: (
<>
{e.response.data.error} <br />
<a
href={asu + e.response.data.log}
target="_blank"
rel="noopener noreferrer"
>
Build logs
</a>
</>
),
});
} else if (e.response.status === 422) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: <>{e.response.data.error}</>,
});
} else {
this.setState({
isBuilding: false,
showUnexpectedErrorBar: true,
});
}
}
};
buildImage = async () => {
try {
this.closeConfirmBuildDialog();
const { packages, selection, data, uciDefaults } = this.state;
const {
device: { id: board, target },
version: versionId,
} = selection;
let {
[versionId]: { name: version },
} = data;
version = version.toLowerCase();
this.setState({
isBuilding: true,
builtImages: [],
});
let buildResponse = await this.dataService.buildImage(
board,
packages,
target,
version,
uciDefaults
);
if (
buildResponse.status === 202 &&
buildResponse.data['request_hash'] !== undefined
) {
const request_hash = buildResponse.data['request_hash'];
await sleep(buildStatusCheckInterval);
await this.buildImageCheck(request_hash);
} else if (buildResponse.status === 200) {
await this.displayBuiltImageData(buildResponse);
} else {
throw buildResponse.data;
}
} catch (e) {
console.log(e);
if (e.response.status === 409) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: (
<>
{e.response.data.error} <br />
<a
href={asu + e.response.data.log}
target="_blank"
rel="noopener noreferrer"
>
Build logs
</a>
</>
),
});
} else if (e.response.status === 422) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: <>{e.response.data.error}</>,
});
} else {
this.setState({
isBuilding: false,
showUnexpectedErrorBar: true,
});
}
}
};
cancelBuild = () => {
this.setState({
isBuilding: false,
configChanged: true,
});
};
closeErrorDialog = () => {
this.setState({
openErrorDialog: false,
});
};
render() {
let warning432 = <> </>;
if (
this.state.showDeviceData &&
this.state.deviceLoaded &&
parseInt(
(this.state.selection.device['image_size'] || '').slice(0, -1)
) <= 4000
) {
warning432 = (
<Paper className="warning-432" elevation={0}>
<Grid container direction="row" justify="center" alignItems="center">
<Grid item>
<WarningIcon className="icon" />
</Grid>
<Grid item xs>
{this.props.t('warning432')}
</Grid>
</Grid>
</Paper>
);
}
let deviceInfoGrid = <></>;
if (this.state.builtImages.length > 0 && !this.state.configChanged) {
deviceInfoGrid = (
<Grid container className="device-info">
<Grid item xs>
{this.props.t('Model')}:{' '}
<b> {this.state.selection.device['title']} </b> <br />
{this.props.t('Target')}: {this.state.selection.device['target']}{' '}
<br />
{this.props.t('Version')}: {'('}
{this.state.data[this.state.selection.version].name}
{this.state.data[this.state.selection.version].revision}
{')'}
<ExpansionPanel className="installed-packages" elevation={0}>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
id="packages-manifest"
>
<Typography className="installed-packages-title">
Installed Packages ({this.state.builtDeviceManifest.length})
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<div>
{this.state.builtDeviceManifest.map(package_name => (
<div key={package_name}>{package_name}</div>
))}
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</Grid>
<Grid item xs className="downloads">
<b>{this.props.t('Downloads')}: </b>
{this.state.builtImages.map(image => (
<div key={image.url}>
<Button
className="download-button"
href={image.url}
color="primary"
variant="contained"
onClick={() => this.downloadingImageIndicatorShow()}
>
<CloudDownloadIcon className="download-icon" />
{image.type}
</Button>
</div>
))}
&nbsp;
{this.state.downloading && <CircularProgress size={20} />}
</Grid>
</Grid>
);
}
const notLoaded = <CircularProgress />;
const onLoad = (
<>
<Typography variant="h5">
{this.props.t('Download OpenWrt firmware for your device!')}
</Typography>
<Typography>
{this.props.t(
'Please use the input below to download firmware for your device!'
)}
</Typography>
<br />
<ClickAwayListener onClickAway={this.hideSearchResults}>
<div className="search-container">
<FormControl className="version-select">
<InputLabel htmlFor="version-select" className="version-label">
{this.props.t('Version')}
</InputLabel>
<Select
value={this.state.selection.version}
onChange={this.setRelease}
disabled={this.state.isBuilding}
input={
<OutlinedInput
name="version"
id="version-select"
labelWidth={60}
/>
}
>
{this.state.data.map((version, i) => (
<MenuItem value={i} key={version.revision}>
<em>{version.name}</em>
</MenuItem>
))}
</Select>
</FormControl>
<FormControl className="search-field">
<SearchTextField
id="outlined-adornment-search-devices"
labeltext={this.props.t('Search your device')}
value={this.state.query}
onChange={this.search}
onClick={this.search}
disabled={this.state.isBuilding}
/>
{this.state.showSearch && this.state.searchResults.length !== 0 && (
<Paper elevation={4} className="search-results">
<List>
{this.state.searchResults.map(res => {
return (
<ListItem
key={res}
button
onClick={() => this.selectDevice(res)}
>
<ListItemText primary={<div>{res}</div>} />
</ListItem>
);
})}
</List>
</Paper>
)}
{this.state.searchResults.length === 0 && this.state.showSearch && (
<Paper elevation={4} className="search-results">
<ListItem>
<ListItemText primary={this.props.t('No results')} />
</ListItem>
</Paper>
)}
</FormControl>
</div>
</ClickAwayListener>
{this.state.showDeviceData && !this.state.deviceLoaded && (
<>
<br />
{notLoaded}
</>
)}
{this.state.showDeviceData && this.state.deviceLoaded && (
<>
{warning432}
<br />
{this.state.showAdvanced && (
<AppBar
className="interface-switch-bar"
position="relative"
elevation={0}
>
<Tabs
value={this.state.basicInterface}
onChange={this.changeInterface}
>
<Tab
className="interface-switch"
label={this.props.t('Basic')}
disabled={this.state.isBuilding}
/>
<Tab
className="interface-switch"
label={this.props.t('Advanced')}
disabled={this.state.isBuilding}
/>
</Tabs>
</AppBar>
)}
{this.state.basicInterface === 0 ? (
<TabContainer>
<Grid container className="device-info">
<Grid item xs>
{this.props.t('Model')}:{' '}
<b> {this.state.selection.device['title']} </b> <br />
{this.props.t('Target')}:{' '}
{this.state.selection.device['target']} <br />
{this.props.t('Version')}:{' '}
{this.state.data[this.state.selection.version].name} (
{this.state.data[this.state.selection.version].revision})
<ExpansionPanel
className="installed-packages"
elevation={0}
>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
id="packages-manifest"
>
<Typography className="installed-packages-title">
Installed Packages (
{this.state.selection.device.deviceManifest.length})
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className="packages">
<div>
{this.state.selection.device.deviceManifest.map(
package_name => (
<div key={package_name}>{package_name}</div>
)
)}
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</Grid>
<Grid item xs className="downloads">
<b>{this.props.t('Downloads')}: </b>
{this.state.selection.device.images.map(image => (
<div key={image.name}>
<Button
className="download-button"
href={
asu_vanilla +
this.state.data[this.state.selection.version].path +
'/targets/' +
this.state.selection.device.target +
'/' +
image.name
}
color="primary"
variant="contained"
onClick={() => this.downloadingImageIndicatorShow()}
>
<CloudDownloadIcon className="download-icon" />
{
image.name
.split('-')
.reverse()[0]
.split('.')[0]
}
</Button>
</div>
))}
&nbsp;
{this.state.downloading && <CircularProgress size={20} />}
</Grid>
</Grid>
</TabContainer>
) : (
<TabContainer>
<Paper elevation={0} className="package-list-input">
<Grid container>
<Grid item xs={12} md={8}>
<h3>Packages</h3>
{this.state.packages.map((package_name, i) => (
<Chip
className="package"
key={package_name + i}
size="small"
onDelete={() => this.deletePackage(i)}
label={package_name}
/>
))}
<Tooltip
title={
<span>
Use comma or new line separated array. <br />
Press enter to apply.
</span>
}
>
<Input
multiline
value={this.state.packageName}
onKeyUp={this.addPackage}
onChange={this.changeAddPackageInput}
placeholder={this.props.t('Add package(s)')}
/>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<h3>UCI defaults (alpha)</h3>
<TextField
style={{ width: '100%' }}
multiline
variant="outlined"
rows={10}
value={this.state.uciDefaults}
onChange={this.uciDefaultsEdit}
placeholder={this.props.t('Edit UCI defaults')}
/>
</Grid>
</Grid>
<br />
{this.state.configChanged && !this.state.isBuilding && (
<Button
variant="outlined"
color="primary"
onClick={
confirmationPopupOnBuildResquest
? this.openConfirmBuildDialog
: this.buildImage
}
>
<BuildIcon />
&nbsp;
{this.props.t('Build')}
</Button>
)}
{this.state.isBuilding && (
<>
<Button
variant="outlined"
size="small"
onClick={this.cancelBuild}
>
&nbsp;
{this.props.t('Cancel')}
</Button>
&nbsp; &nbsp;
<CircularProgress
size={20}
style={{ verticalAlign: 'middle' }}
/>
&nbsp; Building image &nbsp;
{this.state.queuePosition !== -1 && (
<span>
{' '}
(Position in queue: {this.state.queuePosition}){' '}
</span>
)}
...
</>
)}
{deviceInfoGrid}
</Paper>
</TabContainer>
)}
</>
)}
</>
);
return (
<>
<ErrorSnackBar
open={this.state.showUnexpectedErrorBar}
closeHandle={this.closeUnexpectedErrorBar}
/>
<AlertDialog
cancelHandler={this.closeConfirmBuildDialog}
acceptHandler={this.buildImage}
open={this.confirmingBuild}
body={
<>
{this.props.t(
'Building image requires computation resources, so we would request you to check if this selection is what you want'
)}
</>
}
title={this.props.t(
'Please confirm that you want to perform this action'
)}
cancelComponent={this.props.t('Cancel')}
acceptComponent={
<>
{this.props.t('Build')} &nbsp; <BuildIcon />
</>
}
/>
<AlertDialog
cancelHandler={this.closeErrorDialog}
open={this.state.openErrorDialog}
body={this.state.errorDialogMessage}
title={this.props.t(
'There is an error with the packages you selected'
)}
cancelComponent={this.props.t('Dismiss')}
/>
<Container className="home-container">
<Paper className="home-container-paper">
{this.state.devicesLoaded ? onLoad : notLoaded}
</Paper>
</Container>
</>
);
}
}
Home.propTypes = {
t: PropTypes.func,
};
export default withTranslation()(Home);

View file

@ -1,188 +0,0 @@
$bg-color: #f0f0f0;
.home-container {
margin-top: 30px;
margin-bottom: 100px;
@media all and (max-width: 820px) {
margin-top: 15px;
margin-bottom: 100px;
}
.home-container-paper {
padding: 30px;
text-align: left;
@media all and (max-width: 820px) {
padding: 15px;
}
.warning-432 {
background-color: $bg-color;
padding: 10px;
margin-top: 20px;
border: 1px solid #e3e3e3;
color: #666;
.icon {
margin: 0 20px 0 10px;
color: #f9a825;
}
}
.device-info {
margin-bottom: 15px;
.installed-packages {
width: 80%;
border: 1px solid #e3e3e3;
border-radius: 7px;
margin-top: 16px !important;
@media all and (max-width: 820px) {
width: 100%;
}
}
.installed-packages .packages {
overflow: auto;
word-wrap: none;
white-space: nowrap;
}
.installed-packages::before {
display: none;
}
.installed-packages-title {
font-weight: bold;
}
}
.version-select {
width: 200px;
.version-label {
background-color: inherit;
padding: 5px;
margin-top: -10px;
margin-left: 10px;
display: block;
z-index: 10;
}
@media all and (max-width: 820px) {
width: 100%;
margin: 0;
padding: 0;
}
}
.search-field {
width: calc(100% - 220px);
margin-left: 20px;
position: relative;
.search-label {
background-color: #fff;
padding: 0 10px;
position: absolute;
white-space: nowrap;
}
.search-results {
position: absolute;
left: 0;
width: 100%;
top: 100%;
z-index: 10;
}
@media all and (max-width: 820px) {
width: 100%;
margin: 20px 0 0;
padding: 0;
}
}
.MuiTypography-h4 {
font-weight: bold;
margin-bottom: 15px;
}
.downloads {
@media all and (max-width: 820px) {
margin-top: 20px;
}
.download-button {
margin-top: 17px;
.download-icon {
margin-right: 10px;
}
}
}
.interface-switch-bar {
border: 1px solid #e3e3e3;
border-radius: 4px;
background-color: #fff;
z-index: 9;
overflow: hidden;
.interface-switch {
color: #000;
}
.advanced-settings {
border: 1px solid #999;
background-color: $bg-color;
border-radius: 6px;
margin-top: 20px;
overflow: hidden;
margin-bottom: 20px;
.icon {
font-size: 1em;
vertical-align: text-top;
}
.options {
padding-top: 30px;
background-color: #fff;
}
}
}
.package-list-input {
.package {
margin: 5px 10px 5px 0px;
border-radius: 30px;
vertical-align: middle;
user-select: none;
}
.package:hover {
background-color: $bg-color;
}
.package:focus,
.package:active {
transition: 0.2s;
background-color: darken($bg-color, 15%);
}
}
}
}
.device-table {
margin-top: 20px;
width: 100%;
td {
padding: 20px 30px;
}
tr:nth-child(odd) {
background-color: rgba(104, 74, 238, 0.07);
}
}

View file

@ -0,0 +1,64 @@
import React, { FunctionComponent, useState } from 'react';
import { Container, Paper, Box, Typography, Grid } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import SearchField from './components/SearchField';
import VersionSelector from './components/VersionSelector';
import ProfileDetails from './components/ProfileDetails';
import config from '../../config';
import { ProfilesEntity } from '../../types/overview';
const Home: FunctionComponent = () => {
const [selectedVersion, setSelectedVersion] = useState(Object.keys(config.versions)[0]);
const [selectedProfile, setSelectedProfile] = useState<ProfilesEntity | null>();
const { t } = useTranslation();
const onVersionChange = (version: string) => {
setSelectedVersion(version);
};
const onProfileChange = (profile: ProfilesEntity) => {
setSelectedProfile(profile);
};
return (
<Container>
<Box paddingY={4}>
<Paper>
<Box padding={3}>
<Box paddingBottom={2}>
<Typography variant="h4" component="h1" align="left">
{t('tr-load')}
</Typography>
</Box>
<Box paddingBottom={2}>
<Typography variant="h6" component="h2" align="left">
{t('tr-message')}
</Typography>
</Box>
<Grid container spacing={2}>
<Grid item xs>
<SearchField selectedVersion={selectedVersion} onProfileChange={onProfileChange} />
</Grid>
<Grid item xs={3}>
<VersionSelector
selectedVersion={selectedVersion}
onVersionChange={onVersionChange}
/>
</Grid>
</Grid>
{selectedProfile && (
<Box>
<ProfileDetails
selectedProfile={selectedProfile}
selectedVersion={selectedVersion}
/>
</Box>
)}
</Box>
</Paper>
</Box>
</Container>
);
};
export default Home;

View file

@ -1,15 +1,15 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import { Container, Paper, Typography } from '@material-ui/core'; import { Container, Paper, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
const page404Styles = makeStyles(theme => ({ const page404Styles = makeStyles((theme) => ({
root: { root: {
padding: theme.spacing(3, 2), padding: theme.spacing(3, 2),
}, },
})); }));
export default function NotFound() { const NotFound: FunctionComponent = () => {
var classes = page404Styles(); const classes = page404Styles();
return ( return (
<Container style={{ marginTop: '50px' }}> <Container style={{ marginTop: '50px' }}>
<Paper className={classes.root} elevation={3}> <Paper className={classes.root} elevation={3}>
@ -20,4 +20,6 @@ export default function NotFound() {
</Paper> </Paper>
</Container> </Container>
); );
} };
export default NotFound;

View file

@ -1,258 +0,0 @@
{
"release_version": "SNAPSHOT",
"release_commit": "r10241+1-865e25e049",
"devices": {
"tplink_archer-c5-v1": {
"device_id": "tplink_archer-c5-v1",
"supported_devices": ["tplink,archer-c5-v1", "archer-c5"],
"vendor": "TP-Link",
"model": "Archer C5",
"variant": "v1",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c5-v1-squashfs-sysupgrade.bin",
"hash": "92e699c555d8b17423105f5d5fcf3faa6b9edd66f90480372aa5da69b0be5576"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c5-v1-squashfs-factory.bin",
"hash": "e42a929f742e6ac141e277f676b86608ed8468afa3479f0f03a94138ff96ad42"
}
]
},
"tplink_archer-c7-v2": {
"device_id": "tplink_archer-c7-v2",
"supported_devices": ["tplink,archer-c7-v2", "archer-c7"],
"vendor": "TP-Link",
"model": "Archer C7",
"variant": "v2",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c7-v2-squashfs-sysupgrade.bin",
"hash": "a9b49a62bbc5dbef0df90cb365d552a366760b8468c5feaed60d3c1845ab8d29"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c7-v2-squashfs-factory.bin",
"hash": "6bea6065b621d5b34736c1394b08721bbe4efa932068bd4b724e9905d6dcb7d9"
},
{
"type": "factory-us",
"name": "openwrt-ath79-generic-tplink_archer-c7-v2-squashfs-factory-us.bin",
"hash": "20e8fc84bb315508874e3feb1734e315c003eed2a1294dcc6ab63050692126c5"
},
{
"type": "factory-eu",
"name": "openwrt-ath79-generic-tplink_archer-c7-v2-squashfs-factory-eu.bin",
"hash": "e7ec8fd5a563f0832531063de52e0c207fbedc305d452e6914328d36a9a364e6"
}
]
},
"tplink_tl-wdr4300": {
"device_id": "tplink_tl-wdr4300",
"supported_devices": ["tplink,tl-wdr4300", "tl-wdr4300"],
"vendor": "TP-Link",
"model": "TL-WDR4300",
"variant": "",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_tl-wdr4300-squashfs-sysupgrade.bin",
"hash": "45b4f33e7f0c357dc61166032bf40891458ee9941e443daf896ae9b825639770"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_tl-wdr4300-squashfs-factory.bin",
"hash": "21c0d11b7343f233f0e35dbf306c1269a0fcd602b0d26dbb5938c80e71e86a27"
}
]
},
"tplink_archer-c7-v4": {
"device_id": "tplink_archer-c7-v4",
"supported_devices": ["tplink,archer-c7-v4", "archer-c7-v4"],
"vendor": "TP-Link",
"model": "Archer C7",
"variant": "v4",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c7-v4-squashfs-sysupgrade.bin",
"hash": "a0c9ea31608f35bcf88bd5090b721263d6f65396ecc23edaecbfb5ffae007a33"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c7-v4-squashfs-factory.bin",
"hash": "7b44e8942a5ce5ceee04ac02e5cd7582c6e13653c8ce83adc5a60efca93bf25e"
}
]
},
"tplink_archer-c59-v1": {
"device_id": "tplink_archer-c59-v1",
"supported_devices": ["tplink,archer-c59-v1", "archer-c59-v1"],
"vendor": "TP-Link",
"model": "Archer C59",
"variant": "v1",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c59-v1-squashfs-sysupgrade.bin",
"hash": "e0e6612dfbb961c62877cc347476d699e7f4b5aeca0355c468ac5716f3696b94"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c59-v1-squashfs-factory.bin",
"hash": "99134ef23c69d055f45e20cfa638a287fc5182bec0825128d9cb0458471727d7"
}
]
},
"tplink_archer-c2-v3": {
"device_id": "tplink_archer-c2-v3",
"supported_devices": ["tplink,archer-c2-v3"],
"vendor": "TP-Link",
"model": "Archer C2",
"variant": "v3",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c2-v3-squashfs-sysupgrade.bin",
"hash": "fd213ce0adaf8ba6434862a208a520887f8d26c6d2a8145acce15d0e243b4578"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c2-v3-squashfs-factory.bin",
"hash": "034a015d3fa00be116055ee11b2a2e7bbf8ac8bf4f26273dda49f2dcb959e3ff"
}
]
},
"tplink_archer-c6-v2": {
"device_id": "tplink_archer-c6-v2",
"supported_devices": ["tplink,archer-c6-v2"],
"vendor": "TP-Link",
"model": "Archer C6",
"variant": "v2",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c6-v2-squashfs-factory.bin",
"hash": "793473938124b55c9808e3d755fadf331dbc555e3e8d48506755b28a49260d2e"
},
{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c6-v2-squashfs-sysupgrade.bin",
"hash": "ec130a2f5d3d3db5f883274584823e6018bb1d8f8f0c7177a0853cd70bc75186"
}
]
},
"tplink_archer-c7-v5": {
"device_id": "tplink_archer-c7-v5",
"supported_devices": ["tplink,archer-c7-v5", "archer-c7-v5"],
"vendor": "TP-Link",
"model": "Archer C7",
"variant": "v5",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin",
"hash": "cf673e3af5f9e985f30a3adf2c7f96e5210b036a4cc43a080c9f5c023e1bdd84"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c7-v5-squashfs-factory.bin",
"hash": "523a5fe3a8616ecd52d5a5117b49f9f114c496f720fc7b8fc97c4f82bf077822"
}
]
},
"tplink_archer-c7-v1": {
"device_id": "tplink_archer-c7-v1",
"supported_devices": ["tplink,archer-c7-v1", "archer-c7"],
"vendor": "TP-Link",
"model": "Archer C7",
"variant": "v1",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c7-v1-squashfs-sysupgrade.bin",
"hash": "13728d071e4c357ca3cbcad6064b26e0caec422191ddbc0cf109c5021bc0ed47"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c7-v1-squashfs-factory.bin",
"hash": "8096c5a5decfa8af6e7195a49d71fbb3da02d1a014a553974bcd6dc59b64778c"
}
]
},
"tplink_archer-c58-v1": {
"device_id": "tplink_archer-c58-v1",
"supported_devices": ["tplink,archer-c58-v1", "archer-c58-v1"],
"vendor": "TP-Link",
"model": "Archer C58",
"variant": "v1",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_archer-c58-v1-squashfs-factory.bin",
"hash": "a740142322a9c6e814b7c42398d47004c15db9f2aec9b913e8d9ad34e18a80ae"
},
{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_archer-c58-v1-squashfs-sysupgrade.bin",
"hash": "cb36b50b646137ff05ed23ed7fbaabffeb032d0b5da214a3586d7b086088c17a"
}
]
},
"tplink_tl-wdr3600": {
"device_id": "tplink_tl-wdr3600",
"supported_devices": ["tplink,tl-wdr3600", "tl-wdr4300"],
"vendor": "TP-Link",
"model": "TL-WDR3600",
"variant": "",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_tl-wdr3600-squashfs-sysupgrade.bin",
"hash": "fbbcbd9e1174026c001d002149a641db8ebeb459892eff442c975d9043bf2b64"
},
{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_tl-wdr3600-squashfs-factory.bin",
"hash": "2d7120d2f4593d6e0a8d5d8164666aaa54c51bcbeef6f9c8c2ab1f2a75e7b728"
}
]
},
"tplink_tl-wdr4900-v2": {
"device_id": "tplink_tl-wdr4900-v2",
"supported_devices": ["tplink,tl-wdr4900-v2"],
"vendor": "TP-Link",
"model": "TL-WDR4900",
"variant": "v2",
"target": "ath79",
"subtarget": "generic",
"images": [{
"type": "factory",
"name": "openwrt-ath79-generic-tplink_tl-wdr4900-v2-squashfs-factory.bin",
"hash": "f4b096db93eb4cc504d2b98972c8a638b14a811eb0643c1fdac40d1ca0e7ae33"
},
{
"type": "sysupgrade",
"name": "openwrt-ath79-generic-tplink_tl-wdr4900-v2-squashfs-sysupgrade.bin",
"hash": "d5286e54feca1a9ce1c2d4b5d6b05b027796b755a00550629741cb9318106d75"
}
]
}
}
}

View file

@ -1,45 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import translationEN from './locales/en.json';
import translationDE from './locales/de.json';
import translationES from './locales/es.json';
import translationRU from './locales/ru.json';
import translationPTBR from './locales/pt_br.json';
import translationTR from './locales/tr.json';
import Config from './config';
const resources = {
en: {
translation: translationEN,
},
de: {
translation: translationDE,
},
es: {
translation: translationES,
},
ru: {
translation: translationRU,
},
pt_br: {
translation: translationPTBR,
},
tr: {
translation: translationTR,
},
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
debug: Config.settings.i18nDebug,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export default i18n;

48
src/i18n.ts Normal file
View file

@ -0,0 +1,48 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import translations from './locales/translations';
const resources = {
ca: {
translation: translations.ca,
},
en: {
translation: translations.en,
},
es: {
translation: translations.es,
},
de: {
translation: translations.de,
},
fr: {
translation: translations.fr,
},
it: {
translation: translations.it,
},
no: {
translation: translations.no,
},
pl: {
translation: translations.pl,
},
tr: {
translation: translations.tr,
},
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
debug: (process.env.REACT_APP_I18N_DEBUG || '0') === '1',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export default i18n;

View file

@ -1,14 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import './i18n';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View file

@ -4,6 +4,8 @@ import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import './i18n';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />

View file

@ -1,34 +0,0 @@
{
"English": "English",
"German": "Deutsch",
"Russian": "русский",
"Brazilian Portuguese": "português (Brasil)",
"Turkish": "Türkçe",
"Spanish": "español",
"Submit": "Einreichen",
"Change Language": "Sprache ändern",
"Search your device": "Gerät suchen",
"No results": "Keine Ergebnisse",
"Model": "Modell",
"Vendor": "Verkäufer",
"Variant": "Variante",
"Name": "Name",
"Release Version": "Release Version",
"Basic": "Anfänger",
"Advanced": "Fortgeschrittener",
"Build": "Erstellen",
"Cancel": "Abbrechen",
"Add package(s)": "Paket(e) hinzufügen",
"Edit UCI defaults": "UCI-Standardeinstellungen bearbeiten",
"Version": "Version",
"Please confirm that you want to perform this action": "Bitte bestätigen Sie, dass Sie diese Aktion ausführen möchten",
"Building image requires computation resources, so we would request you to check if this selection is what you want": "Das Erstellen eines Images erfordert Rechenressourcen. Wir bitten Sie daher zu prüfen, ob diese Auswahl Ihren Wünschen entspricht",
"warning432": "Geräte mit ≤4MB Flash und / oder ≤32MB RAM funktionieren, sind jedoch sehr eingeschränkt (Sie können meist keine zusätzlichen Pakete installieren oder ausführen), da sie über wenig RAM und Flash-Speicher verfügen. Berücksichtigen Sie dies, wenn Sie ein Gerät auswählen oder wenn Sie OpenWrt auf Ihrem Gerät flashen, da es als unterstützt aufgeführt wird. Zusätzliche Pakete können nicht installiert oder ausgeführt werden, da sie über wenig RAM und wenig Flash-Speicher verfügen.",
"OpenWrt Firmware Selector": "OpenWrt Firmware Auswahl",
"Download OpenWrt firmware for your device!": "Laden Sie die OpenWrt-Firmware für Ihr Gerät herunter!",
"Target": "Ziel",
"Dismiss": "Ablehnen",
"There is an error with the packages you selected": "Es liegt ein Fehler mit dem ausgewählten Paket vor.",
"Downloads": "Downloads",
"Please use the input below to download firmware for your device!": "Bitte nutzen Sie das untenstehende Feld, um die Firmware für Ihr Gerät herunterzuladen!"
}

View file

@ -1,34 +0,0 @@
{
"English": "English",
"German": "Deutsch",
"Russian": "русский",
"Brazilian Portuguese": "português (Brasil)",
"Turkish": "Türkçe",
"Spanish": "español",
"Submit": "Submit",
"Change Language": "Change Language",
"Search your device": "Search your device",
"No results": "No results",
"Model": "Model",
"Vendor": "Vendor",
"Variant": "Variant",
"Name": "Name",
"Release Version": "Release Version",
"Basic": "Basic",
"Advanced": "Advanced",
"Build": "Build",
"Cancel": "Cancel",
"Add package(s)": "Add package(s)",
"Edit UCI defaults": "Edit UCI defaults",
"Version": "Version",
"Please confirm that you want to perform this action": "Please confirm that you want to perform this action",
"Building image requires computation resources, so we would request you to check if this selection is what you want": "Building image requires computation resources, so we would request you to check if this selection is what you want",
"warning432": "Devices with ≤4MB flash and/or ≤32MB ram will work but they will be very limited (usually they can't install or run additional packages) because they have low RAM and flash space. Consider this when choosing a device to buy, or when deciding to flash OpenWrt on your device because it is listed as supported.",
"OpenWrt Firmware Selector": "OpenWrt Firmware Selector",
"Download OpenWrt firmware for your device!": "Download OpenWrt firmware for your device!",
"Target": "Target",
"Dismiss": "Dismiss",
"There is an error with the packages you selected": "There is an error with the packages you selected",
"Downloads": "Downloads",
"Please use the input below to download firmware for your device!": "Please use the input below to download firmware for your device!"
}

View file

@ -1,34 +0,0 @@
{
"English": "English",
"German": "Deutsch",
"Russian": "русский",
"Brazilian Portuguese": "português (Brasil)",
"Turkish": "Türkçe",
"Spanish": "español",
"Submit": "Enviar",
"Change Language": "Cambiar idioma",
"Search your device": "Busca tu dispositivo",
"No results": "No hay resultados",
"Model": "Modelo",
"Vendor": "Fabricante",
"Variant": "Variante",
"Name": "Nombre",
"Release Version": "Versión de lanzamiento",
"Basic": "Básico",
"Advanced": "Avanzado",
"Build": "Construir",
"Cancel": "Cancelar",
"Add package(s)": "Agregar paquete (s)",
"Edit UCI defaults": "Editar valores predeterminados de UCI",
"Version": "Versión",
"Please confirm that you want to perform this action": "Confirme que desea realizar esta acción.",
"Building image requires computation resources, so we would request you to check if this selection is what you want": "La creación de imágenes requiere recursos de cálculo, por lo que le solicitamos que verifique si esta selección es lo que desea",
"warning432": "Los dispositivos con ≤4MB Flash y / o ≤32MB RAM funcionan pero están limitados (generalmente no pueden instalar o ejecutar paquetes adicionales) porque tienen poca RAM y memoria flash. Tenga esto en cuenta al elegir qué dispositivo comprar o al instalar OpenWrt en su dispositivo porque está listado como compatible.",
"OpenWrt Firmware Selector": "Selector de firmware OpenWrt",
"Download OpenWrt firmware for your device!": "¡Descargue el firmware OpenWrt para su dispositivo!",
"Target": "Objetivo",
"Dismiss": "Descartar",
"There is an error with the packages you selected": "Hay un error con los paquetes seleccionados",
"Downloads": "Descargas",
"Please use the input below to download firmware for your device!": "¡Utilice el siguiente campo para descargar el firmware de su dispositivo!"
}

17
src/locales/index.ts Normal file
View file

@ -0,0 +1,17 @@
type Locales = {
[key: string]: string;
};
const locales: Locales = {
ca: 'Català',
en: 'English',
es: 'Español',
de: 'Deutsch',
fr: 'Français',
it: 'Italiano',
no: 'Norsk',
pl: 'Polski',
tr: 'Türkçe',
};
export default locales;

View file

@ -1,34 +0,0 @@
{
"English": "English",
"German": "Deutsch",
"Russian": "русский",
"Brazilian Portuguese": "português (Brasil)",
"Turkish": "Türkçe",
"Spanish": "español",
"Submit": "Enviar",
"Change Language": "Escolher Idioma",
"Search your device": "Procure seu dispositivo",
"No results": "Sem resultados",
"Model": "Modelo",
"Vendor": "Vendor",
"Variant": "Variant",
"Name": "Nome",
"Release Version": "Versão de Lançamento",
"Basic": "Básico",
"Advanced": "Avançado",
"Build": "Construir",
"Cancel": "Cancelar",
"Add package(s)": "Adicionar pacote(s)",
"Edit UCI defaults": "Editar padrões UCI",
"Version": "Versão",
"Please confirm that you want to perform this action": "Por favor, confirme se você realmente deseja fazer isso",
"Building image requires computation resources, so we would request you to check if this selection is what you want": "Construir imagens exige recursos computacionais, então confirme se esta seleção é realmente o que você quer",
"warning432": "Dispositivos com 4MB de memória flash ou menos e/ou 32MB de RAM ou menos vão suportar o programa, mas serão bem limitados (normalmente não será possível o uso/instalação de pacotes adicionais) por causa da baixa quantidade de RAM e armazenamento interno. Considere isso na compra do dispositivo, ou quando decidir instalar OpenWrt no seu dispositivo pois é listado como suportado.",
"OpenWrt Firmware Selector": "Seletor de Firmware do OpenWrt",
"Download OpenWrt firmware for your device!": "Baixe o firmware OpenWrt para seu dispositvo!",
"Target": "Alvo",
"Dismiss": "Dispensar",
"There is an error with the packages you selected": "Existe um erro com os pacotes que você selecionou",
"Downloads": "Downloads",
"Please use the input below to download firmware for your device!": "Por favor, utilize o campo abaixo para baixar o firmware para seu dispositivo!"
}

View file

@ -1,34 +0,0 @@
{
"English": "English",
"German": "Deutsch",
"Russian": "русский",
"Brazilian Portuguese": "português (Brasil)",
"Turkish": "Türkçe",
"Spanish": "español",
"Submit": "Подтвердить",
"Change Language": "Сменить язык",
"Search your device": "Найдите своё устройство",
"No results": "Нет результатов",
"Model": "Модель",
"Vendor": "Vendor",
"Variant": "Variant",
"Name": "Название",
"Release Version": "Актуальная версия",
"Basic": "Базовые",
"Advanced": "Расширенные",
"Build": "Создать",
"Cancel": "Отменить",
"Add package(s)": "Добавить пакет(ы)",
"Edit UCI defaults": "Изменить настройки UCI",
"Version": "Версия",
"Please confirm that you want to perform this action": "Пожалуйста, подтвердите действие",
"Building image requires computation resources, so we would request you to check if this selection is what you want": "Создание образа требует затраты ресурсов. Пожалуйста, убедитесь что вы согласны с этим. ",
"warning432": "Стабильная работа гарантируется при наличии <4 мегабайт флеш-памяти и/или <32 мегабайт оперативной памяти.",
"OpenWrt Firmware Selector": "Установщик прошивок OpenWrt",
"Download OpenWrt firmware for your device!": "Установите прошивку для своего OpenWrt маршрутизатора!",
"Target": "Цель",
"Dismiss": "Отклонено",
"There is an error with the packages you selected": "Что-то не так с выбранными пакетами.",
"Downloads": "Загрузки",
"Please use the input below to download firmware for your device!": "Пожалуйста, воспользуйтесь формой ниже чтобы установить прошивку!"
}

View file

@ -1,34 +0,0 @@
{
"English": "English",
"German": "Deutsch",
"Russian": "русский",
"Brazilian Portuguese": "português (Brasil)",
"Turkish": "Türkçe",
"Spanish": "español",
"Submit": "Gönder",
"Change Language": "Dil değiştir",
"Search your device": "Cihazınızı arayın",
"No results": "Sonuç bulunamadı",
"Model": "Model",
"Vendor": "Satıcı",
"Variant": "Varyant",
"Name": "İsim",
"Release Version": "Sürüm Versiyonu",
"Basic": "Basit",
"Advanced": "Gelişmiş",
"Build": "Yapım",
"Cancel": "İptal",
"Add package(s)": "Paket ekle",
"Edit UCI defaults": "UCI detaylarını düzenle",
"Version": "Versiyon",
"Please confirm that you want to perform this action": "Lütfen bu işlemi yapmak istediğinizi doğrulayın",
"Building image requires computation resources, so we would request you to check if this selection is what you want": "İmaj oluşturmak işlemci kaynaklarına ihtiyaç duyacağından, gerçekten bunu istediğinizi kontrol etmenizi istiyoruz",
"warning432": "Flashı 4MB'dan az ve/veya rami 32MB'dan az olan cihazlar çalışacaktır fakat çok kısıtlı olacaklardır (genelde ek paket kuramaz veya kullanamazlar) çünkü çok az RAM ve flash belleğine sahiptirler. Cihazınız destekleniyor olmasına rağmen, bir cihaz satın alırken veya cihazınıza OpenWrt yüklerken bunları göz önünde bulundurun.",
"OpenWrt Firmware Selector": "OpenWrt Yazılım Seçici",
"Download OpenWrt firmware for your device!": "Cihazınız için OpenWrt yazılımı indirin!",
"Target": "Hedef",
"Dismiss": "Son ver",
"There is an error with the packages you selected": "Seçtiğiniz paketlerle alakalı hata oluştu",
"Downloads": "İndirilenler",
"Please use the input below to download firmware for your device!": "Lütfen aşağıdaki bilgiler ile cihazınıza uygun yazılım indirin!"
}

279
src/locales/translations.ts Normal file
View file

@ -0,0 +1,279 @@
/* exported translations */
const translations = {
ca: {
'tr-load': 'Descarregueu el microprogramari OpenWrt per al vostre dispositiu',
'tr-title': 'Selector de microprogramari OpenWrt',
'tr-message':
'Introduïu el nom o el model del vostre dispositiu i seleccioneu la versió estable (per defecte) o la darrera imatge compilada ("snapshot")',
'tr-version-build': 'Compilació',
'tr-custom-build': 'Compilació personalitzada',
'tr-customize': 'Personalitzar',
'tr-request-build': 'Demanar la compilació',
'tr-model': 'Model',
'tr-target': 'Plataforma',
'tr-version': 'Versió',
'tr-date': 'Data',
'tr-downloads': 'Descàrregues',
'tr-custom-downloads': 'Descàrregues personalitzades',
'tr-factory-help':
'Empreu la imatge "factory" per instal·lar OpenWrt a un dispositius per primera vegada. Normalment ho podreu fer través de la interfície web del microprogramari original.',
'tr-sysupgrade-help':
'Empreu la imatge "sysupgrade" per actualitzar un dispositiu que ja tingui OpenWrt instal·lat. La imatge es pot instal·lar a través de la interfície web LuCI o del terminal.',
'tr-kernel-help': 'El nucli de Linux en una imatge separada.',
'tr-rootfs-help': 'El sistema de fitxers arrel en una imatge separada.',
'tr-sdcard-help': 'Una imatge feta per escriure-la a una targeta SD.',
'tr-tftp-help':
"Les imatges TFTP images es fan servir per instal·lar-les a un dispositiu mitjançant el mètode TFTP del carregador d'arrencada.",
'tr-other-help': "Un altre tipus d'imatge.",
'tr-build-successful': 'La compilació ha tingut èxit',
'tr-build-failed': 'La compilació ha fallat',
'tr-request-image': 'Demanar la imatge',
'tr-check-again': "Proveu de nou d'aquí 5 segons...",
},
en: {
'tr-server-link': 'Files',
'tr-notfound': 'No model found!',
'tr-load': 'Download OpenWrt Firmware for your Device',
'tr-title': 'OpenWrt Firmware Selector',
'tr-message':
'Type the name or model of your device, then select the recommended build or some other.',
'tr-version-build': 'About this build',
'tr-custom-build': 'Custom Build',
'tr-customize': 'Customize',
'tr-request-build': 'Request Build',
'tr-model': 'Model',
'tr-target': 'Platform',
'tr-version': 'Version',
'tr-date': 'Date',
'tr-downloads': 'Download an image',
'tr-custom-downloads': 'Custom Downloads',
'tr-factory-help':
'Use a Factory image to flash a router with OpenWrt for the first time. You normally do this via the web interface of the original firmware.',
'tr-sysupgrade-help':
'Use a Sysupgrade image to update a router that already runs OpenWrt. The image can be used with the LuCI web interface or the terminal.',
'tr-kernel-help': 'Linux kernel as a separate image.',
'tr-rootfs-help': 'Root file system as a separate image.',
'tr-sdcard-help': 'Image that is meant to be flashed onto a SD-Card.',
'tr-tftp-help': 'TFTP images are used to flash a device via the TFTP method of the bootloader.',
'tr-other-help': 'Other image type.',
'tr-build-successful': 'Build successful',
'tr-build-failed': 'Build failed',
'tr-request-image': 'Request image',
'tr-check-again': 'Check again in 5 seconds...',
},
es: {
'tr-notfound': '¡Modelo no encontrado!',
'tr-load': 'Descargue el firmware OpenWrt para su dispositivo',
'tr-title': 'Selector de firmware OpenWrt',
'tr-message':
'Escriba el nombre o modelo de su dispositivo, luego seleccione la versión recomendada o alguna otra.',
'tr-version-build': 'Acerca de esta compilación',
'tr-custom-build': 'Compilación personalizada',
'tr-customize': 'Personalizar',
'tr-request-build': 'Solicitar compilación',
'tr-model': 'Modelo',
'tr-target': 'Plataforma',
'tr-version': 'Versión',
'tr-date': 'Date',
'tr-downloads': 'Descargar una imagen',
'tr-custom-downloads': 'Descargas personalizadas',
'tr-factory-help':
'Utilice una imagen factory para instalar OpenWrt en un enrutador por primera vez. Normalmente se hace a través de la interfaz web del firmware original.',
'tr-sysupgrade-help':
'Utilice una imagen sysupgrade para actualizar un enrutador que ya ejecuta OpenWrt. La imagen se puede utilizar con la interfaz web de LuCI o el terminal.',
'tr-kernel-help': 'Kernel de Linux como una imagen separada.',
'tr-rootfs-help': 'Sistema de archivos raíz como una imagen separada.',
'tr-sdcard-help': 'Imagen destinada a flashear en una tarjeta SD.',
'tr-tftp-help':
'Las imágenes TFTP se utilizan para actualizar un dispositivo mediante el método TFTP del gestor de arranque.',
'tr-other-help': 'Otro tipo de imagen.',
'tr-build-successful': 'Compilación exitosa',
'tr-build-failed': 'Compilación fallida',
'tr-request-image': 'Solicitar imagen',
'tr-check-again': 'Vuelva a comprobar en 5 segundos...',
},
no: {
'tr-load': 'Last ned OpenWrt fastvare for din enhet!',
'tr-title': 'OpenWrt fastvare utvelger',
'tr-message': 'Bruk feltene nedenfor for å laste ned fastvare til enheten din!',
'tr-version-build': 'Sammensetning',
'tr-custom-build': 'Tilpasset sammensetning',
'tr-customize': 'Tilpasse',
'tr-request-build': 'Be om sammensetning',
'tr-model': 'Modell',
'tr-target': 'Platform',
'tr-version': 'Versjon',
'tr-date': 'Dato',
'tr-downloads': 'Nedlastninger',
'tr-custom-downloads': 'Tilpassede nedlastninger',
'tr-factory-help':
'Factory avbildningen er for å laste rutere med OpenWrt første gang. Vanligvis via webgrensesnittet til den originale fastvaren.',
'tr-sysupgrade-help':
'Sysupgrade avbildningen er for rutere som allerede benytter OpenWrt. Avbildningen innstaleres gjennom webgrensesnittet eller terminalen.',
'tr-kernel-help': 'Linux kjernen som en egen avbildning.',
'tr-rootfs-help': 'Rotfilsystem som en egen avbildning.',
'tr-sdcard-help': 'Avbildning som er ment for et SD-kort.',
'tr-tftp-help':
'TFTP avbildninger er for å laste enheter via TFTP metoden i oppstartsprosedyren.',
'tr-other-help': 'Andre avbildningstyper.',
'tr-build-successful': 'Vellykket sammensetning',
'tr-build-failed': 'Sammensetningen feilet',
'tr-request-image': 'Be om avbildning',
'tr-check-again': 'Sjekk pånytt om 5 sekunder...',
},
de: {
'tr-server-link': 'Dateien',
'tr-notfound': 'Kein Model gefunden!',
'tr-load': 'Lade die OpenWrt Firmware für dein Gerät!',
'tr-title': 'OpenWrt Firmware Selector',
'tr-message': 'Bitte benutze die Eingabe um die passende Firmware zu finden!',
'tr-version-build': 'Release Build',
'tr-custom-build': 'Custom Build',
'tr-customize': 'Customize',
'tr-request-build': 'Request Build',
'tr-model': 'Model',
'tr-target': 'Target',
'tr-version': 'Version',
'tr-date': 'Datum',
'tr-downloads': 'Downloads',
'tr-custom-downloads': 'Custom Downloads',
'tr-factory-help':
'Factory Abbilder werden über die Weboberfläche der originalen Firmware eingespielt.',
'tr-sysupgrade-help':
'Sysupgrade Abbilder werden für Geräte verwendet, die bereits OpenWrt laufen haben. Es ist möglich, existierende Einstellungen beizubehalten.',
'tr-kernel-help': 'Linux Kernel als separates Abbild.',
'tr-rootfs-help': 'Das Root Dateisystem als separates Abbild.',
'tr-sdcard-help': 'Image für SD Speicherkarten.',
'tr-tftp-help':
'TFTP Dateien können verwendet werden, um ein Gerät über die TFTP Method des Bootloader zu flashen.',
'tr-other-help': 'Sonstiger Imagetyp.',
'tr-build-successful': 'Build erfolgreich',
'tr-build-failed': 'Build fehlgeschlagen',
'tr-request-image': 'Frage nach image',
'tr-check-again': 'Nochmal nachfragen in 5 Sekunden...',
},
fr: {
'tr-load': 'Télécharger le firmware OpenWrt de votre périphérique !',
'tr-title': 'Sélecteur de Firmware',
'tr-message':
'Utiliser les entrées ci-dessous pour télécharger le firmware de votre périphérique !',
'tr-version-build': 'Build',
'tr-custom-build': 'Build Personnalisé',
'tr-customize': 'Personnalisation',
'tr-request-build': 'Requête de Build',
'tr-model': 'Modèle',
'tr-target': 'Platform',
'tr-version': 'Version',
'tr-date': 'Date',
'tr-downloads': 'Téléchargements',
'tr-custom-downloads': 'Téléchargements Personnalusés',
'tr-factory-help':
"Les images Factory sont prévues pour flasher les routers avec OpenWrt pour la première fois. Habituellement à partir de l'interface web du firmware d'origine.",
'tr-sysupgrade-help':
"Les images Sysupgrade sont prévues pour flasher les routers fonctionnant déjà avec OpenWrt. L'image peut être installée à travers l'interface web ou par le terminal.",
'tr-kernel-help': 'Linux kernel comme image séparée.',
'tr-rootfs-help': 'Root file system comme image séparée.',
'tr-sdcard-help': 'Image prévue pour être flashée sur une carte SD.',
'tr-tftp-help':
'TFTP images prévues pour flasher le périphérique via le démarrage par méthode TFTP.',
'tr-other-help': "Autre type d'image.",
'tr-build-successful': 'Succès du Build',
'tr-build-failed': 'Échec du Build',
'tr-request-image': "Demade d'image",
'tr-check-again': 'Essayer à nouveau dans 5 secondes...',
},
it: {
'tr-load': 'Scarica il firmware OpenWrt per il tuo dispositivo!',
'tr-title': 'OpenWrt Firmware Selector',
'tr-message': 'Usa la casella sottostante per scaricare il firmware per il tuo dispositivo!',
'tr-version-build': 'Build',
'tr-custom-build': 'Custom Build',
'tr-customize': 'Personalizza',
'tr-request-build': 'Richiedi Build',
'tr-model': 'Modell',
'tr-target': 'Platform',
'tr-version': 'Version',
'tr-date': 'Data',
'tr-downloads': 'Downloads',
'tr-custom-downloads': 'Download Personalizzati',
'tr-factory-help':
"Factory Image sono usate per installare OpenWrt su router per la prima volta. Di solito l'immagine può essere applicata via l'interfaccia web del firmware originale.",
'tr-sysupgrade-help':
"Sysupgrade Image sono usate per flashare router in cui OpenWrt è già installato. L'immagine può essere applicata via interfaccia web o terminale.",
'tr-kernel-help': 'Linux kernel come immagine separata.',
'tr-rootfs-help': 'Root file system come immagine separata.',
'tr-sdcard-help': 'Immagine da flashare su scheda SD-Card separata.',
'tr-tftp-help':
'Immagini TFTP images sono usate per flashare un dispositivo con il metodo TFTP del bootloader.',
'tr-other-help': 'Other image type.',
'tr-build-successful': 'Build compilata con successo',
'tr-build-failed': 'Build fallita',
'tr-request-image': 'Richiedi immagine',
'tr-check-again': 'Prova di nuovo in 5 secondi...',
},
pl: {
'tr-server-link': 'Pliki',
'tr-notfound': 'Nie znaleziono modelu!',
'tr-load': 'Pobieranie oprogramowania OpenWrt',
'tr-title': 'OpenWrt Firmware Selector',
'tr-message':
'Wprowadź nazwę lub model swojego urządzenia, a następnie wybierz wersję zalecaną lub inną.',
'tr-version-build': 'Informacje o obrazie',
'tr-custom-build': 'Informacje o zmodyfikowanym obrazie',
'tr-customize': 'Modyfikacja',
'tr-request-build': 'Żądanie budowy obrazu',
'tr-model': 'Model',
'tr-target': 'Platforma',
'tr-version': 'Wersja',
'tr-date': 'Data',
'tr-downloads': 'Obrazy do pobrania',
'tr-custom-downloads': 'Zmodyfikowane obrazy do pobrania',
'tr-factory-help':
'Użyj obrazu factory do pierwszej instalacji OpenWrt. Zwykle można go użyć wykorzystując interfejs graficzny oryginalnego oprogramowania.',
'tr-sysupgrade-help':
'Użyj obrazu sysuprade do aktualizacji routera z zainstalowanym już OpenWrt. Obraz można użyć przez interfejs graficzny LuCI lub konsolę.',
'tr-kernel-help': 'Osobny obraz z kernelem linuksowym.',
'tr-rootfs-help': 'Osobny obraz z systemem plików.',
'tr-sdcard-help': 'Obraz do wgrania na kartę SD.',
'tr-tftp-help':
'Obraz TFTP służący do aktualizacji urządzenia z wykorzystaniem metody TFTP bootloadera.',
'tr-other-help': 'Inny typ obrazu.',
'tr-build-successful': 'Budowanie zakończone pomyślnie',
'tr-build-failed': 'Błąd budowania',
'tr-request-image': 'Żądanie obrazu',
'tr-check-again': 'Sprawdź ponownie za 5 sekund...',
},
tr: {
'tr-load': 'Cihazınız için OpenWrt yazılımını indirin!',
'tr-title': 'OpenWrt Yazılım Seçicisi',
'tr-message':
'Cihazınızın adını/modelini girin, ardından Stabil sürümü(varsayılan) veya nightly "snapshot" imajini seçin.',
'tr-version-build': 'Sürüm',
'tr-custom-build': 'Özel Sürüm',
'tr-customize': 'Özelleştir',
'tr-request-build': 'Sürüm Oluştur',
'tr-model': 'Model',
'tr-target': 'Platform',
'tr-version': 'Versiyon',
'tr-date': 'Tarih',
'tr-downloads': 'İndirmeler',
'tr-custom-downloads': 'Özel İndirmeler',
'tr-factory-help':
'Bir yönlendiriciyi OpenWrt ile ilk kez flashlamak için bir Fabrika imaji kullanın. Bu normalde orijinal aygıt yazılımının web arayüzü aracılığıyla yapılır.',
'tr-sysupgrade-help':
'Zaten OpenWrt çalıştıran bir yönlendiriciyi güncellemek için bir Sysupgrade imajı kullanın. Imaj, LuCI web arayüzü veya terminal ile kullanılabilir.',
'tr-kernel-help': 'Linux kernel ayrı bir imaj olarak.',
'tr-rootfs-help': 'Kök Dosya Sistemi ayrı bir imaj olarak.',
'tr-sdcard-help': "SD-Kart 'a kurulması planlanan imaj",
'tr-tftp-help':
"TFTP imajları, Bootloader 'ın TFTP yöntemi ile bir cihaza kurulması için kullanılır.",
'tr-other-help': 'Diğer imaj türü.',
'tr-build-successful': 'Oluşturma başarılı',
'tr-build-failed': 'Oluşturma başarısız',
'tr-request-image': 'Imaj oluştur',
'tr-check-again': '5 saniye icinde tekrar dene...',
},
};
export default translations;

View file

@ -1,6 +1,6 @@
import { ReportHandler } from 'web-vitals'; import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler): void => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry);

View file

@ -1,45 +0,0 @@
import axios from 'axios';
import config from '../config';
const { CORSbyPass, base_api: api } = config;
const base_api = CORSbyPass + api;
class DataService {
getVersions = versionsPath => axios.get(versionsPath);
getOverview = overviewPath => axios.get(overviewPath);
getDeviceData = devicePath => axios.get(devicePath);
getDeviceManifest = async manifest_path => {
const manifest = await axios.get(manifest_path);
return manifest.data.split('\n');
};
getDevicePackages = (version, target, profile) =>
axios.get(
base_api +
'packages_image?distro=openwrt&version=' +
version.toLowerCase() +
'&target=' +
target +
'&profile=' +
profile.toLowerCase()
);
buildImage = (board, packages, target, version, uciDefaults) =>
axios.post(base_api + 'build-request', {
profile: board,
board,
defaults: uciDefaults,
distro: 'openwrt',
packages,
target,
version,
});
buildStatusCheck = request_hash =>
axios.get(base_api + 'build-request/' + request_hash);
}
export default DataService;

42
src/services/data.ts Normal file
View file

@ -0,0 +1,42 @@
import axios from 'axios';
import config from '../config';
class DataService {
// getVersions = versionsPath => axios.get(versionsPath);
//
// getOverview = overviewPath => axios.get(overviewPath);
//
// getDeviceData = devicePath => axios.get(devicePath);
//
// getDeviceManifest = async manifest_path => {
// const manifest = await axios.get(manifest_path);
// return manifest.data.split('\n');
// };
//
// getDevicePackages = (version, target, profile) =>
// axios.get(
// base_api +
// 'packages_image?distro=openwrt&version=' +
// version.toLowerCase() +
// '&target=' +
// target +
// '&profile=' +
// profile.toLowerCase()
// );
//
// buildImage = (board, packages, target, version, uciDefaults) =>
// axios.post(base_api + 'build-request', {
// profile: board,
// board,
// defaults: uciDefaults,
// distro: 'openwrt',
// packages,
// target,
// version,
// });
//
// buildStatusCheck = request_hash =>
// axios.get(base_api + 'build-request/' + request_hash);
}
export default DataService;

15
src/types/overview.ts Normal file
View file

@ -0,0 +1,15 @@
export interface Overview {
image_url: string;
profiles?: ProfilesEntity[] | null;
release: string;
}
export interface ProfilesEntity {
id: string;
target: string;
titles?: TitlesEntity[] | null;
}
export interface TitlesEntity {
title?: string;
model?: string;
vendor?: string;
}

24
src/types/profile.ts Normal file
View file

@ -0,0 +1,24 @@
export interface Profile {
arch_packages: string;
build_at: string;
default_packages?: string[] | null;
device_packages?: string[] | null;
id: string;
image_prefix: string;
images?: ImagesEntity[] | null;
metadata_version: number;
target: string;
titles?: TitlesEntity[] | null;
version_code: string;
version_number: string;
}
export interface ImagesEntity {
name: string;
sha256: string;
type: string;
}
export interface TitlesEntity {
title?: string;
model?: string;
vendor?: string;
}

7127
yarn.lock

File diff suppressed because it is too large Load diff