feat(update): use `tailwindcss`
This commit is contained in:
parent
aea7cc5b31
commit
988e848719
|
@ -13,7 +13,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build && electron-builder",
|
||||
"watch-tailwind": "tailwindcss --watch",
|
||||
"build": "tsc && vite build && electron-builder && tailwindcss",
|
||||
"preview": "vite preview",
|
||||
"pree2e": "vite build --mode=test",
|
||||
"e2e": "playwright test"
|
||||
|
@ -26,10 +27,12 @@
|
|||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"electron": "^26.0.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-electron": "^0.13.0-beta.3",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
// 'tailwindcss/nesting': {}, // https://tailwindcss.com/docs/using-with-preprocessors#nesting
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
57
src/App.tsx
57
src/App.tsx
|
@ -1,23 +1,37 @@
|
|||
import { useState } from 'react'
|
||||
import Update from '@/components/update'
|
||||
import logoVite from './assets/logo-vite.svg'
|
||||
import logoElectron from './assets/logo-electron.svg'
|
||||
import './App.css'
|
||||
import { useState } from "react";
|
||||
import logoVite from "./assets/logo-vite.svg";
|
||||
import logoElectron from "./assets/logo-electron.svg";
|
||||
import "./App.css";
|
||||
import UpdateElectron from "@/components/update-tailwind";
|
||||
|
||||
console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`)
|
||||
console.log(
|
||||
"[App.tsx]",
|
||||
`Hello world from Electron ${process.versions.electron}!`,
|
||||
);
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
const [count, setCount] = useState(0);
|
||||
return (
|
||||
<div className='App'>
|
||||
<div className='logo-box'>
|
||||
<a href='https://github.com/electron-vite/electron-vite-react' target='_blank'>
|
||||
<img src={logoVite} className='logo vite' alt='Electron + Vite logo' />
|
||||
<img src={logoElectron} className='logo electron' alt='Electron + Vite logo' />
|
||||
<div className="App">
|
||||
<div className="logo-box">
|
||||
<a
|
||||
href="https://github.com/electron-vite/electron-vite-react"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src={logoVite}
|
||||
className="logo vite"
|
||||
alt="Electron + Vite logo"
|
||||
/>
|
||||
<img
|
||||
src={logoElectron}
|
||||
className="logo electron"
|
||||
alt="Electron + Vite logo"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<h1>Electron + Vite + React</h1>
|
||||
<div className='card'>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
|
@ -25,16 +39,21 @@ function App() {
|
|||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className='read-the-docs'>
|
||||
<p className="read-the-docs">
|
||||
Click on the Electron + Vite logo to learn more
|
||||
</p>
|
||||
<div className='flex-center'>
|
||||
Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src='./node.svg' alt='Node logo' />
|
||||
<div className="flex-center">
|
||||
Place static files into the<code>/public</code> folder{" "}
|
||||
<img
|
||||
style={{ width: "5em" }}
|
||||
src="./node.svg"
|
||||
alt="Node logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Update />
|
||||
<UpdateElectron />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const ModalTemplate: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
title?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
cancelText?: string;
|
||||
okText?: string;
|
||||
onCancel?: () => void;
|
||||
onOk?: () => void;
|
||||
width?: number;
|
||||
}>
|
||||
> = (props) => {
|
||||
const {
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
cancelText = "Cancel",
|
||||
okText = "OK",
|
||||
onCancel,
|
||||
onOk,
|
||||
width = 530,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="w-screen h-screen fixed top-0 left-0 z-10 bg-modalMask" />
|
||||
<div className="fixed top-1/2 left-1/2 z-20 -translate-x-1/2 -translate-y-1/2">
|
||||
<div
|
||||
className="shadow-modalContent overflow-hidden -border-r-4"
|
||||
style={{ width }}
|
||||
>
|
||||
<div className="flex leading-[38px] bg-crimson">
|
||||
<div className="font-bold w-0 flex-grow">{title}</div>
|
||||
<span
|
||||
className="w-[30px] h-[30px] m-[4px] text-center cursor-pointer leading-[30px]"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<svg
|
||||
className="w-[17px] h-[17px]"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M557.312 513.248l265.28-263.904c12.544-12.48 12.608-32.704 0.128-45.248-12.512-12.576-32.704-12.608-45.248-0.128l-265.344 263.936-263.04-263.84C236.64 191.584 216.384 191.52 203.84 204 191.328 216.48 191.296 236.736 203.776 249.28l262.976 263.776L201.6 776.8c-12.544 12.48-12.608 32.704-0.128 45.248 6.24 6.272 14.464 9.44 22.688 9.44 8.16 0 16.32-3.104 22.56-9.312l265.216-263.808 265.44 266.24c6.24 6.272 14.432 9.408 22.656 9.408 8.192 0 16.352-3.136 22.592-9.344 12.512-12.48 12.544-32.704 0.064-45.248L557.312 513.248z"
|
||||
p-id="2764"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-[10px] bg-white text-darkGrey1">{children}</div>
|
||||
{typeof footer !== "undefined" ? (
|
||||
<div className="p-[10px] bg-white flex justify-end">
|
||||
<button
|
||||
className="p-[7px] bg-crimson text-sm ml-[10px] first:ml-0"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
className="p-[7px] bg-crimson text-sm ml-[10px] first:ml-0"
|
||||
onClick={onOk}
|
||||
>
|
||||
{okText}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
footer
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Modal = (
|
||||
props: Parameters<typeof ModalTemplate>[0] & { open: boolean },
|
||||
) => {
|
||||
const { open, ...omit } = props;
|
||||
|
||||
return createPortal(open ? ModalTemplate(omit) : null, document.body);
|
||||
};
|
||||
|
||||
export default Modal;
|
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
|
||||
const Progress: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
percent?: number;
|
||||
}>
|
||||
> = (props) => {
|
||||
const { percent = 0 } = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="border-[1px] rounded-[3px] border-solid border-black h-[6px] w-[300px]">
|
||||
<div
|
||||
className="h-[6px] rounded-[4px] bg-gradient-to-r from-purple1 via-transparent to-crimson"
|
||||
style={{ width: `${3 * percent}px` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="m-[0,10px]">
|
||||
{(percent ?? 0).toString().substring(0, 4)}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Progress;
|
|
@ -0,0 +1,25 @@
|
|||
# electron-updater-tailwindcss
|
||||
|
||||
[tailwindcss docs](https://tailwindcss.com/).
|
||||
|
||||
|
||||
## If you don't want to use tailwindcss, want to use the default css style:
|
||||
|
||||
[`<Update/>` Written entirely in CSS](../update/)
|
||||
|
||||
### remove dependencies:
|
||||
```diff
|
||||
- autoprefixer
|
||||
- tailwindcss
|
||||
```
|
||||
### remove files:
|
||||
```diff
|
||||
- postcss.config.cjs
|
||||
- tailwind.config.cjs
|
||||
```
|
||||
### remove import:
|
||||
```diff
|
||||
//src/main.tsx
|
||||
- import "@/components/update-tailwind/tailwind.css";
|
||||
```
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import type { ProgressInfo } from "electron-updater";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Modal from "@/components/update-tailwind/Modal";
|
||||
import Progress from "@/components/update-tailwind/Progress";
|
||||
|
||||
const UpdateElectron = () => {
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||
const [versionInfo, setVersionInfo] = useState<VersionInfo>();
|
||||
const [updateError, setUpdateError] = useState<ErrorType>();
|
||||
const [progressInfo, setProgressInfo] = useState<Partial<ProgressInfo>>();
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [modalBtn, setModalBtn] = useState<{
|
||||
cancelText?: string;
|
||||
okText?: string;
|
||||
onCancel?: () => void;
|
||||
onOk?: () => void;
|
||||
}>({
|
||||
onCancel: () => setModalOpen(false),
|
||||
onOk: () => ipcRenderer.invoke("start-download"),
|
||||
});
|
||||
|
||||
const checkUpdate = async () => {
|
||||
setChecking(true);
|
||||
/**
|
||||
* @type {import('electron-updater').UpdateCheckResult | null | { message: string, error: Error }}
|
||||
*/
|
||||
const result = await ipcRenderer.invoke("check-update");
|
||||
setProgressInfo({ percent: 0 });
|
||||
setChecking(false);
|
||||
setModalOpen(true);
|
||||
if (result?.error) {
|
||||
setUpdateAvailable(false);
|
||||
setUpdateError(result?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateCanAvailable = useCallback(
|
||||
(_event: Electron.IpcRendererEvent, arg1: VersionInfo) => {
|
||||
setVersionInfo(arg1);
|
||||
setUpdateError(undefined);
|
||||
// Can be update
|
||||
if (arg1.update) {
|
||||
setModalBtn((state) => ({
|
||||
...state,
|
||||
cancelText: "Cancel",
|
||||
okText: "Update",
|
||||
onOk: () => ipcRenderer.invoke("start-download"),
|
||||
}));
|
||||
setUpdateAvailable(true);
|
||||
} else {
|
||||
setUpdateAvailable(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onUpdateError = useCallback(
|
||||
(_event: Electron.IpcRendererEvent, arg1: ErrorType) => {
|
||||
setUpdateAvailable(false);
|
||||
setUpdateError(arg1);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onDownloadProgress = useCallback(
|
||||
(_event: Electron.IpcRendererEvent, arg1: ProgressInfo) => {
|
||||
setProgressInfo(arg1);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onUpdateDownloaded = useCallback(
|
||||
(_event: Electron.IpcRendererEvent, ...args: any[]) => {
|
||||
setProgressInfo({ percent: 100 });
|
||||
setModalBtn((state) => ({
|
||||
...state,
|
||||
cancelText: "Later",
|
||||
okText: "Install now",
|
||||
onOk: () => ipcRenderer.invoke("quit-and-install"),
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Get version information and whether to update
|
||||
ipcRenderer.on("update-can-available", onUpdateCanAvailable);
|
||||
ipcRenderer.on("update-error", onUpdateError);
|
||||
ipcRenderer.on("download-progress", onDownloadProgress);
|
||||
ipcRenderer.on("update-downloaded", onUpdateDownloaded);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.off("update-can-available", onUpdateCanAvailable);
|
||||
ipcRenderer.off("update-error", onUpdateError);
|
||||
ipcRenderer.off("download-progress", onDownloadProgress);
|
||||
ipcRenderer.off("update-downloaded", onUpdateDownloaded);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
cancelText={modalBtn?.cancelText}
|
||||
okText={modalBtn?.okText}
|
||||
onCancel={modalBtn?.onCancel}
|
||||
onOk={modalBtn?.onOk}
|
||||
footer={updateAvailable ? /* hide footer */ null : undefined}
|
||||
>
|
||||
<div>
|
||||
{updateError ? (
|
||||
<div>
|
||||
<p>Error downloading the latest version.</p>
|
||||
<p>{updateError.message}</p>
|
||||
</div>
|
||||
) : updateAvailable ? (
|
||||
<div>
|
||||
<div>The last version is: v{versionInfo?.newVersion}</div>
|
||||
<div className="ml-[40px]">
|
||||
v{versionInfo?.version} -> v{versionInfo?.newVersion}
|
||||
</div>
|
||||
<div className="ml-[40px]">
|
||||
<div className="mr-[4px]">Update progress:</div>
|
||||
<div className="w-0 flex-grow">
|
||||
<Progress percent={progressInfo?.percent}></Progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-[20px] text-center">
|
||||
{JSON.stringify(versionInfo ?? {}, null, 2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
<button
|
||||
disabled={checking}
|
||||
onClick={checkUpdate}
|
||||
>
|
||||
{checking ? "Checking..." : "Check update"}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateElectron;
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -23,7 +23,7 @@ const ModalTemplate: React.FC<React.PropsWithChildren<{
|
|||
} = props
|
||||
|
||||
return (
|
||||
<div className='update-modal'>
|
||||
<div className='update-modals'>
|
||||
<div className='update-modal__mask' />
|
||||
<div className='update-modal__warp'>
|
||||
<div className='update-modal__content' style={{ width }}>
|
||||
|
|
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
|
|||
import App from './App'
|
||||
import './samples/node-api'
|
||||
import './index.css'
|
||||
import "@/components/update-tailwind/tailwind.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
crimson: "#e01e5a",
|
||||
darkGrey1: "#333",
|
||||
purple1: "#8256d0",
|
||||
modalMask: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
boxShadow: {
|
||||
modalContent: "0 0 10px -4px #8256d0",
|
||||
},
|
||||
},
|
||||
},
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
plugins: [],
|
||||
};
|
Loading…
Reference in New Issue