mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 03:23:02 +00:00
refactor: batch 3
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 || (
|
||||||
|
|||||||
21
src/components/KeyListContainer.tsx
Normal file
21
src/components/KeyListContainer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 框架打包的本地版,提供适用于 Windows、Linux 和 Mac 平台的可执行文件。
|
利用 Electron 框架打包的本地版,提供适用于 Windows、Linux 和 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
BIN
src/faq/assets/ld_settings_misc@2x.webp
Normal file
BIN
src/faq/assets/ld_settings_misc@2x.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
src/faq/assets/mumu_settings_misc@2x.webp
Normal file
BIN
src/faq/assets/mumu_settings_misc@2x.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
export function InstructionsPC() {
|
export function InstructionsPC() {
|
||||||
return (
|
return <p>使用 Windows 客户端下载的文件不需要导入密钥。</p>;
|
||||||
<>
|
|
||||||
<Text>使用 Windows 客户端下载的文件不需要导入密钥。</Text>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/features/settings/panels/QMCv2/assets/noop.asm.txt
Normal file
16
src/features/settings/panels/QMCv2/assets/noop.asm.txt
Normal 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'
|
||||||
BIN
src/features/settings/panels/QMCv2/assets/noop.exe
Normal file
BIN
src/features/settings/panels/QMCv2/assets/noop.exe
Normal file
Binary file not shown.
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/util/applyTemplate.ts
Normal file
3
src/util/applyTemplate.ts
Normal 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]) : ''));
|
||||||
|
}
|
||||||
28
src/util/toastImportResult.tsx
Normal file
28
src/util/toastImportResult.tsx
Normal 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
5
src/vite-env.d.ts
vendored
@@ -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
15
support/b64-loader.ts
Normal 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}';`;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user