Compare commits

...

7 Commits

Author SHA1 Message Date
alger
e17941dfb0 feat: 调整迷你播放栏列表项高度,从52px更改为69px,以优化滚动体验 2025-04-06 21:11:17 +08:00
Alger
9aff694116 Merge pull request #119 from algerkong/feat/shortcut
 feat: 添加快捷键管理功能,支持全局和应用内快捷键的启用/禁用,优化快捷键配置界面
2025-04-05 20:36:13 +08:00
algerkong
c2983ba079 feat: 添加快捷键管理功能,支持全局和应用内快捷键的启用/禁用,优化快捷键配置界面
feat: #117
2025-04-05 20:33:34 +08:00
algerkong
541ff2b76c feat: 优化桌面歌词 歌词行动态样式计算,提升歌词显示效果
feat: #115
2025-04-04 22:46:23 +08:00
algerkong
55b50d764b feat: 优化歌词界面配置 2025-04-04 22:19:35 +08:00
algerkong
30ff7b2930 feat: 优化历史歌曲获取功能, 分离网易云音乐和B站视频处理逻辑 2025-04-04 21:16:27 +08:00
algerkong
a24f901d1d feat: 优化B站视频代理URL获取逻辑 2025-04-04 21:16:09 +08:00
19 changed files with 873 additions and 374 deletions

View File

@@ -183,7 +183,12 @@ export default {
dark: 'Dark'
},
hideMiniPlayBar: 'Hide Mini Play Bar',
hideLyrics: 'Hide Lyrics'
hideLyrics: 'Hide Lyrics',
tabs: {
interface: 'Interface',
display: 'Display',
typography: 'Typography'
}
},
shortcutSettings: {
title: 'Shortcut Settings',
@@ -192,6 +197,8 @@ export default {
shortcutConflict: 'Shortcut Conflict',
inputPlaceholder: 'Click to input shortcut',
resetShortcuts: 'Reset',
disableAll: 'Disable All',
enableAll: 'Enable All',
togglePlay: 'Play/Pause',
prevPlay: 'Previous',
nextPlay: 'Next',
@@ -199,12 +206,18 @@ export default {
volumeDown: 'Volume Down',
toggleFavorite: 'Favorite/Unfavorite',
toggleWindow: 'Show/Hide Window',
scopeGlobal: 'Global',
scopeApp: 'App Only',
enabled: 'Enabled',
disabled: 'Disabled',
messages: {
resetSuccess: 'Shortcuts reset successfully, please save',
conflict: 'Shortcut conflict, please reset',
saveSuccess: 'Shortcuts saved successfully',
saveError: 'Failed to save shortcuts',
cancelEdit: 'Edit cancelled'
cancelEdit: 'Edit cancelled',
disableAll: 'All shortcuts disabled, please save to apply',
enableAll: 'All shortcuts enabled, please save to apply'
}
}
};

View File

@@ -183,7 +183,12 @@ export default {
dark: '暗色'
},
hideMiniPlayBar: '隐藏迷你播放栏',
hideLyrics: '隐藏歌词'
hideLyrics: '隐藏歌词',
tabs: {
interface: '界面',
typography: '文字',
display: '显示'
}
},
shortcutSettings: {
title: '快捷键设置',
@@ -192,6 +197,8 @@ export default {
shortcutConflict: '快捷键冲突',
inputPlaceholder: '点击输入快捷键',
resetShortcuts: '恢复默认',
disableAll: '全部禁用',
enableAll: '全部启用',
togglePlay: '播放/暂停',
prevPlay: '上一首',
nextPlay: '下一首',
@@ -199,12 +206,18 @@ export default {
volumeDown: '音量减少',
toggleFavorite: '收藏/取消收藏',
toggleWindow: '显示/隐藏窗口',
scopeGlobal: '全局',
scopeApp: '应用内',
enabled: '启用',
disabled: '禁用',
messages: {
resetSuccess: '已恢复默认快捷键,请记得保存',
conflict: '存在冲突的快捷键,请重新设置',
saveSuccess: '快捷键设置已保存',
saveError: '保存快捷键失败,请重试',
cancelEdit: '已取消修改'
cancelEdit: '已取消修改',
disableAll: '已禁用所有快捷键,请记得保存',
enableAll: '已启用所有快捷键,请记得保存'
}
}
};

View File

@@ -7,66 +7,93 @@ ipcMain.on('get-platform', (event) => {
event.returnValue = process.platform;
});
// 定义快捷键配置接口
export interface ShortcutConfig {
key: string;
enabled: boolean;
scope: 'global' | 'app';
}
export interface ShortcutsConfig {
[key: string]: ShortcutConfig;
}
// 定义默认快捷键
export const defaultShortcuts = {
togglePlay: 'CommandOrControl+Alt+P',
prevPlay: 'CommandOrControl+Alt+Left',
nextPlay: 'CommandOrControl+Alt+Right',
volumeUp: 'CommandOrControl+Alt+Up',
volumeDown: 'CommandOrControl+Alt+Down',
toggleFavorite: 'CommandOrControl+Alt+L',
toggleWindow: 'CommandOrControl+Alt+Shift+M'
export const defaultShortcuts: ShortcutsConfig = {
togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' },
prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' },
nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' },
volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' },
volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' },
toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' },
toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' }
};
let mainWindowRef: Electron.BrowserWindow | null = null;
// 注册快捷键
export function registerShortcuts(mainWindow: Electron.BrowserWindow) {
export function registerShortcuts(
mainWindow: Electron.BrowserWindow,
shortcutsConfig?: ShortcutsConfig
) {
mainWindowRef = mainWindow;
const store = getStore();
const shortcuts = store.get('shortcuts');
const shortcuts =
shortcutsConfig || (store.get('shortcuts') as ShortcutsConfig) || defaultShortcuts;
// 注销所有已注册的快捷键
globalShortcut.unregisterAll();
// 显示/隐藏主窗口
globalShortcut.register(shortcuts.toggleWindow, () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
// 对旧格式数据进行兼容处理
if (shortcuts && typeof shortcuts.togglePlay === 'string') {
// 将 shortcuts 强制转换为 unknown再转为 Record<string, string>
const oldShortcuts = { ...shortcuts } as unknown as Record<string, string>;
const newShortcuts: ShortcutsConfig = {};
Object.entries(oldShortcuts).forEach(([key, value]) => {
newShortcuts[key] = {
key: value,
enabled: true,
scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global'
};
});
store.set('shortcuts', newShortcuts);
registerShortcuts(mainWindow, newShortcuts);
return;
}
// 注册全局快捷键
Object.entries(shortcuts).forEach(([action, config]) => {
const { key, enabled, scope } = config as ShortcutConfig;
// 只注册启用且作用域为全局的快捷键
if (!enabled || scope !== 'global') return;
try {
switch (action) {
case 'toggleWindow':
globalShortcut.register(key, () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
break;
default:
globalShortcut.register(key, () => {
mainWindow.webContents.send('global-shortcut', action);
});
break;
}
} catch (error) {
console.error(`注册快捷键 ${key} 失败:`, error);
}
});
// 播放/暂停
globalShortcut.register(shortcuts.togglePlay, () => {
mainWindow.webContents.send('global-shortcut', 'togglePlay');
});
// 上一首
globalShortcut.register(shortcuts.prevPlay, () => {
mainWindow.webContents.send('global-shortcut', 'prevPlay');
});
// 下一首
globalShortcut.register(shortcuts.nextPlay, () => {
mainWindow.webContents.send('global-shortcut', 'nextPlay');
});
// 音量增加
globalShortcut.register(shortcuts.volumeUp, () => {
mainWindow.webContents.send('global-shortcut', 'volumeUp');
});
// 音量减少
globalShortcut.register(shortcuts.volumeDown, () => {
mainWindow.webContents.send('global-shortcut', 'volumeDown');
});
// 收藏当前歌曲
globalShortcut.register(shortcuts.toggleFavorite, () => {
mainWindow.webContents.send('global-shortcut', 'toggleFavorite');
});
// 通知渲染进程更新应用内快捷键
mainWindow.webContents.send('update-app-shortcuts', shortcuts);
}
// 初始化快捷键
@@ -85,4 +112,11 @@ export function initializeShortcuts(mainWindow: Electron.BrowserWindow) {
registerShortcuts(mainWindowRef);
}
});
// 监听快捷键更新事件
ipcMain.on('update-shortcuts', (_, shortcutsConfig: ShortcutsConfig) => {
if (mainWindowRef) {
registerShortcuts(mainWindowRef, shortcutsConfig);
}
});
}

View File

@@ -25,6 +25,7 @@ import { isElectron, isLyricWindow } from '@/utils';
import { initAudioListeners } from './hooks/MusicHook';
import { isMobile } from './utils';
import { useAppShortcuts } from './utils/appShortcuts';
import { initShortcut } from './utils/shortcut';
const { locale } = useI18n();
@@ -101,6 +102,9 @@ if (isElectron) {
});
}
// 使用应用内快捷键
useAppShortcuts();
onMounted(async () => {
if (isLyricWindow.value) {
return;

View File

@@ -1,4 +1,5 @@
import type { IBilibiliPlayUrl, IBilibiliVideoDetail } from '@/types/bilibili';
import { getSetData, isElectron } from '@/utils';
import request from '@/utils/request';
interface ISearchParams {
@@ -121,10 +122,13 @@ export const getBilibiliPlayUrl = (
});
};
// `http://127.0.0.1:30666/bilibili/stream-proxy?url=${encodeURIComponent(`https:${item.pic}`)}`,
export const getBilibiliProxyUrl = (url: string) => {
const setData = getSetData();
const baseURL = isElectron
? `http://127.0.0.1:${setData?.musicApiPort}`
: import.meta.env.VITE_API;
const AUrl = url.startsWith('http') ? url : `https:${url}`;
return `${import.meta.env.VITE_API}/bilibili/stream-proxy?url=${encodeURIComponent(AUrl)}`;
return `${baseURL}/bilibili/stream-proxy?url=${encodeURIComponent(AUrl)}`;
};
export const getBilibiliAudioUrl = async (bvid: string, cid: number): Promise<string> => {

View File

@@ -17,6 +17,8 @@ declare module 'vue' {
NCarouselItem: typeof import('naive-ui')['NCarouselItem']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDrawer: typeof import('naive-ui')['NDrawer']

View File

@@ -2,94 +2,119 @@
<div class="settings-panel transparent-popover">
<div class="settings-title">{{ t('settings.lyricSettings.title') }}</div>
<div class="settings-content">
<div class="settings-item">
<span>{{ t('settings.lyricSettings.pureMode') }}</span>
<n-switch v-model:value="config.pureModeEnabled" />
</div>
<n-tabs type="line" animated size="small">
<!-- 显示设置 -->
<n-tab-pane :name="'display'" :tab="t('settings.lyricSettings.tabs.display')">
<div class="tab-content">
<div class="settings-grid">
<div class="settings-item">
<span>{{ t('settings.lyricSettings.pureMode') }}</span>
<n-switch v-model:value="config.pureModeEnabled" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideCover') }}</span>
<n-switch v-model:value="config.hideCover" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.centerDisplay') }}</span>
<n-switch v-model:value="config.centerLyrics" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.showTranslation') }}</span>
<n-switch v-model:value="config.showTranslation" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideLyrics') }}</span>
<n-switch v-model:value="config.hideLyrics" />
</div>
</div>
</div>
</n-tab-pane>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideCover') }}</span>
<n-switch v-model:value="config.hideCover" />
</div>
<!-- 界面设置 -->
<n-tab-pane :name="'interface'" :tab="t('settings.lyricSettings.tabs.interface')">
<div class="tab-content">
<div class="settings-grid">
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hidePlayBar') }}</span>
<n-switch v-model:value="config.hidePlayBar" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideMiniPlayBar') }}</span>
<n-switch v-model:value="config.hideMiniPlayBar" />
</div>
</div>
<div class="theme-section">
<div class="section-title">{{ t('settings.lyricSettings.backgroundTheme') }}</div>
<n-radio-group v-model:value="config.theme" name="theme" class="theme-radio-group">
<n-space>
<n-radio value="default">{{
t('settings.lyricSettings.themeOptions.default')
}}</n-radio>
<n-radio value="light">{{
t('settings.lyricSettings.themeOptions.light')
}}</n-radio>
<n-radio value="dark">{{
t('settings.lyricSettings.themeOptions.dark')
}}</n-radio>
</n-space>
</n-radio-group>
</div>
</div>
</n-tab-pane>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.centerDisplay') }}</span>
<n-switch v-model:value="config.centerLyrics" />
</div>
<!-- 文字设置 -->
<n-tab-pane :name="'typography'" :tab="t('settings.lyricSettings.tabs.typography')">
<div class="tab-content">
<div class="slider-section">
<div class="slider-item">
<span>{{ t('settings.lyricSettings.fontSize') }}</span>
<n-slider
v-model:value="config.fontSize"
:step="1"
:min="12"
:max="32"
:marks="{
12: t('settings.lyricSettings.fontSizeMarks.small'),
22: t('settings.lyricSettings.fontSizeMarks.medium'),
32: t('settings.lyricSettings.fontSizeMarks.large')
}"
/>
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.showTranslation') }}</span>
<n-switch v-model:value="config.showTranslation" />
</div>
<div class="slider-item">
<span>{{ t('settings.lyricSettings.letterSpacing') }}</span>
<n-slider
v-model:value="config.letterSpacing"
:step="0.2"
:min="-2"
:max="10"
:marks="{
'-2': t('settings.lyricSettings.letterSpacingMarks.compact'),
0: t('settings.lyricSettings.letterSpacingMarks.default'),
10: t('settings.lyricSettings.letterSpacingMarks.loose')
}"
/>
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hidePlayBar') }}</span>
<n-switch v-model:value="config.hidePlayBar" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideMiniPlayBar') }}</span>
<n-switch v-model:value="config.hideMiniPlayBar" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideLyrics') }}</span>
<n-switch v-model:value="config.hideLyrics" />
</div>
<div class="settings-slider">
<span>{{ t('settings.lyricSettings.fontSize') }}</span>
<n-slider
v-model:value="config.fontSize"
:step="1"
:min="12"
:max="32"
:marks="{
12: t('settings.lyricSettings.fontSizeMarks.small'),
22: t('settings.lyricSettings.fontSizeMarks.medium'),
32: t('settings.lyricSettings.fontSizeMarks.large')
}"
/>
</div>
<div class="settings-slider">
<span>{{ t('settings.lyricSettings.letterSpacing') }}</span>
<n-slider
v-model:value="config.letterSpacing"
:step="0.2"
:min="-2"
:max="10"
:marks="{
'-2': t('settings.lyricSettings.letterSpacingMarks.compact'),
0: t('settings.lyricSettings.letterSpacingMarks.default'),
10: t('settings.lyricSettings.letterSpacingMarks.loose')
}"
/>
</div>
<div class="settings-slider">
<span>{{ t('settings.lyricSettings.lineHeight') }}</span>
<n-slider
v-model:value="config.lineHeight"
:step="0.1"
:min="1"
:max="3"
:marks="{
1: t('settings.lyricSettings.lineHeightMarks.compact'),
1.5: t('settings.lyricSettings.lineHeightMarks.default'),
3: t('settings.lyricSettings.lineHeightMarks.loose')
}"
/>
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.backgroundTheme') }}</span>
<n-radio-group v-model:value="config.theme" name="theme">
<n-radio value="default">{{ t('settings.lyricSettings.themeOptions.default') }}</n-radio>
<n-radio value="light">{{ t('settings.lyricSettings.themeOptions.light') }}</n-radio>
<n-radio value="dark">{{ t('settings.lyricSettings.themeOptions.dark') }}</n-radio>
</n-radio-group>
</div>
<div class="slider-item">
<span>{{ t('settings.lyricSettings.lineHeight') }}</span>
<n-slider
v-model:value="config.lineHeight"
:step="0.1"
:min="1"
:max="3"
:marks="{
1: t('settings.lyricSettings.lineHeightMarks.compact'),
1.5: t('settings.lyricSettings.lineHeightMarks.default'),
3: t('settings.lyricSettings.lineHeightMarks.loose')
}"
/>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
</div>
</div>
</template>
@@ -98,39 +123,12 @@
import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
const { t } = useI18n();
interface LyricConfig {
hideCover: boolean;
centerLyrics: boolean;
fontSize: number;
letterSpacing: number;
lineHeight: number;
showTranslation: boolean;
theme: 'default' | 'light' | 'dark';
hidePlayBar: boolean;
hideMiniPlayBar: boolean;
pureModeEnabled: boolean;
hideLyrics: boolean;
}
const config = ref<LyricConfig>({
hideCover: false,
centerLyrics: false,
fontSize: 22,
letterSpacing: 0,
lineHeight: 2,
showTranslation: true,
theme: 'default',
hidePlayBar: false,
hideMiniPlayBar: false,
pureModeEnabled: false,
hideLyrics: false
});
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
const emit = defineEmits(['themeChange']);
// 监听配置变化并保存到本地存储
watch(
() => config.value,
(newConfig) => {
@@ -139,7 +137,6 @@ watch(
{ deep: true }
);
// 监听主题变化
watch(
() => config.value.theme,
(newTheme) => {
@@ -147,14 +144,12 @@ watch(
}
);
// 更新 CSS 变量
const updateCSSVariables = (config: LyricConfig) => {
document.documentElement.style.setProperty('--lyric-font-size', `${config.fontSize}px`);
document.documentElement.style.setProperty('--lyric-letter-spacing', `${config.letterSpacing}px`);
document.documentElement.style.setProperty('--lyric-line-height', config.lineHeight.toString());
};
// 加载保存的配置
onMounted(() => {
const savedConfig = localStorage.getItem('music-full-config');
if (savedConfig) {
@@ -170,14 +165,50 @@ defineExpose({
<style scoped lang="scss">
.settings-panel {
@apply p-4 w-72 rounded-lg relative overflow-hidden backdrop-blur-lg bg-black/10;
@apply p-4 w-80 rounded-lg relative overflow-hidden backdrop-blur-lg bg-black/10;
.settings-title {
@apply text-base font-bold mb-4;
color: var(--text-color-active);
}
.settings-content {
@apply space-y-4;
:deep(.n-tabs-nav) {
@apply mb-3;
}
:deep(.n-tab-pane) {
@apply p-0;
}
:deep(.n-tabs-tab) {
@apply text-xs;
color: var(--text-color-primary);
&.n-tabs-tab--active {
color: var(--text-color-active);
}
}
:deep(.n-tabs-tab-wrapper) {
@apply pb-0;
}
:deep(.n-tabs-pane-wrapper) {
@apply px-2;
}
:deep(.n-tabs-bar) {
background-color: var(--text-color-active);
}
}
.tab-content {
@apply py-2;
}
.settings-grid {
@apply grid grid-cols-1 gap-3;
}
.settings-item {
@@ -188,22 +219,38 @@ defineExpose({
}
}
.settings-slider {
@apply space-y-2;
@apply mb-10 !important;
.section-title {
@apply text-sm font-medium mb-2;
color: var(--text-color-primary);
}
.theme-section {
@apply mt-4;
}
.slider-section {
@apply space-y-6;
}
.slider-item {
@apply space-y-2 mb-10 !important;
span {
@apply text-sm;
color: var(--text-color-primary);
}
}
.theme-radio-group {
@apply flex;
}
}
// 修改 slider 字体颜色
:deep(.n-slider-mark) {
color: var(--text-color-primary) !important;
}
// 修改 radio 字体颜色
:deep(.n-radio__label) {
color: var(--text-color-active) !important;
@apply text-xs;
}
</style>

View File

@@ -253,7 +253,7 @@ const togglePlaylist = () => {
const scrollToPlayList = () => {
setTimeout(() => {
const currentIndex = playerStore.playListIndex;
const itemHeight = 52; // 每个列表项的高度
const itemHeight = 69; // 每个列表项的高度
palyListRef.value?.scrollTo({
top: currentIndex * itemHeight,
behavior: 'smooth'
@@ -585,4 +585,16 @@ const setMusicFull = () => {
.playlist-items {
padding: 4px 0;
}
.dark {
.song-info {
.song-title {
color: var(--text-color-1, #fff);
}
.song-artist {
color: var(--text-color-2, #fff);
}
}
}
</style>

View File

@@ -16,24 +16,54 @@
<div class="shortcut-info">
<span class="shortcut-label">{{ getShortcutLabel(key) }}</span>
</div>
<div class="shortcut-input">
<n-input
:value="formatShortcut(shortcut)"
:status="duplicateKeys[key] ? 'error' : undefined"
:placeholder="t('settings.shortcutSettings.inputPlaceholder')"
readonly
@keydown="(e) => handleKeyDown(e, key)"
@focus="() => startRecording(key)"
@blur="stopRecording"
/>
<n-tooltip v-if="duplicateKeys[key]" trigger="hover">
<template #trigger>
<n-icon class="error-icon" size="18">
<i class="ri-error-warning-line"></i>
</n-icon>
</template>
{{ t('settings.shortcutSettings.shortcutConflict') }}
</n-tooltip>
<div class="shortcut-controls">
<div class="shortcut-input">
<n-input
:value="formatShortcut(shortcut.key)"
:status="duplicateKeys[key] ? 'error' : undefined"
:placeholder="t('settings.shortcutSettings.inputPlaceholder')"
:disabled="!shortcut.enabled"
readonly
@keydown="(e) => handleKeyDown(e, key)"
@focus="() => startRecording(key)"
@blur="stopRecording"
/>
<n-tooltip v-if="duplicateKeys[key]" trigger="hover">
<template #trigger>
<n-icon class="error-icon" size="18">
<i class="ri-error-warning-line"></i>
</n-icon>
</template>
{{ t('settings.shortcutSettings.shortcutConflict') }}
</n-tooltip>
</div>
<div class="shortcut-options">
<n-tooltip trigger="hover">
<template #trigger>
<n-switch v-model:value="shortcut.enabled" size="small" />
</template>
{{
shortcut.enabled
? t('settings.shortcutSettings.enabled')
: t('settings.shortcutSettings.disabled')
}}
</n-tooltip>
<n-tooltip v-if="shortcut.enabled" trigger="hover">
<template #trigger>
<n-select
v-model:value="shortcut.scope"
:options="scopeOptions"
size="small"
style="width: 100px"
/>
</template>
{{
shortcut.scope === 'global'
? t('settings.shortcutSettings.scopeGlobal')
: t('settings.shortcutSettings.scopeApp')
}}
</n-tooltip>
</div>
</div>
</div>
</n-space>
@@ -46,6 +76,12 @@
<n-button size="small" @click="resetShortcuts">{{
t('settings.shortcutSettings.resetShortcuts')
}}</n-button>
<n-button size="small" type="warning" @click="disableAllShortcuts">{{
t('settings.shortcutSettings.disableAll')
}}</n-button>
<n-button size="small" type="success" @click="enableAllShortcuts">{{
t('settings.shortcutSettings.enableAll')
}}</n-button>
<n-button type="primary" size="small" :disabled="hasConflict" @click="handleSave">
{{ t('common.save') }}
</n-button>
@@ -66,26 +102,37 @@ import { isElectron } from '@/utils';
const { t } = useI18n();
interface ShortcutConfig {
key: string;
enabled: boolean;
scope: 'global' | 'app';
}
interface Shortcuts {
togglePlay: string;
prevPlay: string;
nextPlay: string;
volumeUp: string;
volumeDown: string;
toggleFavorite: string;
toggleWindow: string;
togglePlay: ShortcutConfig;
prevPlay: ShortcutConfig;
nextPlay: ShortcutConfig;
volumeUp: ShortcutConfig;
volumeDown: ShortcutConfig;
toggleFavorite: ShortcutConfig;
toggleWindow: ShortcutConfig;
}
const defaultShortcuts: Shortcuts = {
togglePlay: 'CommandOrControl+Alt+P',
prevPlay: 'Alt+Left',
nextPlay: 'Alt+Right',
volumeUp: 'Alt+Up',
volumeDown: 'Alt+Down',
toggleFavorite: 'CommandOrControl+Alt+L',
toggleWindow: 'CommandOrControl+Alt+Shift+M'
togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' },
prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' },
nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' },
volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' },
volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' },
toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' },
toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' }
};
const scopeOptions = [
{ label: t('settings.shortcutSettings.scopeGlobal'), value: 'global' },
{ label: t('settings.shortcutSettings.scopeApp'), value: 'app' }
];
const shortcuts = ref<Shortcuts>(
isElectron
? window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts') || defaultShortcuts
@@ -93,7 +140,7 @@ const shortcuts = ref<Shortcuts>(
);
// 临时存储编辑中的快捷键
const tempShortcuts = ref<Shortcuts>({ ...shortcuts.value });
const tempShortcuts = ref<Shortcuts>(cloneDeep(shortcuts.value));
// 监听快捷键更新
if (isElectron) {
@@ -101,7 +148,7 @@ if (isElectron) {
const newShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
if (newShortcuts) {
shortcuts.value = newShortcuts;
tempShortcuts.value = { ...newShortcuts };
tempShortcuts.value = cloneDeep(newShortcuts);
}
});
}
@@ -116,12 +163,27 @@ onMounted(() => {
console.log('storedShortcuts', storedShortcuts);
if (storedShortcuts) {
shortcuts.value = storedShortcuts;
tempShortcuts.value = { ...storedShortcuts };
tempShortcuts.value = cloneDeep(storedShortcuts);
} else {
shortcuts.value = { ...defaultShortcuts };
tempShortcuts.value = { ...defaultShortcuts };
tempShortcuts.value = cloneDeep(defaultShortcuts);
window.electron.ipcRenderer.send('set-store-value', 'shortcuts', defaultShortcuts);
}
// 转换旧格式的快捷键数据到新格式
if (storedShortcuts && typeof storedShortcuts.togglePlay === 'string') {
const convertedShortcuts = {} as Shortcuts;
Object.entries(storedShortcuts).forEach(([key, value]) => {
convertedShortcuts[key as keyof Shortcuts] = {
key: value as string,
enabled: true,
scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global'
};
});
shortcuts.value = convertedShortcuts;
tempShortcuts.value = cloneDeep(convertedShortcuts);
window.electron.ipcRenderer.send('set-store-value', 'shortcuts', convertedShortcuts);
}
}
});
@@ -144,13 +206,21 @@ const message = useMessage();
// 检查快捷键冲突
const duplicateKeys = computed(() => {
const result: Record<string, boolean> = {};
const usedShortcuts = new Set<string>();
const usedShortcuts = new Map<string, string>();
Object.entries(tempShortcuts.value).forEach(([key, shortcut]) => {
if (usedShortcuts.has(shortcut)) {
result[key] = true;
// 只检查启用的快捷键
if (!shortcut.enabled) return;
const conflictKey = usedShortcuts.get(shortcut.key);
if (conflictKey) {
// 只有相同作用域的快捷键才会被认为冲突
const conflictScope = tempShortcuts.value[conflictKey as keyof Shortcuts].scope;
if (shortcut.scope === conflictScope) {
result[key] = true;
}
} else {
usedShortcuts.add(shortcut);
usedShortcuts.set(shortcut.key, key);
}
});
@@ -161,6 +231,8 @@ const duplicateKeys = computed(() => {
const hasConflict = computed(() => Object.keys(duplicateKeys.value).length > 0);
const startRecording = (key: keyof Shortcuts) => {
if (!tempShortcuts.value[key].enabled) return;
isRecording.value = true;
currentKey.value = key;
// 禁用全局快捷键
@@ -220,12 +292,12 @@ const handleKeyDown = (e: KeyboardEvent, key: keyof Shortcuts) => {
}
if (!['Control', 'Alt', 'Shift', 'Meta', 'Command'].includes(keyName)) {
tempShortcuts.value[key] = [...modifiers, keyName].join('+');
tempShortcuts.value[key].key = [...modifiers, keyName].join('+');
}
};
const resetShortcuts = () => {
tempShortcuts.value = { ...defaultShortcuts };
tempShortcuts.value = cloneDeep(defaultShortcuts);
message.success(t('settings.shortcutSettings.messages.resetSuccess'));
};
@@ -245,7 +317,7 @@ const saveShortcuts = () => {
// 先保存到 store
window.electron.ipcRenderer.send('set-store-value', 'shortcuts', shortcutsToSave);
// 然后更新快捷键
window.electron.ipcRenderer.send('update-shortcuts');
window.electron.ipcRenderer.send('update-shortcuts', shortcutsToSave);
message.success(t('settings.shortcutSettings.messages.saveSuccess'));
} catch (error) {
console.error('保存快捷键失败:', error);
@@ -255,7 +327,7 @@ const saveShortcuts = () => {
};
const cancelEdit = () => {
tempShortcuts.value = { ...shortcuts.value };
tempShortcuts.value = cloneDeep(shortcuts.value);
message.info(t('settings.shortcutSettings.messages.cancelEdit'));
emit('update:show', false);
};
@@ -309,7 +381,7 @@ watch(visible, (newVal) => {
// 处理弹窗关闭后的事件
const handleAfterLeave = () => {
// 重置临时数据
tempShortcuts.value = { ...shortcuts.value };
tempShortcuts.value = cloneDeep(shortcuts.value);
};
// 处理取消按钮点击
@@ -324,6 +396,22 @@ const handleSave = () => {
visible.value = false;
emit('change', shortcuts.value);
};
// 全部禁用快捷键
const disableAllShortcuts = () => {
Object.keys(tempShortcuts.value).forEach((key) => {
tempShortcuts.value[key as keyof Shortcuts].enabled = false;
});
message.info(t('settings.shortcutSettings.messages.disableAll'));
};
// 全部启用快捷键
const enableAllShortcuts = () => {
Object.keys(tempShortcuts.value).forEach((key) => {
tempShortcuts.value[key as keyof Shortcuts].enabled = true;
});
message.info(t('settings.shortcutSettings.messages.enableAll'));
};
</script>
<style lang="scss" scoped>
@@ -359,25 +447,32 @@ const handleSave = () => {
}
.shortcut-info {
@apply flex flex-col;
@apply flex flex-col min-w-[150px];
.shortcut-label {
@apply text-base font-medium;
}
}
.shortcut-input {
@apply flex items-center gap-2;
min-width: 200px;
.shortcut-controls {
@apply flex items-center gap-3 flex-1;
:deep(.n-input) {
.n-input__input-el {
@apply text-center font-mono;
.shortcut-input {
@apply flex items-center gap-2 flex-1;
:deep(.n-input) {
.n-input__input-el {
@apply text-center font-mono;
}
}
.error-icon {
@apply text-red-500;
}
}
.error-icon {
@apply text-red-500;
.shortcut-options {
@apply flex items-center gap-2;
}
}
}

View File

@@ -29,6 +29,7 @@
</n-popover>
<div
v-if="!config.hideCover"
class="music-img"
:class="{ 'only-cover': config.hideLyrics }"
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
@@ -74,7 +75,7 @@
<div
class="music-content"
:class="{
center: config.centerLyrics && config.hideCover,
center: config.centerLyrics,
hide: config.hideLyrics
}"
>
@@ -159,6 +160,7 @@ import {
import { useArtist } from '@/hooks/useArtist';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
import { getImgUrl, isMobile } from '@/utils';
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
@@ -173,34 +175,8 @@ const isDark = ref(false);
const showStickyHeader = ref(false);
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
interface LyricConfig {
hideCover: boolean;
centerLyrics: boolean;
fontSize: number;
letterSpacing: number;
lineHeight: number;
showTranslation: boolean;
theme: 'default' | 'light' | 'dark';
hidePlayBar: boolean;
pureModeEnabled: boolean;
hideMiniPlayBar: boolean;
hideLyrics: boolean;
}
// 移除 computed 配置
const config = ref<LyricConfig>({
hideCover: false,
centerLyrics: false,
fontSize: 22,
letterSpacing: 0,
lineHeight: 1.5,
showTranslation: true,
theme: 'default',
hidePlayBar: false,
pureModeEnabled: false,
hideMiniPlayBar: false,
hideLyrics: false
});
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
// 监听设置组件的配置变化
watch(
@@ -617,7 +593,7 @@ defineExpose({
transition: all 0.3s ease;
&.center {
@apply w-full;
@apply w-auto;
.music-lrc {
@apply w-full max-w-3xl mx-auto;
}

View File

@@ -10,6 +10,7 @@ import pinia from '@/store';
import App from './App.vue';
import directives from './directive';
import { initAppShortcuts } from './utils/appShortcuts';
const app = createApp(App);
@@ -21,3 +22,6 @@ app.use(pinia);
app.use(router);
app.use(i18n);
app.mount('#app');
// 初始化应用内快捷键
initAppShortcuts();

View File

@@ -0,0 +1,27 @@
export interface LyricConfig {
hideCover: boolean;
centerLyrics: boolean;
fontSize: number;
letterSpacing: number;
lineHeight: number;
showTranslation: boolean;
theme: 'default' | 'light' | 'dark';
hidePlayBar: boolean;
pureModeEnabled: boolean;
hideMiniPlayBar: boolean;
hideLyrics: boolean;
}
export const DEFAULT_LYRIC_CONFIG: LyricConfig = {
hideCover: false,
centerLyrics: false,
fontSize: 22,
letterSpacing: 0,
lineHeight: 2,
showTranslation: true,
theme: 'default',
hidePlayBar: false,
hideMiniPlayBar: true,
pureModeEnabled: false,
hideLyrics: false
};

View File

@@ -0,0 +1,210 @@
import { onMounted, onUnmounted } from 'vue';
import i18n from '@/../i18n/renderer';
import { audioService } from '@/services/audioService';
import { usePlayerStore, useSettingsStore } from '@/store';
import { isElectron } from '.';
import { showShortcutToast } from './shortcutToast';
interface ShortcutConfig {
key: string;
enabled: boolean;
scope: 'global' | 'app';
}
interface ShortcutsConfig {
[key: string]: ShortcutConfig;
}
const { t } = i18n.global;
// 全局存储快捷键配置
let appShortcuts: ShortcutsConfig = {};
/**
* 处理快捷键动作
* @param action 快捷键动作
*/
export async function handleShortcutAction(action: string) {
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const currentSound = audioService.getCurrentSound();
const showToast = (message: string, iconName: string) => {
if (settingsStore.isMiniMode) {
return;
}
showShortcutToast(message, iconName);
};
switch (action) {
case 'togglePlay':
if (playerStore.play) {
await audioService.pause();
showToast(t('player.playBar.pause'), 'ri-pause-circle-line');
} else {
await audioService.play();
showToast(t('player.playBar.play'), 'ri-play-circle-line');
}
break;
case 'prevPlay':
playerStore.prevPlay();
showToast(t('player.playBar.prev'), 'ri-skip-back-line');
break;
case 'nextPlay':
playerStore.nextPlay();
showToast(t('player.playBar.next'), 'ri-skip-forward-line');
break;
case 'volumeUp':
if (currentSound && currentSound?.volume() < 1) {
currentSound?.volume((currentSound?.volume() || 0) + 0.1);
showToast(
`${t('player.playBar.volume')}${Math.round((currentSound?.volume() || 0) * 100)}%`,
'ri-volume-up-line'
);
}
break;
case 'volumeDown':
if (currentSound && currentSound?.volume() > 0) {
currentSound?.volume((currentSound?.volume() || 0) - 0.1);
showToast(
`${t('player.playBar.volume')}${Math.round((currentSound?.volume() || 0) * 100)}%`,
'ri-volume-down-line'
);
}
break;
case 'toggleFavorite': {
const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id));
const numericId = Number(playerStore.playMusic.id);
if (isFavorite) {
playerStore.removeFromFavorite(numericId);
} else {
playerStore.addToFavorite(numericId);
}
showToast(
isFavorite
? t('player.playBar.favorite', { name: playerStore.playMusic.name })
: t('player.playBar.unFavorite', { name: playerStore.playMusic.name }),
isFavorite ? 'ri-heart-fill' : 'ri-heart-line'
);
break;
}
default:
console.log('未知的快捷键动作:', action);
break;
}
}
/**
* 检查按键是否匹配快捷键
* @param e KeyboardEvent
* @param shortcutKey 快捷键字符串
* @returns 是否匹配
*/
function matchShortcut(e: KeyboardEvent, shortcutKey: string): boolean {
const keys = shortcutKey.split('+');
const pressedKey = e.key.length === 1 ? e.key.toUpperCase() : e.key;
// 检查修饰键
const hasCommandOrControl = keys.includes('CommandOrControl');
const hasAlt = keys.includes('Alt');
const hasShift = keys.includes('Shift');
// 检查主键
let mainKey = keys.find((k) => !['CommandOrControl', 'Alt', 'Shift'].includes(k));
if (!mainKey) return false;
// 处理特殊键
if (mainKey === 'Left' && pressedKey === 'ArrowLeft') mainKey = 'ArrowLeft';
if (mainKey === 'Right' && pressedKey === 'ArrowRight') mainKey = 'ArrowRight';
if (mainKey === 'Up' && pressedKey === 'ArrowUp') mainKey = 'ArrowUp';
if (mainKey === 'Down' && pressedKey === 'ArrowDown') mainKey = 'ArrowDown';
// 检查是否所有条件都匹配
return (
hasCommandOrControl === (e.ctrlKey || e.metaKey) &&
hasAlt === e.altKey &&
hasShift === e.shiftKey &&
mainKey === pressedKey
);
}
/**
* 全局键盘事件处理函数
* @param e KeyboardEvent
*/
function handleKeyDown(e: KeyboardEvent) {
// 如果在输入框中则不处理快捷键
if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
return;
}
Object.entries(appShortcuts).forEach(([action, config]) => {
if (config.enabled && config.scope === 'app' && matchShortcut(e, config.key)) {
e.preventDefault();
handleShortcutAction(action);
}
});
}
/**
* 更新应用内快捷键
* @param shortcuts 快捷键配置
*/
export function updateAppShortcuts(shortcuts: ShortcutsConfig) {
appShortcuts = shortcuts;
}
/**
* 初始化应用内快捷键
*/
export function initAppShortcuts() {
if (isElectron) {
// 监听全局快捷键事件
window.electron.ipcRenderer.on('global-shortcut', async (_, action: string) => {
handleShortcutAction(action);
});
// 监听应用内快捷键更新
window.electron.ipcRenderer.on('update-app-shortcuts', (_, shortcuts: ShortcutsConfig) => {
updateAppShortcuts(shortcuts);
});
// 获取初始快捷键配置
const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
if (storedShortcuts) {
updateAppShortcuts(storedShortcuts);
}
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyDown);
}
}
/**
* 清理应用内快捷键
*/
export function cleanupAppShortcuts() {
if (isElectron) {
// 移除全局事件监听
window.electron.ipcRenderer.removeAllListeners('global-shortcut');
window.electron.ipcRenderer.removeAllListeners('update-app-shortcuts');
// 移除键盘事件监听
document.removeEventListener('keydown', handleKeyDown);
}
}
/**
* 使用应用内快捷键的组合函数
*/
export function useAppShortcuts() {
onMounted(() => {
initAppShortcuts();
});
onUnmounted(() => {
cleanupAppShortcuts();
});
}

View File

@@ -101,3 +101,14 @@ export const isElectron = (window as any).electron !== undefined;
export const isLyricWindow = computed(() => {
return window.location.hash.includes('lyric');
});
export const getSetData = (): any => {
let setData = null;
if (window.electron) {
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
} else {
const settingsStore = useSettingsStore();
setData = settingsStore.setData;
}
return setData;
};

View File

@@ -1,20 +1,10 @@
import axios, { InternalAxiosRequestConfig } from 'axios';
import { useSettingsStore } from '@/store/modules/settings';
import { useUserStore } from '@/store/modules/user';
import { isElectron } from '.';
import { getSetData, isElectron } from '.';
let setData: any = null;
const getSetData = () => {
if (window.electron) {
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
} else {
const settingsStore = useSettingsStore();
setData = settingsStore.setData;
}
return setData;
};
// 扩展请求配置接口
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
@@ -39,7 +29,7 @@ const RETRY_DELAY = 500;
// 请求拦截器
request.interceptors.request.use(
(config: CustomAxiosRequestConfig) => {
getSetData();
setData = getSetData();
config.baseURL = window.electron
? `http://127.0.0.1:${setData?.musicApiPort}`
: import.meta.env.VITE_API;

View File

@@ -1,81 +1,10 @@
import i18n from '@/../i18n/renderer';
import { audioService } from '@/services/audioService';
import { usePlayerStore, useSettingsStore } from '@/store';
import { isElectron } from '.';
import { showShortcutToast } from './shortcutToast';
const { t } = i18n.global;
import { handleShortcutAction } from './appShortcuts';
export function initShortcut() {
if (isElectron) {
window.electron.ipcRenderer.on('global-shortcut', async (_, action: string) => {
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const currentSound = audioService.getCurrentSound();
const showToast = (message: string, iconName: string) => {
if (settingsStore.isMiniMode) {
return;
}
showShortcutToast(message, iconName);
};
switch (action) {
case 'togglePlay':
if (playerStore.play) {
await audioService.pause();
showToast(t('player.playBar.pause'), 'ri-pause-circle-line');
} else {
await audioService.play();
showToast(t('player.playBar.play'), 'ri-play-circle-line');
}
break;
case 'prevPlay':
playerStore.prevPlay();
showToast(t('player.playBar.prev'), 'ri-skip-back-line');
break;
case 'nextPlay':
playerStore.nextPlay();
showToast(t('player.playBar.next'), 'ri-skip-forward-line');
break;
case 'volumeUp':
if (currentSound && currentSound?.volume() < 1) {
currentSound?.volume((currentSound?.volume() || 0) + 0.1);
showToast(
`${t('player.playBar.volume')}${Math.round((currentSound?.volume() || 0) * 100)}%`,
'ri-volume-up-line'
);
}
break;
case 'volumeDown':
if (currentSound && currentSound?.volume() > 0) {
currentSound?.volume((currentSound?.volume() || 0) - 0.1);
showToast(
`${t('player.playBar.volume')}${Math.round((currentSound?.volume() || 0) * 100)}%`,
'ri-volume-down-line'
);
}
break;
case 'toggleFavorite': {
const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id));
const numericId = Number(playerStore.playMusic.id);
if (isFavorite) {
playerStore.removeFromFavorite(numericId);
} else {
playerStore.addToFavorite(numericId);
}
showToast(
isFavorite
? t('player.playBar.favorite', { name: playerStore.playMusic.name })
: t('player.playBar.unFavorite', { name: playerStore.playMusic.name }),
isFavorite ? 'ri-heart-fill' : 'ri-heart-line'
);
break;
}
default:
console.log('未知的快捷键动作:', action);
break;
}
handleShortcutAction(action);
});
}
}

View File

@@ -35,6 +35,7 @@
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { getBilibiliProxyUrl, getBilibiliVideoDetail } from '@/api/bilibili';
import { getMusicDetail } from '@/api/music';
import SongItem from '@/components/common/SongItem.vue';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
@@ -71,27 +72,89 @@ const getHistorySongs = async () => {
const endIndex = startIndex + pageSize;
const currentPageItems = musicList.value.slice(startIndex, endIndex);
const currentIds = currentPageItems.map((item) => item.id as number);
const res = await getMusicDetail(currentIds);
// 分离网易云音乐和B站视频
const neteaseItems = currentPageItems.filter((item) => item.source !== 'bilibili');
const bilibiliItems = currentPageItems.filter((item) => item.source === 'bilibili');
if (res.data.songs) {
const newSongs = res.data.songs.map((song: SongResult) => {
const historyItem = currentPageItems.find((item) => item.id === song.id);
return {
...song,
picUrl: song.al?.picUrl || '',
count: historyItem?.count || 0
};
});
if (currentPage.value === 1) {
displayList.value = newSongs;
} else {
displayList.value = [...displayList.value, ...newSongs];
// 处理网易云音乐
let neteaseSongs: SongResult[] = [];
if (neteaseItems.length > 0) {
const currentIds = neteaseItems.map((item) => item.id as number);
const res = await getMusicDetail(currentIds);
if (res.data.songs) {
neteaseSongs = res.data.songs.map((song: SongResult) => {
const historyItem = neteaseItems.find((item) => item.id === song.id);
return {
...song,
picUrl: song.al?.picUrl || '',
count: historyItem?.count || 0,
source: 'netease'
};
});
}
noMore.value = displayList.value.length >= musicList.value.length;
}
// 处理B站视频
const bilibiliSongs: SongResult[] = [];
for (const item of bilibiliItems) {
try {
const bvid = item.bilibiliData?.bvid;
if (!bvid) continue;
const res = await getBilibiliVideoDetail(bvid);
const videoDetail = res.data;
// 找到对应的分P
const page = videoDetail.pages.find((p) => p.cid === item.bilibiliData?.cid);
if (!page) continue;
bilibiliSongs.push({
id: `${videoDetail.aid}--${page.cid}`,
name: `${page.part || ''} - ${videoDetail.title}`,
picUrl: getBilibiliProxyUrl(videoDetail.pic),
ar: [
{
name: videoDetail.owner.name,
id: videoDetail.owner.mid
}
],
al: {
name: videoDetail.title,
picUrl: getBilibiliProxyUrl(videoDetail.pic)
},
source: 'bilibili',
count: item.count || 0,
bilibiliData: {
bvid,
cid: page.cid
}
} as SongResult);
} catch (error) {
console.error('获取B站视频详情失败:', error);
}
}
// 合并两种来源的数据,并保持原有顺序
const newSongs = currentPageItems
.map((item) => {
if (item.source === 'bilibili') {
return bilibiliSongs.find(
(song) =>
song.bilibiliData?.bvid === item.bilibiliData?.bvid &&
song.bilibiliData?.cid === item.bilibiliData?.cid
);
}
return neteaseSongs.find((song) => song.id === item.id);
})
.filter((song): song is SongResult => !!song);
if (currentPage.value === 1) {
displayList.value = newSongs;
} else {
displayList.value = [...displayList.value, ...newSongs];
}
noMore.value = displayList.value.length >= musicList.value.length;
} catch (error) {
console.error(t('history.getHistoryFailed'), error);
} finally {

View File

@@ -59,7 +59,7 @@
v-for="(line, index) in staticData.lrcArray"
:key="index"
class="lyric-line"
:style="lyricLineStyle"
:style="getDynamicLineStyle(line)"
:class="{
'lyric-line-current': index === currentIndex,
'lyric-line-passed': index < currentIndex,
@@ -207,16 +207,41 @@ const wrapperStyle = computed(() => {
// 计算容器中心点
const containerCenter = containerHeight.value / 2;
// 计算当前行到顶部的距离包含padding
const currentLineTop =
currentIndex.value * lineHeight.value + containerHeight.value * 0.2 + lineHeight.value; // 加上顶部padding
// 计算每行的实际高度
const getLineHeight = (line: { text: string; trText: string }) => {
const baseHeight = lineHeight.value;
if (line.trText) {
const extraHeight = Math.round(fontSize.value * 0.6 * 1.4);
return baseHeight + extraHeight;
}
return baseHeight;
};
// 计算当前行之前所有行的累积高度
let accumulatedHeight = containerHeight.value * 0.2; // 顶部padding
for (let i = 0; i < currentIndex.value; i++) {
if (i < staticData.value.lrcArray.length) {
accumulatedHeight += getLineHeight(staticData.value.lrcArray[i]);
} else {
accumulatedHeight += lineHeight.value;
}
}
// 加上当前行的一半高度,使其居中
const currentLineHeight =
currentIndex.value < staticData.value.lrcArray.length
? getLineHeight(staticData.value.lrcArray[currentIndex.value])
: lineHeight.value;
accumulatedHeight += currentLineHeight;
// 计算偏移量,使当前行居中
const targetOffset = containerCenter - currentLineTop;
const targetOffset = containerCenter - accumulatedHeight;
// 计算内容总高度包含padding
const contentHeight =
staticData.value.lrcArray.length * lineHeight.value + containerHeight.value * 0.4; // 上下padding各20vh
let contentHeight = containerHeight.value * 0.4; // 上下padding总和
for (const line of staticData.value.lrcArray) {
contentHeight += getLineHeight(line);
}
// 计算最小和最大偏移量
const minOffset = -(contentHeight - containerHeight.value);
@@ -231,9 +256,25 @@ const wrapperStyle = computed(() => {
};
});
const lyricLineStyle = computed(() => ({
height: `${lineHeight.value}px`
}));
// 新增:根据是否有翻译文本动态计算每行的样式
const getDynamicLineStyle = (line: { text: string; trText: string }) => {
// 默认行高
const defaultHeight = lineHeight.value;
// 如果有翻译文本,增加额外高度
if (line.trText) {
// 计算翻译文本的额外高度 (字体大小的0.6倍 * 行高比例1.4)
const extraHeight = Math.round(fontSize.value * 0.6 * 1.4);
return {
height: `${defaultHeight + extraHeight}px`
};
}
return {
height: `${defaultHeight}px`
};
};
// 更新容器高度和行高
const updateContainerHeight = () => {
if (!containerRef.value) return;
@@ -625,16 +666,22 @@ body {
&.dark {
--text-color: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--text-secondary: #ffffffea;
--highlight-color: #1db954;
--control-bg: rgba(124, 124, 124, 0.3);
&:hover {
background: rgba(44, 44, 44, 0.466);
}
}
&.light {
--text-color: #333333;
--text-secondary: rgba(51, 51, 51, 0.6);
--text-secondary: #39393989;
--highlight-color: #1db954;
--control-bg: rgba(255, 255, 255, 0.3);
&:hover {
background: rgba(0, 0, 0, 0.434);
}
}
}

18
src/types/shortcuts.ts Normal file
View File

@@ -0,0 +1,18 @@
/**
* 快捷键配置
*/
export interface ShortcutConfig {
/** 快捷键字符串 */
key: string;
/** 是否启用 */
enabled: boolean;
/** 作用范围: global(全局) 或 app(仅应用内) */
scope: 'global' | 'app';
}
/**
* 快捷键配置集合
*/
export interface ShortcutsConfig {
[key: string]: ShortcutConfig;
}