refactor: batch 2
All checks were successful
Build and Deploy / build (push) Successful in 2m4s

This commit is contained in:
鲁树人 2025-05-17 11:20:52 +09:00
parent 246ba48135
commit 75b43e1e84
16 changed files with 384 additions and 272 deletions

View File

@ -36,6 +36,7 @@
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router": "^7.6.0", "react-router": "^7.6.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-toastify": "^11.0.5",
"sass": "^1.89.0", "sass": "^1.89.0",
"sql.js": "^1.13.0", "sql.js": "^1.13.0",
"workbox-build": "^7.3.0" "workbox-build": "^7.3.0"

21
pnpm-lock.yaml generated
View File

@ -77,6 +77,9 @@ importers:
react-syntax-highlighter: react-syntax-highlighter:
specifier: ^15.6.1 specifier: ^15.6.1
version: 15.6.1(react@19.1.0) version: 15.6.1(react@19.1.0)
react-toastify:
specifier: ^11.0.5
version: 11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
sass: sass:
specifier: ^1.89.0 specifier: ^1.89.0
version: 1.89.0 version: 1.89.0
@ -1971,6 +1974,10 @@ packages:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'} engines: {node: '>=18'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -3414,6 +3421,12 @@ packages:
peerDependencies: peerDependencies:
react: '>= 0.14.0' react: '>= 0.14.0'
react-toastify@11.0.5:
resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==}
peerDependencies:
react: ^18 || ^19
react-dom: ^18 || ^19
react@19.1.0: react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -6102,6 +6115,8 @@ snapshots:
slice-ansi: 5.0.0 slice-ansi: 5.0.0
string-width: 7.2.0 string-width: 7.2.0
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@ -7562,6 +7577,12 @@ snapshots:
react: 19.1.0 react: 19.1.0
refractor: 3.6.0 refractor: 3.6.0
react-toastify@11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
clsx: 2.1.1
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react@19.1.0: {} react@19.1.0: {}
readdirp@4.1.2: {} readdirp@4.1.2: {}

View File

@ -10,6 +10,8 @@ import { persistSettings } from '~/features/settings/persistSettings';
import { setupStore } from '~/store'; import { setupStore } from '~/store';
import { Footer } from '~/components/Footer'; import { Footer } from '~/components/Footer';
import { FaqTab } from '~/tabs/FaqTab'; import { FaqTab } from '~/tabs/FaqTab';
import { SETTINGS_TABS } from '~/features/settings/settingsTabs';
import { Bounce, ToastContainer } from 'react-toastify';
// Private to this file only. // Private to this file only.
const store = setupStore(); const store = setupStore();
@ -40,11 +42,26 @@ export function AppRoot() {
<main className="flex-1 flex justify-center"> <main className="flex-1 flex justify-center">
<Routes> <Routes>
<Route path="/" Component={MainTab} /> <Route path="/" Component={MainTab} />
<Route path="/settings" Component={SettingsTab} /> <Route path="/settings" Component={SettingsTab}>
{Object.entries(SETTINGS_TABS).map(([key, { Tab }]) => (
<Route key={key} path={key} Component={Tab} />
))}
</Route>
<Route path="/questions" Component={FaqTab} /> <Route path="/questions" Component={FaqTab} />
</Routes> </Routes>
</main> </main>
<ToastContainer
position="bottom-center"
autoClose={5000}
newestOnTop
closeOnClick={false}
pauseOnFocusLoss
draggable
theme="colored"
transition={Bounce}
/>
<Footer /> <Footer />
</Provider> </Provider>
</BrowserRouter> </BrowserRouter>

View File

@ -27,7 +27,7 @@ export function Dialog({ closeButton, backdropClose, title, children, show, onCl
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form> </form>
)} )}
<h3 className="font-bold text-lg">{title}</h3> <h3 className="font-bold text-lg pb-3">{title}</h3>
{children} {children}
</div> </div>
{backdropClose && ( {backdropClose && (

View File

@ -7,9 +7,9 @@ export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) { export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {
return ( return (
<a rel="noreferrer noopener nofollow" className={`link ${className}`} {...props}> <a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
{children} {children}
{icon && <FiExternalLink />} {icon && <FiExternalLink className="inline size-sm ml-1" />}
</a> </a>
); );
} }

View File

@ -0,0 +1,25 @@
import { Dialog } from '~/components/Dialog.tsx';
import React, { useState } from 'react';
interface InfoModalProps {
title?: React.ReactNode;
description?: React.ReactNode;
children?: React.ReactNode;
}
export function InfoModal(props: InfoModalProps) {
const { title, description, children } = props;
const [showModal, setShowModal] = useState(false);
return (
<div>
<button className="btn btn-info btn-sm" type="button" onClick={() => setShowModal(true)}>
{children || '这是什么?'}
</button>
<Dialog closeButton backdropClose show={showModal} onClose={() => setShowModal(false)} title={title}>
{description}
</Dialog>
</div>
);
}

View File

@ -0,0 +1,33 @@
import React, { Fragment, useId } from 'react';
export type InstructionTab = {
id: string | number;
label: React.ReactNode;
content: React.ReactNode;
};
export interface InstructionsTabsProps {
tabs: InstructionTab[];
}
export function InstructionsTabs({ tabs }: InstructionsTabsProps) {
const id = useId();
return (
<div className="tabs tabs-lift h-[20rem] pb-4">
{tabs.map(({ id: _tabId, label, content }, index) => (
<Fragment key={_tabId}>
<label className="tab">
<input type="radio" name={id} defaultChecked={index === 0} />
{label}
</label>
<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto">{content}</div>
</Fragment>
))}
{/*<label className="tab">*/}
{/* <input type="radio" name={id} />a*/}
{/*</label>*/}
{/*<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto"></div>*/}
{/*<input type="radio" name={id} className="tab" aria-label="安卓" defaultChecked />*/}
</div>
);
}

View File

@ -0,0 +1,85 @@
import { PiFileAudio } from 'react-icons/pi';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import React from 'react';
export interface KeyInputProps {
sequence: number;
name: string;
value: string;
isValidKey?: boolean;
onSetName: (name: string) => void;
onSetValue: (value: string) => void;
onDelete: () => void;
nameLabel?: React.ReactNode;
valueLabel?: React.ReactNode;
namePlaceholder?: string;
valuePlaceholder?: string;
}
export function KeyInput(props: KeyInputProps) {
const {
nameLabel,
valueLabel,
namePlaceholder,
valuePlaceholder,
sequence,
name,
value,
onSetName,
onSetValue,
onDelete,
isValidKey,
} = props;
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">
{sequence}
</div>
<div className="join join-vertical flex-1">
<label className="input w-full rounded-tl-md rounded-tr-md">
<span className="cucursor-default inline-flex items-center gap-1 select-none">
{nameLabel || (
<>
<PiFileAudio />
</>
)}
</span>
<input
type="text"
className="font-mono"
placeholder={namePlaceholder}
value={name}
onChange={(e) => onSetName(e.target.value)}
/>
</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">
{valueLabel || (
<>
<MdVpnKey />
</>
)}
</span>
<input
type="text"
className="font-mono"
placeholder={valuePlaceholder}
value={value}
onChange={(e) => onSetValue(e.target.value)}
/>
<span className={isValidKey ? 'text-green-600' : 'text-red-600'}>
<code>{value.length || '?'}</code>
</span>
</label>
</div>
<button type="button" className="btn btn-error btn-sm px-1 btn-outline" onClick={onDelete}>
<MdDelete className="size-6" />
</button>
</li>
);
}

20
src/components/Ruby.tsx Normal file
View File

@ -0,0 +1,20 @@
import React from 'react';
export interface RubyProps {
caption: React.ReactNode;
children: React.ReactNode;
className?: string;
}
export function Ruby(props: RubyProps) {
const { caption, children, ...rest } = props;
return (
<ruby {...rest}>
{children}
<rp>(</rp>
<rt>{caption}</rt>
<rp>)</rp>
</ruby>
);
}

View File

@ -1,158 +1,102 @@
import {
Box,
Button,
Center,
chakra,
Flex,
HStack,
Icon,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Spacer,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useToast,
VStack,
} from '@chakra-ui/react';
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
import { useState, type FC } from 'react';
import { MdExpandMore, MdMenu, MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { useAppDispatch, useAppSelector } from '~/hooks'; import { useAppDispatch, useAppSelector } from '~/hooks';
import { commitStagingChange, discardStagingChanges } from './settingsSlice'; import { commitStagingChange, discardStagingChanges } from './settingsSlice';
import { PanelKWMv2Key } from './panels/PanelKWMv2Key';
import { selectIsSettingsNotSaved } from './settingsSelector'; import { selectIsSettingsNotSaved } from './settingsSelector';
import { PanelQingTing } from './panels/PanelQingTing'; import { NavLink, Outlet } from 'react-router';
import { PanelKGGKey } from '~/features/settings/panels/PanelKGGKey.tsx'; import { SETTINGS_TABS } from '~/features/settings/settingsTabs.tsx';
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
const TABS: { name: string; Tab: FC }[] = [ import { toast } from 'react-toastify';
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
{ name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
{ name: 'KGG 密钥', Tab: PanelKGGKey },
{ name: '蜻蜓 FM', Tab: PanelQingTing },
{
name: '其它/待定',
Tab: () => <Text></Text>,
},
];
export function Settings() { export function Settings() {
const toast = useToast();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isLargeWidthDevice = false;
const [tabIndex, setTabIndex] = useState(0);
const handleTabChange = (idx: number) => {
setTabIndex(idx);
};
const handleResetSettings = () => { const handleResetSettings = () => {
dispatch(discardStagingChanges()); dispatch(discardStagingChanges());
toast({ toast.info(() => (
status: 'info', <div>
title: '未储存的设定已舍弃', <h3 className="text-lg font-bold"></h3>
description: '已还原到更改前的状态。', <p className="text-sm"></p>
isClosable: true, </div>
}); ));
}; };
const handleApplySettings = () => { const handleApplySettings = () => {
dispatch(commitStagingChange()); dispatch(commitStagingChange());
toast({ toast.success('设定已应用');
status: 'success',
title: '设定已应用',
isClosable: true,
});
}; };
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved); const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
return ( return (
<Flex flexDir="column" flex={1}> <div className="flex flex-col flex-1 container w-full">
<Menu> <div role="tablist" className="tabs tabs-border w-full justify-center gap-2">
<MenuButton {Object.entries(SETTINGS_TABS).map(([id, { name }]) => (
as={Button} <NavLink className="link link-neutral" key={id} to={`/settings/${id}`} role="tab">
leftIcon={<MdMenu />} {name}
rightIcon={<MdExpandMore />} </NavLink>
colorScheme="gray" ))}
variant="outline" </div>
w="full"
flexShrink={0}
hidden={isLargeWidthDevice}
mb="4"
>
{TABS[tabIndex].name}
</MenuButton>
<Portal>
<MenuList w="100px">
{TABS.map(({ name }, i) => (
<MenuItem key={name} onClick={() => setTabIndex(i)}>
{name}
</MenuItem>
))}
</MenuList>
</Portal>
</Menu>
<Tabs {/* TODO: ensure this flex div does not overflow */}
orientation={isLargeWidthDevice ? 'vertical' : 'horizontal'} <div className="flex flex-1 flex-col h-full overflow-auto">
align="start" <Outlet />
variant="line-i" </div>
display="flex"
flex={1}
index={tabIndex}
onChange={handleTabChange}
>
<TabList hidden={!isLargeWidthDevice} minW="8em" width="8em" textAlign="right" justifyContent="center">
{TABS.map(({ name }) => (
<Tab key={name}>{name}</Tab>
))}
</TabList>
<TabPanels> <footer className="flex flex-row gap-2 w-full py-3">
{TABS.map(({ name, Tab }) => ( <div className="grow">
<Flex as={TabPanel} flex={1} flexDir="column" h="100%" key={name}> {isSettingsNotSaved ? (
<Flex h="100%" flex={1} minH={0}> <span>
<Tab /> <span className="text-red-600"></span>
</Flex> </span>
) : (
<span className="text-base-700"></span>
)}
</div>
<VStack mt="4" alignItems="flex-start" w="full"> <div className="flex flex-row gap-2">
<Flex flexDir="row" gap="2" w="full"> <button
<Center> className="btn btn-sm btn-ghost text-error"
{isSettingsNotSaved ? ( onClick={handleResetSettings}
<Box color="gray"> title="放弃未储存的更改,将设定还原未储存前的状态。"
{' '} >
<chakra.span color="red" wordBreak="keep-all"> <MdOutlineSettingsBackupRestore className="size-4" />
</button>
</chakra.span> <button className="btn btn-sm btn-primary" onClick={handleApplySettings}>
</Box>
) : ( </button>
<Box color="gray"></Box> </div>
)} </footer>
</Center>
<Spacer /> {/* <VStack mt="4" alignItems="flex-start" w="full">*/}
<HStack gap="2" justifyContent="flex-end"> {/* <Flex flexDir="row" gap="2" w="full">*/}
<IconButton {/* <Center>*/}
icon={<Icon as={MdOutlineSettingsBackupRestore} />} {/* {isSettingsNotSaved ? (*/}
onClick={handleResetSettings} {/* <Box color="gray">*/}
colorScheme="red" {/* 有未储存的更改{' '}*/}
variant="ghost" {/* <chakra.span color="red" wordBreak="keep-all">*/}
title="放弃未储存的更改,将设定还原未储存前的状态。" {/* 设定将在保存后生效*/}
aria-label="放弃未储存的更改" {/* </chakra.span>*/}
/> {/* </Box>*/}
<Button onClick={handleApplySettings}></Button> {/* ) : (*/}
</HStack> {/* <Box color="gray">设定将在保存后生效</Box>*/}
</Flex> {/* )}*/}
</VStack> {/* </Center>*/}
</Flex> {/* <Spacer />*/}
))} {/* <HStack gap="2" justifyContent="flex-end">*/}
</TabPanels> {/* <IconButton*/}
</Tabs> {/* icon={<Icon as={MdOutlineSettingsBackupRestore} />}*/}
</Flex> {/* onClick={handleResetSettings}*/}
{/* colorScheme="red"*/}
{/* variant="ghost"*/}
{/* title="放弃未储存的更改,将设定还原未储存前的状态。"*/}
{/* aria-label="放弃未储存的更改"*/}
{/* />*/}
{/* <Button onClick={handleApplySettings}>保存</Button>*/}
{/* </HStack>*/}
{/* </Flex>*/}
{/* </VStack>*/}
{/* </Flex>*/}
{/* ))}*/}
{/* </TabPanels>*/}
{/*</Tabs>*/}
</div>
); );
} }

View File

@ -1,4 +1,4 @@
import { Select, useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys, qmc2ImportKeys } from '../settingsSlice'; import { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys, qmc2ImportKeys } from '../settingsSlice';
import { selectStagingQMCv2Settings } from '../settingsSelector'; import { selectStagingQMCv2Settings } from '../settingsSelector';
@ -12,10 +12,11 @@ import { getFileName } from '~/util/pathHelper';
import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions'; import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions';
import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions'; import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions';
import { AddKey } from '~/components/AddKey'; import { AddKey } from '~/components/AddKey';
import { Dialog } from '~/components/Dialog'; import { InfoModal } from '~/components/InfoModal.tsx';
import { Ruby } from '~/components/Ruby.tsx';
import { ExtLink } from '~/components/ExtLink.tsx';
export function PanelQMCv2Key() { export function PanelQMCv2Key() {
const [showFuzzyNameSearchInfo, setShowFuzzyNameSearchInfo] = useState(false);
const toast = useToast(); const toast = useToast();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings); const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
@ -74,18 +75,13 @@ export function PanelQMCv2Key() {
}; };
return ( return (
<div className="flex min-h-0 flex-col flex-1"> <>
<h2 className="text-2xl font-bold">QMCv2 </h2> <h2 className="text-2xl font-bold">QMCv2 </h2>
<p> <p>QQ FM QMCv2</p>
<span>QQ FM QMCv2</span> <p>QQ Mac iOS FM</p>
<span>
使QQ Mac iOS 使 FM
线
</span>
</p>
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center my-2">
<label className="label"> <label className="label">
<input <input
className="checkbox" className="checkbox"
@ -95,56 +91,51 @@ export function PanelQMCv2Key() {
/> />
</label> </label>
<button className="btn btn-info btn-sm" type="button" onClick={() => setShowFuzzyNameSearchInfo(true)}> <InfoModal
?
</button>
<Dialog
closeButton
backdropClose
show={showFuzzyNameSearchInfo}
onClose={() => setShowFuzzyNameSearchInfo(false)}
title="莱文斯坦距离" title="莱文斯坦距离"
description={
<div>
<p>使</p>
<p>
使
<ExtLink href="https://zh.wikipedia.org/zh-cn/%E8%90%8A%E6%96%87%E6%96%AF%E5%9D%A6%E8%B7%9D%E9%9B%A2">
<Ruby caption="Levenshtein distance"></Ruby>
</ExtLink>
</p>
<p></p>
<p></p>
</div>
}
> >
<p>使</p> ?
<p> </InfoModal>
使
<ruby>
<rp> (</rp>
<rt>Levenshtein distance</rt>
<rp>)</rp>
</ruby>
</p>
<p></p>
<p></p>
</Dialog>
</div> </div>
<h3 className="mt-2 text-1xl font-bold"></h3> <h3 className="mt-2 text-1xl font-bold"></h3>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} /> <AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
<div className="flex-1 min-h-0 overflow-auto pr-4"> <div className="flex-1 min-h-0 overflow-auto pr-4 pt-3">
<ul className="list bg-base-100 rounded-box shadow-md"> {qmc2Keys.length > 0 && (
{qmc2Keys.map(({ id, ekey, name }, i) => ( <ul className="list bg-base-100 rounded-box shadow-md border border-base-300">
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} /> {qmc2Keys.map(({ id, ekey, name }, i) => (
))} <QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
</ul> ))}
{qmc2Keys.length === 0 && <p className="p-4 pb-2 text-xs tracking-wide"></p>} </ul>
)}
{qmc2Keys.length === 0 && <p className="p-4 pb-2 tracking-wide"></p>}
</div> </div>
<ImportSecretModal <ImportSecretModal
clientName={ clientName={
<Select <select
value={secretType} value={secretType}
onChange={(e) => setSecretType(e.target.value as 'qm' | 'douban')} onChange={(e) => setSecretType(e.target.value as 'qm' | 'douban')}
variant="flushed" className="inline mx-1 px-1 border-b border-accent/50 bg-base-100"
display="inline"
css={{ paddingLeft: '0.75rem', width: 'auto' }}
> >
<option value="qm">QQ </option> <option value="qm">QQ </option>
<option value="douban"> FM</option> <option value="douban"> FM</option>
</Select> </select>
} }
show={showImportModal} show={showImportModal}
onClose={() => setShowImportModal(false)} onClose={() => setShowImportModal(false)}
@ -153,6 +144,6 @@ export function PanelQMCv2Key() {
{secretType === 'qm' && <QMCv2QQMusicAllInstructions />} {secretType === 'qm' && <QMCv2QQMusicAllInstructions />}
{secretType === 'douban' && <QMCv2DoubanAllInstructions />} {secretType === 'douban' && <QMCv2DoubanAllInstructions />}
</ImportSecretModal> </ImportSecretModal>
</div> </>
); );
} }

View File

@ -1,10 +1,16 @@
import { Text } from '@chakra-ui/react';
export function InstructionsPC() { export function InstructionsPC() {
return ( return (
<> <>
<Text>使 Windows 19.51 </Text> <p>
<Text>使 Windows 19.57 </Text> 使 <span className="text-primary">19.51 </span>
<mark></mark>
</p>
<p className="mt-2">
使 <span className="text-error">19.57 </span>
<mark></mark>
<br />
</p>
</> </>
); );
} }

View File

@ -1,17 +1,14 @@
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction'; import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
export function QMCv2DoubanAllInstructions() { export function QMCv2DoubanAllInstructions() {
return ( const tabs: InstructionTab[] = [
<> {
<TabList> id: 'android',
<Tab></Tab> label: '安卓',
</TabList> content: <AndroidADBPullInstruction dir="/data/data/com.douban.radio/databases" file="music_audio_play" />,
<TabPanels flex={1} overflow="auto"> },
<TabPanel> ];
<AndroidADBPullInstruction dir="/data/data/com.douban.radio/databases" file="music_audio_play" />
</TabPanel> return <InstructionsTabs tabs={tabs} />;
</TabPanels>
</>
);
} }

View File

@ -1,54 +1,25 @@
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { qmc2DeleteKey, qmc2UpdateKey } from '../../settingsSlice'; import { qmc2DeleteKey, qmc2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks'; import { useAppDispatch } from '~/hooks';
import { memo } from 'react'; import { memo } from 'react';
import { KeyInput } from '~/components/KeyInput.tsx';
export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: string; ekey: string; i: number }) => { export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: string; ekey: string; i: number }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const updateKey = (prop: 'name' | 'ekey', e: React.ChangeEvent<HTMLInputElement>) => const ekeyLen = ekey.length;
dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value })); const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
const deleteKey = () => dispatch(qmc2DeleteKey({ id }));
const isValidEKey = [364, 704].includes(ekey.length);
return ( return (
<li className="list-row items-center"> <KeyInput
<div className="flex items-center justify-center w-8 h-8 text-sm font-bold text-gray-500 bg-gray-200 rounded-full"> name={name}
{i + 1} value={ekey}
</div> isValidKey={isValidEKey}
onSetName={(value) => dispatch(qmc2UpdateKey({ id, field: 'name', value }))}
<div className="join join-vertical flex-1"> onSetValue={(value) => dispatch(qmc2UpdateKey({ id, field: 'ekey', value }))}
<label className="input w-full rounded-tl-md rounded-tr-md"> onDelete={() => dispatch(qmc2DeleteKey({ id }))}
<span className="cursor-default select-none"></span> sequence={i + 1}
<input namePlaceholder="文件名,包括后缀名。如 “AAA - BBB.mflac”"
type="text" valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
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>
<button type="button" className="btn btn-error btn-sm px-1 btn-outline" onClick={deleteKey}>
<MdDelete className="size-6" />
</button>
</li>
); );
}); });

View File

@ -1,32 +1,20 @@
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction'; import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsIOS } from './InstructionsIOS'; import { InstructionsIOS } from './InstructionsIOS';
import { InstructionsMac } from './InstructionsMac'; import { InstructionsMac } from './InstructionsMac';
import { InstructionsPC } from './InstructionsPC'; import { InstructionsPC } from './InstructionsPC';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
export function QMCv2QQMusicAllInstructions() { export function QMCv2QQMusicAllInstructions() {
return ( const tabs: InstructionTab[] = [
<> {
<TabList> id: 'android',
<Tab></Tab> label: '安卓',
<Tab>iOS</Tab> content: <AndroidADBPullInstruction dir="/data/data/com.tencent.qqmusic/databases" file="player_process_db" />,
<Tab>Mac</Tab> },
<Tab>Windows</Tab> { id: 'ios', label: 'iOS', content: <InstructionsIOS /> },
</TabList> { id: 'mac', label: 'Mac', content: <InstructionsMac /> },
<TabPanels flex={1} overflow="auto"> { id: 'windows', label: 'Windows', content: <InstructionsPC /> },
<TabPanel> ];
<AndroidADBPullInstruction dir="/data/data/com.tencent.qqmusic/databases" file="player_process_db" />
</TabPanel> return <InstructionsTabs tabs={tabs} />;
<TabPanel>
<InstructionsIOS />
</TabPanel>
<TabPanel>
<InstructionsMac />
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</>
);
} }

View File

@ -0,0 +1,13 @@
import type { FC } from 'react';
import { PanelQMCv2Key } from '~/features/settings/panels/PanelQMCv2Key.tsx';
import { PanelKWMv2Key } from '~/features/settings/panels/PanelKWMv2Key.tsx';
import { PanelKGGKey } from '~/features/settings/panels/PanelKGGKey.tsx';
import { PanelQingTing } from '~/features/settings/panels/PanelQingTing.tsx';
export const SETTINGS_TABS: Record<string, { name: string; Tab: FC }> = {
qmc: { name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
kwm: { name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
kgg: { name: 'KGG 密钥', Tab: PanelKGGKey },
qtfm: { name: '蜻蜓 FM', Tab: PanelQingTing },
// misc: { name: '其它/待定', Tab: () => <p>这里空空如也~</p> },
} as const;