trying to link chakra ui into the app

This commit is contained in:
trippjoe 2022-02-07 17:26:37 -05:00
parent ccd0e4685d
commit 4d242b1d8e
18 changed files with 453 additions and 324 deletions

View file

@ -61,9 +61,9 @@ async function createWindow() {
title: "OpenGOAL Launcher", title: "OpenGOAL Launcher",
webPreferences: { webPreferences: {
devTools: isDev, devTools: isDev,
nodeIntegration: false, nodeIntegration: true,
nodeIntegrationInWorker: false, // nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false, // nodeIntegrationInSubFrames: false,
contextIsolation: true, contextIsolation: true,
enableRemoteModule: false, enableRemoteModule: false,
additionalArguments: [`storePath:${app.getPath("userData")}`], additionalArguments: [`storePath:${app.getPath("userData")}`],

View file

@ -0,0 +1,35 @@
import { Button, Text, Progress, VStack, Divider } from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
const { receive, send } = window.api;
function GettingStarted() {
const [status, setStatus] = useState('Awaiting Jak ISO File');
function handleClick() {
send('getISO');
}
useEffect(() => {
receive('status', newStatus => {
console.log(newStatus);
setStatus(newStatus);
});
}, [])
return (
<VStack alignSelf="center" minH="inherit" p='6' spacing="4" justifyContent="center">
{status === 'Awaiting Jak ISO File' ?
<>
<Text fontWeight="bold">Please select your Jak and Daxter ISO below</Text>
<Button colorScheme="telegram" onClick={handleClick} w="50%" shadow='base'>Open your ISO File</Button>
<Divider w="50%" />
</> : null
}
<Progress size='lg' w="50%" colorScheme="green" rounded="md" isIndeterminate />
{status ? <Text fontWeight="extrabold">Status: {status}</Text> : null}
</VStack>
);
}
export default GettingStarted;

View file

@ -0,0 +1,13 @@
import React from "react";
import { Heading, Img } from "@chakra-ui/react";
const logo = require('../assets/images/header-logo.png');
function Header() {
return (
<Heading py={4}>
<Img src={logo} width="30vw" m="0 auto" draggable="false" />
</Heading>
);
}
export default Header;

View file

@ -0,0 +1,17 @@
import React from "react";
import { VStack, Button } from '@chakra-ui/react';
const { send } = window.api;
function handleLaunch() {
send('launch');
}
function Home() {
return (
<VStack alignSelf="center" minH="inherit" p='6' spacing="4" justifyContent="center">
<Button colorScheme="telegram" onClick={handleLaunch} w="50%" shadow='base'>Launch Game</Button>
</VStack>
);
}
export default Home;

View file

@ -0,0 +1,29 @@
import { Button, VStack } from "@chakra-ui/react";
import React from "react";
const data = require('../assets/data/links.json');
function Feature({ title, link }) {
return (
<Button w="50%" shadow='base' colorScheme="telegram">
<a href={link} rel="noreferrer" target="_blank">
{title}
</a>
</Button>
)
}
function ImportantLinks() {
return (
<VStack alignSelf="center" minH="inherit" p='6' spacing="4" justifyContent="center">
{data.map((item, index) => (
<Feature
key={index}
title={item.name}
link={item.link}
/>
))}
</VStack>
)
}
export default ImportantLinks;

View file

@ -0,0 +1,37 @@
import React from "react";
import { Flex, Tabs, TabList, TabPanels, TabPanel, Tab } from "@chakra-ui/react";
import Home from "./Home";
import GettingStarted from "./GettingStarted";
// import ImportantLinks from "./ImportantLinks";
// import Settings from "./Settings";
function Landing() {
return (
<Flex direction="column" mx={4}>
<Tabs isFitted isLazy>
<TabList>
<Tab>Home</Tab>
<Tab>Getting Started</Tab>
<Tab>Links</Tab>
<Tab>Settings</Tab>
</TabList>
<TabPanels>
<TabPanel minH="70vh">
<Home />
</TabPanel>
<TabPanel minH="70vh">
<GettingStarted />
</TabPanel>
{/* <TabPanel minH="70vh">
<ImportantLinks />
</TabPanel>
<TabPanel minH="70vh">
<Settings />
</TabPanel> */}
</TabPanels>
</Tabs>
</Flex>
);
}
export default Landing;

View file

@ -0,0 +1,26 @@
import React from "react";
import { Button, VStack } from '@chakra-ui/react';
import { ColorModeSwitcher } from '../ColorModeSwitcher';
const { invoke } = window.api;
async function handleCheckUpdates() {
try {
let response = await invoke('checkUpdates');
console.log(response);
} catch (err) {
console.log(err);
}
}
function Settings() {
return (
<VStack alignSelf="center" minH="inherit" p='6' spacing="4" justifyContent="center">
<ColorModeSwitcher as={Button} w="50%" shadow='base' />
<Button onClick={handleCheckUpdates} w="50%" shadow='base' colorScheme="telegram">Check for Updates</Button>
{/* <Button onClick={decompile} w="50%" shadow='base' colorScheme="telegram">Decompile Game</Button> */}
{/* <Button onClick={handleBuildGame} w="50%" shadow='base' colorScheme="telegram">Build Game</Button> */}
</VStack >
);
}
export default Settings;

View file

@ -12,7 +12,7 @@ class SubItem extends React.Component {
componentDidMount() { componentDidMount() {
// Set up binding in code whenever the context menu item // Set up binding in code whenever the context menu item
// of id "alert" is selected // of id "alert" is selected
window.api.contextMenu.onReceive("softAlert", function(args) { window.api.contextMenu.onReceive("softAlert", function (args) {
console.log(`This alert was brought to you by secure-electron-context-menu by ${args.attributes.name}`); console.log(`This alert was brought to you by secure-electron-context-menu by ${args.attributes.name}`);
// Note - we have access to the "params" object as defined here: https://www.electronjs.org/docs/api/web-contents#event-context-menu // Note - we have access to the "params" object as defined here: https://www.electronjs.org/docs/api/web-contents#event-context-menu
@ -22,14 +22,14 @@ class SubItem extends React.Component {
render() { render() {
return ( return (
<div id="subitem"> <div id="subitem">
<div <div
cm-template="softAlertTemplate" cm-template="softAlertTemplate"
cm-id={this.props.id} cm-id={this.props.id}
cm-payload-name={`Child (${this.props.id})`}> cm-payload-name={`Child (${this.props.id})`}>
ID ({this.props.id}): Try right-clicking me for a custom context menu ID ({this.props.id}): Try right-clicking me for a custom context menu
</div>
</div> </div>
</div>
); );
} }
} }

View file

@ -1,252 +1,250 @@
import React from "react"; // import React from "react";
import ROUTES from "Constants/routes"; // import ROUTES from "Constants/routes";
import { // import {
validateLicenseRequest, // validateLicenseRequest,
validateLicenseResponse, // validateLicenseResponse,
} from "secure-electron-license-keys"; // } from "secure-electron-license-keys";
class Nav extends React.Component { // class Nav extends React.Component {
constructor(props) { // constructor(props) {
super(props); // super(props);
this.history = props.history; // this.history = props.history;
this.state = { // this.state = {
mobileMenuActive: false, // mobileMenuActive: false,
licenseModalActive: false, // licenseModalActive: false,
// license-specific // // license-specific
licenseValid: false, // licenseValid: false,
allowedMajorVersions: "", // allowedMajorVersions: "",
allowedMinorVersions: "", // allowedMinorVersions: "",
appVersion: "", // appVersion: "",
licenseExpiry: "", // licenseExpiry: "",
}; // };
this.toggleMenu = this.toggleMenu.bind(this); // this.toggleMenu = this.toggleMenu.bind(this);
this.toggleLicenseModal = this.toggleLicenseModal.bind(this); // this.toggleLicenseModal = this.toggleLicenseModal.bind(this);
this.navigate = this.navigate.bind(this); // this.navigate = this.navigate.bind(this);
} // }
componentWillUnmount() { // componentWillUnmount() {
window.api.licenseKeys.clearRendererBindings(); // window.api.licenseKeys.clearRendererBindings();
} // }
componentDidMount() { // componentDidMount() {
// Set up binding to listen when the license key is // // Set up binding to listen when the license key is
// validated by the main process // // validated by the main process
const _ = this; // const _ = this;
window.api.licenseKeys.onReceive(validateLicenseResponse, function (data) { // window.api.licenseKeys.onReceive(validateLicenseResponse, function (data) {
// If the license key/data is valid // // If the license key/data is valid
if (data.success) { // if (data.success) {
// Here you would compare data.appVersion to // // Here you would compare data.appVersion to
// data.major, data.minor and data.patch to // // data.major, data.minor and data.patch to
// ensure that the user's version of the app // // ensure that the user's version of the app
// matches their license // // matches their license
_.setState({ // _.setState({
licenseValid: true, // licenseValid: true,
allowedMajorVersions: data.major, // allowedMajorVersions: data.major,
allowedMinorVersions: data.minor, // allowedMinorVersions: data.minor,
allowedPatchVersions: data.patch, // allowedPatchVersions: data.patch,
appVersion: data.appVersion, // appVersion: data.appVersion,
licenseExpiry: data.expire, // licenseExpiry: data.expire,
}); // });
} else { // } else {
_.setState({ // _.setState({
licenseValid: false, // licenseValid: false,
}); // });
} // }
}); // });
} // }
toggleMenu(event) { // toggleMenu(event) {
this.setState({ // this.setState({
mobileMenuActive: !this.state.mobileMenuActive, // mobileMenuActive: !this.state.mobileMenuActive,
}); // });
} // }
toggleLicenseModal(event) { // toggleLicenseModal(event) {
const previous = this.state.licenseModalActive; // const previous = this.state.licenseModalActive;
// Only send license request if the modal // // Only send license request if the modal
// is not already open // // is not already open
if (!previous) { // if (!previous) {
window.api.licenseKeys.send(validateLicenseRequest); // window.api.licenseKeys.send(validateLicenseRequest);
} // }
this.setState({ // this.setState({
licenseModalActive: !this.state.licenseModalActive, // licenseModalActive: !this.state.licenseModalActive,
}); // });
} // }
// Using a custom method to navigate because we // // Using a custom method to navigate because we
// need to close the mobile menu if we navigate to // // need to close the mobile menu if we navigate to
// another page // // another page
navigate(url) { // navigate(url) {
this.setState( // this.setState(
{ // {
mobileMenuActive: false, // mobileMenuActive: false,
}, // },
function () { // function () {
this.history.push(url); // this.history.push(url);
} // }
); // );
} // }
renderLicenseModal() { // renderLicenseModal() {
return ( // return (
<div // <div
className={`modal ${this.state.licenseModalActive ? "is-active" : ""}`}> // className={`modal ${this.state.licenseModalActive ? "is-active" : ""}`}>
<div className="modal-background"></div> // <div className="modal-background"></div>
<div className="modal-content"> // <div className="modal-content">
{this.state.licenseValid ? ( // {this.state.licenseValid ? (
<div className="box"> // <div className="box">
The license key for this product has been validated and the // The license key for this product has been validated and the
following versions of this app are allowed for your use: // following versions of this app are allowed for your use:
<div> // <div>
<strong>Major versions:</strong>{" "} // <strong>Major versions:</strong>{" "}
{this.state.allowedMajorVersions} <br /> // {this.state.allowedMajorVersions} <br />
<strong>Minor versions:</strong>{" "} // <strong>Minor versions:</strong>{" "}
{this.state.allowedMinorVersions} <br /> // {this.state.allowedMinorVersions} <br />
<strong>Patch versions:</strong>{" "} // <strong>Patch versions:</strong>{" "}
{this.state.allowedPatchVersions} <br /> // {this.state.allowedPatchVersions} <br />
<strong>Expires on:</strong>{" "} // <strong>Expires on:</strong>{" "}
{!this.state.licenseExpiry // {!this.state.licenseExpiry
? "never!" // ? "never!"
: this.state.licenseExpiry}{" "} // : this.state.licenseExpiry}{" "}
<br />( // <br />(
<em> // <em>
App version: // App version:
{` v${this.state.appVersion.major}.${this.state.appVersion.minor}.${this.state.appVersion.patch}`} // {` v${this.state.appVersion.major}.${this.state.appVersion.minor}.${this.state.appVersion.patch}`}
</em> // </em>
) // )
<br /> // <br />
</div> // </div>
</div> // </div>
) : ( // ) : (
<div className="box"> // <div className="box">
<div>The license key is not valid.</div> // <div>The license key is not valid.</div>
<div> // <div>
If you'd like to create a license key, follow these steps: // If you'd like to create a license key, follow these steps:
<ol style={{ marginLeft: "30px" }}> // <ol style={{ marginLeft: "30px" }}>
<li> // <li>
Install this package globally ( // Install this package globally (
<strong>npm i secure-electron-license-keys-cli -g</strong>). // <strong>npm i secure-electron-license-keys-cli -g</strong>).
</li> // </li>
<li> // <li>
Run <strong>secure-electron-license-keys-cli</strong>. // Run <strong>secure-electron-license-keys-cli</strong>.
</li> // </li>
<li> // <li>
Copy <strong>public.key</strong> and{" "} // Copy <strong>public.key</strong> and{" "}
<strong>license.data</strong> into the <em>root</em> folder // <strong>license.data</strong> into the <em>root</em> folder
of this app. // of this app.
</li> // </li>
<li> // <li>
Re-run this app (ie. <strong>npm run dev</strong>). // Re-run this app (ie. <strong>npm run dev</strong>).
</li> // </li>
<li> // <li>
If you'd like to further customize your license keys, copy // If you'd like to further customize your license keys, copy
this link into your browser:{" "} // this link into your browser:{" "}
<a href="https://github.com/reZach/secure-electron-license-keys-cli"> // <a href="https://github.com/reZach/secure-electron-license-keys-cli">
https://github.com/reZach/secure-electron-license-keys-cli // https://github.com/reZach/secure-electron-license-keys-cli
</a> // </a>
. // .
</li> // </li>
</ol> // </ol>
</div> // </div>
</div> // </div>
)} // )}
</div> // </div>
<button // <button
className="modal-close is-large" // className="modal-close is-large"
aria-label="close" // aria-label="close"
onClick={this.toggleLicenseModal}></button> // onClick={this.toggleLicenseModal}></button>
</div> // </div>
); // );
} // }
render() { // render() {
return ( // return (
<nav // <nav
className="navbar is-dark" // className="navbar is-dark"
role="navigation" // role="navigation"
aria-label="main navigation"> // aria-label="main navigation">
<div className="navbar-brand"> // <div className="navbar-brand">
<a // <a
role="button" // role="button"
className={`navbar-burger ${ // className={`navbar-burger ${this.state.mobileMenuActive ? "is-active" : ""
this.state.mobileMenuActive ? "is-active" : "" // }`}
}`} // data-target="navbarBasicExample"
data-target="navbarBasicExample" // aria-label="menu"
aria-label="menu" // aria-expanded="false"
aria-expanded="false" // onClick={this.toggleMenu}>
onClick={this.toggleMenu}> // <span aria-hidden="true"></span>
<span aria-hidden="true"></span> // <span aria-hidden="true"></span>
<span aria-hidden="true"></span> // <span aria-hidden="true"></span>
<span aria-hidden="true"></span> // </a>
</a> // </div>
</div> // <div
<div // id="navbarBasicExample"
id="navbarBasicExample" // className={`navbar-menu ${this.state.mobileMenuActive ? "is-active" : ""
className={`navbar-menu ${ // }`}>
this.state.mobileMenuActive ? "is-active" : "" // <div className="navbar-start">
}`}> // <a
<div className="navbar-start"> // className="navbar-item"
<a // onClick={() => this.navigate(ROUTES.WELCOME)}>
className="navbar-item" // Home
onClick={() => this.navigate(ROUTES.WELCOME)}> // </a>
Home
</a>
<a // <a
className="navbar-item" // className="navbar-item"
onClick={() => this.navigate(ROUTES.ABOUT)}> // onClick={() => this.navigate(ROUTES.ABOUT)}>
About // About
</a> // </a>
<div className="navbar-item has-dropdown is-hoverable"> // <div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link">Sample pages</a> // <a className="navbar-link">Sample pages</a>
<div className="navbar-dropdown"> // <div className="navbar-dropdown">
<a // <a
className="navbar-item" // className="navbar-item"
onClick={() => this.navigate(ROUTES.MOTD)}> // onClick={() => this.navigate(ROUTES.MOTD)}>
Using the Electron store // Using the Electron store
</a> // </a>
<a // <a
className="navbar-item" // className="navbar-item"
onClick={() => this.navigate(ROUTES.LOCALIZATION)}> // onClick={() => this.navigate(ROUTES.LOCALIZATION)}>
Changing locales // Changing locales
</a> // </a>
<a // <a
className="navbar-item" // className="navbar-item"
onClick={() => this.navigate(ROUTES.UNDOREDO)}> // onClick={() => this.navigate(ROUTES.UNDOREDO)}>
Undo/redoing actions // Undo/redoing actions
</a> // </a>
<a // <a
className="navbar-item" // className="navbar-item"
onClick={() => this.navigate(ROUTES.CONTEXTMENU)}> // onClick={() => this.navigate(ROUTES.CONTEXTMENU)}>
Custom context menu // Custom context menu
</a> // </a>
</div> // </div>
</div> // </div>
</div> // </div>
{this.renderLicenseModal()} // {this.renderLicenseModal()}
<div className="navbar-end"> // <div className="navbar-end">
<div className="navbar-item"> // <div className="navbar-item">
<div className="buttons"> // <div className="buttons">
<a // <a
className="button is-light" // className="button is-light"
onClick={this.toggleLicenseModal}> // onClick={this.toggleLicenseModal}>
Check license // Check license
</a> // </a>
</div> // </div>
</div> // </div>
</div> // </div>
</div> // </div>
</nav> // </nav>
); // );
} // }
} // }
export default Nav; // export default Nav;

View file

@ -1,12 +0,0 @@
html {
height: 100%;
}
body {
margin: 0px;
height: -webkit-fill-available;
}
#target {
height: -webkit-fill-available;
}

View file

@ -3,7 +3,6 @@ import { ConnectedRouter } from "connected-react-router";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import Routes from "Core/routes"; import Routes from "Core/routes";
import Nav from "./nav"; import Nav from "./nav";
import "./root.css";
class Root extends React.Component { class Root extends React.Component {
render() { render() {
@ -13,8 +12,8 @@ class Root extends React.Component {
<React.Fragment> <React.Fragment>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<Nav history={history}></Nav> {/* <Nav history={history}></Nav> */}
<Routes></Routes> <Routes></Routes>
</ConnectedRouter> </ConnectedRouter>
</Provider> </Provider>
</React.Fragment> </React.Fragment>

View file

@ -25,19 +25,17 @@ const ContextMenu = loadable(() =>
import(/* webpackChunkName: "ContextMenuChunk" */ "Pages/contextmenu/contextmenu") import(/* webpackChunkName: "ContextMenuChunk" */ "Pages/contextmenu/contextmenu")
); );
class Routes extends React.Component { function Routes() {
render() { return (
return ( <Switch>
<Switch> <Route exact path={ROUTES.WELCOME} component={Welcome}></Route>
<Route exact path={ROUTES.WELCOME} component={Welcome}></Route> <Route path={ROUTES.ABOUT} component={About}></Route>
<Route path={ROUTES.ABOUT} component={About}></Route> <Route path={ROUTES.MOTD} component={Motd}></Route>
<Route path={ROUTES.MOTD} component={Motd}></Route> <Route path={ROUTES.LOCALIZATION} component={Localization}></Route>
<Route path={ROUTES.LOCALIZATION} component={Localization}></Route> <Route path={ROUTES.UNDOREDO} component={UndoRedo}></Route>
<Route path={ROUTES.UNDOREDO} component={UndoRedo}></Route> <Route path={ROUTES.CONTEXTMENU} component={ContextMenu}></Route>
<Route path={ROUTES.CONTEXTMENU} component={ContextMenu}></Route> </Switch>
</Switch> );
);
}
} }
export default Routes; export default Routes;

View file

@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title></title> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenGOAL Launcher</title>
</head> </head>
<body> <body>

View file

@ -4,15 +4,11 @@ import i18n from "I18n/i18n.config";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import Root from "Core/root"; import Root from "Core/root";
import store, { history } from "Redux/store/store"; import store, { history } from "Redux/store/store";
import "bulma/css/bulma.css";
import { ChakraProvider } from '@chakra-ui/react';
ReactDOM.render( ReactDOM.render(
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<Suspense fallback="loading"> <Suspense fallback="loading">
<ChakraProvider> <Root store={store} history={history}></Root>
<Root store={store} history={history}></Root>
</ChakraProvider>
</Suspense> </Suspense>
</I18nextProvider>, </I18nextProvider>,
document.getElementById("target") document.getElementById("target")

View file

@ -0,0 +1,37 @@
import React from "react";
// import ROUTES from "Constants/routes";
// import { Link } from "react-router-dom";
import { Flex, Tabs, TabList, TabPanels, TabPanel, Tab } from "@chakra-ui/react";
import Home from "../../components/Home";
import GettingStarted from "../../components/GettingStarted";
function Welcome() {
return (
<Flex direction="column" mx={4}>
<Tabs isFitted isLazy>
<TabList>
<Tab>Home</Tab>
<Tab>Getting Started</Tab>
<Tab>Links</Tab>
<Tab>Settings</Tab>
</TabList>
<TabPanels>
<TabPanel minH="70vh">
<Home />
</TabPanel>
<TabPanel minH="70vh">
<GettingStarted />
</TabPanel>
{/* <TabPanel minH="70vh">
<ImportantLinks />
</TabPanel>
<TabPanel minH="70vh">
<Settings />
</TabPanel> */}
</TabPanels>
</Tabs>
</Flex>
)
}
export default Welcome;

View file

@ -1,39 +0,0 @@
import React from "react";
import ROUTES from "Constants/routes";
import { Link } from "react-router-dom";
class Welcome extends React.Component {
render() {
return (
<React.Fragment>
<section className="section">
<div className="container">
<section className="hero is-info">
<div className="hero-body">
<p className="title">
Thank you for trying out the secure-electron-template!
</p>
<p className="subtitle">
Please navigate to view the features of this template.
</p>
</div>
</section>
</div>
</section>
<section className="section">
<div className="container">
<h2 className="title is-2">Samples</h2>
<div>
<Link to={ROUTES.MOTD}>Using the Electron store.</Link> <br />
<Link to={ROUTES.LOCALIZATION}>Changing locales.</Link> <br />
<Link to={ROUTES.UNDOREDO}>Undo/redoing actions.</Link> <br />
<Link to={ROUTES.CONTEXTMENU}>Custom context menu.</Link> <br />
</div>
</div>
</section>
</React.Fragment>
);
}
}
export default Welcome;

5
package-lock.json generated
View file

@ -5121,11 +5121,6 @@
"sax": "^1.2.4" "sax": "^1.2.4"
} }
}, },
"bulma": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz",
"integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g=="
},
"bytes": { "bytes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",

View file

@ -116,7 +116,6 @@
"@emotion/styled": "^11.6.0", "@emotion/styled": "^11.6.0",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@reduxjs/toolkit": "^1.7.1", "@reduxjs/toolkit": "^1.7.1",
"bulma": "^0.9.3",
"connected-react-router": "^6.9.2", "connected-react-router": "^6.9.2",
"easy-redux-undo": "^1.0.5", "easy-redux-undo": "^1.0.5",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",