refactor: batch 3

This commit is contained in:
鲁树人
2025-05-18 02:41:20 +09:00
parent 75b43e1e84
commit 2e4e57be45
52 changed files with 933 additions and 1136 deletions

View File

@@ -1,5 +1,9 @@
@import 'tailwindcss'; @import 'tailwindcss';
@plugin "daisyui"; @plugin "daisyui" {
themes:
emerald --default,
dracula --prefersdark;
}
@theme { @theme {
--font-display: --font-display:
system-ui, Sarasa UI SC, Source Han Sans CN, Noto Sans CJK SC, sans-serif, Apple Color Emoji, Segoe UI Emoji; system-ui, Sarasa UI SC, Source Han Sans CN, Noto Sans CJK SC, sans-serif, Apple Color Emoji, Segoe UI Emoji;

View File

@@ -1,24 +1,46 @@
import type { RefObject } from 'react';
import { MdAdd, MdDeleteForever, MdFileUpload } from 'react-icons/md'; import { MdAdd, MdDeleteForever, MdFileUpload } from 'react-icons/md';
export interface AddKeyProps { export interface AddKeyProps {
addKey: () => void; addKey: () => void;
importKeyFromFile?: () => void; importKeyFromFile?: () => void;
clearKeys?: () => void; clearKeys?: () => void;
refContainer?: RefObject<HTMLElement | null>;
} }
export function AddKey({ addKey, importKeyFromFile, clearKeys }: AddKeyProps) { export function AddKey({ addKey, refContainer, importKeyFromFile, clearKeys }: AddKeyProps) {
const scrollToLastKey = () => {
const container = refContainer?.current;
if (container) {
const inputs = container.querySelectorAll('input[data-name="key-input--name"]');
const lastInput = inputs[inputs.length - 1] as HTMLInputElement | null;
if (lastInput) {
lastInput.focus({ preventScroll: true });
lastInput.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}
};
const handleAddKey = () => {
addKey();
setTimeout(scrollToLastKey);
};
return ( return (
<div className="flex flex-row justify-between items-center"> <div className="flex flex-row justify-between items-center">
<div className="join"> <div className="join">
<button className="btn join-item" onClick={addKey}> <button type="button" className="join-item btn flex items-center gap-2" onClick={handleAddKey}>
<MdAdd /> <MdAdd className="text-lg" />
</button> </button>
<button className="btn join-item" onClick={importKeyFromFile}> <button type="button" className="join-item btn flex items-center gap-2" onClick={importKeyFromFile}>
<MdFileUpload /> <MdFileUpload className="text-lg" />
</button> </button>
<button className="btn btn-error join-item" onClick={clearKeys}> <button type="button" className="join-item btn flex items-center gap-2 btn-error" onClick={clearKeys}>
<MdDeleteForever /> <MdDeleteForever className="text-lg" />
</button> </button>
</div> </div>

View File

@@ -0,0 +1,57 @@
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
import { ExtLink } from '../ExtLink';
import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw';
import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw';
import { applyTemplate } from '~/util/applyTemplate';
export interface AdbInstructionTemplateProps {
dir: string;
file: string;
platform: 'win32' | 'linux';
}
const URL_USB_DEBUGGING = 'https://developer.android.com/studio/debug/dev-options?hl=zh-cn#Enable-debugging';
const LANGUAGE_MAP = {
win32: { language: 'ps1', template: PowerShellAdbDumpCommandTemplate },
linux: { language: 'sh', template: ShellAdbDumpCommandTemplate },
};
export function AdbInstructionTemplate({ dir, file, platform }: AdbInstructionTemplateProps) {
const { language, template } = LANGUAGE_MAP[platform];
const command = applyTemplate(template, { dir, file });
return (
<ol className="list-decimal pl-4">
<li>
<p>
<code>adb</code>
</p>
{platform === 'win32' && (
<div>
<span>
💡
<ExtLink href="https://scoop.sh/#/apps?q=adb">使 Scoop </ExtLink>
</span>
</div>
)}
</li>
<li> PowerShell </li>
<li>
<ExtLink href={URL_USB_DEBUGGING}> USB </ExtLink>
</li>
<li></li>
<li>
<p> USB </p>
<SyntaxHighlighter language={language} style={hljsStyleGitHub}>
{command}
</SyntaxHighlighter>
</li>
<li>
<code>{file}</code>
</li>
</ol>
);
}

View File

@@ -1,171 +1,73 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Code,
Heading,
ListItem,
OrderedList,
Text,
chakra,
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw';
import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw';
import { ExtLink } from '../ExtLink'; import { ExtLink } from '../ExtLink';
import { Ruby } from '../Ruby';
const applyTemplate = (tpl: string, values: Record<string, unknown>) => { import { useId } from 'react';
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : '<nil>')); import { RootExplorerGuide } from './RootExplorerGuide';
}; import { AdbInstructionTemplate } from './AdbInstructionTemplate';
import { HiWord } from '../HelpText/HiWord';
export interface AndroidADBPullInstructionProps { export interface AndroidADBPullInstructionProps {
dir: string; dir: string;
file: string; file: string;
} }
const URL_AMAZE = 'https://github.com/TeamAmaze/AmazeFileManager/releases/latest';
const URL_MT2 = 'https://mt2.cn/download/';
export function AndroidADBPullInstruction({ dir, file }: AndroidADBPullInstructionProps) { export function AndroidADBPullInstruction({ dir, file }: AndroidADBPullInstructionProps) {
const psAdbDumpCommand = applyTemplate(PowerShellAdbDumpCommandTemplate, { dir, file }); const androidInstructionId = useId();
const shAdbDumpCommand = applyTemplate(ShellAdbDumpCommandTemplate, { dir, file });
return ( return (
<> <>
<Text> <p>
<Ruby caption="root"></Ruby>访访
<ruby> </p>
<p>
<rp> (</rp>
<rt>
<code>root</code>
</rt>
<rp>)</rp>
</ruby>
访访
</Text>
<Text>
<chakra.span color="red.400"></chakra.span> <HiWord></HiWord>
</Text> </p>
<Accordion allowToggle mt="2"> <div className="join join-vertical bg-base-100 mt-2 max-w-full">
<AccordionItem> <div className="collapse collapse-arrow join-item border-base-300 border">
<Heading as="h3" size="md"> <input type="radio" name={androidInstructionId} />
<AccordionButton> <div className="collapse-title font-semibold"></div>
<Box as="span" flex="1" textAlign="left"> <div className="collapse-content text-sm min-w-0">
<ol className="list-decimal pl-4">
</Box> <li>
<AccordionIcon /> <code>root</code> <ExtLink href={URL_AMAZE}>Amaze </ExtLink>
</AccordionButton> <ExtLink href={URL_MT2}>MT </ExtLink>
</Heading> </li>
<AccordionPanel pb={4}> <li>
<OrderedList> root
<ListItem> <RootExplorerGuide />
<Text> </li>
<Code>root</Code> <li>
</Text> <p>
</ListItem> 访 <code>{dir}/</code>
<ListItem> </p>
<Text> <p> </p>
访 <Code>{dir}/</Code> </li>
</Text> <li>
</ListItem> <code>{file}</code> 访
<ListItem> </li>
<Text> <li></li>
<Code>{file}</Code> 访 </ol>
<br /> </div>
</div>
</Text> <div className="collapse collapse-arrow join-item border-base-300 border">
</ListItem> <input type="radio" name={androidInstructionId} />
<ListItem> <div className="collapse-title font-semibold"> PC 使 ADB / PowerShell</div>
<Text></Text> <div className="collapse-content text-sm min-w-0">
</ListItem> <AdbInstructionTemplate dir={dir} file={file} platform="win32" />
</OrderedList> </div>
</AccordionPanel> </div>
</AccordionItem> <div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={androidInstructionId} />
<AccordionItem> <div className="collapse-title font-semibold"> Linux / Mac 使 ADB / Shell</div>
<Heading as="h3" size="md"> <div className="collapse-content text-sm min-w-0">
<AccordionButton> <AdbInstructionTemplate dir={dir} file={file} platform="linux" />
<Box as="span" flex="1" textAlign="left"> </div>
PC ADB / PowerShell </div>
</Box> </div>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<Text>
<Code>adb</Code>
</Text>
<Text>
💡
<ExtLink href="https://scoop.sh/#/apps?q=adb">
使 Scoop <ExternalLinkIcon />
</ExtLink>
</Text>
</ListItem>
<ListItem>
<Text> PowerShell 7 </Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text></Text>
<SyntaxHighlighter language="ps1" style={hljsStyleGitHub}>
{psAdbDumpCommand}
</SyntaxHighlighter>
</ListItem>
<ListItem>
<Text>
<Code>{file}</Code>
</Text>
</ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Linux / Mac ADB / Shell
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<Text>
<Code>adb</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text></Text>
<SyntaxHighlighter language="bash" style={hljsStyleGitHub}>
{shAdbDumpCommand}
</SyntaxHighlighter>
</ListItem>
<ListItem>
<Text>
<Code>{file}</Code>
</Text>
</ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
</Accordion>
</> </>
); );
} }

View File

@@ -0,0 +1,64 @@
import { FiMenu, FiMoreVertical } from 'react-icons/fi';
import { Header5 } from '../HelpText/Headers';
import { Ruby } from '../Ruby';
import { VQuote } from '../HelpText/VQuote';
export function RootExplorerGuide() {
return (
<div className="@container inline-flex flex-col items-start w-full pl-4">
<div className="flex flex-col items-start gap-4 @md:flex-row">
<div>
<Header5 className="[&]:mt-0 [&]:pt-0">Amaze </Header5>
<ul className="ml-2 list-disc list-inside">
<li>
<div className="inline-flex items-center gap-1">
<FiMenu />
</div>
</li>
<li>
<VQuote>
<Ruby caption="Settings"></Ruby>
</VQuote>
</li>
<li>
<VQuote>
<Ruby caption="Behaviour"></Ruby>
</VQuote>
</li>
<li>
<VQuote>
<Ruby caption="Advanced"></Ruby>
</VQuote>
<VQuote>
<Ruby caption="Root Explorer"></Ruby>
</VQuote>
</li>
</ul>
</div>
<div>
<Header5 className="[&]:mt-0 [&]:pt-0">MT </Header5>
<ul className="ml-2 list-disc list-inside">
<li>
<div className="inline-flex items-center gap-1">
<FiMenu />
</div>
</li>
<li>
<div className="inline-flex items-center">
<FiMoreVertical className="ml-1" />
<VQuote></VQuote>
</div>
</li>
<li>
<VQuote> Root </VQuote>
</li>
</ul>
</div>
</div>
</div>
);
}

View File

@@ -7,7 +7,7 @@ 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" target="_blank" className={`link ${className}`} {...props}> <a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
{children} {children}
{icon && <FiExternalLink className="inline size-sm ml-1" />} {icon && <FiExternalLink className="inline size-sm ml-1" />}
</a> </a>

View File

@@ -1,6 +1,6 @@
import React from 'react'; import type { ReactNode } from 'react';
export function FilePathBlock({ children }: { children: React.ReactNode }) { export function FilePathBlock({ children }: { children: ReactNode }) {
return ( return (
<pre className="whitespace-pre-wrap break-all"> <pre className="whitespace-pre-wrap break-all">
<code>{children}</code> <code>{children}</code>

View File

@@ -1,4 +1,3 @@
import { Heading } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
export interface HeaderProps { export interface HeaderProps {
@@ -9,34 +8,24 @@ export interface HeaderProps {
export function Header3({ children, className, id }: HeaderProps) { export function Header3({ children, className, id }: HeaderProps) {
return ( return (
<Heading <h3 id={id} className={`text-2xl pt-3 pb-1 font-bold border-b border-base-300 text-neutral-800 ${className}`}>
as="h3"
id={id}
className={className}
pt={3}
pb={1}
borderBottom={'1px solid'}
borderColor="gray.300"
color="gray.800"
size="lg"
>
{children} {children}
</Heading> </h3>
); );
} }
export function Header4({ children, className, id }: HeaderProps) { export function Header4({ children, className, id }: HeaderProps) {
return ( return (
<Heading as="h4" id={id} className={className} pt={3} pb={1} color="gray.700" size="md"> <h4 id={id} className={`text-xl pt-3 pb-1 font-semibold text-neutral-800 ${className}`}>
{children} {children}
</Heading> </h4>
); );
} }
export function Header5({ children, className, id }: HeaderProps) { export function Header5({ children, className, id }: HeaderProps) {
return ( return (
<Heading as="h5" id={id} className={className} pt={3} pb={1} color="gray.700" size="sm"> <h5 id={id} className={`text-lg pt-3 pb-1 font-semibold text-neutral-800 ${className}`}>
{children} {children}
</Heading> </h5>
); );
} }

View File

@@ -1,9 +1,3 @@
import { Mark } from '@chakra-ui/react'; export function HiWord({ className = '', children }: { className?: string; children: React.ReactNode }) {
return <mark className={`bg-orange-100 rounded-md px-2 mx-1 ${className}`}>{children}</mark>;
export function HiWord({ children }: { children: React.ReactNode }) {
return (
<Mark bg="orange.100" borderRadius={5} px={2} mx={1}>
{children}
</Mark>
);
} }

View File

@@ -1,13 +1,9 @@
import { chakra, css } from '@chakra-ui/react';
const cssUnselectable = css({ pointerEvents: 'none', userSelect: 'none' });
export function VQuote({ children }: { children: React.ReactNode }) { export function VQuote({ children }: { children: React.ReactNode }) {
return ( return (
<> <>
<chakra.span css={cssUnselectable}></chakra.span> <span className="select-none"></span>
{children} {children}
<chakra.span css={cssUnselectable}></chakra.span> <span className="select-none"></span>
</> </>
); );
} }

View File

@@ -13,21 +13,18 @@ export interface InstructionsTabsProps {
export function InstructionsTabs({ tabs }: InstructionsTabsProps) { export function InstructionsTabs({ tabs }: InstructionsTabsProps) {
const id = useId(); const id = useId();
return ( return (
<div className="tabs tabs-lift h-[20rem] pb-4"> <div className="tabs tabs-lift max-h-[32rem] pb-4">
{tabs.map(({ id: _tabId, label, content }, index) => ( {tabs.map(({ id: _tabId, label, content }, index) => (
<Fragment key={_tabId}> <Fragment key={_tabId}>
<label className="tab"> <label className="tab">
<input type="radio" name={id} defaultChecked={index === 0} /> <input type="radio" name={id} defaultChecked={index === 0} />
{label} {label}
</label> </label>
<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto">{content}</div> <div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto max-h-[30rem]">
{content}
</div>
</Fragment> </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> </div>
); );
} }

View File

@@ -1,15 +1,12 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsCommand } from 'react-icons/bs'; import { BsCommand } from 'react-icons/bs';
import { Ruby } from '../Ruby';
export function MacCommandKey() { export function MacCommandKey() {
return ( return (
<ruby> <Ruby caption="command">
<Kbd> <kbd className="kbd">
<Icon as={BsCommand} /> <BsCommand className="text-sm" />
</Kbd> </kbd>
<rp> (</rp> </Ruby>
<rt>command</rt>
<rp>)</rp>
</ruby>
); );
} }

View File

@@ -1,15 +1,12 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsShift } from 'react-icons/bs'; import { BsShift } from 'react-icons/bs';
import { Ruby } from '../Ruby';
export function ShiftKey() { export function ShiftKey() {
return ( return (
<ruby> <Ruby caption="shift">
<Kbd> <kbd className="kbd">
<Icon as={BsShift} /> <BsShift className="text-sm" />
</Kbd> </kbd>
<rp> (</rp> </Ruby>
<rt>shift</rt>
<rp>)</rp>
</ruby>
); );
} }

View File

@@ -1,6 +1,6 @@
import { PiFileAudio } from 'react-icons/pi'; import { PiFileAudio } from 'react-icons/pi';
import { MdDelete, MdVpnKey } from 'react-icons/md'; import { MdDelete, MdVpnKey } from 'react-icons/md';
import React from 'react'; import type { ReactNode } from 'react';
export interface KeyInputProps { export interface KeyInputProps {
sequence: number; sequence: number;
@@ -12,24 +12,34 @@ export interface KeyInputProps {
onSetValue: (value: string) => void; onSetValue: (value: string) => void;
onDelete: () => void; onDelete: () => void;
nameLabel?: React.ReactNode; quality?: string;
valueLabel?: React.ReactNode; onSetQuality?: (quality: string) => void;
nameLabel?: ReactNode;
valueLabel?: ReactNode;
qualityLabel?: ReactNode;
namePlaceholder?: string; namePlaceholder?: string;
valuePlaceholder?: string; valuePlaceholder?: string;
qualityPlaceholder?: string;
} }
export function KeyInput(props: KeyInputProps) { export function KeyInput(props: KeyInputProps) {
const { const {
nameLabel, nameLabel,
valueLabel, valueLabel,
qualityLabel,
namePlaceholder, namePlaceholder,
qualityPlaceholder,
valuePlaceholder, valuePlaceholder,
sequence, sequence,
name, name,
quality,
value, value,
onSetName, onSetName,
onSetValue, onSetValue,
onDelete, onDelete,
onSetQuality,
isValidKey, isValidKey,
} = props; } = props;
@@ -40,22 +50,39 @@ export function KeyInput(props: KeyInputProps) {
</div> </div>
<div className="join join-vertical flex-1"> <div className="join join-vertical flex-1">
<label className="input w-full rounded-tl-md rounded-tr-md"> <div className="flex">
<span className="cucursor-default inline-flex items-center gap-1 select-none"> <label className="input w-full rounded-tl-md last:rounded-tr-md">
{nameLabel || ( <span className="cucursor-default inline-flex items-center gap-1 select-none">
<> {nameLabel || (
<PiFileAudio /> <>
</> <PiFileAudio />
)} </>
</span> )}
<input </span>
type="text" <input
className="font-mono" type="text"
placeholder={namePlaceholder} className="font-mono"
value={name} placeholder={namePlaceholder}
onChange={(e) => onSetName(e.target.value)} value={name}
/> onChange={(e) => onSetName(e.target.value)}
</label> data-name="key-input--name"
/>
</label>
{onSetQuality && (
<label className="input min-w-0 max-w-[10rem] ml-[-1px] rounded-tr-md">
<span className="cucursor-default inline-flex items-center gap-1 select-none">
{qualityLabel || '音质'}
</span>
<input
type="text"
className="font-mono"
placeholder={qualityPlaceholder}
value={quality}
onChange={(e) => onSetQuality(e.target.value)}
/>
</label>
)}
</div>
<label className="input w-full rounded-bl-md rounded-br-md mt-[-1px]"> <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"> <span className="cursor-default inline-flex items-center gap-1 select-none">
{valueLabel || ( {valueLabel || (

View File

@@ -0,0 +1,21 @@
import type { ReactNode, RefObject } from 'react';
export interface KeyListContainerProps {
keys: unknown[];
children?: ReactNode;
ref?: RefObject<HTMLDivElement | null>;
}
export function KeyListContainer({ keys, children, ref }: KeyListContainerProps) {
const count = keys.length;
return (
<div ref={ref} className="flex grow min-h-0 overflow-auto pr-4 pt-3">
{count > 0 && (
<ul className="list bg-base-100 rounded-box shadow-md border border-base-300 w-full min-h-0 max-h-[30rem] overflow-auto">
{children}
</ul>
)}
{count === 0 && <p></p>}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Link } from '@chakra-ui/react'; import { ExtLink } from './ExtLink';
export interface ProjectIssueProps { export interface ProjectIssueProps {
id: number | string; id: number | string;
@@ -7,9 +7,9 @@ export interface ProjectIssueProps {
export function ProjectIssue({ id, title }: ProjectIssueProps) { export function ProjectIssue({ id, title }: ProjectIssueProps) {
return ( return (
<Link isExternal target="_blank" href={`https://git.unlock-music.dev/um/um-react/issues/${id}`}> <ExtLink target="_blank" href={`https://git.unlock-music.dev/um/um-react/issues/${id}`}>
{`#${id}`} {`#${id}`}
{title && ` - ${title}`} {title && ` - ${title}`}
</Link> </ExtLink>
); );
} }

View File

@@ -1,31 +1,25 @@
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text } from '@chakra-ui/react';
import { Header4 } from '~/components/HelpText/Headers'; import { Header4 } from '~/components/HelpText/Headers';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions'; import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx'; import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
import { RiErrorWarningLine } from 'react-icons/ri';
export function KugouFAQ() { export function KugouFAQ() {
return ( return (
<> <>
<Header4></Header4> <Header4></Header4>
<List spacing={2}> <p>
<ListItem> <code>kgg</code> Windows
<Text> </p>
<code>kgg</code> Windows <p></p>
</Text>
<Text></Text>
<Container p={2}> <div className="p-2 @container">
<Alert status="warning" borderRadius={5}> <div className="alert alert-warning">
<AlertIcon /> <RiErrorWarningLine className="size-6" />
<Flex flexDir="column"> <p> root </p>
<Text> root </Text> </div>
</Flex> </div>
</Alert>
</Container>
<SegmentKeyImportInstructions tab="酷狗密钥" clientInstructions={<KugouAllInstructions />} /> <SegmentKeyImportInstructions tab="酷狗密钥" clientInstructions={<KugouAllInstructions />} />
</ListItem>
</List>
</> </>
); );
} }

View File

@@ -1,52 +1,45 @@
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text } from '@chakra-ui/react';
import { Header4 } from '~/components/HelpText/Headers'; import { Header4 } from '~/components/HelpText/Headers';
import { VQuote } from '~/components/HelpText/VQuote'; import { VQuote } from '~/components/HelpText/VQuote';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer'; import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { HiWord } from '~/components/HelpText/HiWord'; import { HiWord } from '~/components/HelpText/HiWord';
import { KWMv2AllInstructions } from '~/features/settings/panels/KWMv2/KWMv2AllInstructions'; import { KWMv2AllInstructions } from '~/features/settings/panels/KWMv2/KWMv2AllInstructions';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions'; import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
import { RiErrorWarningLine } from 'react-icons/ri';
export function KuwoFAQ() { export function KuwoFAQ() {
return ( return (
<> <>
<Header4></Header4> <Header4></Header4>
<List spacing={2}> <SegmentTryOfficialPlayer />
<ListItem> <p>
<SegmentTryOfficialPlayer /> <HiWord></HiWord>
</ListItem> <VQuote>
<ListItem> <strong></strong>
<Text> </VQuote>
<HiWord></HiWord>
<VQuote> <VQuote>
<strong></strong> <strong></strong>
</VQuote> </VQuote>
<VQuote> </p>
<strong></strong> <p></p>
</VQuote> <p>PC平台暂未推出使用新版加密的音质</p>
{'音质的音乐文件采用新版加密。'}
</Text>
<Text></Text>
<Text>PC平台暂未推出使用新版加密的音质</Text>
<Container p={2}> <div className="alert alert-warning">
<Alert status="warning" borderRadius={5}> <RiErrorWarningLine className="text-2xl" />
<AlertIcon /> <div>
<Flex flexDir="column"> <p> root </p>
<Text> root </Text> <p>
<Text> <strong className="pr-2"></strong>
<strong></strong> </p>
</Text> <p>
<Text> <strong className="pr-2"></strong>
<strong></strong>使使 使使
</Text> </p>
</Flex> </div>
</Alert> </div>
</Container>
<SegmentKeyImportInstructions tab="KWMv2 密钥" clientInstructions={<KWMv2AllInstructions />} /> <SegmentKeyImportInstructions tab="KWMv2 密钥" clientInstructions={<KWMv2AllInstructions />} />
</ListItem>
</List>
</> </>
); );
} }

View File

@@ -1,139 +1,161 @@
import { Alert, AlertIcon, Code, Container, Flex, Img, ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { ExtLink } from '~/components/ExtLink'; import { ExtLink } from '~/components/ExtLink';
import { Header4 } from '~/components/HelpText/Headers'; import { Header4, Header5 } from '~/components/HelpText/Headers';
import { VQuote } from '~/components/HelpText/VQuote'; import { VQuote } from '~/components/HelpText/VQuote';
import { ProjectIssue } from '~/components/ProjectIssue'; import { ProjectIssue } from '~/components/ProjectIssue';
import LdPlayerSettingsScreen from './assets/ld_settings_misc.webp'; import { RiErrorWarningLine } from 'react-icons/ri';
import LdPlayerSettingsMisc2x from './assets/ld_settings_misc@2x.webp';
import MumuSettingsMisc2x from './assets/mumu_settings_misc@2x.webp';
export function OtherFAQ() { export function OtherFAQ() {
return ( return (
<> <>
<Header4></Header4> <Header4></Header4>
<Text></Text> <p></p>
<Text>使</Text> <p>使</p>
<Header4></Header4> <Header4></Header4>
<Text> <p>
{'暂时没有实现,不过你可以在 '} {'暂时没有实现,不过你可以在 '}
<ProjectIssue id={34} title="[UI] 全部下载功能" /> <ProjectIssue id={34} title="[UI] 全部下载功能" />
{' 以及 '} {' 以及 '}
<ProjectIssue id={43} title="批量下载" /> <ProjectIssue id={43} title="批量下载" />
{' 追踪该问题。'} {' 追踪该问题。'}
</Text> </p>
<Header4>安卓: 浏览器支持说明</Header4> <Header4>安卓: 浏览器支持说明</Header4>
<Text> 使 Chrome Firefox </Text> <p> 使 Chrome Firefox </p>
<Text></Text> <div className="flex flex-col md:flex-row gap-2 md:gap-8">
<UnorderedList> <div>
<ListItem>Via </ListItem> <Header5></Header5>
<ListItem></ListItem> <ul className="list-disc list-inside pl-2">
<ListItem>UC </ListItem> <li>Via </li>
</UnorderedList> <li></li>
<Text></Text> <li>UC </li>
<UnorderedList> </ul>
<ListItem></ListItem> </div>
<ListItem></ListItem>
<ListItem></ListItem> <div>
</UnorderedList> <Header5></Header5>
<ul className="list-disc list-inside pl-2">
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
<Header4>安卓: root </Header4> <Header4>安卓: root </Header4>
<Text> <p>
root 使 root 使
使 NFC 使 NFC
</Text> </p>
<Text>使</Text> <p>使</p>
<Text> <p>
root 广 Windows 11 root 便
<VQuote>
<ExtLink href="https://learn.microsoft.com/zh-cn/windows/android/wsa/"> <ExtLink href="https://mumu.163.com/"> MuMu 12</ExtLink>
<ruby> </VQuote>
Android Windows (WSA)
<rp> (</rp> <VQuote>
<rt> <ExtLink href="https://www.ldmnq.com/"> 9</ExtLink>
<code>Windows Subsystem for Android</code> </VQuote>
</rt>
<rp>)</rp>
</ruby>
</ExtLink>
</Text> </p>
<Container p={2}> <div className="my-4 alert alert-warning">
<Alert status="warning" borderRadius={5}> <RiErrorWarningLine className="text-lg" />
<AlertIcon /> <div>
<Flex flexDir="column"> <p>
<Text> 使<strong></strong>
<strong></strong>使<strong></strong> </p>
{';使用前请自行评估风险。'} <p>使</p>
</Text> </div>
</Flex> </div>
</Alert>
</Container>
<UnorderedList> <p> root </p>
<ListItem> <div className="grid grid-cols-1 gap-2 md:gap-4 lg:grid-cols-2">
<Text> <div>
{'WSA 可以参考 '} <Header5> MuMu模拟器</Header5>
<ExtLink href="https://github.com/LSPosed/MagiskOnWSALocal">MagiskOnWSALocal</ExtLink> <ul className="list-disc pl-6">
{' 的说明操作。'} <li>
</Text> <VQuote></VQuote>
</ListItem> </li>
<ListItem> <li>
<Text> <VQuote></VQuote>
<VQuote></VQuote> <VQuote></VQuote> root </li>
</Text> <li>
<Img borderRadius={5} border="1px solid #ccc" src={LdPlayerSettingsScreen}></Img> <VQuote>Root权限</VQuote>
</ListItem> </li>
</UnorderedList> </ul>
<img className="rounded-md border border-base-300" loading="lazy" srcSet={`${MumuSettingsMisc2x} 2x`} />
</div>
<div>
<Header5></Header5>
<ul className="list-disc pl-6">
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote>ROOT </VQuote><VQuote></VQuote>
</li>
</ul>
<img className="rounded-md border border-base-300" loading="lazy" srcSet={`${LdPlayerSettingsMisc2x} 2x`} />
</div>
</div>
<Header4></Header4> <Header4></Header4>
<UnorderedList> <ul className="list-disc pl-6">
<ListItem> <li>
<Text> <p>
<ExtLink href="https://github.com/CarlGao4/um-react-electron"> <ExtLink className="mr-2" href="https://github.com/CarlGao4/um-react-electron">
<strong> <strong>
<Code>um-react-electron</Code> <code>um-react-electron</code>
</strong> </strong>
</ExtLink> </ExtLink>
Electron WindowsLinux Mac Electron WindowsLinux Mac
</Text> </p>
<UnorderedList> <ul className="list-disc pl-6">
<ListItem> <li>
<Text> <p>
<ExtLink href="https://github.com/CarlGao4/um-react-electron/releases/latest">GitHub </ExtLink> <ExtLink href="https://github.com/CarlGao4/um-react-electron/releases/latest">GitHub </ExtLink>
</Text> </p>
</ListItem> </li>
</UnorderedList> </ul>
</ListItem> </li>
<ListItem> <li>
<Text> <p>
<ExtLink href="https://git.unlock-music.dev/um/um-react-wry"> <ExtLink className="mr-2" href="https://git.unlock-music.dev/um/um-react-wry">
<strong> <strong>
<Code>um-react-wry</Code> <code>um-react-wry</code>
</strong> </strong>
</ExtLink> </ExtLink>
: 使 WRY Win64 使 WRY Win64
<ExtLink href="https://go.microsoft.com/fwlink/p/?LinkId=2124703"> Edge WebView2 </ExtLink> <ExtLink href="https://go.microsoft.com/fwlink/p/?LinkId=2124703"> Edge WebView2 </ExtLink>
{'Win10+ 操作系统自带)'} {'Win10+ 操作系统自带)'}
</Text> </p>
<UnorderedList> <ul className="list-disc pl-6">
<ListItem> <li>
<Text> <p>
<ExtLink href="https://git.unlock-music.dev/um/um-react/releases/latest"></ExtLink> <ExtLink href="https://git.unlock-music.dev/um/um-react/releases/latest"></ExtLink>
{' | 寻找文件名为 '} {' | 寻找文件名为 '}
<Code>um-react-win64-</Code> <code>um-react-win64-</code>
</Text> </p>
</ListItem> </li>
</UnorderedList> </ul>
</ListItem> </li>
</UnorderedList> </ul>
<Header4></Header4> <Header4></Header4>
<Text> <p className="flex flex-row gap-1">
{'欢迎进入 '}
<ExtLink href={'https://t.me/unlock_music_chat'}>Telegram - </ExtLink> <ExtLink href={'https://t.me/unlock_music_chat'}>- </ExtLink>
{' 一起探讨。'}
</Text> </p>
</> </>
); );
} }

View File

@@ -1,159 +1,18 @@
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box } from '@chakra-ui/react';
import { Alert, AlertIcon, Container, Flex, ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { Header4 } from '~/components/HelpText/Headers'; import { Header4 } from '~/components/HelpText/Headers';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer'; import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions'; import { QMCv2QQMusicAllInstructions } from '~/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions';
import { ExtLink } from '~/components/ExtLink';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsIOS } from '~/features/settings/panels/QMCv2/InstructionsIOS';
import { InstructionsMac } from '~/features/settings/panels/QMCv2/InstructionsMac';
export function QQMusicFAQ() { export function QQMusicFAQ() {
return ( return (
<> <>
<Header4></Header4> <Header4></Header4>
<SegmentTryOfficialPlayer /> <SegmentTryOfficialPlayer />
<Text></Text> <p></p>
<Text> <p className="mb-2">
<strong></strong>使 <strong></strong>使
</Text> </p>
<Accordion allowToggle my={2}>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Windows
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Text>
Windows 19.51
</Text>
<Text> QQ Windows v19.51 </Text>
<UnorderedList pl={3}>
<ListItem>
<Text>
<ExtLink href="https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
<code>qq.com</code>
</ExtLink>
</Text>
</ListItem>
<ListItem>
<Text>
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
<code>Archive.org</code>
</ExtLink>
</Text>
</ListItem>
</UnorderedList>
</AccordionPanel>
</AccordionItem>
<AccordionItem> <QMCv2QQMusicAllInstructions />
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Mac
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Container p={2}>
<Alert status="warning" borderRadius={5}>
<AlertIcon />
<Flex flexDir="column">
<Text>Mac 8.8.0 </Text>
<Text>
<ExtLink href="https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg">
<code>Archive.org</code>
</ExtLink>
</Text>
</Flex>
</Alert>
</Container>
<SegmentKeyImportInstructions
tab="QMCv2 密钥"
keyInstructionText="查看密钥提取说明:"
clientInstructions={
<Box p={2}>
<InstructionsMac />
</Box>
}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
(Android)
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Container p={2}>
<Alert status="warning" borderRadius={5}>
<AlertIcon />
<Flex flexDir="column">
<Text> root </Text>
</Flex>
</Alert>
</Container>
<Text>QQ </Text>
<Text>
使QQ OEM
</Text>
<SegmentKeyImportInstructions
tab="QMCv2 密钥"
keyInstructionText="查看密钥提取说明:"
clientInstructions={
<Box p={2}>
<AndroidADBPullInstruction dir="/data/data/com.tencent.qqmusic/databases" file="player_process_db" />
</Box>
}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
iOS (iPhone, iPad)
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Container p={2}>
<Alert status="warning" borderRadius={5}>
<AlertIcon />
<Flex flexDir="column">
<Text>iOS </Text>
</Flex>
</Alert>
</Container>
<SegmentKeyImportInstructions
tab="QMCv2 密钥"
keyInstructionText="查看密钥提取说明:"
clientInstructions={
<Box p={2}>
<InstructionsIOS />
</Box>
}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</> </>
); );
} }

View File

@@ -1,24 +1,13 @@
import { Flex, IconButton } from '@chakra-ui/react'; import { MdFileUpload } from 'react-icons/md';
import { MdExpandMore } from 'react-icons/md';
import { HiWord } from '~/components/HelpText/HiWord';
import { VQuote } from '~/components/HelpText/VQuote';
export function SegmentAddKeyDropdown() { export function SegmentAddKeyDropdown() {
return ( return (
<Flex as="span" alignItems="center" flexWrap="wrap"> <span className="inline-flex items-center flex-wrap">
<VQuote></VQuote>
<HiWord></HiWord> <button type="button" className="btn flex items-center gap-2">
<IconButton <MdFileUpload className="text-lg" />
colorScheme="purple"
variant="outline" </button>
size="sm" </span>
icon={<MdExpandMore />}
ml="2"
borderTopLeftRadius={0}
borderBottomLeftRadius={0}
pointerEvents="none"
aria-label="下拉按钮"
/>
</Flex>
); );
} }

View File

@@ -1,9 +1,7 @@
import { Flex, Icon, ListItem, OrderedList, Tabs, Text } from '@chakra-ui/react';
import { SegmentTopNavSettings } from './SegmentTopNavSettings'; import { SegmentTopNavSettings } from './SegmentTopNavSettings';
import { VQuote } from '~/components/HelpText/VQuote'; import { VQuote } from '~/components/HelpText/VQuote';
import { SegmentAddKeyDropdown } from './SegmentAddKeyDropdown'; import { SegmentAddKeyDropdown } from './SegmentAddKeyDropdown';
import React from 'react'; import React from 'react';
import { MdFileUpload } from 'react-icons/md';
export interface SegmentKeyImportInstructionsProps { export interface SegmentKeyImportInstructionsProps {
clientInstructions: React.ReactNode; clientInstructions: React.ReactNode;
@@ -18,32 +16,22 @@ export function SegmentKeyImportInstructions({
}: SegmentKeyImportInstructionsProps) { }: SegmentKeyImportInstructionsProps) {
return ( return (
<> <>
<Text></Text> <p></p>
<OrderedList> <ol className="list-decimal pl-5">
<ListItem> <li>
<SegmentTopNavSettings /> <SegmentTopNavSettings />
</ListItem> </li>
<ListItem> <li>
<VQuote>{tab}</VQuote> <VQuote>{tab}</VQuote>
</ListItem> </li>
<ListItem> <li>
<SegmentAddKeyDropdown /> <SegmentAddKeyDropdown />
</ListItem> </li>
<ListItem> <li>
<Flex flexDir="row" alignItems="center"> <p>{keyInstructionText}</p>
{'选择 '} {clientInstructions}
<VQuote> </li>
<Icon as={MdFileUpload} boxSize={5} mr={2} /> </ol>
</VQuote>
</Flex>
</ListItem>
<ListItem>
<Text>{keyInstructionText}</Text>
<Tabs display="flex" flexDir="column" border="1px solid" borderColor="gray.300" borderRadius={5}>
{clientInstructions}
</Tabs>
</ListItem>
</OrderedList>
</> </>
); );
} }

View File

@@ -1,12 +1,10 @@
import { Alert, AlertIcon, Container } from '@chakra-ui/react'; import { RiErrorWarningLine } from 'react-icons/ri';
export function SegmentTryOfficialPlayer() { export function SegmentTryOfficialPlayer() {
return ( return (
<Container p={2} my={2} pt={0}> <div className="alert alert-warning">
<Alert status="info" borderRadius={5}> <RiErrorWarningLine className="text-2xl" />
<AlertIcon /> <p></p>
</div>
</Alert>
</Container>
); );
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,24 +0,0 @@
import { Box, Image } from '@chakra-ui/react';
import noCoverFallbackImageURL from '~/assets/no-cover.svg';
interface AlbumImageProps {
url?: string;
name?: string;
}
export function AlbumImage({ name, url }: AlbumImageProps) {
const coverAlternativeText = name ? `${name} 的专辑封面` : '专辑封面';
return (
<Box w="160px" h="160px" m="auto">
<Image
border="2px solid"
borderColor="gray.400"
borderRadius="50%"
objectFit="cover"
src={url || noCoverFallbackImageURL}
alt={coverAlternativeText}
/>
</Box>
);
}

View File

@@ -1,5 +1,6 @@
import { Box, Button, chakra, Collapse, Text, useDisclosure } from '@chakra-ui/react'; import { toast } from 'react-toastify';
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError'; import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
import { applyTemplate } from '~/util/applyTemplate';
export interface FileErrorProps { export interface FileErrorProps {
error: null | string; error: null | string;
@@ -7,33 +8,55 @@ export interface FileErrorProps {
} }
const errorMap = new Map<string | null | DecryptErrorType, string>([ const errorMap = new Map<string | null | DecryptErrorType, string>([
[DecryptErrorType.UNSUPPORTED_FILE, '尚未支持的文件格式'], [DecryptErrorType.UNSUPPORTED_FILE, '支持的文件类型'],
]); ]);
const ERROR_TEMPLATE = `解密错误:{{summary}}
详细信息:
\`\`\`text
{{error}}
\`\`\`
<!-- 报告错误时请提交上述【全部内容】 -->
`;
export function FileError({ error, code }: FileErrorProps) { export function FileError({ error, code }: FileErrorProps) {
const { isOpen, onToggle } = useDisclosure(); const summary = errorMap.get(code) ?? '未知错误';
const errorSummary = errorMap.get(code) ?? '未知错误';
const copyError = () => {
if (error) {
navigator.clipboard
.writeText(applyTemplate(ERROR_TEMPLATE, { summary, error }))
.then(() => {
toast.success('错误信息已复制到剪贴板');
})
.catch((e) => {
toast.error(`复制错误信息失败: ${e}`);
});
}
};
return ( return (
<Box> <>
<Text> <p>
<chakra.span>
<span className="text-red-600">{summary}</span>
<chakra.span color="red.700">{errorSummary}</chakra.span> </p>
</chakra.span>
{error && (
<Button ml="2" onClick={onToggle} type="button">
</Button>
)}
</Text>
{error && ( {error && (
<Collapse in={isOpen} animateOpacity> <div className="collapse border-error border w-full text-left my-2 py-0">
<Box as="pre" display="inline-block" mt="2" px="4" py="2" bg="red.100" color="red.800" rounded="md"> <input className="[&&&]:py-2 [&&&]:min-h-[1.5rem]" type="checkbox" />
{error} <div className="collapse-title font-semibold text-center [&&&]:min-h-[1.5rem] [&&&]:py-2"></div>
</Box> <div className="collapse-content text-sm overflow-hidden">
</Collapse> <pre className="overflow-x-auto w-full">{error}</pre>
<p className="mt-2 text-center">
<button type="button" className="btn btn-secondary" onClick={copyError}>
</button>
</p>
</div>
</div>
)} )}
</Box> </>
); );
} }

View File

@@ -26,7 +26,7 @@ export function FileRow({ id, file }: FileRowProps) {
{metadata?.name ?? nameWithoutExt} {metadata?.name ?? nameWithoutExt}
</h2> </h2>
<div> <div className="w-full grow">
{file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />} {file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />}
{isDecrypted && ( {isDecrypted && (
<audio <audio

View File

@@ -1,22 +0,0 @@
import { Box, Text } from '@chakra-ui/react';
import type { AudioMetadata } from './fileListingSlice';
export interface SongMetadataProps {
metadata: AudioMetadata;
}
export function SongMetadata({ metadata }: SongMetadataProps) {
return (
<Box>
<Text>
: <span data-testid="audio-meta-album-name">{metadata.album}</span>
</Text>
<Text>
: <span data-testid="audio-meta-song-artist">{metadata.artist}</span>
</Text>
<Text>
: <span data-testid="audio-meta-album-artist">{metadata.albumArtist}</span>
</Text>
</Box>
);
}

View File

@@ -1,33 +1,28 @@
import { Code, ListItem, OrderedList, Text, chakra } from '@chakra-ui/react'; import { HiWord } from '~/components/HelpText/HiWord';
const KUWO_IOS_DIR = '/var/mobile/Containers/Data/Application/<酷我数据目录>/mmkv';
export function InstructionsIOS() { export function InstructionsIOS() {
return ( return (
<> <>
<Text>访 iOS </Text> <p>访 iOS </p>
<Text> <p>
<chakra.span color="red.400"></chakra.span> <span className="text-red-600"></span>
</Text> </p>
<OrderedList> <ol className="list-decimal pl-6">
<ListItem> <li>
<Text> 访
访 <br />
<Code wordBreak="break-word">{KUWO_IOS_DIR}</Code> <code className="break-words">
</Text> /var/mobile/Containers/Data/Application/<HiWord className="text-nowrap">{'<>'}</HiWord>/mmkv
</ListItem> </code>
<ListItem> </li>
<Text> <li>
<Code>kw_ekey</Code> 访 <code>kw_ekey</code> 访
</Text> </li>
</ListItem> <li>
<ListItem> <code>kw_ekey</code>
<Text> </li>
<Code>kw_ekey</Code> </ol>
</Text>
</ListItem>
</OrderedList>
</> </>
); );
} }

View File

@@ -1,9 +1,3 @@
import { Text } from '@chakra-ui/react';
export function InstructionsPC() { export function InstructionsPC() {
return ( return <p>使 Windows </p>;
<>
<Text>使 Windows </Text>
</>
);
} }

View File

@@ -1,30 +1,16 @@
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction'; import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsPC } from './InstructionsPC'; import { InstructionsPC } from './InstructionsPC';
import { InstructionsIOS } from './InstructionsIOS'; import { InstructionsIOS } from './InstructionsIOS';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs';
export function KWMv2AllInstructions() { export function KWMv2AllInstructions() {
return ( const ANDROID_DIR = '/data/data/cn.kuwo.player/files/mmkv';
<> const ANDROID_FILE = 'cn.kuwo.player.mmkv.defaultconfig';
<TabList> const tabs: InstructionTab[] = [
<Tab></Tab> { id: 'android', label: '安卓', content: <AndroidADBPullInstruction dir={ANDROID_DIR} file={ANDROID_FILE} /> },
<Tab>iOS</Tab> { id: 'ios', label: 'iOS', content: <InstructionsIOS /> },
<Tab>Windows</Tab> { id: 'windows', label: 'Windows', content: <InstructionsPC /> },
</TabList> ];
<TabPanels flex={1} overflow="auto">
<TabPanel> return <InstructionsTabs tabs={tabs} />;
<AndroidADBPullInstruction
dir="/data/data/cn.kuwo.player/files/mmkv"
file="cn.kuwo.player.mmkv.defaultconfig"
/>
</TabPanel>
<TabPanel>
<InstructionsIOS />
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</>
);
} }

View File

@@ -1,81 +1,41 @@
import { import { PiFileAudio, PiHash } from 'react-icons/pi';
HStack,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
ListItem,
Text,
VStack,
} from '@chakra-ui/react';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { kwm2DeleteKey, kwm2UpdateKey } from '../../settingsSlice'; import { kwm2DeleteKey, kwm2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks'; import { useAppDispatch } from '~/hooks';
import { memo } from 'react'; import { memo } from 'react';
import { StagingKWMv2Key } from '../../keyFormats'; import { StagingKWMv2Key } from '../../keyFormats';
import { KeyInput } from '~/components/KeyInput';
export const KWMv2EKeyItem = memo(({ id, ekey, quality, rid, i }: StagingKWMv2Key & { i: number }) => { export const KWMv2EKeyItem = memo(({ id, ekey, quality, rid, i }: StagingKWMv2Key & { i: number }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const updateKey = (prop: keyof StagingKWMv2Key, e: React.ChangeEvent<HTMLInputElement>) => const ekeyLen = ekey.length;
dispatch(kwm2UpdateKey({ id, field: prop, value: e.target.value })); const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
const deleteKey = () => dispatch(kwm2DeleteKey({ id }));
return ( return (
<ListItem mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}> <KeyInput
<HStack> name={rid}
<Text w="2em" textAlign="center"> quality={quality}
{i + 1} value={ekey}
</Text> isValidKey={isValidEKey}
onSetName={(value) => dispatch(kwm2UpdateKey({ id, field: 'rid', value }))}
<VStack flex={1}> onSetQuality={(value) => dispatch(kwm2UpdateKey({ id, field: 'quality', value }))}
<HStack flex={1} w="full"> onSetValue={(value) => dispatch(kwm2UpdateKey({ id, field: 'ekey', value }))}
<Input onDelete={() => dispatch(kwm2DeleteKey({ id }))}
variant="flushed" sequence={i + 1}
placeholder="资源 ID" nameLabel={
value={rid} <>
onChange={(e) => updateKey('rid', e)} ID
type="number" <PiHash className="hidden md:inline-block" />
maxW="8em" </>
/> }
<Input qualityLabel={
variant="flushed" <>
placeholder="音质格式" <PiFileAudio className="hidden md:inline-block" />
value={quality} </>
onChange={(e) => updateKey('quality', e)} }
flex={1} namePlaceholder="音频哈希。不建议手动填写。"
/> qualityPlaceholder="比特率 ID"
</HStack> valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
/>
<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>
); );
}); });

View File

@@ -1,34 +1,50 @@
import { Code, Heading, ListItem, OrderedList, Text } from '@chakra-ui/react'; import { RiFileCopyLine } from 'react-icons/ri';
import { toast } from 'react-toastify';
import { ExtLink } from '~/components/ExtLink';
import { FilePathBlock } from '~/components/FilePathBlock.tsx'; import { FilePathBlock } from '~/components/FilePathBlock.tsx';
export function InstructionsPC() { export function InstructionsPC() {
const DB_PATH = '%APPDATA%\\KuGou8\\KGMusicV3.db';
const copyDbPathToClipboard = () => {
navigator.clipboard
.writeText(DB_PATH)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
return ( return (
<> <>
<Text> Windows 使 <abbr title="SQLite w/ SQLCipher">SQLite</abbr> </Text> <p>
<Text></Text> Windows 使
<FilePathBlock>%APPDATA%\KuGou8\KGMusicV3.db</FilePathBlock> <ExtLink className="link-info px-1" href="https://www.zetetic.net/sqlcipher/">
SQLCipher
</ExtLink>
</p>
<p></p>
<p className="flex items-center gap-1">
<FilePathBlock>{DB_PATH}</FilePathBlock>
</p>
<Heading as="h3" size="md" mt="4"> <h3 className="font-bold text-xl mt-4"></h3>
<ol className="list-decimal pl-6">
</Heading> <li>
<OrderedList> <button className="btn btn-sm btn-outline btn-accent mr-2" onClick={copyDbPathToClipboard}>
<ListItem> <RiFileCopyLine className="text-xl" />
<Text> <span></span>
<Code>KGMusicV3.db</Code> </button>
</Text> <code>KGMusicV3.db</code>
</ListItem> </li>
<ListItem> <li></li>
<Text></Text> <li>
</ListItem> <code>KGMusicV3.db</code>
<ListItem> </li>
<Text> <li></li>
<Code>KGMusicV3.db</Code> </ol>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
</OrderedList>
</> </>
); );
} }

View File

@@ -1,25 +1,14 @@
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction'; import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsPC } from './InstructionsPC'; import { InstructionsPC } from './InstructionsPC';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs';
export function KugouAllInstructions() { export function KugouAllInstructions() {
return ( const ANDROID_DIR = '/data/data/com.kugou.android/files/mmkv';
<> const ANDROID_FILE = 'mggkey_multi_process';
<TabList> const tabs: InstructionTab[] = [
<Tab></Tab> { id: 'android', label: '安卓', content: <AndroidADBPullInstruction dir={ANDROID_DIR} file={ANDROID_FILE} /> },
<Tab>Windows</Tab> { id: 'windows', label: 'Windows', content: <InstructionsPC /> },
</TabList> ];
<TabPanels flex={1} overflow="auto">
<TabPanel> return <InstructionsTabs tabs={tabs} />;
<AndroidADBPullInstruction
dir="/data/data/com.kugou.android/files/mmkv"
file="mggkey_multi_process"
/>
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</>
);
} }

View File

@@ -1,72 +1,26 @@
import {
HStack,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
ListItem,
Text,
VStack,
} from '@chakra-ui/react';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { kugouDeleteKey, kugouUpdateKey } from '../../settingsSlice'; import { kugouDeleteKey, kugouUpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks'; import { useAppDispatch } from '~/hooks';
import { memo } from 'react'; import { memo } from 'react';
import { StagingKugouKey } from '../../keyFormats'; import { StagingKugouKey } from '../../keyFormats';
import { KeyInput } from '~/components/KeyInput';
export const KugouEKeyItem = memo(({ id, ekey, audioHash, i }: StagingKugouKey & { i: number }) => { export const KugouEKeyItem = memo(({ id, ekey, audioHash, i }: StagingKugouKey & { i: number }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const updateKey = (prop: keyof StagingKugouKey, e: React.ChangeEvent<HTMLInputElement>) => const ekeyLen = ekey.length;
dispatch(kugouUpdateKey({ id, field: prop, value: e.target.value })); const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
const deleteKey = () => dispatch(kugouDeleteKey({ id }));
return ( return (
<ListItem mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}> <KeyInput
<HStack> name={audioHash}
<Text w="2em" textAlign="center"> value={ekey}
{i + 1} isValidKey={isValidEKey}
</Text> onSetName={(value) => dispatch(kugouUpdateKey({ id, field: 'audioHash', value }))}
onSetValue={(value) => dispatch(kugouUpdateKey({ id, field: 'ekey', value }))}
<VStack flex={1}> onDelete={() => dispatch(kugouDeleteKey({ id }))}
<HStack flex={1} w="full"> sequence={i + 1}
<Input namePlaceholder="音频哈希。不建议手动填写。"
variant="flushed" valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
placeholder="音频哈希。不建议手动填写。" />
value={audioHash}
onChange={(e) => updateKey('audioHash', e)}
/>
</HStack>
<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>
); );
}); });

View File

@@ -1,5 +1,4 @@
import { Box, Flex, Heading, List, Text, useToast } from '@chakra-ui/react'; import { useRef, useState } from 'react';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { ImportSecretModal } from '~/components/ImportSecretModal'; import { ImportSecretModal } from '~/components/ImportSecretModal';
@@ -12,9 +11,10 @@ import { KugouEKeyItem } from '~/features/settings/panels/Kugou/KugouEKeyItem.ts
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx'; import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
import { parseAndroidKugouMMKV } from '~/util/mmkv/kugou.ts'; import { parseAndroidKugouMMKV } from '~/util/mmkv/kugou.ts';
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor.ts'; import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor.ts';
import { KeyListContainer } from '~/components/KeyListContainer';
import { toastImportResult } from '~/util/toastImportResult';
export function PanelKGGKey() { export function PanelKGGKey() {
const toast = useToast();
const dispatch = useDispatch(); const dispatch = useDispatch();
const kugouKeys = useSelector(selectStagingKugouV5Keys); const kugouKeys = useSelector(selectStagingKugouV5Keys);
const [showImportModal, setShowImportModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false);
@@ -30,49 +30,34 @@ export function PanelKGGKey() {
keys = extractor.extractKugouKeyFromEncryptedDb(await file.arrayBuffer()); keys = extractor.extractKugouKeyFromEncryptedDb(await file.arrayBuffer());
} }
if (keys?.length === 0) { if (keys && keys.length > 0) {
toast({
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (keys) {
dispatch(kugouImportKeys(keys)); dispatch(kugouImportKeys(keys));
setShowImportModal(false); setShowImportModal(false);
toast({
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
description: '记得按下「保存」来应用。',
isClosable: true,
status: 'success',
});
} else {
toast({
title: `不支持的文件:${file.name}`,
isClosable: true,
status: 'error',
});
} }
toastImportResult(file.name, keys);
}; };
const refKeyContainer = useRef<HTMLDivElement>(null);
return ( return (
<Flex minH={0} flexDir="column" flex={1}> <div className="container flex flex-col grow min-h-0 w-full">
<Heading as="h2" size="lg"> <h2 className="text-2xl font-bold"> (KGG / KGM v5)</h2>
(KGG / KGM v5)
</Heading>
<Text>使 KGG / KGM v5 </Text> <p>使 KGG / KGM v5 </p>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} /> <h3 className="mt-2 text-xl font-bold"></h3>
<AddKey
addKey={addKey}
refContainer={refKeyContainer}
importKeyFromFile={() => setShowImportModal(true)}
clearKeys={clearAll}
/>
<Box flex={1} minH={0} overflow="auto" pr="4"> <KeyListContainer ref={refKeyContainer} keys={kugouKeys}>
<List spacing={3}> {kugouKeys.map(({ id, audioHash, ekey }, i) => (
{kugouKeys.map(({ id, audioHash, ekey }, i) => ( <KugouEKeyItem key={id} id={id} ekey={ekey} audioHash={audioHash} i={i} />
<KugouEKeyItem key={id} id={id} ekey={ekey} audioHash={audioHash} i={i} /> ))}
))} </KeyListContainer>
</List>
{kugouKeys.length === 0 && <Text></Text>}
</Box>
<ImportSecretModal <ImportSecretModal
clientName="酷狗音乐" clientName="酷狗音乐"
@@ -82,6 +67,6 @@ export function PanelKGGKey() {
> >
<KugouAllInstructions /> <KugouAllInstructions />
</ImportSecretModal> </ImportSecretModal>
</Flex> </div>
); );
} }

View File

@@ -1,25 +1,5 @@
import { import { useRef, useState } from 'react';
Box,
Button,
ButtonGroup,
Code,
Flex,
HStack,
Heading,
Icon,
IconButton,
List,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Text,
useToast,
} from '@chakra-ui/react';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
import { ImportSecretModal } from '~/components/ImportSecretModal'; import { ImportSecretModal } from '~/components/ImportSecretModal';
import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo'; import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo';
@@ -29,9 +9,11 @@ import { selectStagingKWMv2Keys } from '../settingsSelector';
import { KWMv2EKeyItem } from './KWMv2/KWMv2EKeyItem'; import { KWMv2EKeyItem } from './KWMv2/KWMv2EKeyItem';
import type { StagingKWMv2Key } from '../keyFormats'; import type { StagingKWMv2Key } from '../keyFormats';
import { KWMv2AllInstructions } from './KWMv2/KWMv2AllInstructions'; import { KWMv2AllInstructions } from './KWMv2/KWMv2AllInstructions';
import { AddKey } from '~/components/AddKey';
import { KeyListContainer } from '~/components/KeyListContainer';
import { toastImportResult } from '~/util/toastImportResult';
export function PanelKWMv2Key() { export function PanelKWMv2Key() {
const toast = useToast();
const dispatch = useDispatch(); const dispatch = useDispatch();
const kwm2Keys = useSelector(selectStagingKWMv2Keys); const kwm2Keys = useSelector(selectStagingKWMv2Keys);
const [showImportModal, setShowImportModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false);
@@ -46,70 +28,33 @@ export function PanelKWMv2Key() {
keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer())); keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer()));
} }
if (keys?.length === 0) { if (keys && keys.length > 0) {
toast({
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (keys) {
dispatch(kwm2ImportKeys(keys)); dispatch(kwm2ImportKeys(keys));
setShowImportModal(false); setShowImportModal(false);
toast({
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
description: '记得按下「保存」来应用。',
isClosable: true,
status: 'success',
});
} else {
toast({
title: `不支持的文件:${file.name}`,
isClosable: true,
status: 'error',
});
} }
toastImportResult(file.name, keys);
}; };
const refKeyContainer = useRef<HTMLDivElement>(null);
return ( return (
<Flex minH={0} flexDir="column" flex={1}> <div className="container flex flex-col grow min-h-0 w-full">
<Heading as="h2" size="lg"> <h2 className="text-2xl font-bold">KwmV2</h2>
KwmV2 <p>
</Heading> V2 <code>mflac</code> <code>mgg</code>
</p>
<p></p>
<AddKey
addKey={addKey}
refContainer={refKeyContainer}
importKeyFromFile={() => setShowImportModal(true)}
clearKeys={clearAll}
/>
<Text> <KeyListContainer ref={refKeyContainer} keys={kwm2Keys}>
V2 <Code>mflac</Code> 沿 <Code>kwm</Code>{''} {kwm2Keys.map(({ id, ekey, quality, rid }, i) => (
<KWMv2EKeyItem key={id} id={id} ekey={ekey} quality={quality} rid={rid} i={i} />
</Text> ))}
</KeyListContainer>
<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>
</HStack>
<Box flex={1} minH={0} overflow="auto" pr="4">
<List spacing={3}>
{kwm2Keys.map(({ id, ekey, quality, rid }, i) => (
<KWMv2EKeyItem key={id} id={id} ekey={ekey} quality={quality} rid={rid} i={i} />
))}
</List>
{kwm2Keys.length === 0 && <Text></Text>}
</Box>
<ImportSecretModal <ImportSecretModal
clientName="酷我音乐" clientName="酷我音乐"
@@ -119,6 +64,6 @@ export function PanelKWMv2Key() {
> >
<KWMv2AllInstructions /> <KWMv2AllInstructions />
</ImportSecretModal> </ImportSecretModal>
</Flex> </div>
); );
} }

View File

@@ -1,8 +1,7 @@
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';
import React, { useState } from 'react'; import React, { useRef, useState } from 'react';
import { QMCv2EKeyItem } from './QMCv2/QMCv2EKeyItem'; import { QMCv2EKeyItem } from './QMCv2/QMCv2EKeyItem';
import { ImportSecretModal } from '~/components/ImportSecretModal'; import { ImportSecretModal } from '~/components/ImportSecretModal';
import { StagingQMCv2Key } from '../keyFormats'; import { StagingQMCv2Key } from '../keyFormats';
@@ -15,9 +14,10 @@ import { AddKey } from '~/components/AddKey';
import { InfoModal } from '~/components/InfoModal.tsx'; import { InfoModal } from '~/components/InfoModal.tsx';
import { Ruby } from '~/components/Ruby.tsx'; import { Ruby } from '~/components/Ruby.tsx';
import { ExtLink } from '~/components/ExtLink.tsx'; import { ExtLink } from '~/components/ExtLink.tsx';
import { KeyListContainer } from '~/components/KeyListContainer';
import { toastImportResult } from '~/util/toastImportResult';
export function PanelQMCv2Key() { export function PanelQMCv2Key() {
const toast = useToast();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings); const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
const [showImportModal, setShowImportModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false);
@@ -34,46 +34,31 @@ export function PanelQMCv2Key() {
try { try {
const fileBuffer = await file.arrayBuffer(); const fileBuffer = await file.arrayBuffer();
let qmc2Keys: null | Omit<StagingQMCv2Key, 'id'>[] = null; let keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
if (/(player_process[_.]db|music_audio_play)(\.db)?$/i.test(file.name)) { if (/(player_process[_.]db|music_audio_play)(\.db)?$/i.test(file.name)) {
const extractor = await DatabaseKeyExtractor.getInstance(); const extractor = await DatabaseKeyExtractor.getInstance();
qmc2Keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer); keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer);
if (!qmc2Keys) {
alert(`不是支持的 SQLite 数据库文件。`);
return;
}
} else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1|(\.mmkv$)/i.test(file.name)) { } else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1|(\.mmkv$)/i.test(file.name)) {
const fileBuffer = await file.arrayBuffer(); const fileBuffer = await file.arrayBuffer();
const map = parseAndroidQmEKey(new DataView(fileBuffer)); const map = parseAndroidQmEKey(new DataView(fileBuffer));
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey })); keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
} }
if (qmc2Keys?.length === 0) { if (keys && keys.length > 0) {
toast({ dispatch(qmc2ImportKeys(keys));
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (qmc2Keys) {
dispatch(qmc2ImportKeys(qmc2Keys));
setShowImportModal(false); setShowImportModal(false);
toast({
title: `导入成功 (${qmc2Keys.length})`,
description: '记得保存更改来应用。',
isClosable: true,
status: 'success',
});
} else {
alert(`不支持的文件:${file.name}`);
} }
toastImportResult(file.name, keys);
} catch (e) { } catch (e) {
console.error('error during import: ', e); console.error('error during import: ', e);
alert(`导入数据库时发生错误:${e}`); alert(`导入数据库时发生错误:${e}`);
} }
}; };
const refKeyContainer = useRef<HTMLDivElement>(null);
return ( return (
<> <>
<h2 className="text-2xl font-bold">QMCv2 </h2> <h2 className="text-2xl font-bold">QMCv2 </h2>
@@ -112,19 +97,19 @@ export function PanelQMCv2Key() {
</InfoModal> </InfoModal>
</div> </div>
<h3 className="mt-2 text-1xl font-bold"></h3> <h3 className="mt-2 text-xl font-bold"></h3>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} /> <AddKey
addKey={addKey}
refContainer={refKeyContainer}
importKeyFromFile={() => setShowImportModal(true)}
clearKeys={clearAll}
/>
<div className="flex-1 min-h-0 overflow-auto pr-4 pt-3"> <KeyListContainer ref={refKeyContainer} keys={qmc2Keys}>
{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} /> </KeyListContainer>
))}
</ul>
)}
{qmc2Keys.length === 0 && <p className="p-4 pb-2 tracking-wide"></p>}
</div>
<ImportSecretModal <ImportSecretModal
clientName={ clientName={

View File

@@ -1,26 +1,14 @@
import {
Box,
Code,
Flex,
FormControl,
FormHelperText,
FormLabel,
Heading,
Input,
ListItem,
Text,
UnorderedList,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from '~/hooks'; import { useAppDispatch, useAppSelector } from '~/hooks';
import { ExtLink } from '~/components/ExtLink'; import { ExtLink } from '~/components/ExtLink';
import { ChangeEvent, ClipboardEvent } from 'react'; import { ChangeEvent, ClipboardEvent, useId } from 'react';
import { VQuote } from '~/components/HelpText/VQuote'; import { VQuote } from '~/components/HelpText/VQuote';
import { selectStagingQtfmAndroidKey } from '../settingsSelector'; import { selectStagingQtfmAndroidKey } from '../settingsSelector';
import { qtfmAndroidUpdateKey } from '../settingsSlice'; import { qtfmAndroidUpdateKey } from '../settingsSlice';
import { workerClientBus } from '~/decrypt-worker/client.ts'; import { workerClientBus } from '~/decrypt-worker/client.ts';
import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts'; import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts';
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts'; import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts';
import { Ruby } from '~/components/Ruby';
import { HiWord } from '~/components/HelpText/HiWord';
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest'; const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
@@ -64,64 +52,72 @@ export function PanelQingTing() {
setSecretKey(e.target.value); setSecretKey(e.target.value);
}; };
const idSecretKey = useId();
return ( return (
<Flex minH={0} flexDir="column" flex={1}> <div className="min-h-0 flex-col grow px-1">
<Heading as="h2" size="lg"> <h2 className="text-2xl font-bold mb-4">
<VQuote> FM</VQuote> <VQuote> FM</VQuote>
</Heading> </h2>
<Text> <p>
<VQuote> FM</VQuote> <VQuote> FM</VQuote>
</Text> </p>
<Box mt={3} mb={3}>
<FormControl> <div className="my-4">
<FormLabel></FormLabel> <fieldset className="fieldset">
<Input type="text" onPaste={handleDataPaste} value={secretKey} onChange={handleDataInput} /> <legend className="fieldset-legend text-lg">
<FormHelperText> <label htmlFor={idSecretKey}></label>
{'粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过 '} </legend>
<input
id={idSecretKey}
type="text"
className="input font-mono"
onPaste={handleDataPaste}
value={secretKey}
onChange={handleDataInput}
/>
<p className="label">
<ExtLink href={QTFM_DEVICE_ID_URL}> <ExtLink href={QTFM_DEVICE_ID_URL}>
<Code>qtfm-device-id</Code> <code>qtfm-device-id</code>
</ExtLink> </ExtLink>
{' 获取的设备信息)。'} root
</FormHelperText> </p>
</FormControl> </fieldset>
</Box> </div>
<Heading as="h3" size="md" pt={3} pb={2}> <h3 className="text-xl font-bold my-2"></h3>
<ul className="list-disc pl-6">
</Heading> <li>
<UnorderedList> <p>
<ListItem>
<Text>
<Code>[]/Android/data/fm.qingting.qtradio/files/Music/</Code> <VQuote>
</Text> <code>
<HiWord>[]</HiWord>/Android/data/fm.qingting.qtradio/files/Music/
<UnorderedList> </code>
<ListItem> </VQuote>
<Text> </p>
<ul className="list-disc pl-6">
<li>
<p>
使 使
<ruby> <Ruby caption="root"></Ruby>
<rp> (</rp>
<rt>root</rt>
<rp>)</rp>
</ruby>
访 访
</Text> </p>
</ListItem> </li>
</UnorderedList> </ul>
</ListItem> </li>
<ListItem> <li>
<Text> <p>
<Code>.p~!</Code> <code>.p~!</code>
</Text> </p>
</ListItem> </li>
<ListItem> <li>
<Text></Text> <p></p>
</ListItem> </li>
</UnorderedList> </ul>
</Flex> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Heading, Text, Code, Kbd, OrderedList, ListItem, Link } from '@chakra-ui/react'; import { Heading, Text, Code, OrderedList, ListItem, Link } from '@chakra-ui/react';
import { FilePathBlock } from '~/components/FilePathBlock'; import { FilePathBlock } from '~/components/FilePathBlock';
import { MacCommandKey } from '~/components/Key/MacCommandKey'; import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey'; import { ShiftKey } from '~/components/Key/ShiftKey';
@@ -42,7 +42,7 @@ export function InstructionsMac() {
{' + '} {' + '}
<MacCommandKey /> <MacCommandKey />
{' + '} {' + '}
<Kbd>{'G'}</Kbd> <kbd className="kbd">{'G'}</kbd>
</Text> </Text>
</ListItem> </ListItem>
<ListItem> <ListItem>

View File

@@ -1,16 +1,57 @@
import { ExtLink } from '~/components/ExtLink';
import { HiWord } from '~/components/HelpText/HiWord';
import NoopExecutable from './assets/noop.exe?base64';
import NoopExecutableSource from './assets/noop.asm.txt?base64';
export function InstructionsPC() { export function InstructionsPC() {
return ( return (
<> <>
<p>
使 <span className="text-primary">19.51 </span>
<mark></mark>
</p>
<p className="mt-2"> <p className="mt-2">
使 <span className="text-error">19.57 </span> 使 <span className="text-error">19.57 </span>
<mark></mark> <HiWord></HiWord>
<br /> <br />
</p> </p>
<p className="mt-4">
使 <span className="text-primary">19.51 </span>
<HiWord></HiWord>
</p>
<p className="mt-4">
QQ Windows <HiWord>19.51</HiWord>
</p>
<ul className="list-disc pl-6">
<li>
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
<code>Archive.org</code>
</ExtLink>
</li>
<li>
<ExtLink href="https://t.me/um_lsr_ch/24"> Telegram </ExtLink>
</li>
</ul>
<p className="mt-4">
QQ
<a
className="link px-1"
download="QQMusicUp.exe"
href={`data:application/vnd.microsoft.portable-executable;base64,${NoopExecutable}`}
>
<code>QQMusicUp.exe</code>
</a>
<a
className="link px-1"
download="QQMusicUp.asm"
href={`data:text/x-asm;charset=utf-8;base64,${NoopExecutableSource}`}
>
</a>
</p>
<p className="mt-2">使</p>
</> </>
); );
} }

View File

@@ -0,0 +1,16 @@
; QQ 音乐更新 - 占位文件
; 使用 FASM 编译即可。
format PE GUI 4.0
entry start
include 'win32a.inc'
section '.text' code readable executable
start:
invoke ExitProcess, 0
section '.idata' import data readable writeable
library kernel,'KERNEL32.DLL'
import kernel,\
ExitProcess,'ExitProcess'

Binary file not shown.

View File

@@ -1,5 +1,4 @@
import { FC, Fragment } from 'react'; import { FC, Fragment } from 'react';
import { Center, Container, Heading, Link, ListItem, UnorderedList } from '@chakra-ui/react';
import { Header3 } from '~/components/HelpText/Headers'; import { Header3 } from '~/components/HelpText/Headers';
import { KuwoFAQ } from '~/faq/KuwoFAQ'; import { KuwoFAQ } from '~/faq/KuwoFAQ';
import { OtherFAQ } from '~/faq/OtherFAQ'; import { OtherFAQ } from '~/faq/OtherFAQ';
@@ -21,24 +20,24 @@ const faqEntries: FAQEntry[] = [
export function FaqTab() { export function FaqTab() {
return ( return (
<Container pb={10} maxW="container.md"> <section className="container pb-10">
<Center> <h2 className="text-3xl font-bold text-center"></h2>
<Heading as="h2"></Heading>
</Center>
<Header3></Header3> <Header3></Header3>
<UnorderedList> <ul className="list-disc list-inside">
{faqEntries.map(({ id, title }) => ( {faqEntries.map(({ id, title }) => (
<ListItem key={id}> <li key={id}>
<Link href={`#faq-${id}`}>{title}</Link> <a className="link link-info no-underline" href={`#faq-${id}`}>
</ListItem> {title}
</a>
</li>
))} ))}
</UnorderedList> </ul>
{faqEntries.map(({ id, title, Help }) => ( {faqEntries.map(({ id, title, Help }) => (
<Fragment key={id}> <Fragment key={id}>
<Header3 id={`faq-${id}`}>{title}</Header3> <Header3 id={`faq-${id}`}>{title}</Header3>
<Help /> <Help />
</Fragment> </Fragment>
))} ))}
</Container> </section>
); );
} }

View File

@@ -1,9 +1,5 @@
import { Settings } from '~/features/settings/Settings'; import { Settings } from '~/features/settings/Settings';
export function SettingsTab() { export function SettingsTab() {
return ( return <Settings />;
<div className="flex p-0">
<Settings />
</div>
);
} }

View File

@@ -0,0 +1,3 @@
export function applyTemplate(tpl: string, values: Record<string, unknown>) {
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : ''));
}

View File

@@ -0,0 +1,28 @@
import { toast } from 'react-toastify';
export function toastImportResult(name: string, keys: unknown[] | null) {
if (keys?.length === 0) {
toast.warning(() => (
<div className="flex flex-col">
<h3 className="text-lg font-bold"></h3>
<div></div>
</div>
));
} else if (keys) {
toast.success(() => (
<div className="flex flex-col">
<h3 className="text-lg font-bold"> {keys.length} </h3>
<div></div>
</div>
));
} else {
toast.error(() => (
<div className="flex flex-col">
<h3 className="text-lg font-bold"></h3>
<div>
<code>{name}</code>
</div>
</div>
));
}
}

5
src/vite-env.d.ts vendored
View File

@@ -6,3 +6,8 @@ module 'virtual:pwa-register' {
*/ */
declare function registerSW(_opts: unknown): () => void; declare function registerSW(_opts: unknown): () => void;
} }
declare module '*?base64' {
const content: string;
export default content;
}

15
support/b64-loader.ts Normal file
View File

@@ -0,0 +1,15 @@
import { readFile } from 'node:fs/promises';
import { Plugin } from 'vite';
export const base64Loader: Plugin = {
name: 'base64-loader',
async transform(_: unknown, id: string) {
const [path, query] = id.split('?');
if (query != 'base64') return null;
const data = await readFile(path);
const base64 = data.toString('base64');
return `export default '${base64}';`;
},
};

View File

@@ -11,6 +11,7 @@ import { VitePWA } from 'vite-plugin-pwa';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import { tryCommand } from './support/command'; import { tryCommand } from './support/command';
import { base64Loader } from './support/b64-loader';
const projectRoot = url.fileURLToPath(new URL('.', import.meta.url)); const projectRoot = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8')); const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8'));
@@ -45,6 +46,7 @@ export default defineConfig({
}, },
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
base64Loader,
replace({ replace({
preventAssignment: true, preventAssignment: true,
values: { values: {