(null);
+ const [sdkVersion, setSdkVersion] = useState('...');
+ useEffect(() => {
+ getSDKVersion().then(setSdkVersion);
+ }, []);
return (
-
-
- App: __APP_VERSION__
- SDK: {sdkVersion}
-
- }
- bg="gray.300"
- color="black"
- >
-
-
-
+ <>
+ refDialog.current?.showModal()}>
+
+
+
+
+ >
);
}
diff --git a/src/components/SelectFile.tsx b/src/components/SelectFile.tsx
index 971f395..0e5e87c 100644
--- a/src/components/SelectFile.tsx
+++ b/src/components/SelectFile.tsx
@@ -1,5 +1,4 @@
-import { Box, Text } from '@chakra-ui/react';
-import { UnlockIcon } from '@chakra-ui/icons';
+import { FiUnlock } from 'react-icons/fi';
import { useAppDispatch } from '~/hooks';
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
@@ -12,7 +11,7 @@ export function SelectFile() {
console.debug(
'react-dropzone/onDropAccepted(%o, %o)',
files.length,
- files.map((x) => x.name)
+ files.map((x) => x.name),
);
for (const file of files) {
@@ -26,7 +25,7 @@ export function SelectFile() {
id: fileId,
blobURI,
fileName,
- })
+ }),
);
dispatch(processFile({ fileId }));
}
@@ -34,19 +33,13 @@ export function SelectFile() {
return (
-
-
-
-
+
+
拖放或
-
- 点我选择
-
+ 点我选择
需要解密的文件
-
- 在浏览器内对文件进行解锁,零上传
-
-
+
+ 在浏览器内对文件进行解锁,零上传
);
}
diff --git a/src/faq/AndroidEmulatorFAQ.tsx b/src/faq/AndroidEmulatorFAQ.tsx
new file mode 100644
index 0000000..557a1ea
--- /dev/null
+++ b/src/faq/AndroidEmulatorFAQ.tsx
@@ -0,0 +1,75 @@
+import { ExtLink } from '~/components/ExtLink';
+import { Header2, Header3, Header4 } from '~/components/HelpText/Headers';
+import { VQuote } from '~/components/HelpText/VQuote';
+import { RiErrorWarningLine } from 'react-icons/ri';
+
+import LdPlayerSettingsMisc2x from './assets/ld_settings_misc@2x.webp';
+import MumuSettingsMisc2x from './assets/mumu_settings_misc@2x.webp';
+import { ImageFigure } from '~/components/ImageFigure';
+
+export function AndroidEmulatorFAQ() {
+ return (
+ <>
+ 安卓模拟器
+ 目前市面上主流的可以很方便 root 的安卓模拟器有两个:
+
+
+ -
+ 网易 MuMu 模拟器(安卓 12) - Hyper-V 兼容较好
+
+ -
+ 雷电模拟器(安卓 9)
+
+
+
+ 上述两款模拟器均包含广告,使用时请注意。
+
+
+
+
+ 根据应用的风控策略,使用模拟器登录的账号有可能会导致账号被封锁。
+
+
+ 读者在使用前请自行评估风险。
+
+ 启用 root
+ 上述的两款模拟器都有提供比较直接的启用 root 的方法。
+
+ 网易 MuMu 模拟器
+
+ -
+ 打开设置中心
+
+ -
+ 选择其他
+
+ -
+ 勾选开启手机Root权限
+
+
+
+
+ 网易木木模拟器设置界面
+
+
+
+ 雷电模拟器
+
+ -
+ 打开模拟器设置
+
+ -
+ 选择其他
+
+ -
+ 设置ROOT 权限为开启状态
+
+
+
+
+ 雷电模拟器设置界面
+
+
+ >
+ );
+}
diff --git a/src/faq/FAQAbout.tsx b/src/faq/FAQAbout.tsx
new file mode 100644
index 0000000..7ff1826
--- /dev/null
+++ b/src/faq/FAQAbout.tsx
@@ -0,0 +1,45 @@
+import { Header2, Header3 } from '~/components/HelpText/Headers';
+import { FaRust } from 'react-icons/fa';
+
+export function FAQAboutProject() {
+ return (
+
+ );
+}
diff --git a/src/faq/FAQPages.tsx b/src/faq/FAQPages.tsx
new file mode 100644
index 0000000..4552098
--- /dev/null
+++ b/src/faq/FAQPages.tsx
@@ -0,0 +1,22 @@
+import type { ComponentType } from 'react';
+import { QQMusicFAQ } from './QQMusicFAQ';
+import { KuwoFAQ } from './KuwoFAQ';
+import { KugouFAQ } from './KugouFAQ';
+import { OtherFAQ } from './OtherFAQ';
+import { AndroidEmulatorFAQ } from './AndroidEmulatorFAQ';
+import { FAQAboutProject } from './FAQAbout';
+
+export type FAQEntry = {
+ id: string;
+ name: string;
+ Component: ComponentType;
+};
+
+export const FAQ_PAGES: FAQEntry[] = [
+ { id: 'qqmusic', name: 'QQ 音乐', Component: QQMusicFAQ },
+ { id: 'kuwo', name: '酷我音乐', Component: KuwoFAQ },
+ { id: 'kugou', name: '酷狗音乐', Component: KugouFAQ },
+ { id: 'android-emu', name: '安卓模拟器', Component: AndroidEmulatorFAQ },
+ { id: 'other', name: '其它问题', Component: OtherFAQ },
+ { id: 'about', name: '关于项目', Component: FAQAboutProject },
+];
diff --git a/src/faq/FaqHome.tsx b/src/faq/FaqHome.tsx
new file mode 100644
index 0000000..cf24401
--- /dev/null
+++ b/src/faq/FaqHome.tsx
@@ -0,0 +1,15 @@
+import { ExtLink } from '~/components/ExtLink';
+import { Header2 } from '~/components/HelpText/Headers';
+
+export function FaqHome() {
+ return (
+
+
答疑
+
从目录选择一项来查看相关说明。
+
+ 也欢迎造访
+ “音乐解锁-交流” 交流群 进行交流。
+
+
+ );
+}
diff --git a/src/faq/KugouFAQ.tsx b/src/faq/KugouFAQ.tsx
index 75a4c31..6c55edc 100644
--- a/src/faq/KugouFAQ.tsx
+++ b/src/faq/KugouFAQ.tsx
@@ -1,31 +1,25 @@
-import { Alert, AlertIcon, Container, Flex, List, ListItem, Text } from '@chakra-ui/react';
-import { Header4 } from '~/components/HelpText/Headers';
+import { Header2, Header3 } from '~/components/HelpText/Headers';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
+import { RiErrorWarningLine } from 'react-icons/ri';
export function KugouFAQ() {
return (
<>
- 解锁失败
-
-
-
- 酷狗现在对部分用户推送了 kgg 加密格式(安卓、Windows 客户端)。
-
- 根据平台不同,你需要提取密钥数据库。
+ 酷狗音乐
+ 解锁失败
+
+ 酷狗现在对部分用户推送了 kgg 加密格式(安卓、Windows 客户端)。
+
+ 根据平台不同,你需要提取密钥数据库。
-
-
-
-
- 安卓用户提取密钥需要 root 权限,或注入文件提供器。
-
-
-
+
+
+
安卓用户提取密钥需要 root 权限,或注入文件提供器。
+
- } />
-
-
+ 导入密钥
+ } />
>
);
}
diff --git a/src/faq/KuwoFAQ.tsx b/src/faq/KuwoFAQ.tsx
index 6afe7c3..2d26a12 100644
--- a/src/faq/KuwoFAQ.tsx
+++ b/src/faq/KuwoFAQ.tsx
@@ -1,52 +1,47 @@
-import { Alert, AlertIcon, Container, Flex, List, ListItem, Text } from '@chakra-ui/react';
-import { Header4 } from '~/components/HelpText/Headers';
+import { Header2, Header3 } from '~/components/HelpText/Headers';
import { VQuote } from '~/components/HelpText/VQuote';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { HiWord } from '~/components/HelpText/HiWord';
import { KWMv2AllInstructions } from '~/features/settings/panels/KWMv2/KWMv2AllInstructions';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
+import { RiErrorWarningLine } from 'react-icons/ri';
export function KuwoFAQ() {
return (
<>
- 解锁失败
-
-
-
-
-
-
- 日前,仅手机客户端下载的
-
- 至臻全景声
-
- 及
-
- 至臻母带
-
- {'音质的音乐文件采用新版加密。'}
-
- 其他音质目前不需要提取密钥。
- PC平台暂未推出使用新版加密的音质。
+ 酷我音乐
+ 解锁失败
+
+
+ 日前,仅手机客户端下载的
+
+ 至臻全景声
+
+ 及
+
+ 至臻母带
+
+ 音质的音乐文件采用新版加密。
+
+ 其他音质目前不需要提取密钥。
+ PC平台暂未推出使用新版加密的音质。
-
-
-
-
- 安卓用户提取密钥需要 root 权限,或注入文件提供器。
-
- 注意:已知部分第三方修改版会破坏密钥写入功能,导致无法提取密钥。
-
-
- 注意:项目组不提倡使用、也不提供第三方修改版。使用前请自行评估风险。
-
-
-
-
+ 导入密钥
+
+
+
+
安卓用户提取密钥需要 root 权限,或注入文件提供器。
+
+ 注意已知部分第三方修改版会破坏密钥写入功能,导致无法提取密钥。
+
+
+ 注意
+ 项目组不提倡使用、也不提供第三方修改版。使用前请自行评估风险。请开通会员支持正版音乐。
+
+
+
- } />
-
-
+ } />
>
);
}
diff --git a/src/faq/OtherFAQ.tsx b/src/faq/OtherFAQ.tsx
index 5b33698..0304b45 100644
--- a/src/faq/OtherFAQ.tsx
+++ b/src/faq/OtherFAQ.tsx
@@ -1,139 +1,109 @@
-import { Alert, AlertIcon, Code, Container, Flex, Img, ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { ExtLink } from '~/components/ExtLink';
-import { Header4 } from '~/components/HelpText/Headers';
-import { VQuote } from '~/components/HelpText/VQuote';
+import { Header2, Header3, Header4 } from '~/components/HelpText/Headers';
import { ProjectIssue } from '~/components/ProjectIssue';
-import LdPlayerSettingsScreen from './assets/ld_settings_misc.webp';
+
+import { NavLink } from 'react-router';
export function OtherFAQ() {
return (
<>
- 解密后没有封面等信息
- 该项目进行解密处理。如果加密前的资源没有内嵌元信息或封面,解密的文件也没有。
- 请使用第三方工具进行编辑或管理元信息。
+ 其它问题
+ 解密后没有封面等信息
+ 该项目进行解密处理。如果加密前的资源没有内嵌元信息或封面,解密的文件也没有。
+ 请使用第三方工具进行编辑或管理元信息。
- 批量下载
-
+ 批量下载
+
{'暂时没有实现,不过你可以在 '}
{' 以及 '}
{' 追踪该问题。'}
-
+
- 安卓: 浏览器支持说明
- ⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
- 已知有问题的浏览器:
-
- Via 浏览器
- 夸克浏览器
- UC 浏览器
-
- 可能会遇到的问题包括:
-
- 网页白屏
- 无法下载解密后内容
- 下载的文件名错误
-
+ 安卓: 浏览器支持说明
+ ⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
+
+
+
已知有问题的浏览器
+
+ - Via 浏览器
+ - 夸克浏览器
+ - UC 浏览器
+
+
-
安卓: root 相关说明
-
+
+
可能会遇到的问题包括
+
+ - 网页白屏
+ - 无法下载解密后内容
+ - 下载的文件名错误
+
+
+
+
+ 安卓 root
+
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
- 例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付功能等限制。
-
- 如果希望不破坏系统完整性,你可以考虑使用模拟器。
-
- 目前常见的带有 root 特权支持的的安卓模拟器方案,分别是雷电模拟器(※ 官方版有内置广告)和微软在 Windows 11
- 开始支援的
-
-
- 适用于 Android™ 的 Windows 子系统 (WSA)
-
-
-
-
-
+ 例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付等限制。
+
+
+ 如果希望不破坏系统完整性,你可以考虑在电脑上使用
+
+ 安卓模拟器
+
。
-
+
-
-
-
-
-
- 注意:根据应用的风控策略,使用模拟器登录的账号有可能会导致账号被封锁
- {';使用前请自行评估风险。'}
-
-
-
-
-
-
-
-
- {'WSA 可以参考 '}
- MagiskOnWSALocal
- {' 的说明操作。'}
-
-
-
-
- 雷电模拟器可以在模拟器设置 → 其他设置中启用 root 特权。
-
-
-
-
-
- 相关项目
-
-
-
-
+ 相关项目
+
-
-
-
+ 利用 Electron 框架打包的本地版,提供适用于 Windows、Linux 和 Mac 平台的可执行文件。
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
- um-react-wry
+ um-react-wry
- : 使用 WRY 框架封装的 Win64 单文件(需要
+ 使用 WRY 框架封装的 Win64 单文件(需要
安装 Edge WebView2 运行时
{',Win10+ 操作系统自带)'}
-
-
-
-
+
+
-
-
-
-
+ um-react-win64- 开头的附件
+
+
+
+
+
- 有更多问题?
-
- {'欢迎进入 '}
- Telegram “音乐解锁-交流” 交流群
- {' 一起探讨。'}
-
+ 有更多问题?
+
+ 欢迎加入
+ “音乐解锁-交流” 交流群
+ 一起讨论。
+
>
);
}
diff --git a/src/faq/QQMusicFAQ.tsx b/src/faq/QQMusicFAQ.tsx
index 8242c4a..08e93ae 100644
--- a/src/faq/QQMusicFAQ.tsx
+++ b/src/faq/QQMusicFAQ.tsx
@@ -1,159 +1,25 @@
-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 { Header2, Header3 } from '~/components/HelpText/Headers';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
-import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
-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';
+import { QMCv2QQMusicAllInstructions } from '~/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions';
export function QQMusicFAQ() {
return (
<>
- 解锁失败
+ QQ 音乐
+ 解锁失败
- 重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。
-
+ 新版本的 QQ 音乐客户端下载的文件通常都需要导入密钥数据库。
+ 每一个资源(即一首歌的某个音质)都有独立的密钥,下载音乐时会被写出到密钥数据库中。
+ 因此若是解密失败,很有可能是因为你需要导入密钥,或降级客户端。
+
+ 关于下载
+ 重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。
+
部分平台获取的加密文件未包含密钥。选择你下载文件时使用的客户端来查看说明。
-
-
-
-
-
-
- Windows
-
-
-
-
-
-
- 目前 Windows 客户端 19.51 或更低版本下载的歌曲文件无需密钥,其余平台的官方正式版本均需要提取密钥。
-
- 你可以通过下方的链接获取 QQ 音乐 Windows 客户端 v19.51 的安装程序:
-
-
-
-
- qq.com 官方下载地址(推荐)
-
-
-
-
-
-
- Archive.org 存档
-
-
-
-
-
-
+
-
-
-
-
- Mac
-
-
-
-
-
-
-
-
-
- Mac 需要降级到 8.8.0 或以下版本。
-
-
- Archive.org 存档
-
-
-
-
-
-
-
-
-
- }
- />
-
-
-
-
-
-
-
- 安卓 (Android)
-
-
-
-
-
-
-
-
-
- 安卓提取密钥需要 root 特权,建议用电脑操作。
-
-
-
-
- QQ 音乐官方版本需要提取密钥才能解密。
-
- 你也可以尝试使用【QQ 音乐简洁版】或 OEM 定制版(如小米、魅族定制版)。简洁、定制版本目前不需要提取密钥。
-
-
-
-
-
- }
- />
-
-
-
-
-
-
-
- iOS (iPhone, iPad)
-
-
-
-
-
-
-
-
-
- iOS 用户提取歌曲困难,建议换用电脑操作;
-
-
-
-
-
-
-
- }
- />
-
-
-
+ 导入密钥或降级客户端
+
>
);
}
diff --git a/src/faq/SegmentAddKeyDropdown.tsx b/src/faq/SegmentAddKeyDropdown.tsx
index 69f04bb..53c819e 100644
--- a/src/faq/SegmentAddKeyDropdown.tsx
+++ b/src/faq/SegmentAddKeyDropdown.tsx
@@ -1,24 +1,13 @@
-import { Flex, IconButton } from '@chakra-ui/react';
-import { MdExpandMore } from 'react-icons/md';
-import { HiWord } from '~/components/HelpText/HiWord';
-import { VQuote } from '~/components/HelpText/VQuote';
+import { MdFileUpload } from 'react-icons/md';
export function SegmentAddKeyDropdown() {
return (
-
- 按下添加一条密钥按钮
- 右侧的
- }
- ml="2"
- borderTopLeftRadius={0}
- borderBottomLeftRadius={0}
- pointerEvents="none"
- aria-label="下拉按钮"
- />
-
+
+ 按下
+
+
);
}
diff --git a/src/faq/SegmentKeyImportInstructions.tsx b/src/faq/SegmentKeyImportInstructions.tsx
index 636b528..0fb76d6 100644
--- a/src/faq/SegmentKeyImportInstructions.tsx
+++ b/src/faq/SegmentKeyImportInstructions.tsx
@@ -1,9 +1,7 @@
-import { Flex, Icon, ListItem, OrderedList, Tabs, Text } from '@chakra-ui/react';
import { SegmentTopNavSettings } from './SegmentTopNavSettings';
import { VQuote } from '~/components/HelpText/VQuote';
import { SegmentAddKeyDropdown } from './SegmentAddKeyDropdown';
import React from 'react';
-import { MdFileUpload } from 'react-icons/md';
export interface SegmentKeyImportInstructionsProps {
clientInstructions: React.ReactNode;
@@ -18,32 +16,22 @@ export function SegmentKeyImportInstructions({
}: SegmentKeyImportInstructionsProps) {
return (
<>
- 导入密钥可以参考下面的步骤:
-
-
+ 导入密钥可以参考下面的步骤:
+
+ -
-
-
+
+
设定区域选择{tab}
-
-
+
+
-
-
-
- {'选择 '}
-
- 从文件导入密钥…
-
-
-
-
- {keyInstructionText}
-
- {clientInstructions}
-
-
-
+
+
+ {keyInstructionText}
+ {clientInstructions}
+
+
>
);
}
diff --git a/src/faq/SegmentTryOfficialPlayer.tsx b/src/faq/SegmentTryOfficialPlayer.tsx
index f5407e7..ef8bc93 100644
--- a/src/faq/SegmentTryOfficialPlayer.tsx
+++ b/src/faq/SegmentTryOfficialPlayer.tsx
@@ -1,12 +1,10 @@
-import { Alert, AlertIcon, Container } from '@chakra-ui/react';
+import { RiErrorWarningLine } from 'react-icons/ri';
-export function SegmentTryOfficialPlayer() {
+export function SegmentTryOfficialPlayer({ className = '' }: { className?: string }) {
return (
-
-
-
- 尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。
-
-
+
+
+
尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。
+
);
}
diff --git a/src/faq/assets/ld_settings_misc.webp b/src/faq/assets/ld_settings_misc.webp
deleted file mode 100644
index 6ffa914..0000000
Binary files a/src/faq/assets/ld_settings_misc.webp and /dev/null differ
diff --git a/src/faq/assets/ld_settings_misc@2x.webp b/src/faq/assets/ld_settings_misc@2x.webp
new file mode 100644
index 0000000..986438c
Binary files /dev/null and b/src/faq/assets/ld_settings_misc@2x.webp differ
diff --git a/src/faq/assets/mumu_settings_misc@2x.webp b/src/faq/assets/mumu_settings_misc@2x.webp
new file mode 100644
index 0000000..4fc97cb
Binary files /dev/null and b/src/faq/assets/mumu_settings_misc@2x.webp differ
diff --git a/src/features/file-listing/AlbumImage.tsx b/src/features/file-listing/AlbumImage.tsx
deleted file mode 100644
index 22c8dd2..0000000
--- a/src/features/file-listing/AlbumImage.tsx
+++ /dev/null
@@ -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 (
-
-
-
- );
-}
diff --git a/src/features/file-listing/FileError.tsx b/src/features/file-listing/FileError.tsx
index d2e6830..cfad5ea 100644
--- a/src/features/file-listing/FileError.tsx
+++ b/src/features/file-listing/FileError.tsx
@@ -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 { applyTemplate } from '~/util/applyTemplate';
export interface FileErrorProps {
error: null | string;
@@ -7,33 +8,55 @@ export interface FileErrorProps {
}
const errorMap = new Map([
- [DecryptErrorType.UNSUPPORTED_FILE, '尚未支持的文件格式'],
+ [DecryptErrorType.UNSUPPORTED_FILE, '不支持的文件类型'],
]);
+const ERROR_TEMPLATE = `解密错误:{{summary}}
+
+详细错误信息:
+\`\`\`text
+{{error}}
+\`\`\`
+
+
+`;
+
export function FileError({ error, code }: FileErrorProps) {
- const { isOpen, onToggle } = useDisclosure();
- const errorSummary = errorMap.get(code) ?? '未知错误';
+ const summary = errorMap.get(code) ?? '未知错误';
+
+ const copyError = () => {
+ if (error) {
+ navigator.clipboard
+ .writeText(applyTemplate(ERROR_TEMPLATE, { summary, error }))
+ .then(() => {
+ toast.success('错误信息已复制到剪贴板');
+ })
+ .catch((e) => {
+ toast.error(`复制错误信息失败: ${e}`);
+ });
+ }
+ };
return (
-
-
-
- 解密错误:
- {errorSummary}
-
- {error && (
-
- )}
-
+ <>
+
+ 解密错误:
+ {summary}
+
{error && (
-
-
- {error}
-
-
+
)}
-
+ >
);
}
diff --git a/src/features/file-listing/FileListing.tsx b/src/features/file-listing/FileListing.tsx
index 09bec14..88ffa7d 100644
--- a/src/features/file-listing/FileListing.tsx
+++ b/src/features/file-listing/FileListing.tsx
@@ -1,5 +1,3 @@
-import { VStack } from '@chakra-ui/react';
-
import { selectFiles } from './fileListingSlice';
import { useAppSelector } from '~/hooks';
import { FileRow } from './FileRow';
@@ -8,10 +6,10 @@ export function FileListing() {
const files = useAppSelector(selectFiles);
return (
-
+
{Object.entries(files).map(([id, file]) => (
))}
-
+
);
}
diff --git a/src/features/file-listing/FileRow.tsx b/src/features/file-listing/FileRow.tsx
index 294144f..b83bc66 100644
--- a/src/features/file-listing/FileRow.tsx
+++ b/src/features/file-listing/FileRow.tsx
@@ -1,24 +1,7 @@
-import { useRef } from 'react';
-import {
- Box,
- Button,
- Card,
- CardBody,
- Collapse,
- GridItem,
- Link,
- VStack,
- Wrap,
- WrapItem,
- useDisclosure,
-} from '@chakra-ui/react';
-import { FileRowResponsiveGrid } from './FileRowResponsiveGrid';
import { DecryptedAudioFile, deleteFile, ProcessState } from './fileListingSlice';
import { useAppDispatch } from '~/hooks';
-import { AnimationDefinition } from 'framer-motion';
-import { AlbumImage } from './AlbumImage';
-import { SongMetadata } from './SongMetadata';
import { FileError } from './FileError';
+import classNames from 'classnames';
interface FileRowProps {
id: string;
@@ -26,90 +9,46 @@ interface FileRowProps {
}
export function FileRow({ id, file }: FileRowProps) {
- const { isOpen, onClose } = useDisclosure({ defaultIsOpen: true });
const dispatch = useAppDispatch();
const isDecrypted = file.state === ProcessState.COMPLETE;
- const metadata = file.metadata;
- const nameWithoutExt = file.fileName.replace(/\.[a-z\d]{3,6}$/, '');
- const decryptedName = nameWithoutExt + '.' + file.ext;
-
- const audioPlayerRef = useRef(null);
- const togglePlay = () => {
- const player = audioPlayerRef.current;
- if (!player) {
- return;
- }
-
- if (player.paused) {
- player.play();
- } else {
- player.pause();
- }
- };
-
- const onCollapseAnimationComplete = (definition: AnimationDefinition) => {
- if (definition === 'exit') {
- dispatch(deleteFile({ id }));
- }
- };
+ const decryptedName = file.cleanName + '.' + file.ext;
return (
-
-
-
-
-
-
-
-
-
- {metadata?.name ?? nameWithoutExt}
-
-
-
- {isDecrypted && metadata && }
- {file.state === ProcessState.ERROR && }
-
-
-
- {file.decrypted && }
+
+
+
+
+ {file.cleanName}
+
+ {isDecrypted && file.ext &&
{file.ext}
}
+
-
- {isDecrypted && (
- <>
-
-
-
-
- {file.decrypted && (
-
-
-
- )}
-
- >
- )}
-
-
-
-
-
-
-
-
-
-
+
+ {file.state === ProcessState.ERROR &&
}
+ {isDecrypted && (
+
+ )}
+
+
+
+
+ 下载
+
+
+
+
+
);
}
diff --git a/src/features/file-listing/FileRowResponsiveGrid.tsx b/src/features/file-listing/FileRowResponsiveGrid.tsx
deleted file mode 100644
index 24e2783..0000000
--- a/src/features/file-listing/FileRowResponsiveGrid.tsx
+++ /dev/null
@@ -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,
- },
-});
diff --git a/src/features/file-listing/SongMetadata.tsx b/src/features/file-listing/SongMetadata.tsx
deleted file mode 100644
index 1756fa4..0000000
--- a/src/features/file-listing/SongMetadata.tsx
+++ /dev/null
@@ -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 (
-
-
- 专辑: {metadata.album}
-
-
- 艺术家: {metadata.artist}
-
-
- 专辑艺术家: {metadata.albumArtist}
-
-
- );
-}
diff --git a/src/features/file-listing/__tests__/FileListing.test.tsx b/src/features/file-listing/__tests__/FileListing.test.tsx
index a694595..eebc45f 100644
--- a/src/features/file-listing/__tests__/FileListing.test.tsx
+++ b/src/features/file-listing/__tests__/FileListing.test.tsx
@@ -14,5 +14,5 @@ test('should be able to render a list of 3 items', () => {
});
expect(screen.getAllByTestId('file-row')).toHaveLength(3);
- expect(screen.getByText('Für Alice')).toBeInTheDocument();
+ expect(screen.getByText('ready')).toBeInTheDocument();
});
diff --git a/src/features/file-listing/__tests__/FileRow.test.tsx b/src/features/file-listing/__tests__/FileRow.test.tsx
index 4cc497e..1e58397 100644
--- a/src/features/file-listing/__tests__/FileRow.test.tsx
+++ b/src/features/file-listing/__tests__/FileRow.test.tsx
@@ -3,22 +3,15 @@ import { untouchedFile } from './__fixture__/file-list';
import { FileRow } from '../FileRow';
import { completedFile } from './__fixture__/file-list';
-test('should render no metadata when unavailable', () => {
+test('should render basic title (ready)', () => {
renderWithProviders();
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('ready');
- expect(screen.queryByTestId('audio-meta-album-name')).toBeFalsy();
- expect(screen.queryByTestId('audio-meta-song-artist')).toBeFalsy();
- expect(screen.queryByTestId('audio-meta-album-artist')).toBeFalsy();
});
-test('should render metadata when file has been processed', () => {
+test('should render basic title (done)', () => {
renderWithProviders();
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
- expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('Für Alice');
- expect(screen.getByTestId('audio-meta-album-name')).toHaveTextContent("NOW That's What I Call Cryptography 2023");
- expect(screen.getByTestId('audio-meta-song-artist')).toHaveTextContent('Jixun');
- expect(screen.getByTestId('audio-meta-album-artist')).toHaveTextContent('Cipher Lovers');
});
diff --git a/src/features/file-listing/__tests__/__fixture__/file-list.ts b/src/features/file-listing/__tests__/__fixture__/file-list.ts
index 4bdfdab..11fd37e 100644
--- a/src/features/file-listing/__tests__/__fixture__/file-list.ts
+++ b/src/features/file-listing/__tests__/__fixture__/file-list.ts
@@ -1,6 +1,7 @@
import { DecryptedAudioFile, ProcessState } from '../../fileListingSlice';
export const untouchedFile: DecryptedAudioFile = {
+ cleanName: 'ready',
fileName: 'ready.bin',
raw: 'blob://localhost/file-a',
decrypted: '',
@@ -13,6 +14,7 @@ export const untouchedFile: DecryptedAudioFile = {
export const completedFile: DecryptedAudioFile = {
fileName: 'hello-b.bin',
+ cleanName: 'hello-b',
raw: 'blob://localhost/file-b',
decrypted: 'blob://localhost/file-b-decrypted',
ext: 'flac',
@@ -30,6 +32,7 @@ export const completedFile: DecryptedAudioFile = {
export const fileWithError: DecryptedAudioFile = {
fileName: 'hello-c.bin',
+ cleanName: 'hello-c',
raw: 'blob://localhost/file-c',
decrypted: 'blob://localhost/file-c-decrypted',
ext: 'flac',
diff --git a/src/features/file-listing/fileListingSlice.ts b/src/features/file-listing/fileListingSlice.ts
index 02a792e..17e43cb 100644
--- a/src/features/file-listing/fileListingSlice.ts
+++ b/src/features/file-listing/fileListingSlice.ts
@@ -5,9 +5,11 @@ import type { RootState } from '~/store';
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
import type {
DecryptCommandOptions,
- FetchMusicExNamePayload, ParseKugouHeaderPayload, ParseKugouHeaderResponse,
+ FetchMusicExNamePayload,
+ ParseKugouHeaderPayload,
+ ParseKugouHeaderResponse,
ParseKuwoHeaderPayload,
- ParseKuwoHeaderResponse
+ ParseKuwoHeaderResponse,
} from '~/decrypt-worker/types';
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
@@ -15,8 +17,9 @@ import {
selectKugouKey,
selectKWMv2Key,
selectQMCv2KeyByFileName,
- selectQtfmAndroidKey
+ selectQtfmAndroidKey,
} from '../settings/settingsSelector';
+import { cleanFilename } from '~/util/cleanFilename';
export enum ProcessState {
QUEUED = 'QUEUED',
@@ -40,6 +43,7 @@ export interface AudioMetadata {
export interface DecryptedAudioFile {
fileName: string;
+ cleanName: string;
raw: string; // blob uri
ext: string;
decrypted: string; // blob uri
@@ -106,6 +110,7 @@ export const fileListingSlice = createSlice({
addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => {
state.files[payload.id] = {
fileName: payload.fileName,
+ cleanName: cleanFilename(payload.fileName),
raw: payload.blobURI,
decrypted: '',
ext: '',
diff --git a/src/features/nav/ResponsiveNav.tsx b/src/features/nav/ResponsiveNav.tsx
new file mode 100644
index 0000000..b92bb29
--- /dev/null
+++ b/src/features/nav/ResponsiveNav.tsx
@@ -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 (
+
+ {/* Sidebar */}
+
+
+ {/* Main content */}
+
{children}
+
+ );
+}
diff --git a/src/features/nav/TabNavLink.tsx b/src/features/nav/TabNavLink.tsx
new file mode 100644
index 0000000..517c7c6
--- /dev/null
+++ b/src/features/nav/TabNavLink.tsx
@@ -0,0 +1,21 @@
+import classNames from 'classnames';
+import type { RefAttributes } from 'react';
+import { NavLink, type NavLinkProps } from 'react-router';
+
+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,
+ },
+ );
+
+export function TabNavLink({ children, ...props }: NavLinkProps & RefAttributes) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx
index b4de13b..c366d99 100644
--- a/src/features/settings/Settings.tsx
+++ b/src/features/settings/Settings.tsx
@@ -1,163 +1,75 @@
-import {
- Box,
- Button,
- Center,
- chakra,
- Flex,
- HStack,
- Icon,
- IconButton,
- Menu,
- MenuButton,
- MenuItem,
- MenuList,
- Portal,
- Spacer,
- Tab,
- TabList,
- TabPanel,
- TabPanels,
- Tabs,
- Text,
- useBreakpointValue,
- useToast,
- VStack,
-} from '@chakra-ui/react';
-import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
-import { useState, type FC } from 'react';
-import { MdExpandMore, MdMenu, MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { useAppDispatch, useAppSelector } from '~/hooks';
import { commitStagingChange, discardStagingChanges } from './settingsSlice';
-import { PanelKWMv2Key } from './panels/PanelKWMv2Key';
import { selectIsSettingsNotSaved } from './settingsSelector';
-import { PanelQingTing } from './panels/PanelQingTing';
-import { PanelKGGKey } from '~/features/settings/panels/PanelKGGKey.tsx';
-
-const TABS: { name: string; Tab: FC }[] = [
- { name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
- { name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
- { name: 'KGG 密钥', Tab: PanelKGGKey },
- { name: '蜻蜓 FM', Tab: PanelQingTing },
- {
- name: '其它/待定',
- Tab: () => 这里空空如也~,
- },
-];
+import { 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 { TabNavLink } from '../nav/TabNavLink';
export function Settings() {
- const toast = useToast();
const dispatch = useAppDispatch();
- const isLargeWidthDevice =
- useBreakpointValue({
- base: false,
- lg: true,
- }) ?? false;
- const [tabIndex, setTabIndex] = useState(0);
- const handleTabChange = (idx: number) => {
- setTabIndex(idx);
- };
const handleResetSettings = () => {
dispatch(discardStagingChanges());
- toast({
- status: 'info',
- title: '未储存的设定已舍弃',
- description: '已还原到更改前的状态。',
- isClosable: true,
- });
+ toast.info(() => (
+
+
未储存的设定已舍弃
+
已还原到更改前的状态。
+
+ ));
};
const handleApplySettings = () => {
dispatch(commitStagingChange());
- toast({
- status: 'success',
- title: '设定已应用',
- isClosable: true,
- });
+ toast.success('设定已应用');
};
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
return (
-
-
-
+
+
+
+ 若文件名匹配失败,则使用相似文件名的密钥。
+
+ 该匹配使用「
+
+ 莱文斯坦距离
+
+ 」算法来计算文件名的相似程度。
+
+ 若密钥数量过多,匹配时可能会造成浏览器卡顿或无响应一段时间。
+ 若不确定,请勾选该项。
+
+ }
+ >
+ 这是什么?
+
+
-
-
- 匹配相似文件名
-
-
- 若文件名匹配失败,则使用相似文件名的密钥。
-
- 使用「
-
- 莱文斯坦距离
-
-
-
-
- 」算法计算相似程度。
-
- 若密钥数量过多,匹配时可能会造成浏览器卡顿或无响应一段时间。
- 若不确定,请勾选该项。
-
- }
- >
-
-
-
-
+ 密钥管理
+ setShowImportModal(true)}
+ clearKeys={clearAll}
+ />
-
-
- {qmc2Keys.map(({ id, ekey, name }, i) => (
-
- ))}
-
- {qmc2Keys.length === 0 && 还没有密钥。}
-
+
+ {qmc2Keys.map(({ id, ekey, name }, i) => (
+
+ ))}
+
setSecretType(e.target.value as 'qm' | 'douban')}
- variant="flushed"
- display="inline"
- css={{ paddingLeft: '0.75rem', width: 'auto' }}
+ className="inline mx-1 px-1 border-b border-accent/50 bg-base-100"
>
-
+
}
show={showImportModal}
onClose={() => setShowImportModal(false)}
@@ -181,6 +129,6 @@ export function PanelQMCv2Key() {
{secretType === 'qm' && }
{secretType === 'douban' && }
-
+ >
);
}
diff --git a/src/features/settings/panels/PanelQingTing.tsx b/src/features/settings/panels/PanelQingTing.tsx
index 80ca0d8..b87212c 100644
--- a/src/features/settings/panels/PanelQingTing.tsx
+++ b/src/features/settings/panels/PanelQingTing.tsx
@@ -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 { ExtLink } from '~/components/ExtLink';
-import { ChangeEvent, ClipboardEvent } from 'react';
+import { ChangeEvent, ClipboardEvent, useId } from 'react';
import { VQuote } from '~/components/HelpText/VQuote';
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
import { qtfmAndroidUpdateKey } from '../settingsSlice';
import { workerClientBus } from '~/decrypt-worker/client.ts';
import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.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';
@@ -64,64 +52,76 @@ export function PanelQingTing() {
setSecretKey(e.target.value);
};
+ const idSecretKey = useId();
+
return (
-
-
- 蜻蜓 FM
- 设备密钥
-
+
+
蜻蜓 FM
-
+
蜻蜓 FM的安卓版本需要获取设备密钥,并以此来生成解密密钥。
-
-
-
- 设备密钥
-
-
- {'粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过 '}
+
+
+
+
+
-
- 注意事项
-
-
-
-
+ 注意事项
+
-
-
-
-
+
+
+ [内部储存]/
+ Android/
+
+ data/
+
+ fm.qingting.qtradio/
+
+ files/Music/
+
+
+
+
-
-
-
-
-
- 音频文件文件名为「.p~!」前缀。
-
-
-
- 因为解密密钥与文件名相关,因此解密前请不要更改文件名。
-
-
-
+
+
+
+
+
+
+ 音频文件文件名为「.p~!」前缀。
+
+
+
+ 因为解密密钥与文件名相关,因此解密前请不要更改文件名。
+
+
+
);
}
diff --git a/src/features/settings/panels/QMCv2/InstructionsIOS.tsx b/src/features/settings/panels/QMCv2/InstructionsIOS.tsx
index 3454525..ac40faa 100644
--- a/src/features/settings/panels/QMCv2/InstructionsIOS.tsx
+++ b/src/features/settings/panels/QMCv2/InstructionsIOS.tsx
@@ -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 (
<>
-
- iOS 设备获取应用私有文件比较麻烦,你需要越狱或使用一台 PC 或 Mac 来对 iOS 设备进行完整备份。
- 因此,建议换用 PC 或 Mac 重新下载音乐文件然后再尝试解密。
-
-
-
-
-
-
- 我的 iOS 设备已经越狱
-
-
-
-
-
-
-
-
+
+
iOS 设备获取应用私有文件比较麻烦,你需要越狱或使用一台 PC 或 Mac 来对 iOS 设备进行完整备份。
+
因此,建议换用 PC 或 Mac 重新下载音乐文件然后再尝试解密。
+
-
-
-
-
- 我的 iOS 设备没有越狱
-
-
-
-
-
+
+
+
+
+ 我的 iOS 设备已经越狱{' '}
+
+
+
+
+
+
+
+
+ 我的 iOS 设备没有越狱
+
+
-
-
-
+
+
+
>
);
}
diff --git a/src/features/settings/panels/QMCv2/InstructionsIOSCondition.tsx b/src/features/settings/panels/QMCv2/InstructionsIOSCondition.tsx
index 2527e09..f898b10 100644
--- a/src/features/settings/panels/QMCv2/InstructionsIOSCondition.tsx
+++ b/src/features/settings/panels/QMCv2/InstructionsIOSCondition.tsx
@@ -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/[随机字符]/
+ >
+ ) : (
+ '/AppDomain-'
+ );
return (
<>
-
- 获取密钥数据库文件
-
-
+ 获取密钥数据库文件
+
{useBackup && (
-
- 首先需要在 iOS 客户端的设定允许备份:
-
-
+ -
+ 首先需要在 iOS 客户端的设定允许备份:
+
+
+
)}
- {useBackup && (
-
- 使用你喜欢的备份软件对 iOS 设备进行完整备份;
-
- )}
-
- {useBackup && 打开备份文件,并导航到下述目录:}
- {useJailbreak && 访问下述目录:}
+ {useBackup && - 使用你喜欢的备份软件对 iOS 设备进行完整备份
}
+ -
+ {useBackup && 打开备份文件,并导航到下述目录:}
+ {useJailbreak && 访问下述目录:}
{pathPrefix}com.tencent.QQMusic/Documents/mmkv/
-
-
-
- 提取或导出密钥数据库文件 filenameEkeyMap;
-
-
-
-
- 提交导出的 filenameEkeyMap 文件;
-
-
-
- 按下「保存」来应用更改。
-
-
+
+
+ 提取或导出密钥数据库文件 filenameEkeyMap
+
+
+ 提交导出的 filenameEkeyMap 文件
+
+ 按下「保存」来应用更改。
+
-
- 获取离线文件
-
-
- 访问下述目录:
+ 获取离线文件
+
+ 访问下述目录:
{pathPrefix}com.tencent.QQMusic/Library/Application Support/com.tencent.QQMusic/iData/iMusic
-
- 该目录又存在数个子目录,其子目录下保存的「[字符].m[字符]」文件则是最终的加密文件。
-
-
- 格式:[song_id]-[mid]-[随机数字].m[后缀]
-
-
- 例:{EXAMPLE_NAME_IOS}
-
-
+
+ 该目录又存在数个子目录,其子目录下保存的「[字符].m[字符]」文件则是最终的加密文件。
+
+
+ 格式:[song_id]-[mid]-[随机数字].m[后缀]
+
+
+ 例:{EXAMPLE_NAME_IOS}
+
+
-
- 解密离线文件
-
- 勾选设定界面的「使用近似文件名匹配」可跳过该节内容。
- ⚠ 注意:若密钥过多,匹配过程可能会造成浏览器卡顿或无响应。
-
-
-
- 提取文件的 [mid] 部分,如 {EXAMPLE_MEDIA_ID};
-
-
-
-
- 查找密钥表,得到文件名「{EXAMPLE_NAME_DB}」;
-
-
-
-
- 将文件更名为对应的文件名,如{EXAMPLE_NAME_IOS} ➔
- {EXAMPLE_NAME_DB};
-
-
-
-
- 回到主界面,提交文件「{EXAMPLE_NAME_DB}」。
-
-
-
+ 解密离线文件
+ 勾选设定界面的「使用近似文件名匹配」可跳过该节内容。
+ ⚠ 注意:若密钥过多,匹配过程可能会造成浏览器卡顿或无响应。
+
+ -
+ 提取文件的
[mid] 部分,如 {EXAMPLE_MEDIA_ID}
+
+ -
+ 查找密钥表,得到文件名「
{EXAMPLE_NAME_DB}」
+
+ -
+ 将文件更名为对应的文件名,如
+
+ {EXAMPLE_NAME_IOS}
+
➔ {EXAMPLE_NAME_DB}
+
+ -
+ 回到主界面,提交文件「
{EXAMPLE_NAME_DB}」。
+
+
>
);
}
diff --git a/src/features/settings/panels/QMCv2/InstructionsMac.tsx b/src/features/settings/panels/QMCv2/InstructionsMac.tsx
index 5637373..de1b793 100644
--- a/src/features/settings/panels/QMCv2/InstructionsMac.tsx
+++ b/src/features/settings/panels/QMCv2/InstructionsMac.tsx
@@ -1,59 +1,79 @@
-import { Heading, Text, Code, Kbd, 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 (
<>
- Mac 客户端使用 mmkv 数据库储存密钥。
-
- {'此外,你需要降级到 '}
-
- 2023.09.03 版本的客户端
-
- {'。'}
- 新版本对 mmkv 数据库进行了加密处理。
-
- 该密钥文件通常存储在下述路径:
-
- ~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId
-
+ Mac 客户端使用 mmkv 数据库储存密钥。
+ 此外,你需要降级到 v8.8.0 版本的客户端 —— 更新的版本对密钥数据库进行了加密,目前无公开的获取方案。
-
- 导入密钥
-
-
-
-
- 选中并复制上述的 MMKVStreamEncryptId 文件路径
-
-
-
- 点击上方的「文件选择区域」,打开「文件选择框」
-
-
-
- 按下「
-
- {' + '}
-
- {' + '}
- {'G'}」组合键打开「路径输入框」
-
-
-
-
- 粘贴之前复制的 MMKVStreamEncryptId 文件路径
-
-
-
- 按下「回车键」确认。
-
-
+ 获取 QQ 音乐 Mac 客户端 8.8.0:
+
+ -
+
+ 通过
Archive.org 缓存下载(慢)
+
+
+ -
+
+ 通过 Telegram 下载(需要账号)
+
+
+
+
+ 密钥文件通常存储在下述路径:
+ {DB_PATH}
+
+ 导入密钥
+
+ -
+
+
MMKVStreamEncryptId 文件路径
+
+ -
+ 点击上方的文件选择区域,打开文件选择框
+
+ -
+ 按下
+
+
+ {'+'}
+
+ {'+'}
+ G
+
+ 组合键打开路径输入框
+
+ -
+ 粘贴之前复制的
MMKVStreamEncryptId 文件路径
+
+ - 按下「回车键」确认。
+
>
);
}
diff --git a/src/features/settings/panels/QMCv2/InstructionsPC.tsx b/src/features/settings/panels/QMCv2/InstructionsPC.tsx
index dd7afc1..c897077 100644
--- a/src/features/settings/panels/QMCv2/InstructionsPC.tsx
+++ b/src/features/settings/panels/QMCv2/InstructionsPC.tsx
@@ -1,10 +1,62 @@
-import { Text } from '@chakra-ui/react';
+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';
+
+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 (
<>
- 使用 Windows 19.51 或更低版本下载的歌曲文件无需密钥。
- 使用 Windows 19.57 或更高版本下载的歌曲文件需要导入密钥,但方法尚未公开。
+
+ 使用 19.57 或更高版本下载的歌曲文件
+ 需要导入密钥。
+
+ 目前未公开密钥获取方式,因此不支持。
+
+
+
+ 使用 19.51 或更低版本下载的歌曲文件
+ 无需密钥。
+
+
+
+ 获取 QQ 音乐 Windows 19.51 客户端:
+
+
+ -
+
+ 通过
Archive.org 缓存下载(慢)
+
+
+ -
+
+ 通过 Telegram 下载(需要账号)
+
+
+
+
+
+ 安装完成后可以覆盖 QQ 音乐安装目录下的
+
+ QQMusicUp.exe
+
+ 同名文件,屏蔽自动更新(
+
+ 源码
+
+ )。
+
+ 降级后需要删除新版本下载的文件并重新使用旧版本下载。
>
);
}
diff --git a/src/features/settings/panels/QMCv2/QMCv2DoubanAllInstructions.tsx b/src/features/settings/panels/QMCv2/QMCv2DoubanAllInstructions.tsx
index b3595c5..33208a1 100644
--- a/src/features/settings/panels/QMCv2/QMCv2DoubanAllInstructions.tsx
+++ b/src/features/settings/panels/QMCv2/QMCv2DoubanAllInstructions.tsx
@@ -1,17 +1,14 @@
-import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
+import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
export function QMCv2DoubanAllInstructions() {
- return (
- <>
-
- 安卓
-
-
-
-
-
-
- >
- );
+ const tabs: InstructionTab[] = [
+ {
+ id: 'android',
+ label: '安卓',
+ content: ,
+ },
+ ];
+
+ return ;
}
diff --git a/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx b/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx
index 305e4de..1e3c08b 100644
--- a/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx
+++ b/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx
@@ -1,69 +1,25 @@
-import {
- HStack,
- Icon,
- IconButton,
- Input,
- InputGroup,
- InputLeftElement,
- InputRightElement,
- ListItem,
- Text,
- VStack,
-} from '@chakra-ui/react';
-import { MdDelete, MdVpnKey } from 'react-icons/md';
import { qmc2DeleteKey, qmc2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
import { memo } from 'react';
+import { KeyInput } from '~/components/KeyInput.tsx';
export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: string; ekey: string; i: number }) => {
const dispatch = useAppDispatch();
- const updateKey = (prop: 'name' | 'ekey', e: React.ChangeEvent) =>
- dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
- const deleteKey = () => dispatch(qmc2DeleteKey({ id }));
+ const ekeyLen = ekey.length;
+ const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
return (
-
-
-
- {i + 1}
-
-
-
- updateKey('name', e)}
- />
-
-
-
-
-
- updateKey('ekey', e)}
- />
-
-
- {ekey.length || '?'}
-
-
-
-
-
- }
- variant="ghost"
- colorScheme="red"
- type="button"
- onClick={deleteKey}
- />
-
-
+ dispatch(qmc2UpdateKey({ id, field: 'name', value }))}
+ onSetValue={(value) => dispatch(qmc2UpdateKey({ id, field: 'ekey', value }))}
+ onDelete={() => dispatch(qmc2DeleteKey({ id }))}
+ sequence={i + 1}
+ namePlaceholder="文件名,包括后缀名。如 “AAA - BBB.mflac”"
+ valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
+ />
);
});
diff --git a/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx b/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx
index 239f1b6..df483d6 100644
--- a/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx
+++ b/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx
@@ -1,32 +1,20 @@
-import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsIOS } from './InstructionsIOS';
import { InstructionsMac } from './InstructionsMac';
import { InstructionsPC } from './InstructionsPC';
+import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
-export function QMCv2QQMusicAllInstructions() {
- return (
- <>
-
- 安卓
- iOS
- Mac
- Windows
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
+export function QMCv2QQMusicAllInstructions({ limitHeight }: { limitHeight?: boolean }) {
+ const tabs: InstructionTab[] = [
+ {
+ id: 'android',
+ label: '安卓',
+ content: ,
+ },
+ { id: 'ios', label: 'iOS', content: },
+ { id: 'mac', label: 'Mac', content: },
+ { id: 'windows', label: 'Windows', content: },
+ ];
+
+ return ;
}
diff --git a/src/features/settings/panels/QMCv2/assets/noop.asm.txt b/src/features/settings/panels/QMCv2/assets/noop.asm.txt
new file mode 100644
index 0000000..1773ad6
--- /dev/null
+++ b/src/features/settings/panels/QMCv2/assets/noop.asm.txt
@@ -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'
diff --git a/src/features/settings/panels/QMCv2/assets/noop.exe b/src/features/settings/panels/QMCv2/assets/noop.exe
new file mode 100644
index 0000000..3f97bcd
Binary files /dev/null and b/src/features/settings/panels/QMCv2/assets/noop.exe differ
diff --git a/src/features/settings/settingsTabs.tsx b/src/features/settings/settingsTabs.tsx
new file mode 100644
index 0000000..fdc6c61
--- /dev/null
+++ b/src/features/settings/settingsTabs.tsx
@@ -0,0 +1,13 @@
+import type { FC } from 'react';
+import { PanelQMCv2Key } from '~/features/settings/panels/PanelQMCv2Key.tsx';
+import { PanelKWMv2Key } from '~/features/settings/panels/PanelKWMv2Key.tsx';
+import { PanelKGGKey } from '~/features/settings/panels/PanelKGGKey.tsx';
+import { PanelQingTing } from '~/features/settings/panels/PanelQingTing.tsx';
+
+export const SETTINGS_TABS: Record = {
+ qmc: { name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
+ kwm: { name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
+ kgg: { name: 'KGG 密钥', Tab: PanelKGGKey },
+ qtfm: { name: '蜻蜓 FM', Tab: PanelQingTing },
+ // misc: { name: '其它/待定', Tab: () => 这里空空如也~
},
+} as const;
diff --git a/src/main.tsx b/src/main.tsx
index f5e600c..e15df44 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,4 +1,5 @@
import './pwa';
+import './App.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
diff --git a/src/tabs/FaqTab.tsx b/src/tabs/FaqTab.tsx
index ae6834c..9178fa3 100644
--- a/src/tabs/FaqTab.tsx
+++ b/src/tabs/FaqTab.tsx
@@ -1,44 +1,27 @@
-import { FC, Fragment } from 'react';
-import { Center, Container, Heading, Link, ListItem, UnorderedList } from '@chakra-ui/react';
-import { Header3 } from '~/components/HelpText/Headers';
-import { KuwoFAQ } from '~/faq/KuwoFAQ';
-import { OtherFAQ } from '~/faq/OtherFAQ';
-import { QQMusicFAQ } from '~/faq/QQMusicFAQ';
-import { KugouFAQ } from '~/faq/KugouFAQ.tsx';
-
-type FAQEntry = {
- id: string;
- title: string;
- Help: FC;
-};
-
-const faqEntries: FAQEntry[] = [
- { id: 'qqmusic', title: 'QQ 音乐', Help: QQMusicFAQ },
- { id: 'kuwo', title: '酷我音乐', Help: KuwoFAQ },
- { id: 'kugou', title: '酷狗音乐', Help: KugouFAQ },
- { id: 'other', title: '其它问题', Help: OtherFAQ },
-];
+import { Outlet } from 'react-router';
+import { FAQ_PAGES } from '~/faq/FAQPages';
+import { ResponsiveNav } from '~/features/nav/ResponsiveNav';
+import { TabNavLink } from '~/features/nav/TabNavLink';
export function FaqTab() {
return (
-
-
- 常见问题解答
-
- 答疑目录
-
- {faqEntries.map(({ id, title }) => (
-
- {title}
-
- ))}
-
- {faqEntries.map(({ id, title, Help }) => (
-
- {title}
-
-
- ))}
-
+
+
+ {FAQ_PAGES.map(({ id, name }) => (
+
+ {name}
+
+ ))}
+
+ }
+ >
+
+
+
);
}
diff --git a/src/tabs/MainTab.tsx b/src/tabs/MainTab.tsx
index e6dc61d..3491ce1 100644
--- a/src/tabs/MainTab.tsx
+++ b/src/tabs/MainTab.tsx
@@ -1,4 +1,4 @@
-import { Alert, AlertIcon, Box, Button, Flex, Text, VStack } from '@chakra-ui/react';
+import { RiErrorWarningLine } from 'react-icons/ri';
import { SelectFile } from '../components/SelectFile';
import { FileListing } from '~/features/file-listing/FileListing';
@@ -14,29 +14,32 @@ export function MainTab() {
};
return (
-
-
+
+
{isSettingsNotSaved && (
-
-
-
-
- 有尚未储存的设置,
-
- 设定将在保存后生效
-
-
+
);
}
diff --git a/src/tabs/SettingsTab.tsx b/src/tabs/SettingsTab.tsx
index 2642feb..16693e2 100644
--- a/src/tabs/SettingsTab.tsx
+++ b/src/tabs/SettingsTab.tsx
@@ -1,15 +1,5 @@
-import { Container, Flex, useBreakpointValue } from '@chakra-ui/react';
import { Settings } from '~/features/settings/Settings';
export function SettingsTab() {
- const containerProps = useBreakpointValue({
- base: { p: '0' },
- lg: { p: undefined },
- });
-
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/theme.ts b/src/theme.ts
deleted file mode 100644
index f3248ef..0000000
--- a/src/theme.ts
+++ /dev/null
@@ -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',
- },
- },
-});
diff --git a/src/themes/Tabs.tsx b/src/themes/Tabs.tsx
deleted file mode 100644
index e13a42b..0000000
--- a/src/themes/Tabs.tsx
+++ /dev/null
@@ -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,
- },
-});
diff --git a/src/util/applyTemplate.ts b/src/util/applyTemplate.ts
new file mode 100644
index 0000000..9a8a091
--- /dev/null
+++ b/src/util/applyTemplate.ts
@@ -0,0 +1,3 @@
+export function applyTemplate(tpl: string, values: Record) {
+ return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : ''));
+}
diff --git a/src/util/cleanFilename.ts b/src/util/cleanFilename.ts
new file mode 100644
index 0000000..17a70cc
--- /dev/null
+++ b/src/util/cleanFilename.ts
@@ -0,0 +1,6 @@
+export function cleanFilename(filename: string): string {
+ return filename
+ .replace(/\.[a-z\d]{3,6}$/, '') // Remove file extension
+ .replace(/\.(mgg|kgg|mflac)\d*$/, '') // Remove extra mgg/kgg/mflac extension
+ .replace(/\s?\[mq[a-z\d]*\]\s*$/, ''); // Remove " [mqms*]" suffix
+}
diff --git a/src/util/toastImportResult.tsx b/src/util/toastImportResult.tsx
new file mode 100644
index 0000000..353cdbe
--- /dev/null
+++ b/src/util/toastImportResult.tsx
@@ -0,0 +1,28 @@
+import { toast } from 'react-toastify';
+
+export function toastImportResult(name: string, keys: unknown[] | null) {
+ if (keys?.length === 0) {
+ toast.warning(() => (
+
+
未导入密钥
+
选择的密钥数据库文件未发现任何可用的密钥。
+
+ ));
+ } else if (keys) {
+ toast.success(() => (
+
+
成功导入 {keys.length} 个密钥。
+
记得按下「保存」来应用。
+
+ ));
+ } else {
+ toast.error(() => (
+
+
未导入密钥
+
+ 不支持从提供的密钥文件 {name} 导入密钥。
+
+
+ ));
+ }
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 81f712a..e73ae3a 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -6,3 +6,8 @@ module 'virtual:pwa-register' {
*/
declare function registerSW(_opts: unknown): () => void;
}
+
+declare module '*?base64' {
+ const content: string;
+ export default content;
+}
diff --git a/support/b64-loader.ts b/support/b64-loader.ts
new file mode 100644
index 0000000..449abfb
--- /dev/null
+++ b/support/b64-loader.ts
@@ -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}';`;
+ },
+};
diff --git a/tsconfig.json b/tsconfig.json
index ce94041..2fbac8c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
- "types": ["vitest/globals", "@testing-library/jest-dom"],
+ "types": ["@types/wicg-file-system-access", "vitest/globals", "@testing-library/jest-dom"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
diff --git a/vite.config.ts b/vite.config.ts
index a2a040b..acee847 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -8,8 +8,10 @@ import wasm from 'vite-plugin-wasm';
import replace from '@rollup/plugin-replace';
import topLevelAwait from 'vite-plugin-top-level-await';
import { VitePWA } from 'vite-plugin-pwa';
+import tailwindcss from '@tailwindcss/vite';
import { tryCommand } from './support/command';
+import { base64Loader } from './support/b64-loader';
const projectRoot = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8'));
@@ -38,11 +40,12 @@ export default defineConfig({
],
},
},
- base: './',
optimizeDeps: {
exclude: ['@unlock-music/crypto', 'sql.js'],
},
plugins: [
+ tailwindcss(),
+ base64Loader,
replace({
preventAssignment: true,
values: {
@@ -92,13 +95,14 @@ export default defineConfig({
},
},
build: {
+ minify: true,
rollupOptions: {
output: {
manualChunks: {
- reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', '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'],
},
},
},