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-router": "^7.6.0",
"react-syntax-highlighter": "^15.6.1",
"react-toastify": "^11.0.5",
"sass": "^1.89.0",
"sql.js": "^1.13.0",
"workbox-build": "^7.3.0"

21
pnpm-lock.yaml generated
View File

@ -77,6 +77,9 @@ importers:
react-syntax-highlighter:
specifier: ^15.6.1
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:
specifier: ^1.89.0
version: 1.89.0
@ -1971,6 +1974,10 @@ packages:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -3414,6 +3421,12 @@ packages:
peerDependencies:
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:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
@ -6102,6 +6115,8 @@ snapshots:
slice-ansi: 5.0.0
string-width: 7.2.0
clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -7562,6 +7577,12 @@ snapshots:
react: 19.1.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: {}
readdirp@4.1.2: {}

View File

@ -10,6 +10,8 @@ import { persistSettings } from '~/features/settings/persistSettings';
import { setupStore } from '~/store';
import { Footer } from '~/components/Footer';
import { FaqTab } from '~/tabs/FaqTab';
import { SETTINGS_TABS } from '~/features/settings/settingsTabs';
import { Bounce, ToastContainer } from 'react-toastify';
// Private to this file only.
const store = setupStore();
@ -40,11 +42,26 @@ export function AppRoot() {
<main className="flex-1 flex justify-center">
<Routes>
<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} />
</Routes>
</main>
<ToastContainer
position="bottom-center"
autoClose={5000}
newestOnTop
closeOnClick={false}
pauseOnFocusLoss
draggable
theme="colored"
transition={Bounce}
/>
<Footer />
</Provider>
</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>
</form>
)}
<h3 className="font-bold text-lg">{title}</h3>
<h3 className="font-bold text-lg pb-3">{title}</h3>
{children}
</div>
{backdropClose && (

View File

@ -7,9 +7,9 @@ export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {
return (
<a rel="noreferrer noopener nofollow" className={`link ${className}`} {...props}>
<a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
{children}
{icon && <FiExternalLink />}
{icon && <FiExternalLink className="inline size-sm ml-1" />}
</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 { commitStagingChange, discardStagingChanges } from './settingsSlice';
import { PanelKWMv2Key } from './panels/PanelKWMv2Key';
import { selectIsSettingsNotSaved } from './settingsSelector';
import { PanelQingTing } from './panels/PanelQingTing';
import { PanelKGGKey } from '~/features/settings/panels/PanelKGGKey.tsx';
const TABS: { name: string; Tab: FC }[] = [
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
{ name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
{ name: 'KGG 密钥', Tab: PanelKGGKey },
{ name: '蜻蜓 FM', Tab: PanelQingTing },
{
name: '其它/待定',
Tab: () => <Text></Text>,
},
];
import { NavLink, Outlet } from 'react-router';
import { SETTINGS_TABS } from '~/features/settings/settingsTabs.tsx';
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { toast } from 'react-toastify';
export function Settings() {
const toast = useToast();
const dispatch = useAppDispatch();
const isLargeWidthDevice = false;
const [tabIndex, setTabIndex] = useState(0);
const handleTabChange = (idx: number) => {
setTabIndex(idx);
};
const handleResetSettings = () => {
dispatch(discardStagingChanges());
toast({
status: 'info',
title: '未储存的设定已舍弃',
description: '已还原到更改前的状态。',
isClosable: true,
});
toast.info(() => (
<div>
<h3 className="text-lg font-bold"></h3>
<p className="text-sm"></p>
</div>
));
};
const handleApplySettings = () => {
dispatch(commitStagingChange());
toast({
status: 'success',
title: '设定已应用',
isClosable: true,
});
toast.success('设定已应用');
};
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
return (
<Flex flexDir="column" flex={1}>
<Menu>
<MenuButton
as={Button}
leftIcon={<MdMenu />}
rightIcon={<MdExpandMore />}
colorScheme="gray"
variant="outline"
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)}>
<div className="flex flex-col flex-1 container w-full">
<div role="tablist" className="tabs tabs-border w-full justify-center gap-2">
{Object.entries(SETTINGS_TABS).map(([id, { name }]) => (
<NavLink className="link link-neutral" key={id} to={`/settings/${id}`} role="tab">
{name}
</MenuItem>
</NavLink>
))}
</MenuList>
</Portal>
</Menu>
</div>
<Tabs
orientation={isLargeWidthDevice ? 'vertical' : 'horizontal'}
align="start"
variant="line-i"
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>
{/* TODO: ensure this flex div does not overflow */}
<div className="flex flex-1 flex-col h-full overflow-auto">
<Outlet />
</div>
<TabPanels>
{TABS.map(({ name, Tab }) => (
<Flex as={TabPanel} flex={1} flexDir="column" h="100%" key={name}>
<Flex h="100%" flex={1} minH={0}>
<Tab />
</Flex>
<VStack mt="4" alignItems="flex-start" w="full">
<Flex flexDir="row" gap="2" w="full">
<Center>
<footer className="flex flex-row gap-2 w-full py-3">
<div className="grow">
{isSettingsNotSaved ? (
<Box color="gray">
{' '}
<chakra.span color="red" wordBreak="keep-all">
</chakra.span>
</Box>
<span>
<span className="text-red-600"></span>
</span>
) : (
<Box color="gray"></Box>
<span className="text-base-700"></span>
)}
</Center>
<Spacer />
<HStack gap="2" justifyContent="flex-end">
<IconButton
icon={<Icon as={MdOutlineSettingsBackupRestore} />}
</div>
<div className="flex flex-row gap-2">
<button
className="btn btn-sm btn-ghost text-error"
onClick={handleResetSettings}
colorScheme="red"
variant="ghost"
title="放弃未储存的更改,将设定还原未储存前的状态。"
aria-label="放弃未储存的更改"
/>
<Button onClick={handleApplySettings}></Button>
</HStack>
</Flex>
</VStack>
</Flex>
))}
</TabPanels>
</Tabs>
</Flex>
>
<MdOutlineSettingsBackupRestore className="size-4" />
</button>
<button className="btn btn-sm btn-primary" onClick={handleApplySettings}>
</button>
</div>
</footer>
{/* <VStack mt="4" alignItems="flex-start" w="full">*/}
{/* <Flex flexDir="row" gap="2" w="full">*/}
{/* <Center>*/}
{/* {isSettingsNotSaved ? (*/}
{/* <Box color="gray">*/}
{/* 有未储存的更改{' '}*/}
{/* <chakra.span color="red" wordBreak="keep-all">*/}
{/* 设定将在保存后生效*/}
{/* </chakra.span>*/}
{/* </Box>*/}
{/* ) : (*/}
{/* <Box color="gray">设定将在保存后生效</Box>*/}
{/* )}*/}
{/* </Center>*/}
{/* <Spacer />*/}
{/* <HStack gap="2" justifyContent="flex-end">*/}
{/* <IconButton*/}
{/* icon={<Icon as={MdOutlineSettingsBackupRestore} />}*/}
{/* 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 { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys, qmc2ImportKeys } from '../settingsSlice';
import { selectStagingQMCv2Settings } from '../settingsSelector';
@ -12,10 +12,11 @@ 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';
import { InfoModal } from '~/components/InfoModal.tsx';
import { Ruby } from '~/components/Ruby.tsx';
import { ExtLink } from '~/components/ExtLink.tsx';
export function PanelQMCv2Key() {
const [showFuzzyNameSearchInfo, setShowFuzzyNameSearchInfo] = useState(false);
const toast = useToast();
const dispatch = useDispatch();
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
@ -74,18 +75,13 @@ export function PanelQMCv2Key() {
};
return (
<div className="flex min-h-0 flex-col flex-1">
<>
<h2 className="text-2xl font-bold">QMCv2 </h2>
<p>
<span>QQ FM QMCv2</span>
<span>
使QQ Mac iOS 使 FM
线
</span>
</p>
<p>QQ FM QMCv2</p>
<p>QQ Mac iOS FM</p>
<div className="flex flex-row gap-2 items-center">
<div className="flex flex-row gap-2 items-center my-2">
<label className="label">
<input
className="checkbox"
@ -95,56 +91,51 @@ export function PanelQMCv2Key() {
/>
</label>
<button className="btn btn-info btn-sm" type="button" onClick={() => setShowFuzzyNameSearchInfo(true)}>
?
</button>
<Dialog
closeButton
backdropClose
show={showFuzzyNameSearchInfo}
onClose={() => setShowFuzzyNameSearchInfo(false)}
<InfoModal
title="莱文斯坦距离"
>
description={
<div>
<p>使</p>
<p>
使
<ruby>
<rp> (</rp>
<rt>Levenshtein distance</rt>
<rp>)</rp>
</ruby>
<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>
</Dialog>
</div>
}
>
?
</InfoModal>
</div>
<h3 className="mt-2 text-1xl font-bold"></h3>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
<div className="flex-1 min-h-0 overflow-auto pr-4">
<ul className="list bg-base-100 rounded-box shadow-md">
<div className="flex-1 min-h-0 overflow-auto pr-4 pt-3">
{qmc2Keys.length > 0 && (
<ul className="list bg-base-100 rounded-box shadow-md border border-base-300">
{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>}
)}
{qmc2Keys.length === 0 && <p className="p-4 pb-2 tracking-wide"></p>}
</div>
<ImportSecretModal
clientName={
<Select
<select
value={secretType}
onChange={(e) => setSecretType(e.target.value as 'qm' | 'douban')}
variant="flushed"
display="inline"
css={{ paddingLeft: '0.75rem', width: 'auto' }}
className="inline mx-1 px-1 border-b border-accent/50 bg-base-100"
>
<option value="qm">QQ </option>
<option value="douban"> FM</option>
</Select>
</select>
}
show={showImportModal}
onClose={() => setShowImportModal(false)}
@ -153,6 +144,6 @@ export function PanelQMCv2Key() {
{secretType === 'qm' && <QMCv2QQMusicAllInstructions />}
{secretType === 'douban' && <QMCv2DoubanAllInstructions />}
</ImportSecretModal>
</div>
</>
);
}

View File

@ -1,10 +1,16 @@
import { Text } from '@chakra-ui/react';
export function InstructionsPC() {
return (
<>
<Text>使 Windows 19.51 </Text>
<Text>使 Windows 19.57 </Text>
<p>
使 <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 { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
export function QMCv2DoubanAllInstructions() {
return (
<>
<TabList>
<Tab></Tab>
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<AndroidADBPullInstruction dir="/data/data/com.douban.radio/databases" file="music_audio_play" />
</TabPanel>
</TabPanels>
</>
);
const tabs: InstructionTab[] = [
{
id: 'android',
label: '安卓',
content: <AndroidADBPullInstruction dir="/data/data/com.douban.radio/databases" file="music_audio_play" />,
},
];
return <InstructionsTabs tabs={tabs} />;
}

View File

@ -1,54 +1,25 @@
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { qmc2DeleteKey, qmc2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
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 }) => {
const dispatch = useAppDispatch();
const updateKey = (prop: 'name' | 'ekey', e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
const deleteKey = () => dispatch(qmc2DeleteKey({ id }));
const isValidEKey = [364, 704].includes(ekey.length);
const ekeyLen = ekey.length;
const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
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 位字符,没有空格。"
<KeyInput
name={name}
value={ekey}
onChange={(e) => updateKey('ekey', e)}
isValidKey={isValidEKey}
onSetName={(value) => dispatch(qmc2UpdateKey({ id, field: 'name', value }))}
onSetValue={(value) => dispatch(qmc2UpdateKey({ id, field: 'ekey', value }))}
onDelete={() => dispatch(qmc2DeleteKey({ id }))}
sequence={i + 1}
namePlaceholder="文件名,包括后缀名。如 “AAA - BBB.mflac”"
valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
/>
<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 { InstructionsIOS } from './InstructionsIOS';
import { InstructionsMac } from './InstructionsMac';
import { InstructionsPC } from './InstructionsPC';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
export function QMCv2QQMusicAllInstructions() {
return (
<>
<TabList>
<Tab></Tab>
<Tab>iOS</Tab>
<Tab>Mac</Tab>
<Tab>Windows</Tab>
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<AndroidADBPullInstruction dir="/data/data/com.tencent.qqmusic/databases" file="player_process_db" />
</TabPanel>
<TabPanel>
<InstructionsIOS />
</TabPanel>
<TabPanel>
<InstructionsMac />
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</>
);
const tabs: InstructionTab[] = [
{
id: 'android',
label: '安卓',
content: <AndroidADBPullInstruction dir="/data/data/com.tencent.qqmusic/databases" file="player_process_db" />,
},
{ id: 'ios', label: 'iOS', content: <InstructionsIOS /> },
{ id: 'mac', label: 'Mac', content: <InstructionsMac /> },
{ id: 'windows', label: 'Windows', content: <InstructionsPC /> },
];
return <InstructionsTabs tabs={tabs} />;
}

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;