mirror of
https://git.unlock-music.dev/um/um-react.git
synced 2025-05-23 16:27:41 +08:00
adjust dark mode and layout
All checks were successful
Build and Deploy / build (push) Successful in 2m2s
All checks were successful
Build and Deploy / build (push) Successful in 2m2s
This commit is contained in:
parent
6cb1f9f87f
commit
3ab73d8369
@ -48,6 +48,11 @@ export function AdbInstructionTemplate({ dir, file, platform }: AdbInstructionTe
|
|||||||
<SyntaxHighlighter language={language} style={hljsStyleGitHub}>
|
<SyntaxHighlighter language={language} style={hljsStyleGitHub}>
|
||||||
{command}
|
{command}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
|
<br />※ 安卓模拟器可能需要额外操作,如
|
||||||
|
<ExtLink className="text-nowrap" href="https://g.126.fm/04jewvw">
|
||||||
|
网易 MuMu 模拟器
|
||||||
|
</ExtLink>
|
||||||
|
需要提前使用 <code>adb connect ...</code> 指令连接模拟器。详细请参考官方说明文档并调整上述脚本。
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
提交当前目录下的 <code>{file}</code> 文件。
|
提交当前目录下的 <code>{file}</code> 文件。
|
||||||
|
@ -8,7 +8,7 @@ export interface HeaderProps {
|
|||||||
|
|
||||||
export function Header3({ children, className, id }: HeaderProps) {
|
export function Header3({ children, className, id }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<h3 id={id} className={`text-2xl pt-3 pb-1 font-bold border-b border-base-300 text-neutral-800 ${className}`}>
|
<h3 id={id} className={`text-2xl pt-3 pb-1 font-bold border-b border-base-300 ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
@ -16,7 +16,7 @@ export function Header3({ children, className, id }: HeaderProps) {
|
|||||||
|
|
||||||
export function Header4({ children, className, id }: HeaderProps) {
|
export function Header4({ children, className, id }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<h4 id={id} className={`text-xl pt-3 pb-1 font-semibold text-neutral-800 ${className}`}>
|
<h4 id={id} className={`text-xl pt-3 pb-1 font-semibold ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</h4>
|
</h4>
|
||||||
);
|
);
|
||||||
@ -24,7 +24,7 @@ export function Header4({ children, className, id }: HeaderProps) {
|
|||||||
|
|
||||||
export function Header5({ children, className, id }: HeaderProps) {
|
export function Header5({ children, className, id }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<h5 id={id} className={`text-lg pt-3 pb-1 font-semibold text-neutral-800 ${className}`}>
|
<h5 id={id} className={`text-lg pt-3 pb-1 font-semibold ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</h5>
|
</h5>
|
||||||
);
|
);
|
||||||
|
@ -10,7 +10,7 @@ export function KugouFAQ() {
|
|||||||
<p>
|
<p>
|
||||||
酷狗现在对部分用户推送了 <code>kgg</code> 加密格式(安卓、Windows 客户端)。
|
酷狗现在对部分用户推送了 <code>kgg</code> 加密格式(安卓、Windows 客户端)。
|
||||||
</p>
|
</p>
|
||||||
<p>根据平台不同,你需要提取密钥数据库。</p>
|
<p className="my-4">根据平台不同,你需要提取密钥数据库。</p>
|
||||||
|
|
||||||
<div className="p-2 @container">
|
<div className="p-2 @container">
|
||||||
<div className="alert alert-warning">
|
<div className="alert alert-warning">
|
||||||
|
@ -11,7 +11,7 @@ export function KuwoFAQ() {
|
|||||||
<>
|
<>
|
||||||
<Header4>解锁失败</Header4>
|
<Header4>解锁失败</Header4>
|
||||||
<SegmentTryOfficialPlayer />
|
<SegmentTryOfficialPlayer />
|
||||||
<p>
|
<p className="my-4">
|
||||||
日前,仅<HiWord>手机客户端</HiWord>下载的
|
日前,仅<HiWord>手机客户端</HiWord>下载的
|
||||||
<VQuote>
|
<VQuote>
|
||||||
<strong>至臻全景声</strong>
|
<strong>至臻全景声</strong>
|
||||||
@ -22,10 +22,10 @@ export function KuwoFAQ() {
|
|||||||
</VQuote>
|
</VQuote>
|
||||||
音质的音乐文件采用新版加密。
|
音质的音乐文件采用新版加密。
|
||||||
</p>
|
</p>
|
||||||
<p>其他音质目前不需要提取密钥。</p>
|
<p className="my-4">其他音质目前不需要提取密钥。</p>
|
||||||
<p>PC平台暂未推出使用新版加密的音质。</p>
|
<p className="my-4">PC平台暂未推出使用新版加密的音质。</p>
|
||||||
|
|
||||||
<div className="alert alert-warning">
|
<div className="alert alert-warning mb-4">
|
||||||
<RiErrorWarningLine className="text-2xl" />
|
<RiErrorWarningLine className="text-2xl" />
|
||||||
<div>
|
<div>
|
||||||
<p>安卓用户提取密钥需要 root 权限,或注入文件提供器。</p>
|
<p>安卓用户提取密钥需要 root 权限,或注入文件提供器。</p>
|
||||||
|
@ -50,8 +50,8 @@ export function OtherFAQ() {
|
|||||||
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
|
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
|
||||||
例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付等限制。
|
例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付等限制。
|
||||||
</p>
|
</p>
|
||||||
<p>如果希望不破坏系统完整性,你可以考虑在电脑上使用安卓模拟器。</p>
|
<p className="my-2">如果希望不破坏系统完整性,你可以考虑在电脑上使用安卓模拟器。</p>
|
||||||
<p>
|
<p className="my-2">
|
||||||
很多安卓模拟器都提供了 root 特权支持,可以很方便的启用,例如
|
很多安卓模拟器都提供了 root 特权支持,可以很方便的启用,例如
|
||||||
<VQuote>
|
<VQuote>
|
||||||
<ExtLink href="https://mumu.163.com/">网易 MuMu 模拟器(安卓 12,推荐)</ExtLink>
|
<ExtLink href="https://mumu.163.com/">网易 MuMu 模拟器(安卓 12,推荐)</ExtLink>
|
||||||
|
@ -7,8 +7,8 @@ export function QQMusicFAQ() {
|
|||||||
<>
|
<>
|
||||||
<Header4>解锁失败</Header4>
|
<Header4>解锁失败</Header4>
|
||||||
<SegmentTryOfficialPlayer />
|
<SegmentTryOfficialPlayer />
|
||||||
<p>重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。</p>
|
<p className="my-4">重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。</p>
|
||||||
<p className="mb-2">
|
<p className="mb-4">
|
||||||
部分平台获取的加密文件未包含密钥。选择你<strong>下载文件时</strong>使用的客户端来查看说明。
|
部分平台获取的加密文件未包含密钥。选择你<strong>下载文件时</strong>使用的客户端来查看说明。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ export function SegmentKeyImportInstructions({
|
|||||||
}: SegmentKeyImportInstructionsProps) {
|
}: SegmentKeyImportInstructionsProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>导入密钥可以参考下面的步骤:</p>
|
<p className="mt-2">导入密钥可以参考下面的步骤:</p>
|
||||||
<ol className="list-decimal pl-5">
|
<ol className="list-decimal pl-5">
|
||||||
<li>
|
<li>
|
||||||
<SegmentTopNavSettings />
|
<SegmentTopNavSettings />
|
||||||
@ -28,7 +28,7 @@ export function SegmentKeyImportInstructions({
|
|||||||
<SegmentAddKeyDropdown />
|
<SegmentAddKeyDropdown />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>{keyInstructionText}</p>
|
<p className="mb-2">{keyInstructionText}</p>
|
||||||
{clientInstructions}
|
{clientInstructions}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { RiErrorWarningLine } from 'react-icons/ri';
|
import { RiErrorWarningLine } from 'react-icons/ri';
|
||||||
|
|
||||||
export function SegmentTryOfficialPlayer() {
|
export function SegmentTryOfficialPlayer({ className = '' }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="alert alert-warning">
|
<div className={`alert alert-warning ${className}`}>
|
||||||
<RiErrorWarningLine className="text-2xl" />
|
<RiErrorWarningLine className="text-2xl" />
|
||||||
<p>尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。</p>
|
<p>尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,31 +11,23 @@ interface FileRowProps {
|
|||||||
export function FileRow({ id, file }: FileRowProps) {
|
export function FileRow({ id, file }: FileRowProps) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isDecrypted = file.state === ProcessState.COMPLETE;
|
const isDecrypted = file.state === ProcessState.COMPLETE;
|
||||||
const metadata = file.metadata;
|
|
||||||
|
|
||||||
const nameWithoutExt = file.fileName.replace(/\.[a-z\d]{3,6}$/, '');
|
const decryptedName = file.cleanName + '.' + file.ext;
|
||||||
const decryptedName = nameWithoutExt + '.' + file.ext;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-100 shadow-sm w-full md:w-[30%] " data-testid="file-row">
|
<div className="card bg-base-100 shadow-sm w-full md:w-[30%] " data-testid="file-row">
|
||||||
<div className="card-body items-center text-center px-2">
|
<div className="card-body items-center text-center px-2">
|
||||||
<h2
|
<h2 className="card-title max-w-full whitespace-nowrap flex gap-0" data-testid="audio-meta-song-name">
|
||||||
className="card-title overflow-hidden text-ellipsis block max-w-full whitespace-nowrap"
|
<span className="grow overflow-hidden text-ellipsis" title={decryptedName}>
|
||||||
data-testid="audio-meta-song-name"
|
{file.cleanName}
|
||||||
>
|
</span>
|
||||||
{metadata?.name ?? nameWithoutExt}
|
{isDecrypted && file.ext && <div className="ml-2 badge badge-accent">{file.ext}</div>}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="w-full grow">
|
<div className="w-full grow">
|
||||||
{file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />}
|
{file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />}
|
||||||
{isDecrypted && (
|
{isDecrypted && (
|
||||||
<audio
|
<audio className="w-full" aria-disabled={!file.decrypted} controls autoPlay={false} src={file.decrypted} />
|
||||||
className="max-w-[100%]"
|
|
||||||
aria-disabled={!file.decrypted}
|
|
||||||
controls
|
|
||||||
autoPlay={false}
|
|
||||||
src={file.decrypted}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,5 +14,5 @@ test('should be able to render a list of 3 items', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getAllByTestId('file-row')).toHaveLength(3);
|
expect(screen.getAllByTestId('file-row')).toHaveLength(3);
|
||||||
expect(screen.getByText('Für Alice')).toBeInTheDocument();
|
expect(screen.getByText('ready')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -3,17 +3,14 @@ import { untouchedFile } from './__fixture__/file-list';
|
|||||||
import { FileRow } from '../FileRow';
|
import { FileRow } from '../FileRow';
|
||||||
import { completedFile } from './__fixture__/file-list';
|
import { completedFile } from './__fixture__/file-list';
|
||||||
|
|
||||||
test('should render no metadata when unavailable', () => {
|
test('should render basic title (ready)', () => {
|
||||||
renderWithProviders(<FileRow id="file://ready" file={untouchedFile} />);
|
renderWithProviders(<FileRow id="file://ready" file={untouchedFile} />);
|
||||||
|
|
||||||
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
|
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
|
||||||
expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('ready');
|
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(<FileRow id="file://done" file={completedFile} />);
|
renderWithProviders(<FileRow id="file://done" file={completedFile} />);
|
||||||
|
|
||||||
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
|
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { DecryptedAudioFile, ProcessState } from '../../fileListingSlice';
|
import { DecryptedAudioFile, ProcessState } from '../../fileListingSlice';
|
||||||
|
|
||||||
export const untouchedFile: DecryptedAudioFile = {
|
export const untouchedFile: DecryptedAudioFile = {
|
||||||
|
cleanName: 'ready',
|
||||||
fileName: 'ready.bin',
|
fileName: 'ready.bin',
|
||||||
raw: 'blob://localhost/file-a',
|
raw: 'blob://localhost/file-a',
|
||||||
decrypted: '',
|
decrypted: '',
|
||||||
@ -13,6 +14,7 @@ export const untouchedFile: DecryptedAudioFile = {
|
|||||||
|
|
||||||
export const completedFile: DecryptedAudioFile = {
|
export const completedFile: DecryptedAudioFile = {
|
||||||
fileName: 'hello-b.bin',
|
fileName: 'hello-b.bin',
|
||||||
|
cleanName: 'hello-b',
|
||||||
raw: 'blob://localhost/file-b',
|
raw: 'blob://localhost/file-b',
|
||||||
decrypted: 'blob://localhost/file-b-decrypted',
|
decrypted: 'blob://localhost/file-b-decrypted',
|
||||||
ext: 'flac',
|
ext: 'flac',
|
||||||
@ -30,6 +32,7 @@ export const completedFile: DecryptedAudioFile = {
|
|||||||
|
|
||||||
export const fileWithError: DecryptedAudioFile = {
|
export const fileWithError: DecryptedAudioFile = {
|
||||||
fileName: 'hello-c.bin',
|
fileName: 'hello-c.bin',
|
||||||
|
cleanName: 'hello-c',
|
||||||
raw: 'blob://localhost/file-c',
|
raw: 'blob://localhost/file-c',
|
||||||
decrypted: 'blob://localhost/file-c-decrypted',
|
decrypted: 'blob://localhost/file-c-decrypted',
|
||||||
ext: 'flac',
|
ext: 'flac',
|
||||||
|
@ -5,9 +5,11 @@ import type { RootState } from '~/store';
|
|||||||
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||||
import type {
|
import type {
|
||||||
DecryptCommandOptions,
|
DecryptCommandOptions,
|
||||||
FetchMusicExNamePayload, ParseKugouHeaderPayload, ParseKugouHeaderResponse,
|
FetchMusicExNamePayload,
|
||||||
|
ParseKugouHeaderPayload,
|
||||||
|
ParseKugouHeaderResponse,
|
||||||
ParseKuwoHeaderPayload,
|
ParseKuwoHeaderPayload,
|
||||||
ParseKuwoHeaderResponse
|
ParseKuwoHeaderResponse,
|
||||||
} from '~/decrypt-worker/types';
|
} from '~/decrypt-worker/types';
|
||||||
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||||
@ -15,8 +17,9 @@ import {
|
|||||||
selectKugouKey,
|
selectKugouKey,
|
||||||
selectKWMv2Key,
|
selectKWMv2Key,
|
||||||
selectQMCv2KeyByFileName,
|
selectQMCv2KeyByFileName,
|
||||||
selectQtfmAndroidKey
|
selectQtfmAndroidKey,
|
||||||
} from '../settings/settingsSelector';
|
} from '../settings/settingsSelector';
|
||||||
|
import { cleanFilename } from '~/util/cleanFilename';
|
||||||
|
|
||||||
export enum ProcessState {
|
export enum ProcessState {
|
||||||
QUEUED = 'QUEUED',
|
QUEUED = 'QUEUED',
|
||||||
@ -40,6 +43,7 @@ export interface AudioMetadata {
|
|||||||
|
|
||||||
export interface DecryptedAudioFile {
|
export interface DecryptedAudioFile {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
cleanName: string;
|
||||||
raw: string; // blob uri
|
raw: string; // blob uri
|
||||||
ext: string;
|
ext: string;
|
||||||
decrypted: string; // blob uri
|
decrypted: string; // blob uri
|
||||||
@ -106,6 +110,7 @@ export const fileListingSlice = createSlice({
|
|||||||
addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => {
|
addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => {
|
||||||
state.files[payload.id] = {
|
state.files[payload.id] = {
|
||||||
fileName: payload.fileName,
|
fileName: payload.fileName,
|
||||||
|
cleanName: cleanFilename(payload.fileName),
|
||||||
raw: payload.blobURI,
|
raw: payload.blobURI,
|
||||||
decrypted: '',
|
decrypted: '',
|
||||||
ext: '',
|
ext: '',
|
||||||
|
@ -20,7 +20,7 @@ export function ResponsiveNav({
|
|||||||
className={`@container/nav grow grid grid-cols-1 grid-rows-[auto_1fr] md:grid-rows-1 md:grid-cols-[10rem_1fr] ${className}`}
|
className={`@container/nav grow grid grid-cols-1 grid-rows-[auto_1fr] md:grid-rows-1 md:grid-cols-[10rem_1fr] ${className}`}
|
||||||
>
|
>
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className={`bg-gray-100 md:p-4 md:block ${navigationClassName}`}>{navigation}</aside>
|
<aside className={`bg-base-300 md:p-4 md:block ${navigationClassName}`}>{navigation}</aside>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className={`p-4 grow ${contentClassName}`}>{children}</div>
|
<div className={`p-4 grow ${contentClassName}`}>{children}</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, Fragment } from 'react';
|
import { ComponentType, Fragment } from 'react';
|
||||||
import { Header3 } from '~/components/HelpText/Headers';
|
import { Header3 } from '~/components/HelpText/Headers';
|
||||||
import { KuwoFAQ } from '~/faq/KuwoFAQ';
|
import { KuwoFAQ } from '~/faq/KuwoFAQ';
|
||||||
import { OtherFAQ } from '~/faq/OtherFAQ';
|
import { OtherFAQ } from '~/faq/OtherFAQ';
|
||||||
@ -8,7 +8,7 @@ import { KugouFAQ } from '~/faq/KugouFAQ.tsx';
|
|||||||
type FAQEntry = {
|
type FAQEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
Help: FC;
|
Help: ComponentType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const faqEntries: FAQEntry[] = [
|
const faqEntries: FAQEntry[] = [
|
||||||
@ -20,10 +20,10 @@ const faqEntries: FAQEntry[] = [
|
|||||||
|
|
||||||
export function FaqTab() {
|
export function FaqTab() {
|
||||||
return (
|
return (
|
||||||
<section className="container pb-10">
|
<section className="container pb-10 px-4">
|
||||||
<h2 className="text-3xl font-bold text-center">常见问题解答</h2>
|
<h2 className="text-3xl font-bold text-center">常见问题解答</h2>
|
||||||
<Header3>答疑目录</Header3>
|
<Header3>答疑目录</Header3>
|
||||||
<ul className="list-disc list-inside">
|
<ul className="list-disc pl-6">
|
||||||
{faqEntries.map(({ id, title }) => (
|
{faqEntries.map(({ id, title }) => (
|
||||||
<li key={id}>
|
<li key={id}>
|
||||||
<a className="link link-info no-underline" href={`#faq-${id}`}>
|
<a className="link link-info no-underline" href={`#faq-${id}`}>
|
||||||
|
6
src/util/cleanFilename.ts
Normal file
6
src/util/cleanFilename.ts
Normal file
@ -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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user