mirror of
https://git.unlock-music.dev/um/um-react.git
synced 2025-05-23 16:27:41 +08:00
Compare commits
2 Commits
2e4e57be45
...
6cb1f9f87f
Author | SHA1 | Date | |
---|---|---|---|
|
6cb1f9f87f | ||
|
9518b813bd |
@ -1,8 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-cmn-Hans-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>音乐解锁 - Unlock Music</title>
|
||||
|
||||
<meta name="description" content="音乐解锁 - Unlock Music" />
|
||||
@ -10,6 +10,7 @@
|
||||
<link rel="apple-touch-icon" href="/pwa-512x512.png" sizes="512x512" />
|
||||
<meta name="theme-color" content="#4DBA87" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main id="root"></main>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
11870
package-lock.json
generated
Normal file
11870
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc -p tsconfig.prod.json && vite build && pnpm build:finalize",
|
||||
"build:finalize": "node scripts/write-version.mjs && node scripts/minify-mjs.mjs",
|
||||
"build:finalize": "node scripts/write-version.mjs",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier -w .",
|
||||
"test": "vitest run",
|
||||
@ -17,17 +17,10 @@
|
||||
"prepare": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.3.4",
|
||||
"@chakra-ui/icons": "^2.2.4",
|
||||
"@chakra-ui/react": "^2.10.8",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@unlock-music/crypto": "0.1.10",
|
||||
"classnames": "^2.5.1",
|
||||
"framer-motion": "^12.12.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"radash": "^12.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@ -37,9 +30,7 @@
|
||||
"react-router": "^7.6.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"sass": "^1.89.0",
|
||||
"sql.js": "^1.13.0",
|
||||
"workbox-build": "^7.3.0"
|
||||
"sql.js": "^1.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.26.0",
|
||||
@ -69,6 +60,7 @@
|
||||
"lint-staged": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.40.2",
|
||||
"sass": "^1.89.0",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"terser": "^5.39.2",
|
||||
@ -79,6 +71,7 @@
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"vitest": "^3.1.3",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
1190
pnpm-lock.yaml
generated
1190
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,12 @@ import { Footer } from '~/components/Footer';
|
||||
import { FaqTab } from '~/tabs/FaqTab';
|
||||
import { SETTINGS_TABS } from '~/features/settings/settingsTabs';
|
||||
import { Bounce, ToastContainer } from 'react-toastify';
|
||||
import { SettingsHome } from '~/features/settings/SettingsHome';
|
||||
|
||||
// Private to this file only.
|
||||
const store = setupStore();
|
||||
|
||||
const tabClassNames = ({ isActive }: { isActive: boolean }) => `tab ${isActive ? 'tab-active' : ''}`;
|
||||
const tabClassNames = ({ isActive }: { isActive: boolean }) => `mb-[-2px] tab ${isActive ? 'tab-active' : ''}`;
|
||||
|
||||
export function AppRoot() {
|
||||
useEffect(() => persistSettings(store), []);
|
||||
@ -24,7 +25,7 @@ export function AppRoot() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<div role="tablist" className="tabs tabs-border w-full justify-center">
|
||||
<div role="tablist" className="tabs tabs-border w-full justify-center border-b-2 border-base-200 box-content">
|
||||
<NavLink to="/" role="tab" className={tabClassNames}>
|
||||
<MdHome />
|
||||
应用
|
||||
@ -39,10 +40,11 @@ export function AppRoot() {
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 flex justify-center">
|
||||
<main className="flex-1 flex justify-center min-h-0 overflow-auto">
|
||||
<Routes>
|
||||
<Route path="/" Component={MainTab} />
|
||||
<Route path="/settings" Component={SettingsTab}>
|
||||
<Route index Component={SettingsHome} />
|
||||
{Object.entries(SETTINGS_TABS).map(([key, { Tab }]) => (
|
||||
<Route key={key} path={key} Component={Tab} />
|
||||
))}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { BsCommand } from 'react-icons/bs';
|
||||
import { Ruby } from '../Ruby';
|
||||
|
||||
export function MacCommandKey() {
|
||||
export function MacCommandKey({ className }: { className?: string }) {
|
||||
return (
|
||||
<Ruby caption="command">
|
||||
<Ruby caption="command" className={className}>
|
||||
<kbd className="kbd">
|
||||
<BsCommand className="text-sm" />
|
||||
</kbd>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { BsShift } from 'react-icons/bs';
|
||||
import { Ruby } from '../Ruby';
|
||||
|
||||
export function ShiftKey() {
|
||||
export function ShiftKey({ className }: { className?: string }) {
|
||||
return (
|
||||
<Ruby caption="shift">
|
||||
<Ruby caption="shift" className={className}>
|
||||
<kbd className="kbd">
|
||||
<BsShift className="text-sm" />
|
||||
</kbd>
|
||||
|
@ -9,9 +9,9 @@ export interface KeyListContainerProps {
|
||||
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">
|
||||
<div ref={ref} className="flex grow min-h-0 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">
|
||||
<ul className="list bg-base-100 rounded-box shadow-sm border border-base-300 w-full min-h-0 overflow-auto">
|
||||
{children}
|
||||
</ul>
|
||||
)}
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { Grid, chakra } from '@chakra-ui/react';
|
||||
|
||||
export const FileRowResponsiveGrid = chakra(Grid, {
|
||||
baseStyle: {
|
||||
gridTemplateAreas: {
|
||||
base: `
|
||||
"cover"
|
||||
"title"
|
||||
"meta"
|
||||
"action"
|
||||
`,
|
||||
md: `
|
||||
"cover title action"
|
||||
"cover meta action"
|
||||
`,
|
||||
},
|
||||
gridTemplateRows: {
|
||||
base: 'repeat(auto-fill)',
|
||||
md: 'min-content 1fr',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
base: '1fr',
|
||||
md: '160px 1fr',
|
||||
},
|
||||
gap: 3,
|
||||
},
|
||||
});
|
29
src/features/nav/ResponsiveNav.tsx
Normal file
29
src/features/nav/ResponsiveNav.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
export interface ResponsiveNavProps {
|
||||
navigationClassName?: string;
|
||||
navigation?: React.ReactNode;
|
||||
|
||||
className?: string;
|
||||
|
||||
contentClassName?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ResponsiveNav({
|
||||
className = '',
|
||||
navigationClassName = '',
|
||||
contentClassName = '',
|
||||
children,
|
||||
navigation,
|
||||
}: ResponsiveNavProps) {
|
||||
return (
|
||||
<div
|
||||
className={`@container/nav grow grid grid-cols-1 grid-rows-[auto_1fr] md:grid-rows-1 md:grid-cols-[10rem_1fr] ${className}`}
|
||||
>
|
||||
{/* Sidebar */}
|
||||
<aside className={`bg-gray-100 md:p-4 md:block ${navigationClassName}`}>{navigation}</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<div className={`p-4 grow ${contentClassName}`}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -5,6 +5,8 @@ import { NavLink, Outlet } from 'react-router';
|
||||
import { SETTINGS_TABS } from '~/features/settings/settingsTabs.tsx';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ResponsiveNav } from '../nav/ResponsiveNav';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export function Settings() {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -25,23 +27,37 @@ export function Settings() {
|
||||
};
|
||||
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
|
||||
|
||||
const tabClassNames = ({ isActive }: { isActive: boolean }) =>
|
||||
classNames(
|
||||
'link inline-flex text-nowrap mb-[-2px] no-underline w-full',
|
||||
'border-b-2 md:border-b-0 md:border-r-2',
|
||||
'tab md:grow',
|
||||
{
|
||||
'tab-active bg-accent/10 border-accent': isActive,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 container w-full">
|
||||
<div role="tablist" className="tabs tabs-border w-full justify-center gap-2">
|
||||
<ResponsiveNav
|
||||
className="grow h-full overflow-auto"
|
||||
contentClassName="flex flex-col overflow-auto"
|
||||
navigationClassName="overflow-x-auto pb-[2px] md:pb-0 h-full md:items-center [&]:md:flex"
|
||||
navigation={
|
||||
<div role="tablist" className="tabs gap-1 flex-nowrap md:flex-col grow items-center">
|
||||
{Object.entries(SETTINGS_TABS).map(([id, { name }]) => (
|
||||
<NavLink className="link link-neutral" key={id} to={`/settings/${id}`} role="tab">
|
||||
<NavLink className={tabClassNames} key={id} to={`/settings/${id}`} role="tab">
|
||||
{name}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* TODO: ensure this flex div does not overflow */}
|
||||
<div className="flex flex-1 flex-col h-full overflow-auto">
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</ResponsiveNav>
|
||||
|
||||
<footer className="flex flex-row gap-2 w-full py-3">
|
||||
<div className="grow">
|
||||
<footer className="flex flex-row gap-2 w-full p-2 border-t border-base-200 bg-base-100">
|
||||
<div className="grow inline-flex items-center">
|
||||
{isSettingsNotSaved ? (
|
||||
<span>
|
||||
有未储存的更改,<span className="text-red-600">设定将在保存后生效</span>
|
||||
@ -64,39 +80,6 @@ export function Settings() {
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* <VStack mt="4" alignItems="flex-start" w="full">*/}
|
||||
{/* <Flex flexDir="row" gap="2" w="full">*/}
|
||||
{/* <Center>*/}
|
||||
{/* {isSettingsNotSaved ? (*/}
|
||||
{/* <Box color="gray">*/}
|
||||
{/* 有未储存的更改{' '}*/}
|
||||
{/* <chakra.span color="red" wordBreak="keep-all">*/}
|
||||
{/* 设定将在保存后生效*/}
|
||||
{/* </chakra.span>*/}
|
||||
{/* </Box>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <Box color="gray">设定将在保存后生效</Box>*/}
|
||||
{/* )}*/}
|
||||
{/* </Center>*/}
|
||||
{/* <Spacer />*/}
|
||||
{/* <HStack gap="2" justifyContent="flex-end">*/}
|
||||
{/* <IconButton*/}
|
||||
{/* icon={<Icon as={MdOutlineSettingsBackupRestore} />}*/}
|
||||
{/* onClick={handleResetSettings}*/}
|
||||
{/* colorScheme="red"*/}
|
||||
{/* variant="ghost"*/}
|
||||
{/* title="放弃未储存的更改,将设定还原未储存前的状态。"*/}
|
||||
{/* aria-label="放弃未储存的更改"*/}
|
||||
{/* />*/}
|
||||
{/* <Button onClick={handleApplySettings}>保存</Button>*/}
|
||||
{/* </HStack>*/}
|
||||
{/* </Flex>*/}
|
||||
{/* </VStack>*/}
|
||||
{/* </Flex>*/}
|
||||
{/* ))}*/}
|
||||
{/* </TabPanels>*/}
|
||||
{/*</Tabs>*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
8
src/features/settings/SettingsHome.tsx
Normal file
8
src/features/settings/SettingsHome.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export function SettingsHome() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-bold">设置</h1>
|
||||
<p>在这里你可以设置应用的基本配置。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -26,9 +26,7 @@ export function InstructionsPC() {
|
||||
来加密储存密钥。
|
||||
</p>
|
||||
<p>该密钥数据库通常位于下述路径:</p>
|
||||
<p className="flex items-center gap-1">
|
||||
<FilePathBlock>{DB_PATH}</FilePathBlock>
|
||||
</p>
|
||||
|
||||
<h3 className="font-bold text-xl mt-4">导入密钥</h3>
|
||||
<ol className="list-decimal pl-6">
|
||||
|
@ -56,10 +56,7 @@ export function PanelQingTing() {
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-col grow px-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
<VQuote>蜻蜓 FM</VQuote>
|
||||
设备密钥
|
||||
</h2>
|
||||
<h2 className="text-2xl font-bold mb-4">蜻蜓 FM</h2>
|
||||
|
||||
<p>
|
||||
<VQuote>蜻蜓 FM</VQuote>的安卓版本需要获取设备密钥,并以此来生成解密密钥。
|
||||
@ -78,7 +75,7 @@ export function PanelQingTing() {
|
||||
value={secretKey}
|
||||
onChange={handleDataInput}
|
||||
/>
|
||||
<p className="label">
|
||||
<p className="label flex-wrap">
|
||||
粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过
|
||||
<ExtLink href={QTFM_DEVICE_ID_URL}>
|
||||
<code>qtfm-device-id</code>
|
||||
@ -94,8 +91,15 @@ export function PanelQingTing() {
|
||||
<p>
|
||||
下载的文件位于
|
||||
<VQuote>
|
||||
<code>
|
||||
<HiWord>[内部储存]</HiWord>/Android/data/fm.qingting.qtradio/files/Music/
|
||||
<code className="break-words">
|
||||
<HiWord>[内部储存]</HiWord>/<wbr />
|
||||
Android/
|
||||
<wbr />
|
||||
data/
|
||||
<wbr />
|
||||
fm.qingting.qtradio/
|
||||
<wbr />
|
||||
files/Music/
|
||||
</code>
|
||||
</VQuote>
|
||||
</p>
|
||||
|
@ -1,51 +1,36 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Heading,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { InstructionsIOSCondition } from './InstructionsIOSCondition';
|
||||
import { useId } from 'react';
|
||||
|
||||
export function InstructionsIOS() {
|
||||
const iosInstructionId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Text>iOS 设备获取应用私有文件比较麻烦,你需要越狱或使用一台 PC 或 Mac 来对 iOS 设备进行完整备份。</Text>
|
||||
<Text>因此,建议换用 PC 或 Mac 重新下载音乐文件然后再尝试解密。</Text>
|
||||
</Box>
|
||||
<Accordion allowToggle mt="2">
|
||||
<AccordionItem>
|
||||
<Heading as="h3" size="md">
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
我的 iOS 设备已经越狱
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</Heading>
|
||||
<AccordionPanel pb={4}>
|
||||
<InstructionsIOSCondition jailbreak={true} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<div>
|
||||
<p>iOS 设备获取应用私有文件比较麻烦,你需要越狱或使用一台 PC 或 Mac 来对 iOS 设备进行完整备份。</p>
|
||||
<p>因此,建议换用 PC 或 Mac 重新下载音乐文件然后再尝试解密。</p>
|
||||
</div>
|
||||
|
||||
<AccordionItem>
|
||||
<Heading as="h3" size="md">
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
我的 iOS 设备没有越狱
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</Heading>
|
||||
<AccordionPanel pb={4}>
|
||||
<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={iosInstructionId} />
|
||||
<div className="collapse-title font-semibold">
|
||||
我的 iOS 设备<strong>已经越狱</strong>{' '}
|
||||
</div>
|
||||
<div className="collapse-content text-sm min-w-0">
|
||||
<InstructionsIOSCondition jailbreak={true} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="collapse collapse-arrow join-item border-base-300 border">
|
||||
<input type="radio" name={iosInstructionId} />
|
||||
<div className="collapse-title font-semibold">
|
||||
我的 iOS 设备<strong>没有越狱</strong>
|
||||
</div>
|
||||
<div className="collapse-content text-sm min-w-0">
|
||||
<InstructionsIOSCondition jailbreak={false} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Box, Code, Heading, Image, ListItem, OrderedList, Text } from '@chakra-ui/react';
|
||||
import iosAllowBackup from './iosAllowBackup.webp';
|
||||
import { FilePathBlock } from '~/components/FilePathBlock';
|
||||
import { HiWord } from '~/components/HelpText/HiWord';
|
||||
|
||||
const EXAMPLE_MEDIA_ID = '0011wjLv1bIkvv';
|
||||
const EXAMPLE_NAME_IOS = '333407709-0011wjLv1bIkvv-1.mgalaxy';
|
||||
@ -10,92 +10,77 @@ export function InstructionsIOSCondition({ jailbreak }: { jailbreak: boolean })
|
||||
const useJailbreak = jailbreak;
|
||||
const useBackup = !jailbreak;
|
||||
|
||||
const pathPrefix = jailbreak ? '/var/mobile/Containers/Data/Application/<随机>/' : '/AppDomain-';
|
||||
const pathPrefix = jailbreak ? (
|
||||
<>
|
||||
/var/mobile/Containers/Data/Application/<HiWord className="text-nowrap">[随机字符]</HiWord>/
|
||||
</>
|
||||
) : (
|
||||
'/AppDomain-'
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading as="h3" size="md">
|
||||
获取密钥数据库文件
|
||||
</Heading>
|
||||
<OrderedList>
|
||||
<h4 className="text-lg font-semibold">获取密钥数据库文件</h4>
|
||||
<ol className="list-decimal pl-4">
|
||||
{useBackup && (
|
||||
<ListItem>
|
||||
<Text>首先需要在 iOS 客户端的设定允许备份:</Text>
|
||||
<Image src={iosAllowBackup}></Image>
|
||||
</ListItem>
|
||||
<li>
|
||||
首先需要在 iOS 客户端的设定允许备份:
|
||||
<br />
|
||||
<img src={iosAllowBackup}></img>
|
||||
</li>
|
||||
)}
|
||||
{useBackup && (
|
||||
<ListItem>
|
||||
<Text>使用你喜欢的备份软件对 iOS 设备进行完整备份;</Text>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem>
|
||||
{useBackup && <Text>打开备份文件,并导航到下述目录:</Text>}
|
||||
{useJailbreak && <Text>访问下述目录:</Text>}
|
||||
{useBackup && <li>使用你喜欢的备份软件对 iOS 设备进行完整备份</li>}
|
||||
<li>
|
||||
{useBackup && <span>打开备份文件,并导航到下述目录:</span>}
|
||||
{useJailbreak && <span>访问下述目录:</span>}
|
||||
<FilePathBlock>{pathPrefix}com.tencent.QQMusic/Documents/mmkv/</FilePathBlock>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提取或导出密钥数据库文件 <Code>filenameEkeyMap</Code>;
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提交导出的 <Code>filenameEkeyMap</Code> 文件;
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>按下「保存」来应用更改。</Text>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
</li>
|
||||
<li>
|
||||
提取或导出密钥数据库文件 <code>filenameEkeyMap</code>
|
||||
</li>
|
||||
<li>
|
||||
提交导出的 <code>filenameEkeyMap</code> 文件
|
||||
</li>
|
||||
<li>按下「保存」来应用更改。</li>
|
||||
</ol>
|
||||
|
||||
<Heading as="h3" size="md" mt="3">
|
||||
获取离线文件
|
||||
</Heading>
|
||||
<Box>
|
||||
<Text>访问下述目录:</Text>
|
||||
<h3 className="text-lg font-semibold mt-3">获取离线文件</h3>
|
||||
<section>
|
||||
<p>访问下述目录:</p>
|
||||
<FilePathBlock>
|
||||
{pathPrefix}com.tencent.QQMusic/Library/Application Support/com.tencent.QQMusic/iData/iMusic
|
||||
</FilePathBlock>
|
||||
<Text>
|
||||
该目录又存在数个子目录,其子目录下保存的「<Code>[字符].m[字符]</Code>」文件则是最终的加密文件。
|
||||
</Text>
|
||||
<Text>
|
||||
格式:<Code>[song_id]-[mid]-[随机数字].m[后缀]</Code>
|
||||
</Text>
|
||||
<Text>
|
||||
 例:<Code>{EXAMPLE_NAME_IOS}</Code>
|
||||
</Text>
|
||||
</Box>
|
||||
<p>
|
||||
该目录又存在数个子目录,其子目录下保存的「<code>[字符].m[字符]</code>」文件则是最终的加密文件。
|
||||
</p>
|
||||
<p>
|
||||
格式:<code>[song_id]-[mid]-[随机数字].m[后缀]</code>
|
||||
</p>
|
||||
<p>
|
||||
 例:<code>{EXAMPLE_NAME_IOS}</code>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Heading as="h3" size="md" mt="3">
|
||||
解密离线文件
|
||||
</Heading>
|
||||
<Text>勾选设定界面的「使用近似文件名匹配」可跳过该节内容。</Text>
|
||||
<Text>⚠ 注意:若密钥过多,匹配过程可能会造成浏览器卡顿或无响应。</Text>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提取文件的 <Code>[mid]</Code> 部分,如 <Code>{EXAMPLE_MEDIA_ID}</Code>;
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
查找密钥表,得到文件名「<Code>{EXAMPLE_NAME_DB}</Code>」;
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
将文件更名为对应的文件名,如<Code display="inline">{EXAMPLE_NAME_IOS}</Code> ➔
|
||||
<Code display="inline">{EXAMPLE_NAME_DB}</Code>;
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
回到主界面,提交文件「<Code>{EXAMPLE_NAME_DB}</Code>」。
|
||||
</Text>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<h4 className="text-lg font-semibold mt-3">解密离线文件</h4>
|
||||
<p>勾选设定界面的「使用近似文件名匹配」可跳过该节内容。</p>
|
||||
<p>⚠ 注意:若密钥过多,匹配过程可能会造成浏览器卡顿或无响应。</p>
|
||||
<ol className="list-decimal pl-4 mt-1">
|
||||
<li>
|
||||
提取文件的 <code>[mid]</code> 部分,如 <code>{EXAMPLE_MEDIA_ID}</code>
|
||||
</li>
|
||||
<li>
|
||||
查找密钥表,得到文件名「<code>{EXAMPLE_NAME_DB}</code>」
|
||||
</li>
|
||||
<li>
|
||||
将文件更名为对应的文件名,如
|
||||
<br />
|
||||
<code>{EXAMPLE_NAME_IOS}</code>
|
||||
<br />➔ <code>{EXAMPLE_NAME_DB}</code>
|
||||
</li>
|
||||
<li>
|
||||
回到主界面,提交文件「<code>{EXAMPLE_NAME_DB}</code>」。
|
||||
</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,59 +1,79 @@
|
||||
import { Heading, Text, Code, OrderedList, ListItem, Link } 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';
|
||||
import { VQuote } from '~/components/HelpText/VQuote';
|
||||
import { MacCommandKey } from '~/components/Key/MacCommandKey';
|
||||
import { ShiftKey } from '~/components/Key/ShiftKey';
|
||||
|
||||
const MAC_CLIENT_URL =
|
||||
'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
|
||||
const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
|
||||
const DB_PATH =
|
||||
'~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId';
|
||||
|
||||
export function InstructionsMac() {
|
||||
const copyDbPathToClipboard = () => {
|
||||
navigator.clipboard
|
||||
.writeText(DB_PATH)
|
||||
.then(() => {
|
||||
toast.success('已复制到剪贴板');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`复制失败,请手动复制\n${err}`);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>Mac 客户端使用 mmkv 数据库储存密钥。</Text>
|
||||
<Text>
|
||||
{'此外,你需要降级到 '}
|
||||
<Link isExternal href={MAC_CLIENT_URL}>
|
||||
2023.09.03 版本的客户端
|
||||
</Link>
|
||||
{'。'}
|
||||
新版本对 mmkv 数据库进行了加密处理。
|
||||
</Text>
|
||||
<Text>该密钥文件通常存储在下述路径:</Text>
|
||||
<FilePathBlock>
|
||||
~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId
|
||||
</FilePathBlock>
|
||||
<p>Mac 客户端使用 mmkv 数据库储存密钥。</p>
|
||||
<p>此外,你需要降级到 v8.8.0 版本的客户端 —— 更新的版本对密钥数据库进行了加密,目前无公开的获取方案。</p>
|
||||
|
||||
<Heading as="h3" size="md" mt="4">
|
||||
导入密钥
|
||||
</Heading>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
选中并复制上述的 <Code>MMKVStreamEncryptId</Code> 文件路径
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>点击上方的「文件选择区域」,打开「文件选择框」</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
按下「
|
||||
<ShiftKey />
|
||||
{' + '}
|
||||
<MacCommandKey />
|
||||
{' + '}
|
||||
<kbd className="kbd">{'G'}</kbd>」组合键打开「路径输入框」
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
粘贴之前复制的 <Code>MMKVStreamEncryptId</Code> 文件路径
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>按下「回车键」确认。</Text>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<p className="mt-4">获取 QQ 音乐 Mac 客户端 8.8.0:</p>
|
||||
<ul className="list-disc pl-6">
|
||||
<li>
|
||||
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
|
||||
通过 <code>Archive.org</code> 缓存下载(慢)
|
||||
</ExtLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
|
||||
通过 Telegram 下载(需要账号)
|
||||
</ExtLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="mt-4">密钥文件通常存储在下述路径:</p>
|
||||
<FilePathBlock>{DB_PATH}</FilePathBlock>
|
||||
|
||||
<h4 className="font-bold text-lg mt-4">导入密钥</h4>
|
||||
<ol className="list-decimal pl-6">
|
||||
<li>
|
||||
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={copyDbPathToClipboard}>
|
||||
<RiFileCopyLine className="text-xl" />
|
||||
<span>复制</span>
|
||||
</button>
|
||||
<code>MMKVStreamEncryptId</code> 文件路径
|
||||
</li>
|
||||
<li>
|
||||
点击上方的<VQuote>文件选择区域</VQuote>,打开<VQuote>文件选择框</VQuote>
|
||||
</li>
|
||||
<li>
|
||||
按下
|
||||
<VQuote>
|
||||
<ShiftKey className="mx-1" />
|
||||
{'+'}
|
||||
<MacCommandKey className="mx-1" />
|
||||
{'+'}
|
||||
<kbd className="kbd mx-1">G</kbd>
|
||||
</VQuote>
|
||||
组合键打开<VQuote>路径输入框</VQuote>
|
||||
</li>
|
||||
<li>
|
||||
粘贴之前复制的 <code>MMKVStreamEncryptId</code> 文件路径
|
||||
</li>
|
||||
<li>按下「回车键」确认。</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ import { HiWord } from '~/components/HelpText/HiWord';
|
||||
import NoopExecutable from './assets/noop.exe?base64';
|
||||
import NoopExecutableSource from './assets/noop.asm.txt?base64';
|
||||
|
||||
const PC_CLIENT_URL = 'https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe';
|
||||
const PC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/24';
|
||||
|
||||
export function InstructionsPC() {
|
||||
return (
|
||||
<>
|
||||
@ -23,33 +26,35 @@ export function InstructionsPC() {
|
||||
</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">
|
||||
<ExtLink className="link-info" href={PC_CLIENT_URL}>
|
||||
通过 <code>Archive.org</code> 缓存下载(慢)
|
||||
</ExtLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExtLink href="https://t.me/um_lsr_ch/24">通过 Telegram 下载(需要账号)</ExtLink>
|
||||
<ExtLink className="link-info" href={PC_CLIENT_TG_URL}>
|
||||
通过 Telegram 下载(需要账号)
|
||||
</ExtLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="mt-4">
|
||||
安装完成后可以覆盖 QQ 音乐安装目录下的
|
||||
<a
|
||||
className="link px-1"
|
||||
className="link link-info mx-1"
|
||||
download="QQMusicUp.exe"
|
||||
href={`data:application/vnd.microsoft.portable-executable;base64,${NoopExecutable}`}
|
||||
>
|
||||
<code>QQMusicUp.exe</code>
|
||||
</a>
|
||||
同名文件(
|
||||
同名文件,屏蔽自动更新(
|
||||
<a
|
||||
className="link px-1"
|
||||
className="link"
|
||||
download="QQMusicUp.asm"
|
||||
href={`data:text/x-asm;charset=utf-8;base64,${NoopExecutableSource}`}
|
||||
>
|
||||
源码
|
||||
</a>
|
||||
),屏蔽自动更新。
|
||||
)。
|
||||
</p>
|
||||
<p className="mt-2">降级后需要删除新版本下载的文件并重新使用旧版本下载。</p>
|
||||
</>
|
||||
|
61
src/theme.ts
61
src/theme.ts
@ -1,61 +0,0 @@
|
||||
import { extendTheme } from '@chakra-ui/react';
|
||||
import { tabsTheme } from './themes/Tabs';
|
||||
|
||||
export const theme = extendTheme({
|
||||
fonts: {
|
||||
body: [
|
||||
'-system-ui,-apple-system,BlinkMacSystemFont',
|
||||
'Source Han Sans CN,Noto Sans CJK SC',
|
||||
'Segoe UI,Helvetica,Arial,sans-serif',
|
||||
'Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol',
|
||||
].join(','),
|
||||
mono: [
|
||||
'SFMono-Regular,Menlo,Monaco',
|
||||
'"Sarasa Mono CJK SC"',
|
||||
'Consolas,"Liberation Mono","Courier New",monospace',
|
||||
'"Microsoft YaHei UI"',
|
||||
].join(','),
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
baseStyle: {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
defaultProps: {
|
||||
colorScheme: 'teal',
|
||||
},
|
||||
},
|
||||
Tabs: tabsTheme,
|
||||
Link: {
|
||||
baseStyle: {
|
||||
color: 'blue.600',
|
||||
},
|
||||
},
|
||||
Text: {
|
||||
baseStyle: {
|
||||
mt: 1,
|
||||
},
|
||||
},
|
||||
Header: {
|
||||
baseStyle: {
|
||||
mt: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
global: {
|
||||
'#root': {
|
||||
minHeight: '100vh',
|
||||
maxHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
},
|
||||
sizes: {
|
||||
footer: {
|
||||
container: '5rem',
|
||||
content: '4rem',
|
||||
},
|
||||
},
|
||||
});
|
@ -1,74 +0,0 @@
|
||||
import { tabsAnatomy } from '@chakra-ui/anatomy';
|
||||
import { createMultiStyleConfigHelpers, cssVar } from '@chakra-ui/react';
|
||||
|
||||
const $fg = cssVar('tabs-color');
|
||||
const $bg = cssVar('tabs-bg');
|
||||
|
||||
const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tabsAnatomy.keys);
|
||||
|
||||
const variantLineInvert = definePartsStyle((props) => {
|
||||
const { colorScheme: c, orientation } = props;
|
||||
const isVertical = orientation === 'vertical';
|
||||
const borderProp = isVertical ? 'borderEnd' : 'borderTop';
|
||||
const marginProp = isVertical ? 'marginEnd' : 'marginTop';
|
||||
|
||||
return {
|
||||
tablist: {
|
||||
[borderProp]: '2px solid',
|
||||
borderColor: 'inherit',
|
||||
},
|
||||
tabpanels: {
|
||||
flex: 1,
|
||||
minH: 0,
|
||||
},
|
||||
tabpanel: {
|
||||
padding: 0,
|
||||
},
|
||||
tab: {
|
||||
[borderProp]: '2px solid',
|
||||
borderColor: 'transparent',
|
||||
[marginProp]: '-2px',
|
||||
justifyContent: 'flex-end',
|
||||
_selected: {
|
||||
[$fg.variable]: `colors.${c}.600`,
|
||||
_dark: {
|
||||
[$fg.variable]: `colors.${c}.300`,
|
||||
},
|
||||
borderColor: 'currentColor',
|
||||
},
|
||||
_active: {
|
||||
[$bg.variable]: 'colors.gray.200',
|
||||
_dark: {
|
||||
[$bg.variable]: 'colors.whiteAlpha.300',
|
||||
},
|
||||
},
|
||||
_disabled: {
|
||||
_active: { bg: 'none' },
|
||||
},
|
||||
color: $fg.reference,
|
||||
bg: $bg.reference,
|
||||
},
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDir: isVertical ? 'row' : 'column',
|
||||
gap: 8,
|
||||
minH: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const tabsTheme = defineMultiStyleConfig({
|
||||
baseStyle: {
|
||||
tablist: {
|
||||
userSelect: 'none',
|
||||
},
|
||||
tabpanel: {
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
maxHeight: '100%',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
'line-i': variantLineInvert,
|
||||
},
|
||||
});
|
@ -96,13 +96,14 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
reacts: ['react', 'react-dom', 'react-dropzone', 'react-redux', '@reduxjs/toolkit'],
|
||||
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
|
||||
icons: ['react-icons', '@chakra-ui/icons'],
|
||||
utility: ['radash', 'nanoid', 'react-syntax-highlighter'],
|
||||
core: ['react', 'react-dom'],
|
||||
router: ['react-router'],
|
||||
store: ['react-redux', '@reduxjs/toolkit'],
|
||||
extras: ['react-dropzone', 'react-toastify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user