mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 03:23:02 +00:00
refactor: batch 1
This commit is contained in:
@@ -1,16 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
|
||||
import { MdAdd, MdDeleteForever, MdFileUpload } from 'react-icons/md';
|
||||
|
||||
export interface AddKeyProps {
|
||||
addKey: () => void;
|
||||
@@ -20,28 +8,20 @@ export interface AddKeyProps {
|
||||
|
||||
export function AddKey({ addKey, importKeyFromFile, clearKeys }: AddKeyProps) {
|
||||
return (
|
||||
<HStack pb={2} pt={2}>
|
||||
<ButtonGroup isAttached colorScheme="purple" variant="outline">
|
||||
<Button onClick={addKey} leftIcon={<Icon as={MdAdd} />}>
|
||||
添加一条密钥
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
|
||||
<MenuList>
|
||||
{importKeyFromFile && (
|
||||
<MenuItem onClick={importKeyFromFile} icon={<Icon as={MdFileUpload} boxSize={5} />}>
|
||||
从文件导入密钥…
|
||||
</MenuItem>
|
||||
)}
|
||||
{importKeyFromFile && clearKeys && <MenuDivider />}
|
||||
{clearKeys && (
|
||||
<MenuItem color="red" onClick={clearKeys} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
|
||||
清空密钥
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</ButtonGroup>
|
||||
</HStack>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="join">
|
||||
<button className="btn join-item" onClick={addKey}>
|
||||
<MdAdd /> 添加一条
|
||||
</button>
|
||||
<button className="btn join-item" onClick={importKeyFromFile}>
|
||||
<MdFileUpload />
|
||||
导入数据库…
|
||||
</button>
|
||||
<button className="btn btn-error join-item" onClick={clearKeys}>
|
||||
<MdDeleteForever />
|
||||
清空密钥
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BrowserRouter, NavLink, Route, Routes } from 'react-router';
|
||||
import { MdSettings, MdHome, MdQuestionAnswer } from 'react-icons/md';
|
||||
import { ChakraProvider, Tabs, TabList, TabPanels, Tab, TabPanel, Icon, chakra } from '@chakra-ui/react';
|
||||
|
||||
import { MainTab } from '~/tabs/MainTab';
|
||||
import { SettingsTab } from '~/tabs/SettingsTab';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import { theme } from '~/theme';
|
||||
import { persistSettings } from '~/features/settings/persistSettings';
|
||||
import { setupStore } from '~/store';
|
||||
import { Footer } from '~/components/Footer';
|
||||
@@ -15,43 +14,39 @@ import { FaqTab } from '~/tabs/FaqTab';
|
||||
// Private to this file only.
|
||||
const store = setupStore();
|
||||
|
||||
const tabClassNames = ({ isActive }: { isActive: boolean }) => `tab ${isActive ? 'tab-active' : ''}`;
|
||||
|
||||
export function AppRoot() {
|
||||
useEffect(() => persistSettings(store), []);
|
||||
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<Tabs flex={1} minH={0} display="flex" flexDir="column">
|
||||
<TabList justifyContent="center">
|
||||
<Tab>
|
||||
<Icon as={MdHome} mr="1" />
|
||||
<chakra.span>应用</chakra.span>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Icon as={MdSettings} mr="1" />
|
||||
<chakra.span>设置</chakra.span>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Icon as={MdQuestionAnswer} mr="1" />
|
||||
<chakra.span>答疑</chakra.span>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<div role="tablist" className="tabs tabs-border w-full justify-center">
|
||||
<NavLink to="/" role="tab" className={tabClassNames}>
|
||||
<MdHome />
|
||||
应用
|
||||
</NavLink>
|
||||
<NavLink to="/settings" role="tab" className={tabClassNames}>
|
||||
<MdSettings />
|
||||
设置
|
||||
</NavLink>
|
||||
<NavLink to="/questions" role="tab" className={tabClassNames}>
|
||||
<MdQuestionAnswer />
|
||||
答疑
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<TabPanels overflow="auto" minW={0} flexDir="column" flex={1} display="flex">
|
||||
<TabPanel>
|
||||
<MainTab />
|
||||
</TabPanel>
|
||||
<TabPanel flex={1} display="flex">
|
||||
<SettingsTab />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<FaqTab />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<main className="flex-1 flex justify-center">
|
||||
<Routes>
|
||||
<Route path="/" Component={MainTab} />
|
||||
<Route path="/settings" Component={SettingsTab} />
|
||||
<Route path="/questions" Component={FaqTab} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Provider>
|
||||
</ChakraProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
40
src/components/Dialog.tsx
Normal file
40
src/components/Dialog.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export interface DialogProps {
|
||||
closeButton?: boolean;
|
||||
backdropClose?: boolean;
|
||||
title?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Dialog({ closeButton, backdropClose, title, children, show, onClose }: DialogProps) {
|
||||
const refModel = useRef<HTMLDialogElement>(null);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
refModel.current?.showModal();
|
||||
} else {
|
||||
refModel.current?.close();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<dialog ref={refModel} className="modal">
|
||||
<div className="modal-box">
|
||||
{closeButton && (
|
||||
<form method="dialog" onSubmit={onClose}>
|
||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
)}
|
||||
<h3 className="font-bold text-lg">{title}</h3>
|
||||
{children}
|
||||
</div>
|
||||
{backdropClose && (
|
||||
<form method="dialog" className="modal-backdrop" onSubmit={onClose}>
|
||||
<button>close</button>
|
||||
</form>
|
||||
)}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { AnchorHTMLAttributes } from 'react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Link } from '@chakra-ui/react';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
|
||||
export function ExtLink({ children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) {
|
||||
export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
icon?: boolean;
|
||||
};
|
||||
|
||||
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {
|
||||
return (
|
||||
<Link isExternal {...props} rel="noreferrer noopener nofollow">
|
||||
<a rel="noreferrer noopener nofollow" className={`link ${className}`} {...props}>
|
||||
{children}
|
||||
<ExternalLinkIcon />
|
||||
</Link>
|
||||
{icon && <FiExternalLink />}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classnames from 'classnames';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
export interface FileInputProps {
|
||||
onReceiveFiles: (files: File[]) => void;
|
||||
@@ -14,30 +14,19 @@ export function FileInput({ children, onReceiveFiles }: FileInputProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
<div
|
||||
{...getRootProps()}
|
||||
w="100%"
|
||||
maxW={480}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
transitionDuration="0.5s"
|
||||
p="6"
|
||||
cursor="pointer"
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
alignItems="center"
|
||||
_hover={{
|
||||
borderColor: 'gray.400',
|
||||
bg: 'gray.50',
|
||||
}}
|
||||
{...(isDragActive && {
|
||||
bg: 'blue.50',
|
||||
borderColor: 'blue.700',
|
||||
})}
|
||||
className={classnames(
|
||||
'w-full max-w-xl border rounded-lg transition duration-500 p-6 border-base-300 mx-auto',
|
||||
'cursor-pointer flex flex-col items-center hover:border-gray-400 hover:bg-gray-50',
|
||||
{
|
||||
'bg-blue-50 border-blue-700': isDragActive,
|
||||
},
|
||||
)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Code, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
export function FilePathBlock({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Text as="pre" whiteSpace="pre-wrap" wordBreak="break-all">
|
||||
<Code>{children}</Code>
|
||||
</Text>
|
||||
<pre className="whitespace-pre-wrap break-all">
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,28 @@
|
||||
import { Center, Flex, Link, Text } from '@chakra-ui/react';
|
||||
import { Suspense } from 'react';
|
||||
import { SDKVersion } from './SDKVersion';
|
||||
import { CurrentYear } from './CurrentYear';
|
||||
|
||||
export function Footer() {
|
||||
const appVersionShort = '__APP_VERSION_SHORT__';
|
||||
return (
|
||||
<Center
|
||||
fontSize="sm"
|
||||
textAlign="center"
|
||||
bottom="0"
|
||||
w="full"
|
||||
pt="3"
|
||||
pb="3"
|
||||
borderTop="1px solid"
|
||||
borderColor="gray.300"
|
||||
bg="gray.100"
|
||||
color="gray.800"
|
||||
flexDir="column"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Flex as={Text}>
|
||||
<Link href="https://git.unlock-music.dev/um/um-react" isExternal>
|
||||
<footer className="flex flex-col text-center p-4 bg-base-200">
|
||||
<p className="flex flex-row justify-center items-center h-[1em]">
|
||||
<a className="link link-info mr-1" href="https://git.unlock-music.dev/um/um-react">
|
||||
音乐解锁
|
||||
</Link>
|
||||
{' (__APP_VERSION_SHORT__'}
|
||||
<Suspense>
|
||||
<SDKVersion />
|
||||
</Suspense>
|
||||
{') - 移除已购音乐的加密保护。'}
|
||||
</Flex>
|
||||
<Text>
|
||||
</a>
|
||||
(v{appVersionShort}
|
||||
<SDKVersion />) - 移除已购音乐的加密保护。
|
||||
</p>
|
||||
<p>
|
||||
{'© 2019 - '}
|
||||
<CurrentYear />{' '}
|
||||
<Link href="https://git.unlock-music.dev/um" isExternal>
|
||||
<a className="link link-info" href="https://git.unlock-music.dev/um">
|
||||
UnlockMusic 团队
|
||||
</Link>
|
||||
</a>
|
||||
{' | '}
|
||||
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
|
||||
<a className="link link-info" href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE">
|
||||
使用 MIT 授权协议
|
||||
</Link>
|
||||
</Text>
|
||||
</Center>
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Tabs,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { FileInput } from '~/components/FileInput';
|
||||
|
||||
@@ -18,39 +7,43 @@ export interface ImportSecretModalProps {
|
||||
children: React.ReactNode;
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
onImport: (file: File) => void|Promise<void>;
|
||||
onImport: (file: File) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ImportSecretModal({ clientName, children, show, onClose, onImport }: ImportSecretModalProps) {
|
||||
const handleFileReceived = (files: File[]) => {
|
||||
const promise = onImport(files[0]);
|
||||
if (promise instanceof Promise) {
|
||||
promise.catch(err => {
|
||||
promise.catch((err) => {
|
||||
console.error('could not import: ', err);
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={show} onClose={onClose} closeOnOverlayClick={false} scrollBehavior="inside" size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>从文件导入密钥</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<Flex as={ModalBody} gap={2} flexDir="column" flex={1}>
|
||||
<Center>
|
||||
<FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput>
|
||||
</Center>
|
||||
const refModel = useRef<HTMLDialogElement>(null);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
refModel.current?.showModal();
|
||||
} else {
|
||||
refModel.current?.close();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
<Text as="div" mt={2}>
|
||||
选择你的{clientName && <>「{clientName}」</>}客户端平台以查看对应说明:
|
||||
</Text>
|
||||
<Flex as={Tabs} variant="enclosed" flexDir="column" flex={1} minH={0}>
|
||||
{children}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
return (
|
||||
<dialog ref={refModel} className="modal">
|
||||
<div className="modal-box">
|
||||
<form method="dialog" onSubmit={() => onClose()}>
|
||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<h3 className="font-bold text-lg">从文件导入密钥</h3>
|
||||
<div className="py-4 flex flex-col gap-2 flex-1">
|
||||
<FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput>
|
||||
|
||||
<div className="mt-2">选择你的{clientName && <>「{clientName}」</>}客户端平台以查看对应说明:</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
import { InfoOutlineIcon } from '@chakra-ui/icons';
|
||||
import { Tooltip, VStack, Text, Flex } from '@chakra-ui/react';
|
||||
import { MdInfoOutline } from 'react-icons/md';
|
||||
import { workerClientBus } from '~/decrypt-worker/client';
|
||||
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants';
|
||||
|
||||
import usePromise from 'react-promise-suspense';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const getSDKVersion = async (): Promise<string> => {
|
||||
return workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.VERSION, null);
|
||||
};
|
||||
|
||||
export function SDKVersion() {
|
||||
const sdkVersion = usePromise(getSDKVersion, []);
|
||||
const refDialog = useRef<HTMLDialogElement>(null);
|
||||
const [sdkVersion, setSdkVersion] = useState('...');
|
||||
useEffect(() => {
|
||||
getSDKVersion().then(setSdkVersion);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex as="span" pl="1" alignItems="center" data-testid="sdk-version">
|
||||
<Tooltip
|
||||
hasArrow
|
||||
placement="top"
|
||||
label={
|
||||
<VStack>
|
||||
<Text>App: __APP_VERSION__</Text>
|
||||
<Text>SDK: {sdkVersion}</Text>
|
||||
</VStack>
|
||||
}
|
||||
bg="gray.300"
|
||||
color="black"
|
||||
>
|
||||
<InfoOutlineIcon />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<>
|
||||
<span className="btn btn-ghost inline-flex p-0 h-[1em]" onClick={() => refDialog.current?.showModal()}>
|
||||
<MdInfoOutline />
|
||||
</span>
|
||||
|
||||
<dialog ref={refDialog} className="modal text-left">
|
||||
<div className="modal-box">
|
||||
<form method="dialog">
|
||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<h3 className="font-bold text-lg">详细信息</h3>
|
||||
|
||||
<p>App: __APP_VERSION__</p>
|
||||
<p>SDK: {sdkVersion}</p>
|
||||
</div>
|
||||
<form method="dialog" className="modal-backdrop">
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, Text } from '@chakra-ui/react';
|
||||
import { UnlockIcon } from '@chakra-ui/icons';
|
||||
import { FiUnlock } from 'react-icons/fi';
|
||||
|
||||
import { useAppDispatch } from '~/hooks';
|
||||
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
|
||||
@@ -12,7 +11,7 @@ export function SelectFile() {
|
||||
console.debug(
|
||||
'react-dropzone/onDropAccepted(%o, %o)',
|
||||
files.length,
|
||||
files.map((x) => x.name)
|
||||
files.map((x) => x.name),
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
@@ -26,7 +25,7 @@ export function SelectFile() {
|
||||
id: fileId,
|
||||
blobURI,
|
||||
fileName,
|
||||
})
|
||||
}),
|
||||
);
|
||||
dispatch(processFile({ fileId }));
|
||||
}
|
||||
@@ -34,19 +33,13 @@ export function SelectFile() {
|
||||
|
||||
return (
|
||||
<FileInput multiple onReceiveFiles={handleFileReceived}>
|
||||
<Box pb={3}>
|
||||
<UnlockIcon boxSize={8} />
|
||||
</Box>
|
||||
<Text as="div" textAlign="center">
|
||||
<FiUnlock className="size-8 mb-4" />
|
||||
<p className="text-center">
|
||||
拖放或
|
||||
<Text as="span" color="teal.400">
|
||||
点我选择
|
||||
</Text>
|
||||
<span className="text-teal-700 font-semibold">点我选择</span>
|
||||
需要解密的文件
|
||||
<Text fontSize="sm" opacity="50%">
|
||||
在浏览器内对文件进行解锁,零上传
|
||||
</Text>
|
||||
</Text>
|
||||
</p>
|
||||
<p className="text-sm opacity-50 m-0">在浏览器内对文件进行解锁,零上传</p>
|
||||
</FileInput>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user