refactor: batch 1

This commit is contained in:
鲁树人
2025-05-17 05:59:39 +09:00
parent 089d66cbf4
commit 13c669b4ea
23 changed files with 828 additions and 533 deletions

17
src/App.css Normal file
View File

@@ -0,0 +1,17 @@
@import 'tailwindcss';
@plugin "daisyui";
@theme {
--font-display:
system-ui, Sarasa UI SC, Source Han Sans CN, Noto Sans CJK SC, sans-serif, Apple Color Emoji, Segoe UI Emoji;
--font-mono:
ui-monospace, Consolas, Sarasa Mono CJK SC, Sarasa UI SC, Source Han Sans CN, Noto Sans CJK SC, Microsoft YaHei UI,
monospace, Apple Color Emoji, Segoe UI Emoji;
}
#root {
display: flex;
flex-direction: column;
height: 100vh;
display: flex;
flex-direction: column;
}

View File

@@ -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>
);
}

View File

@@ -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
View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -1,5 +1,3 @@
import { VStack } from '@chakra-ui/react';
import { selectFiles } from './fileListingSlice';
import { useAppSelector } from '~/hooks';
import { FileRow } from './FileRow';
@@ -8,10 +6,10 @@ export function FileListing() {
const files = useAppSelector(selectFiles);
return (
<VStack>
<div className="flex flex-row flex-wrap gap-8">
{Object.entries(files).map(([id, file]) => (
<FileRow key={id} id={id} file={file} />
))}
</VStack>
</div>
);
}

View File

@@ -1,24 +1,9 @@
import { useRef } from 'react';
import {
Box,
Button,
Card,
CardBody,
Collapse,
GridItem,
Link,
VStack,
Wrap,
WrapItem,
useDisclosure,
} from '@chakra-ui/react';
import { FileRowResponsiveGrid } from './FileRowResponsiveGrid';
import { DecryptedAudioFile, deleteFile, ProcessState } from './fileListingSlice';
import { useAppDispatch } from '~/hooks';
import { AnimationDefinition } from 'framer-motion';
import { AlbumImage } from './AlbumImage';
import { SongMetadata } from './SongMetadata';
import { FileError } from './FileError';
import classNames from 'classnames';
interface FileRowProps {
id: string;
@@ -26,7 +11,7 @@ interface FileRowProps {
}
export function FileRow({ id, file }: FileRowProps) {
const { isOpen, onClose } = useDisclosure({ defaultIsOpen: true });
// const { isOpen, onClose } = useDisclosure({ defaultIsOpen: true });
const dispatch = useAppDispatch();
const isDecrypted = file.state === ProcessState.COMPLETE;
const metadata = file.metadata;
@@ -35,81 +20,54 @@ export function FileRow({ id, file }: FileRowProps) {
const decryptedName = nameWithoutExt + '.' + file.ext;
const audioPlayerRef = useRef<HTMLAudioElement>(null);
const togglePlay = () => {
const player = audioPlayerRef.current;
if (!player) {
return;
}
if (player.paused) {
player.play();
} else {
player.pause();
}
};
const onCollapseAnimationComplete = (definition: AnimationDefinition) => {
const _onCollapseAnimationComplete = (definition: AnimationDefinition) => {
if (definition === 'exit') {
dispatch(deleteFile({ id }));
}
};
return (
<Collapse
in={isOpen}
animateOpacity
unmountOnExit
startingHeight={0}
onAnimationComplete={onCollapseAnimationComplete}
style={{ width: '100%', padding: '0.25rem' }}
>
<Card w="full" data-testid="file-row">
<CardBody>
<FileRowResponsiveGrid>
<GridItem area="cover">
<AlbumImage name={metadata?.album} url={metadata?.cover} />
</GridItem>
<GridItem area="title">
<Box w="full" as="h4" fontWeight="semibold" mt="1" textAlign={{ base: 'center', md: 'left' }}>
<span data-testid="audio-meta-song-name">{metadata?.name ?? nameWithoutExt}</span>
</Box>
</GridItem>
<GridItem area="meta">
{isDecrypted && metadata && <SongMetadata metadata={metadata} />}
{file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />}
</GridItem>
<GridItem area="action" alignSelf="center">
<VStack>
{file.decrypted && <audio controls autoPlay={false} src={file.decrypted} ref={audioPlayerRef} />}
<div className="card bg-base-100 shadow-sm w-full md:w-[30%] " data-testid="file-row">
<div className="card-body items-center text-center px-2">
<h2
className="card-title overflow-hidden text-ellipsis block max-w-full whitespace-nowrap"
data-testid="audio-meta-song-name"
>
{metadata?.name ?? nameWithoutExt}
</h2>
<Wrap>
{isDecrypted && (
<>
<WrapItem>
<Button type="button" onClick={togglePlay}>
/
</Button>
</WrapItem>
<WrapItem>
{file.decrypted && (
<Link href={file.decrypted} download={decryptedName}>
<Button as="span"></Button>
</Link>
)}
</WrapItem>
</>
)}
<WrapItem>
<Button type="button" onClick={onClose}>
</Button>
</WrapItem>
</Wrap>
</VStack>
</GridItem>
</FileRowResponsiveGrid>
</CardBody>
</Card>
</Collapse>
<div>
{file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />}
{isDecrypted && (
<audio
className="max-w-[100%]"
aria-disabled={!file.decrypted}
controls
autoPlay={false}
src={file.decrypted}
ref={audioPlayerRef}
/>
)}
</div>
<div className="card-actions justify-end">
<a
href={file.decrypted}
download={decryptedName}
title={`下载: ${decryptedName}`}
className={classNames('btn', {
'btn-primary': file.decrypted,
'cursor-not-allowed pointer-events-none': !file.decrypted,
})}
data-testid="audio-download"
>
</a>
<button type="button" className="btn btn-error" onClick={() => dispatch(deleteFile({ id }))}>
</button>
</div>
</div>
</div>
);
}

View File

@@ -19,7 +19,6 @@ import {
TabPanels,
Tabs,
Text,
useBreakpointValue,
useToast,
VStack,
} from '@chakra-ui/react';
@@ -47,11 +46,7 @@ const TABS: { name: string; Tab: FC }[] = [
export function Settings() {
const toast = useToast();
const dispatch = useAppDispatch();
const isLargeWidthDevice =
useBreakpointValue({
base: false,
lg: true,
}) ?? false;
const isLargeWidthDevice = false;
const [tabIndex, setTabIndex] = useState(0);
const handleTabChange = (idx: number) => {

View File

@@ -1,31 +1,9 @@
import {
Box,
Button,
ButtonGroup,
Checkbox,
Flex,
Heading,
HStack,
Icon,
IconButton,
List,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Select,
Text,
Tooltip,
useToast,
} from '@chakra-ui/react';
import { Select, useToast } from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux';
import { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys, qmc2ImportKeys } from '../settingsSlice';
import { selectStagingQMCv2Settings } from '../settingsSelector';
import React, { useState } from 'react';
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
import { QMCv2EKeyItem } from './QMCv2/QMCv2EKeyItem';
import { InfoOutlineIcon } from '@chakra-ui/icons';
import { ImportSecretModal } from '~/components/ImportSecretModal';
import { StagingQMCv2Key } from '../keyFormats';
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
@@ -33,8 +11,11 @@ import { parseAndroidQmEKey } from '~/util/mmkv/qm';
import { getFileName } from '~/util/pathHelper';
import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions';
import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions';
import { AddKey } from '~/components/AddKey';
import { Dialog } from '~/components/Dialog';
export function PanelQMCv2Key() {
const [showFuzzyNameSearchInfo, setShowFuzzyNameSearchInfo] = useState(false);
const toast = useToast();
const dispatch = useDispatch();
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
@@ -93,73 +74,64 @@ export function PanelQMCv2Key() {
};
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
QMCv2
</Heading>
<div className="flex min-h-0 flex-col flex-1">
<h2 className="text-2xl font-bold">QMCv2 </h2>
<Text>
QQ FM QMCv2使QQ Mac iOS 使
FM线
</Text>
<p>
<span>QQ FM QMCv2</span>
<span>
使QQ Mac iOS 使 FM
线
</span>
</p>
<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>
<MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}>
</MenuItem>
<MenuDivider />
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
</MenuItem>
</MenuList>
</Menu>
</ButtonGroup>
<div className="flex flex-row gap-2 items-center">
<label className="label">
<input
className="checkbox"
type="checkbox"
checked={allowFuzzyNameSearch}
onChange={handleAllowFuzzyNameSearchCheckbox}
/>
</label>
<button className="btn btn-info btn-sm" type="button" onClick={() => setShowFuzzyNameSearchInfo(true)}>
?
</button>
<Dialog
closeButton
backdropClose
show={showFuzzyNameSearchInfo}
onClose={() => setShowFuzzyNameSearchInfo(false)}
title="莱文斯坦距离"
>
<p>使</p>
<p>
使
<ruby>
<rp> (</rp>
<rt>Levenshtein distance</rt>
<rp>)</rp>
</ruby>
</p>
<p></p>
<p></p>
</Dialog>
</div>
<HStack>
<Checkbox isChecked={allowFuzzyNameSearch} onChange={handleAllowFuzzyNameSearchCheckbox}>
<Text></Text>
</Checkbox>
<Tooltip
hasArrow
closeOnClick={false}
label={
<Box>
<Text>使</Text>
<Text>
使
<ruby>
<rp> (</rp>
<rt>Levenshtein distance</rt>
<rp>)</rp>
</ruby>
</Text>
<Text></Text>
<Text></Text>
</Box>
}
>
<InfoOutlineIcon />
</Tooltip>
</HStack>
</HStack>
<h3 className="mt-2 text-1xl font-bold"></h3>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
<Box flex={1} minH={0} overflow="auto" pr="4">
<List spacing={3}>
<div className="flex-1 min-h-0 overflow-auto pr-4">
<ul className="list bg-base-100 rounded-box shadow-md">
{qmc2Keys.map(({ id, ekey, name }, i) => (
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
))}
</List>
{qmc2Keys.length === 0 && <Text></Text>}
</Box>
</ul>
{qmc2Keys.length === 0 && <p className="p-4 pb-2 text-xs tracking-wide"></p>}
</div>
<ImportSecretModal
clientName={
@@ -181,6 +153,6 @@ export function PanelQMCv2Key() {
{secretType === 'qm' && <QMCv2QQMusicAllInstructions />}
{secretType === 'douban' && <QMCv2DoubanAllInstructions />}
</ImportSecretModal>
</Flex>
</div>
);
}

View File

@@ -1,15 +1,3 @@
import {
HStack,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
ListItem,
Text,
VStack,
} from '@chakra-ui/react';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { qmc2DeleteKey, qmc2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
@@ -22,48 +10,45 @@ export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: st
dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
const deleteKey = () => dispatch(qmc2DeleteKey({ id }));
return (
<ListItem mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}>
<HStack>
<Text w="2em" textAlign="center">
{i + 1}
</Text>
const isValidEKey = [364, 704].includes(ekey.length);
<VStack flex={1}>
<Input
variant="flushed"
return (
<li className="list-row items-center">
<div className="flex items-center justify-center w-8 h-8 text-sm font-bold text-gray-500 bg-gray-200 rounded-full">
{i + 1}
</div>
<div className="join join-vertical flex-1">
<label className="input w-full rounded-tl-md rounded-tr-md">
<span className="cursor-default select-none"></span>
<input
type="text"
className="font-mono"
placeholder="文件名,包括后缀名。如 “AAA - BBB.mflac”"
value={name}
onChange={(e) => updateKey('name', e)}
/>
</label>
<label className="input w-full rounded-bl-md rounded-br-md mt-[-1px]">
<span className="cursor-default inline-flex items-center gap-1 select-none">
<MdVpnKey />
</span>
<input
type="text"
className="font-mono"
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
value={ekey}
onChange={(e) => updateKey('ekey', e)}
/>
<span className={isValidEKey ? 'text-green-600' : 'text-red-600'}>
<code>{ekey.length || '?'}</code>
</span>
</label>
</div>
<InputGroup size="xs">
<InputLeftElement pr="2">
<Icon as={MdVpnKey} />
</InputLeftElement>
<Input
variant="flushed"
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
value={ekey}
onChange={(e) => updateKey('ekey', e)}
/>
<InputRightElement>
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
<code>{ekey.length || '?'}</code>
</Text>
</InputRightElement>
</InputGroup>
</VStack>
<IconButton
aria-label="删除该密钥"
icon={<Icon as={MdDelete} boxSize={6} />}
variant="ghost"
colorScheme="red"
type="button"
onClick={deleteKey}
/>
</HStack>
</ListItem>
<button type="button" className="btn btn-error btn-sm px-1 btn-outline" onClick={deleteKey}>
<MdDelete className="size-6" />
</button>
</li>
);
});

View File

@@ -1,4 +1,5 @@
import './pwa';
import './App.css';
import React from 'react';
import ReactDOM from 'react-dom/client';

View File

@@ -1,4 +1,4 @@
import { Alert, AlertIcon, Box, Button, Flex, Text, VStack } from '@chakra-ui/react';
import { RiErrorWarningLine } from 'react-icons/ri';
import { SelectFile } from '../components/SelectFile';
import { FileListing } from '~/features/file-listing/FileListing';
@@ -14,29 +14,32 @@ export function MainTab() {
};
return (
<Box h="full" w="full" pt="4">
<VStack gap="3">
<div className="size-full max-w-[80%] self-center pt-4">
<div className="gap-3 flex flex-col">
{isSettingsNotSaved && (
<Alert borderRadius={7} maxW={400} status="warning">
<AlertIcon />
<Flex flexDir="row" alignItems="center" flexGrow={1} justifyContent="space-between">
<Text m={0}>
<br />
</Text>
<Button type="button" ml={3} size="md" onClick={onClickSaveSettings}>
<div role="alert" className="alert alert-warning gap-2">
<span className="md:flex flex-row items-center gap-1">
<RiErrorWarningLine className="size-6" />
<span className="font-bold hidden md:inline"></span>
</span>
<div>
<span className="block font-bold md:hidden"></span>
<span></span>
<span className="inline-block"></span>
</div>
<div>
<button type="button" className="btn btn-primary btn-sm" onClick={onClickSaveSettings}>
</Button>
</Flex>
</Alert>
</button>
</div>
</div>
)}
<SelectFile />
<Box w="full">
<div className="w-full mt-4">
<FileListing />
</Box>
</VStack>
</Box>
</div>
</div>
</div>
);
}

View File

@@ -1,15 +1,9 @@
import { Container, Flex, useBreakpointValue } from '@chakra-ui/react';
import { Settings } from '~/features/settings/Settings';
export function SettingsTab() {
const containerProps = useBreakpointValue({
base: { p: '0' },
lg: { p: undefined },
});
return (
<Container as={Flex} maxW="container.lg" {...containerProps}>
<div className="flex p-0">
<Settings />
</Container>
</div>
);
}