mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 11:33:02 +00:00
refactor: batch 1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user