refactor: batch 3

This commit is contained in:
鲁树人
2025-05-18 02:41:20 +09:00
parent 75b43e1e84
commit 2e4e57be45
52 changed files with 933 additions and 1136 deletions
+29 -7
View File
@@ -1,24 +1,46 @@
import type { RefObject } from 'react';
import { MdAdd, MdDeleteForever, MdFileUpload } from 'react-icons/md';
export interface AddKeyProps {
addKey: () => void;
importKeyFromFile?: () => 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 (
<div className="flex flex-row justify-between items-center">
<div className="join">
<button className="btn join-item" onClick={addKey}>
<MdAdd /> 添加一条
<button type="button" className="join-item btn flex items-center gap-2" onClick={handleAddKey}>
<MdAdd className="text-lg" /> 添加一条
</button>
<button className="btn join-item" onClick={importKeyFromFile}>
<MdFileUpload />
<button type="button" className="join-item btn flex items-center gap-2" onClick={importKeyFromFile}>
<MdFileUpload className="text-lg" />
导入数据库…
</button>
<button className="btn btn-error join-item" onClick={clearKeys}>
<MdDeleteForever />
<button type="button" className="join-item btn flex items-center gap-2 btn-error" onClick={clearKeys}>
<MdDeleteForever className="text-lg" />
清空密钥
</button>
</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';
const applyTemplate = (tpl: string, values: Record<string, unknown>) => {
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : '<nil>'));
};
import { Ruby } from '../Ruby';
import { useId } from 'react';
import { RootExplorerGuide } from './RootExplorerGuide';
import { AdbInstructionTemplate } from './AdbInstructionTemplate';
import { HiWord } from '../HelpText/HiWord';
export interface AndroidADBPullInstructionProps {
dir: 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) {
const psAdbDumpCommand = applyTemplate(PowerShellAdbDumpCommandTemplate, { dir, file });
const shAdbDumpCommand = applyTemplate(ShellAdbDumpCommandTemplate, { dir, file });
const androidInstructionId = useId();
return (
<>
<Text>
你需要
<ruby>
超级管理员
<rp> (</rp>
<rt>
<code>root</code>
</rt>
<rp>)</rp>
</ruby>
访问权限来访问安卓应用的私有数据。
</Text>
<Text>
<p>
你需要<Ruby caption="root">超级管理员</Ruby>访问权限来访问安卓应用的私有数据。
</p>
<p>
⚠️ 请注意,获取管理员权限通常意味着你的安卓设备
<chakra.span color="red.400">将失去保修资格</chakra.span>。
</Text>
<HiWord>将失去保修资格</HiWord>。
</p>
<Accordion allowToggle mt="2">
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
在安卓手机端操作
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<Text>
启动具有 <Code>root</Code> 特权的文件浏览器
</Text>
</ListItem>
<ListItem>
<Text>
访问 <Code>{dir}/</Code> 目录。
</Text>
</ListItem>
<ListItem>
<Text>
将文件 <Code>{file}</Code> 复制到浏览器可访问的目录。
<br />
(例如下载目录)
</Text>
</ListItem>
<ListItem>
<Text>提交该数据库文件。</Text>
</ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
在 PC 端操作(ADB / PowerShell)
</Box>
<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>
<div className="join join-vertical bg-base-100 mt-2 max-w-full">
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={androidInstructionId} />
<div className="collapse-title font-semibold">在安卓手机端操作</div>
<div className="collapse-content text-sm min-w-0">
<ol className="list-decimal pl-4">
<li>
启动支持 <code>root</code> 特权的文件浏览器,如 <ExtLink href={URL_AMAZE}>Amaze 文件浏览器</ExtLink>、
<ExtLink href={URL_MT2}>MT 管理器</ExtLink> 等。
</li>
<li>
※ 记得启用 root 特权!
<RootExplorerGuide />
</li>
<li>
<p>
访问 <code>{dir}/</code> 目录。
</p>
<p>※ 从侧边栏选择根目录开始。</p>
</li>
<li>
将文件 <code>{file}</code> 复制到浏览器可访问的目录(例如下载目录)。
</li>
<li>提交该数据库文件。</li>
</ol>
</div>
</div>
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={androidInstructionId} />
<div className="collapse-title font-semibold">在 PC 端操作(使用 ADB / PowerShell)</div>
<div className="collapse-content text-sm min-w-0">
<AdbInstructionTemplate dir={dir} file={file} platform="win32" />
</div>
</div>
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={androidInstructionId} />
<div className="collapse-title font-semibold">在 Linux / Mac 系统下操作(使用 ADB / Shell)</div>
<div className="collapse-content text-sm min-w-0">
<AdbInstructionTemplate dir={dir} file={file} platform="linux" />
</div>
</div>
</div>
</>
);
}
@@ -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>
);
}
+1 -1
View File
@@ -7,7 +7,7 @@ export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {
return (
<a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
<a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
{children}
{icon && <FiExternalLink className="inline size-sm ml-1" />}
</a>
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import type { ReactNode } from 'react';
export function FilePathBlock({ children }: { children: React.ReactNode }) {
export function FilePathBlock({ children }: { children: ReactNode }) {
return (
<pre className="whitespace-pre-wrap break-all">
<code>{children}</code>
+6 -17
View File
@@ -1,4 +1,3 @@
import { Heading } from '@chakra-ui/react';
import React from 'react';
export interface HeaderProps {
@@ -9,34 +8,24 @@ export interface HeaderProps {
export function Header3({ children, className, id }: HeaderProps) {
return (
<Heading
as="h3"
id={id}
className={className}
pt={3}
pb={1}
borderBottom={'1px solid'}
borderColor="gray.300"
color="gray.800"
size="lg"
>
<h3 id={id} className={`text-2xl pt-3 pb-1 font-bold border-b border-base-300 text-neutral-800 ${className}`}>
{children}
</Heading>
</h3>
);
}
export function Header4({ children, className, id }: HeaderProps) {
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}
</Heading>
</h4>
);
}
export function Header5({ children, className, id }: HeaderProps) {
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}
</Heading>
</h5>
);
}
+2 -8
View File
@@ -1,9 +1,3 @@
import { Mark } from '@chakra-ui/react';
export function HiWord({ children }: { children: React.ReactNode }) {
return (
<Mark bg="orange.100" borderRadius={5} px={2} mx={1}>
{children}
</Mark>
);
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>;
}
+2 -6
View File
@@ -1,13 +1,9 @@
import { chakra, css } from '@chakra-ui/react';
const cssUnselectable = css({ pointerEvents: 'none', userSelect: 'none' });
export function VQuote({ children }: { children: React.ReactNode }) {
return (
<>
<chakra.span css={cssUnselectable}>「</chakra.span>
<span className="select-none">「</span>
{children}
<chakra.span css={cssUnselectable}>」</chakra.span>
<span className="select-none">」</span>
</>
);
}
+4 -7
View File
@@ -13,21 +13,18 @@ export interface InstructionsTabsProps {
export function InstructionsTabs({ tabs }: InstructionsTabsProps) {
const id = useId();
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) => (
<Fragment key={_tabId}>
<label className="tab">
<input type="radio" name={id} defaultChecked={index === 0} />
{label}
</label>
<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto">{content}</div>
<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto max-h-[30rem]">
{content}
</div>
</Fragment>
))}
{/*<label className="tab">*/}
{/* <input type="radio" name={id} />a*/}
{/*</label>*/}
{/*<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto"></div>*/}
{/*<input type="radio" name={id} className="tab" aria-label="安卓" defaultChecked />*/}
</div>
);
}
+6 -9
View File
@@ -1,15 +1,12 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsCommand } from 'react-icons/bs';
import { Ruby } from '../Ruby';
export function MacCommandKey() {
return (
<ruby>
<Kbd>
<Icon as={BsCommand} />
</Kbd>
<rp> (</rp>
<rt>command</rt>
<rp>)</rp>
</ruby>
<Ruby caption="command">
<kbd className="kbd">
<BsCommand className="text-sm" />
</kbd>
</Ruby>
);
}
+6 -9
View File
@@ -1,15 +1,12 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsShift } from 'react-icons/bs';
import { Ruby } from '../Ruby';
export function ShiftKey() {
return (
<ruby>
<Kbd>
<Icon as={BsShift} />
</Kbd>
<rp> (</rp>
<rt>shift</rt>
<rp>)</rp>
</ruby>
<Ruby caption="shift">
<kbd className="kbd">
<BsShift className="text-sm" />
</kbd>
</Ruby>
);
}
+46 -19
View File
@@ -1,6 +1,6 @@
import { PiFileAudio } from 'react-icons/pi';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import React from 'react';
import type { ReactNode } from 'react';
export interface KeyInputProps {
sequence: number;
@@ -12,24 +12,34 @@ export interface KeyInputProps {
onSetValue: (value: string) => void;
onDelete: () => void;
nameLabel?: React.ReactNode;
valueLabel?: React.ReactNode;
quality?: string;
onSetQuality?: (quality: string) => void;
nameLabel?: ReactNode;
valueLabel?: ReactNode;
qualityLabel?: ReactNode;
namePlaceholder?: string;
valuePlaceholder?: string;
qualityPlaceholder?: string;
}
export function KeyInput(props: KeyInputProps) {
const {
nameLabel,
valueLabel,
qualityLabel,
namePlaceholder,
qualityPlaceholder,
valuePlaceholder,
sequence,
name,
quality,
value,
onSetName,
onSetValue,
onDelete,
onSetQuality,
isValidKey,
} = props;
@@ -40,22 +50,39 @@ export function KeyInput(props: KeyInputProps) {
</div>
<div className="join join-vertical flex-1">
<label className="input w-full rounded-tl-md rounded-tr-md">
<span className="cucursor-default inline-flex items-center gap-1 select-none">
{nameLabel || (
<>
文件 <PiFileAudio />
</>
)}
</span>
<input
type="text"
className="font-mono"
placeholder={namePlaceholder}
value={name}
onChange={(e) => onSetName(e.target.value)}
/>
</label>
<div className="flex">
<label className="input w-full rounded-tl-md last:rounded-tr-md">
<span className="cucursor-default inline-flex items-center gap-1 select-none">
{nameLabel || (
<>
文件 <PiFileAudio />
</>
)}
</span>
<input
type="text"
className="font-mono"
placeholder={namePlaceholder}
value={name}
onChange={(e) => onSetName(e.target.value)}
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]">
<span className="cursor-default inline-flex items-center gap-1 select-none">
{valueLabel || (
+21
View File
@@ -0,0 +1,21 @@
import type { ReactNode, RefObject } from 'react';
export interface KeyListContainerProps {
keys: unknown[];
children?: ReactNode;
ref?: RefObject<HTMLDivElement | null>;
}
export function KeyListContainer({ keys, children, ref }: KeyListContainerProps) {
const count = keys.length;
return (
<div ref={ref} className="flex grow min-h-0 overflow-auto pr-4 pt-3">
{count > 0 && (
<ul className="list bg-base-100 rounded-box shadow-md border border-base-300 w-full min-h-0 max-h-[30rem] overflow-auto">
{children}
</ul>
)}
{count === 0 && <p>还没有添加密钥。</p>}
</div>
);
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { Link } from '@chakra-ui/react';
import { ExtLink } from './ExtLink';
export interface ProjectIssueProps {
id: number | string;
@@ -7,9 +7,9 @@ export interface ProjectIssueProps {
export function ProjectIssue({ id, title }: ProjectIssueProps) {
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}`}
{title && ` - ${title}`}
</Link>
</ExtLink>
);
}