feat(update): use `tailwindcss` (#174)
* feat(update): use `tailwindcss` * chore: clean up code specifications
This commit is contained in:
parent
aea7cc5b31
commit
d78dcf8822
|
@ -13,7 +13,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build && electron-builder",
|
"watch-tailwind": "tailwindcss --watch",
|
||||||
|
"build": "tsc && vite build && electron-builder && tailwindcss",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"pree2e": "vite build --mode=test",
|
"pree2e": "vite build --mode=test",
|
||||||
"e2e": "playwright test"
|
"e2e": "playwright test"
|
||||||
|
@ -26,10 +27,12 @@
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@vitejs/plugin-react": "^4.0.4",
|
"@vitejs/plugin-react": "^4.0.4",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
"electron": "^26.0.0",
|
"electron": "^26.0.0",
|
||||||
"electron-builder": "^24.6.3",
|
"electron-builder": "^24.6.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-electron": "^0.13.0-beta.3",
|
"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: {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Update from '@/components/update'
|
import UpdateElectron from '@/components/update-tailwind'
|
||||||
import logoVite from './assets/logo-vite.svg'
|
import logoVite from './assets/logo-vite.svg'
|
||||||
import logoElectron from './assets/logo-electron.svg'
|
import logoElectron from './assets/logo-electron.svg'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
@ -32,9 +32,9 @@ function App() {
|
||||||
Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src='./node.svg' alt='Node logo' />
|
Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src='./node.svg' alt='Node logo' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Update />
|
<UpdateElectron />
|
||||||
</div>
|
</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;
|
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import './samples/node-api'
|
import './samples/node-api'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import '@/components/update-tailwind/tailwind.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<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