feat: implement shadow DOM; make notification hookable
This commit is contained in:
parent
9a5ec9f0a1
commit
313d24d808
13
index.html
13
index.html
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="https://vitejs.dev/logo.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite + React + TS</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</html>
|
|
@ -11,6 +11,7 @@
|
|||||||
"lint:fix": "eslint . --fix"
|
"lint:fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/cache": "^11.10.5",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@mui/icons-material": "^5.11.11",
|
"@mui/icons-material": "^5.11.11",
|
||||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -1,6 +1,7 @@
|
|||||||
lockfileVersion: 5.4
|
lockfileVersion: 5.4
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@emotion/cache': ^11.10.5
|
||||||
'@emotion/react': ^11.10.6
|
'@emotion/react': ^11.10.6
|
||||||
'@emotion/styled': ^11.10.6
|
'@emotion/styled': ^11.10.6
|
||||||
'@mui/icons-material': ^5.11.11
|
'@mui/icons-material': ^5.11.11
|
||||||
@ -17,6 +18,7 @@ specifiers:
|
|||||||
vite-plugin-monkey: 1.1.4
|
vite-plugin-monkey: 1.1.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@emotion/cache': 11.10.5
|
||||||
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
|
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
|
||||||
'@emotion/styled': 11.10.6_oouaibmszuch5k64ms7uxp2aia
|
'@emotion/styled': 11.10.6_oouaibmszuch5k64ms7uxp2aia
|
||||||
'@mui/icons-material': 5.11.11_4lyzeezzeeal3x6jtb4ni26w7u
|
'@mui/icons-material': 5.11.11_4lyzeezzeeal3x6jtb4ni26w7u
|
||||||
|
66
src/App.tsx
66
src/App.tsx
@ -1,17 +1,18 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ContentCopy, ContentPaste, DeleteForever } from '@mui/icons-material';
|
|
||||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||||
import {
|
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||||
Divider,
|
import ContentPaste from '@mui/icons-material/ContentPaste';
|
||||||
IconButton,
|
import DeleteForever from '@mui/icons-material/DeleteForever';
|
||||||
ListItemIcon,
|
import Alert from '@mui/material/Alert';
|
||||||
ListItemText,
|
import Divider from '@mui/material/Divider';
|
||||||
Menu,
|
import IconButton from '@mui/material/IconButton';
|
||||||
MenuItem,
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
} from '@mui/material';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import notification from './components/notification';
|
import useToast from './hooks/useToast';
|
||||||
|
|
||||||
const Z_INDEX_MAX = 2 ** 31 - 1;
|
const Z_INDEX_MAX = 2 ** 31 - 1;
|
||||||
|
|
||||||
@ -36,6 +37,15 @@ const CustomMenu = styled(Menu)`
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const menuOpen = Boolean(anchorEl);
|
const menuOpen = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const {
|
||||||
|
open: toastOpen,
|
||||||
|
message: toastMessage,
|
||||||
|
severity: toastSeverity,
|
||||||
|
handleOpen: handleToastOpen,
|
||||||
|
handleClose: handleToastClose,
|
||||||
|
} = useToast();
|
||||||
|
|
||||||
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
@ -89,9 +99,9 @@ const App = () => {
|
|||||||
const cookie = await getCookie();
|
const cookie = await getCookie();
|
||||||
const exportSessionData = JSON.stringify(cookie);
|
const exportSessionData = JSON.stringify(cookie);
|
||||||
copyToClipboard(exportSessionData);
|
copyToClipboard(exportSessionData);
|
||||||
notification.success({ message: 'Session 数据已复制到剪贴板' });
|
handleToastOpen('Session 数据已复制到剪贴板', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({ message: '获取 Cookie 失败' });
|
handleToastOpen('获取 Cookie 失败', 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,18 +113,18 @@ const App = () => {
|
|||||||
cookie.forEach(async (item) => {
|
cookie.forEach(async (item) => {
|
||||||
await setCookie(item);
|
await setCookie(item);
|
||||||
});
|
});
|
||||||
notification.success({ message: 'Session 数据已导入' });
|
handleToastOpen('Session 数据已导入', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({ message: '导入 Cookie 失败' });
|
handleToastOpen('导入 Cookie 失败', 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSession = async () => {
|
const deleteSession = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteCookie();
|
await deleteCookie();
|
||||||
notification.success({ message: 'Session 数据已删除' });
|
handleToastOpen('Session 数据已删除', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({ message: '删除 Cookie 失败' });
|
handleToastOpen('删除 Cookie 失败', 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -134,7 +144,8 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
|
{/* 工具按钮 */}
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="primary"
|
color="primary"
|
||||||
id="custom-button"
|
id="custom-button"
|
||||||
@ -146,6 +157,7 @@ const App = () => {
|
|||||||
<AutoFixHighIcon />
|
<AutoFixHighIcon />
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
|
|
||||||
|
{/* 工具菜单 */}
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
id="custom-menu"
|
id="custom-menu"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@ -178,7 +190,23 @@ const App = () => {
|
|||||||
<ListItemText>清除 Session 数据</ListItemText>
|
<ListItemText>清除 Session 数据</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
|
||||||
|
{/* 提示 */}
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||||
|
open={toastOpen}
|
||||||
|
autoHideDuration={2000}
|
||||||
|
onClose={handleToastClose}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={handleToastClose}
|
||||||
|
severity={toastSeverity}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{toastMessage}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
import { Alert, Snackbar } from '@mui/material';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
const defaultDuration = 2000;
|
|
||||||
|
|
||||||
interface NotificationProps {
|
|
||||||
message: string;
|
|
||||||
type: 'success' | 'error' | 'info' | 'warning';
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConfigProps {
|
|
||||||
message: string;
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Notification = (props: NotificationProps) => {
|
|
||||||
const { message, type, duration } = props;
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Snackbar
|
|
||||||
open={open}
|
|
||||||
autoHideDuration={duration}
|
|
||||||
onClose={handleClose}
|
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
||||||
>
|
|
||||||
<Alert severity={type} variant="filled" onClose={handleClose}>
|
|
||||||
{message}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const notification = {
|
|
||||||
dom: null,
|
|
||||||
|
|
||||||
success({ message, duration = defaultDuration }: ConfigProps) {
|
|
||||||
const dom = document.createElement('div');
|
|
||||||
const JSXdom = (
|
|
||||||
<Notification message={message} duration={duration} type="success" />
|
|
||||||
);
|
|
||||||
ReactDOM.render(JSXdom, dom);
|
|
||||||
document.body.appendChild(dom);
|
|
||||||
},
|
|
||||||
|
|
||||||
error({ message, duration = defaultDuration }: ConfigProps) {
|
|
||||||
const dom = document.createElement('div');
|
|
||||||
const JSXdom = (
|
|
||||||
<Notification message={message} duration={duration} type="error" />
|
|
||||||
);
|
|
||||||
ReactDOM.render(JSXdom, dom);
|
|
||||||
document.body.appendChild(dom);
|
|
||||||
},
|
|
||||||
|
|
||||||
warning({ message, duration = defaultDuration }: ConfigProps) {
|
|
||||||
const dom = document.createElement('div');
|
|
||||||
const JSXdom = (
|
|
||||||
<Notification message={message} duration={duration} type="warning" />
|
|
||||||
);
|
|
||||||
ReactDOM.render(JSXdom, dom);
|
|
||||||
document.body.appendChild(dom);
|
|
||||||
},
|
|
||||||
|
|
||||||
info({ message, duration = defaultDuration }: ConfigProps) {
|
|
||||||
const dom = document.createElement('div');
|
|
||||||
const JSXdom = (
|
|
||||||
<Notification message={message} duration={duration} type="warning" />
|
|
||||||
);
|
|
||||||
ReactDOM.render(JSXdom, dom);
|
|
||||||
document.body.appendChild(dom);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default notification;
|
|
43
src/hooks/useToast.ts
Normal file
43
src/hooks/useToast.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type SeverityType = 'success' | 'error' | 'info' | 'warning';
|
||||||
|
|
||||||
|
interface ToastState {
|
||||||
|
open: boolean;
|
||||||
|
message: string;
|
||||||
|
severity: SeverityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastActions {
|
||||||
|
handleOpen: (toastMessage: string, toastSeverity: SeverityType) => void;
|
||||||
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = ToastState & ToastActions;
|
||||||
|
|
||||||
|
const useToast = (): Toast => {
|
||||||
|
const [state, setState] = useState<ToastState>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpen = (toastMessage: string, toastSeverity: SeverityType) => {
|
||||||
|
setState({
|
||||||
|
open: true,
|
||||||
|
message: toastMessage,
|
||||||
|
severity: toastSeverity,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
open: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...state, handleOpen, handleClose };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useToast;
|
67
src/main.tsx
67
src/main.tsx
@ -1,16 +1,67 @@
|
|||||||
|
import createCache from '@emotion/cache';
|
||||||
|
import { CacheProvider } from '@emotion/react';
|
||||||
|
import createTheme from '@mui/material/styles/createTheme';
|
||||||
|
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
ReactDOM.createRoot(
|
// 注入应用容器
|
||||||
(() => {
|
const container = document.createElement('div');
|
||||||
const app = document.createElement('div');
|
container.id = 'monkey';
|
||||||
document.body.append(app);
|
container.style.all = 'initial';
|
||||||
return app;
|
document.body.appendChild(container);
|
||||||
})()
|
|
||||||
).render(
|
// 创建 shadow DOM
|
||||||
|
const shadowContainer = container.attachShadow({ mode: 'closed' });
|
||||||
|
|
||||||
|
// 将应用添加到 shadow DOM 中
|
||||||
|
const app = document.createElement('div');
|
||||||
|
shadowContainer.appendChild(app);
|
||||||
|
|
||||||
|
// 将 emotion 样式添加到 shadow DOM 中
|
||||||
|
const emotionRoot = document.createElement('style');
|
||||||
|
shadowContainer.appendChild(emotionRoot);
|
||||||
|
|
||||||
|
const cache = createCache({
|
||||||
|
key: 'css',
|
||||||
|
prepend: true,
|
||||||
|
container: emotionRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取浏览器的字体大小
|
||||||
|
const { fontSize } = window.getComputedStyle(document.documentElement);
|
||||||
|
const htmlFontSize = parseFloat(fontSize);
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
components: {
|
||||||
|
MuiPopover: {
|
||||||
|
defaultProps: {
|
||||||
|
container: app,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPopper: {
|
||||||
|
defaultProps: {
|
||||||
|
container: app,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiModal: {
|
||||||
|
defaultProps: {
|
||||||
|
container: app,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
htmlFontSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactDOM.createRoot(app).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<CacheProvider value={cache}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
<App />
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</CacheProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
name: 'Session Magician',
|
name: 'Session Magician',
|
||||||
namespace: 'https://www.imbytecat.com/',
|
namespace: 'https://www.imbytecat.com/',
|
||||||
icon: 'https://vitejs.dev/logo.svg',
|
icon: 'https://vitejs.dev/logo.svg',
|
||||||
version: '2.2.3',
|
version: '3.1.0',
|
||||||
description: 'Session Magician & Session Tools & Export/Import Sessions',
|
description: 'Session Magician & Session Tools & Export/Import Sessions',
|
||||||
author: 'imbytecat',
|
author: 'imbytecat',
|
||||||
match: ['*://*/*'],
|
match: ['*://*/*'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user