diff --git a/.drone.yml b/.drone.yml index 027ff27..483cb38 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,7 +5,7 @@ name: default steps: - name: test & build - image: node:18.16.0-bullseye + image: node:18.16.1-bookworm commands: # - git config --global --add safe.directory "/drone/src" - corepack enable @@ -17,7 +17,7 @@ steps: npm_config_registry: https://registry.npmmirror.com - name: publish - image: node:18.16.0-bullseye + image: node:18.16.1-bookworm environment: DRONE_GITEA_SERVER: https://git.unlock-music.dev GITEA_API_KEY: diff --git a/src/components/ImportSecretModal.tsx b/src/components/ImportSecretModal.tsx index 2a83999..2dd9870 100644 --- a/src/components/ImportSecretModal.tsx +++ b/src/components/ImportSecretModal.tsx @@ -28,7 +28,7 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor - 导入密钥数据库 + 从文件导入密钥
diff --git a/src/crypto/pasreKuwo.ts b/src/crypto/parseKuwo.ts similarity index 100% rename from src/crypto/pasreKuwo.ts rename to src/crypto/parseKuwo.ts diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx index a59155b..6e3ab6a 100644 --- a/src/features/settings/Settings.tsx +++ b/src/features/settings/Settings.tsx @@ -1,4 +1,5 @@ import { + chakra, Box, Button, Center, @@ -20,13 +21,15 @@ import { Text, VStack, useBreakpointValue, + useToast, } from '@chakra-ui/react'; import { PanelQMCv2Key } from './panels/PanelQMCv2Key'; import { useState } from 'react'; import { MdExpandMore, MdMenu, MdOutlineSettingsBackupRestore } from 'react-icons/md'; -import { useAppDispatch } from '~/hooks'; +import { useAppDispatch, useAppSelector } from '~/hooks'; import { commitStagingChange, discardStagingChanges } from './settingsSlice'; import { PanelKWMv2Key } from './panels/PanelKWMv2Key'; +import { selectIsSettingsNotSaved } from './settingsSelector'; const TABS: { name: string; Tab: () => JSX.Element }[] = [ { name: 'QMCv2 密钥', Tab: PanelQMCv2Key }, @@ -38,6 +41,7 @@ const TABS: { name: string; Tab: () => JSX.Element }[] = [ ]; export function Settings() { + const toast = useToast(); const dispatch = useAppDispatch(); const isLargeWidthDevice = useBreakpointValue({ @@ -49,8 +53,25 @@ export function Settings() { const handleTabChange = (idx: number) => { setTabIndex(idx); }; - const handleResetSettings = () => dispatch(discardStagingChanges()); - const handleApplySettings = () => dispatch(commitStagingChange()); + const handleResetSettings = () => { + dispatch(discardStagingChanges()); + + toast({ + status: 'info', + title: '未储存的设定已舍弃', + description: '已还原到更改前的状态。', + isClosable: true, + }); + }; + const handleApplySettings = () => { + dispatch(commitStagingChange()); + toast({ + status: 'success', + title: '设定已应用', + isClosable: true, + }); + }; + const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved); return ( @@ -104,7 +125,16 @@ export function Settings() {
- 设置会在保存后生效。 + {isSettingsNotSaved ? ( + + 有未储存的更改{' '} + + 设定将在保存后生效 + + + ) : ( + 设定将在保存后生效 + )}
diff --git a/src/features/settings/panels/KWMv2/KWMv2EKeyItem.tsx b/src/features/settings/panels/KWMv2/KWMv2EKeyItem.tsx index 35244b4..f9969a2 100644 --- a/src/features/settings/panels/KWMv2/KWMv2EKeyItem.tsx +++ b/src/features/settings/panels/KWMv2/KWMv2EKeyItem.tsx @@ -53,7 +53,12 @@ export const KWMv2EKeyItem = memo(({ id, ekey, quality, rid, i }: StagingKWMv2Ke - updateKey('ekey', e)} /> + updateKey('ekey', e)} + /> {ekey.length || '?'} diff --git a/src/features/settings/panels/PanelKWMv2Key.tsx b/src/features/settings/panels/PanelKWMv2Key.tsx index 9c41ac4..ba6d4e7 100644 --- a/src/features/settings/panels/PanelKWMv2Key.tsx +++ b/src/features/settings/panels/PanelKWMv2Key.tsx @@ -49,8 +49,14 @@ export function PanelKWMv2Key() { const fileBuffer = await file.arrayBuffer(); keys = MMKVParser.parseKuwoEKey(new DataView(fileBuffer)); } - - if (keys) { + if (keys?.length === 0) { + toast({ + title: '未导入密钥', + description: '选择的密钥数据库文件未发现任何可用的密钥。', + isClosable: true, + status: 'warning', + }); + } else if (keys) { dispatch(kwm2ImportKeys(keys)); setShowImportModal(false); toast({ @@ -88,7 +94,7 @@ export function PanelKWMv2Key() { }> setShowImportModal(true)} icon={}> - 从文件导入密钥 + 从文件导入密钥… }> diff --git a/src/features/settings/panels/PanelQMCv2Key.tsx b/src/features/settings/panels/PanelQMCv2Key.tsx index 2d9d67a..c309b2c 100644 --- a/src/features/settings/panels/PanelQMCv2Key.tsx +++ b/src/features/settings/panels/PanelQMCv2Key.tsx @@ -71,14 +71,20 @@ export function PanelQMCv2Key() { qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey })); } - if (qmc2Keys) { + if (qmc2Keys?.length === 0) { + toast({ + title: '未导入密钥', + description: '选择的密钥数据库文件未发现任何可用的密钥。', + isClosable: true, + status: 'warning', + }); + } else if (qmc2Keys) { dispatch(qmc2ImportKeys(qmc2Keys)); setShowImportModal(false); toast({ title: `导入成功 (${qmc2Keys.length})`, description: '记得保存更改来应用。', isClosable: true, - duration: 5000, status: 'success', }); } else { @@ -93,7 +99,7 @@ export function PanelQMCv2Key() { return ( - QMCv2 密钥 + QMCv2 解密密钥 @@ -110,7 +116,7 @@ export function PanelQMCv2Key() { }> setShowImportModal(true)} icon={}> - 从文件导入密钥 + 从文件导入密钥… }> @@ -141,6 +147,7 @@ export function PanelQMCv2Key() { 」算法计算相似程度。 若密钥数量过多,匹配时可能会造成浏览器卡顿或无响应一段时间。 + 若不确定,请勾选该项。 } > diff --git a/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx b/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx index bf133d0..305e4de 100644 --- a/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx +++ b/src/features/settings/panels/QMCv2/QMCv2EKeyItem.tsx @@ -30,13 +30,23 @@ export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: st - updateKey('name', e)} /> + updateKey('name', e)} + /> - updateKey('ekey', e)} /> + updateKey('ekey', e)} + /> {ekey.length || '?'} diff --git a/src/features/settings/settingsSelector.ts b/src/features/settings/settingsSelector.ts index 42d61b7..6b695e5 100644 --- a/src/features/settings/settingsSelector.ts +++ b/src/features/settings/settingsSelector.ts @@ -1,9 +1,11 @@ -import { parseKuwoHeader } from '~/crypto/pasreKuwo'; +import { parseKuwoHeader } from '~/crypto/parseKuwo'; import type { RootState } from '~/store'; import { closestByLevenshtein } from '~/util/levenshtein'; import { hasOwn } from '~/util/objects'; import { kwm2StagingToProductionKey } from './keyFormats'; +export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty; + export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2; export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2; diff --git a/src/features/settings/settingsSlice.ts b/src/features/settings/settingsSlice.ts index e5722e2..10bbaf1 100644 --- a/src/features/settings/settingsSlice.ts +++ b/src/features/settings/settingsSlice.ts @@ -37,16 +37,18 @@ export interface ProductionSettings { } export interface SettingsState { + dirty: boolean; staging: StagingSettings; production: ProductionSettings; } const initialState: SettingsState = { + dirty: false, staging: { - qmc2: { allowFuzzyNameSearch: false, keys: [] }, + qmc2: { allowFuzzyNameSearch: true, keys: [] }, kwm2: { keys: [] }, }, production: { - qmc2: { allowFuzzyNameSearch: false, keys: {} }, + qmc2: { allowFuzzyNameSearch: true, keys: {} }, kwm2: { keys: {} }, }, }; @@ -77,6 +79,7 @@ export const settingsSlice = createSlice({ reducers: { setProductionChanges: (_state, { payload }: PayloadAction) => { return { + dirty: false, production: payload, staging: productionToStaging(payload), }; @@ -84,14 +87,17 @@ export const settingsSlice = createSlice({ // qmc2AddKey(state) { state.staging.qmc2.keys.push({ id: nanoid(), name: '', ekey: '' }); + state.dirty = true; }, qmc2ImportKeys(state, { payload }: PayloadAction[]>) { const newItems = payload.map((item) => ({ id: nanoid(), ...item })); state.staging.qmc2.keys.push(...newItems); + state.dirty = true; }, qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) { const qmc2 = state.staging.qmc2; qmc2.keys = qmc2.keys.filter((item) => item.id !== id); + state.dirty = true; }, qmc2UpdateKey( state, @@ -100,25 +106,31 @@ export const settingsSlice = createSlice({ const keyItem = state.staging.qmc2.keys.find((item) => item.id === id); if (keyItem) { keyItem[field] = value; + state.dirty = true; } }, qmc2ClearKeys(state) { state.staging.qmc2.keys = []; + state.dirty = true; }, qmc2AllowFuzzyNameSearch(state, { payload: { enable } }: PayloadAction<{ enable: boolean }>) { state.staging.qmc2.allowFuzzyNameSearch = enable; + state.dirty = true; }, // TODO: reuse the logic somehow? kwm2AddKey(state) { state.staging.kwm2.keys.push({ id: nanoid(), ekey: '', quality: '', rid: '' }); + state.dirty = true; }, kwm2ImportKeys(state, { payload }: PayloadAction[]>) { const newItems = payload.map((item) => ({ id: nanoid(), ...item })); state.staging.kwm2.keys.push(...newItems); + state.dirty = true; }, kwm2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) { const kwm2 = state.staging.kwm2; kwm2.keys = kwm2.keys.filter((item) => item.id !== id); + state.dirty = true; }, kwm2UpdateKey( state, @@ -127,18 +139,22 @@ export const settingsSlice = createSlice({ const keyItem = state.staging.kwm2.keys.find((item) => item.id === id); if (keyItem) { keyItem[field] = value; + state.dirty = true; } }, kwm2ClearKeys(state) { state.staging.kwm2.keys = []; + state.dirty = true; }, // discardStagingChanges: (state) => { + state.dirty = false; state.staging = productionToStaging(state.production); }, commitStagingChange: (state) => { const production = stagingToProduction(state.staging); return { + dirty: false, // Sync back to staging staging: productionToStaging(production), production, diff --git a/src/util/DatabaseKeyExtractor.ts b/src/util/DatabaseKeyExtractor.ts index 5158820..0a4b8e9 100644 --- a/src/util/DatabaseKeyExtractor.ts +++ b/src/util/DatabaseKeyExtractor.ts @@ -32,7 +32,12 @@ export class DatabaseKeyExtractor { return null; } - const keys = db.exec('select file_path, ekey from `audio_file_ekey_table`')[0].values; + const result = db.exec('select file_path, ekey from audio_file_ekey_table'); + if (result.length === 0) { + return []; + } + + const keys = result[0].values; return keys.map(([path, ekey]) => ({ // strip dir name name: getFileName(String(path)),