mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 14:50:50 +08:00
✨ feat: 添加字体配置功能 可配置歌词页面 或全局字体
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"@unblockneteasemusic/server": "^0.27.8-patch.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"font-list": "^1.5.1",
|
||||
"netease-cloud-music-api-alger": "^4.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,6 +46,7 @@
|
||||
"@vue/runtime-core": "^3.5.0",
|
||||
"@vueuse/core": "^11.0.3",
|
||||
"@vueuse/electron": "^11.0.3",
|
||||
"animate.css": "^4.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.7",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -78,8 +80,7 @@
|
||||
"vue": "^3.4.30",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue-tsc": "^2.0.22",
|
||||
"vuex": "^4.1.0",
|
||||
"animate.css": "^4.1.1"
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.alger.music",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from 'path';
|
||||
import { loadLyricWindow } from './lyric';
|
||||
import { initializeConfig } from './modules/config';
|
||||
import { initializeFileManager } from './modules/fileManager';
|
||||
import { initializeFonts } from './modules/fonts';
|
||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||
import { initializeTray } from './modules/tray';
|
||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||
@@ -30,6 +31,8 @@ function initialize() {
|
||||
initializeFileManager();
|
||||
// 初始化窗口管理
|
||||
initializeWindowManager();
|
||||
// 初始化字体管理
|
||||
initializeFonts();
|
||||
|
||||
// 创建主窗口
|
||||
mainWindow = createMainWindow(icon);
|
||||
|
||||
@@ -12,6 +12,17 @@ interface StoreType {
|
||||
author: string;
|
||||
authorUrl: string;
|
||||
musicApiPort: number;
|
||||
closeAction: 'ask' | 'minimize' | 'close';
|
||||
musicQuality: string;
|
||||
fontFamily: string;
|
||||
proxyConfig: {
|
||||
enable: boolean;
|
||||
protocol: string;
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
enableRealIP: boolean;
|
||||
realIP: string;
|
||||
};
|
||||
shortcuts: typeof defaultShortcuts;
|
||||
}
|
||||
|
||||
42
src/main/modules/fonts.ts
Normal file
42
src/main/modules/fonts.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { getFonts } from 'font-list';
|
||||
|
||||
/**
|
||||
* 清理字体名称
|
||||
* @param fontName 原始字体名称
|
||||
* @returns 清理后的字体名称
|
||||
*/
|
||||
function cleanFontName(fontName: string): string {
|
||||
return fontName
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '') // 移除首尾的引号
|
||||
.replace(/\s+/g, ' '); // 将多个空格替换为单个空格
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统字体列表
|
||||
*/
|
||||
async function getSystemFonts(): Promise<string[]> {
|
||||
try {
|
||||
// 使用 font-list 获取系统字体
|
||||
const fonts = await getFonts();
|
||||
// 清理字体名称并去重
|
||||
const cleanedFonts = [...new Set(fonts.map(cleanFontName))];
|
||||
// 添加系统默认字体并排序
|
||||
return ['system-ui', ...cleanedFonts].sort();
|
||||
} catch (error) {
|
||||
console.error('获取系统字体失败:', error);
|
||||
// 如果获取失败,至少返回系统默认字体
|
||||
return ['system-ui'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化字体管理模块
|
||||
*/
|
||||
export function initializeFonts() {
|
||||
// 添加获取系统字体的 IPC 处理
|
||||
ipcMain.handle('get-system-fonts', async () => {
|
||||
return await getSystemFonts();
|
||||
});
|
||||
}
|
||||
90
src/main/modules/update.ts
Normal file
90
src/main/modules/update.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import axios from 'axios';
|
||||
import { exec } from 'child_process';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function setupUpdateHandlers(_mainWindow: BrowserWindow) {
|
||||
ipcMain.on('start-download', async (event, url: string) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
onDownloadProgress: (progressEvent: { loaded: number; total?: number }) => {
|
||||
if (!progressEvent.total) return;
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||
const downloaded = (progressEvent.loaded / 1024 / 1024).toFixed(2);
|
||||
const total = (progressEvent.total / 1024 / 1024).toFixed(2);
|
||||
event.sender.send('download-progress', percent, `已下载 ${downloaded}MB / ${total}MB`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileName = url.split('/').pop() || 'update.exe';
|
||||
const downloadPath = path.join(app.getPath('downloads'), fileName);
|
||||
|
||||
// 创建写入流
|
||||
const writer = fs.createWriteStream(downloadPath);
|
||||
|
||||
// 将响应流写入文件
|
||||
response.data.pipe(writer);
|
||||
|
||||
// 处理写入完成
|
||||
writer.on('finish', () => {
|
||||
event.sender.send('download-complete', true, downloadPath);
|
||||
});
|
||||
|
||||
// 处理写入错误
|
||||
writer.on('error', (error) => {
|
||||
console.error('Write file error:', error);
|
||||
event.sender.send('download-complete', false, '');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
event.sender.send('download-complete', false, '');
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('install-update', (_event, filePath: string) => {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error('Installation file not found:', filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const { platform } = process;
|
||||
|
||||
// 关闭当前应用
|
||||
app.quit();
|
||||
|
||||
// 根据不同平台执行安装
|
||||
if (platform === 'win32') {
|
||||
exec(`"${filePath}"`, (error) => {
|
||||
if (error) {
|
||||
console.error('Error starting installer:', error);
|
||||
}
|
||||
});
|
||||
} else if (platform === 'darwin') {
|
||||
// 挂载 DMG 文件
|
||||
exec(`open "${filePath}"`, (error) => {
|
||||
if (error) {
|
||||
console.error('Error opening DMG:', error);
|
||||
}
|
||||
});
|
||||
} else if (platform === 'linux') {
|
||||
const ext = path.extname(filePath);
|
||||
if (ext === '.AppImage') {
|
||||
exec(`chmod +x "${filePath}" && "${filePath}"`, (error) => {
|
||||
if (error) {
|
||||
console.error('Error running AppImage:', error);
|
||||
}
|
||||
});
|
||||
} else if (ext === '.deb') {
|
||||
exec(`pkexec dpkg -i "${filePath}"`, (error) => {
|
||||
if (error) {
|
||||
console.error('Error installing deb package:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -14,5 +14,7 @@
|
||||
"authorUrl": "https://github.com/algerkong",
|
||||
"musicApiPort": 30488,
|
||||
"closeAction": "ask",
|
||||
"musicQuality": "higher"
|
||||
"musicQuality": "higher",
|
||||
"fontFamily": "system-ui",
|
||||
"fontScope": "global"
|
||||
}
|
||||
|
||||
@@ -14,11 +14,18 @@ const api = {
|
||||
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id),
|
||||
// 歌词缓存相关
|
||||
invoke: (channel: string, ...args: any[]) => {
|
||||
const validChannels = ['get-cached-lyric', 'cache-lyric', 'clear-lyric-cache'];
|
||||
const validChannels = [
|
||||
'get-lyrics',
|
||||
'clear-lyrics-cache',
|
||||
'get-system-fonts',
|
||||
'get-cached-lyric',
|
||||
'cache-lyric',
|
||||
'clear-lyric-cache'
|
||||
];
|
||||
if (validChannels.includes(channel)) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
return Promise.reject(new Error(`Invalid channel: ${channel}`));
|
||||
return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { darkTheme, lightTheme } from 'naive-ui';
|
||||
import { onMounted } from 'vue';
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
|
||||
import homeRouter from '@/router/home';
|
||||
import store from '@/store';
|
||||
@@ -24,9 +24,45 @@ const theme = computed(() => {
|
||||
return store.state.theme;
|
||||
});
|
||||
|
||||
// 监听字体变化并应用
|
||||
watch(
|
||||
() => [store.state.setData.fontFamily, store.state.setData.fontScope],
|
||||
([newFont, fontScope]) => {
|
||||
const appElement = document.body;
|
||||
if (!appElement) return;
|
||||
|
||||
const defaultFonts =
|
||||
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
||||
|
||||
// 只有在全局模式下才应用字体
|
||||
if (fontScope !== 'global') {
|
||||
appElement.style.fontFamily = defaultFonts;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newFont === 'system-ui') {
|
||||
appElement.style.fontFamily = defaultFonts;
|
||||
} else {
|
||||
// 处理多个字体,确保每个字体名都被正确引用
|
||||
const fontList = newFont.split(',').map((font) => {
|
||||
const trimmedFont = font.trim();
|
||||
// 如果字体名包含空格或特殊字符,添加引号(如果还没有引号的话)
|
||||
return /[\s'"()]/.test(trimmedFont) && !/^['"].*['"]$/.test(trimmedFont)
|
||||
? `"${trimmedFont}"`
|
||||
: trimmedFont;
|
||||
});
|
||||
|
||||
// 将选择的字体和默认字体组合
|
||||
appElement.style.fontFamily = `${fontList.join(', ')}, ${defaultFonts}`;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('initializeSettings');
|
||||
store.dispatch('initializeTheme');
|
||||
store.dispatch('initializeSystemFonts');
|
||||
if (isMobile.value) {
|
||||
store.commit(
|
||||
'setMenus',
|
||||
@@ -36,7 +72,7 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.app-container {
|
||||
@apply h-full w-full;
|
||||
user-select: none;
|
||||
|
||||
6
src/renderer/components.d.ts
vendored
6
src/renderer/components.d.ts
vendored
@@ -27,10 +27,15 @@ declare module 'vue' {
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
@@ -41,6 +46,7 @@ declare module 'vue' {
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NTransfer: typeof import('naive-ui')['NTransfer']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
v-model:show="showModal"
|
||||
preset="dialog"
|
||||
:show-icon="false"
|
||||
:mask-closable="true"
|
||||
:mask-closable="!downloading"
|
||||
:closable="!downloading"
|
||||
class="update-app-modal"
|
||||
style="width: 800px; max-width: 90vw"
|
||||
>
|
||||
@@ -15,7 +16,6 @@
|
||||
<div class="app-info">
|
||||
<h2 class="app-name">发现新版本 {{ updateInfo.latestVersion }}</h2>
|
||||
<p class="app-desc mb-2">当前版本 {{ updateInfo.currentVersion }}</p>
|
||||
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="update-info">
|
||||
@@ -23,11 +23,35 @@
|
||||
<div class="update-body" v-html="parsedReleaseNotes"></div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<n-button class="cancel-btn" @click="closeModal">暂不更新</n-button>
|
||||
<n-button type="primary" class="update-btn" @click="handleUpdate">立即更新</n-button>
|
||||
<div v-if="downloading" class="download-status mt-6">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm text-gray-500">{{ downloadStatus }}</span>
|
||||
<span class="text-sm font-medium">{{ downloadProgress }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar-wrapper">
|
||||
<div class="progress-bar" :style="{ width: `${downloadProgress}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-desc mt-4 text-center">
|
||||
<div class="modal-actions" :class="{ 'mt-6': !downloading }">
|
||||
<n-button
|
||||
class="cancel-btn"
|
||||
:disabled="downloading"
|
||||
:loading="downloading"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ '暂不更新' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
class="update-btn"
|
||||
:loading="downloading"
|
||||
:disabled="downloading"
|
||||
@click="handleUpdate"
|
||||
>
|
||||
{{ downloadBtnText }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div v-if="!downloading" class="modal-desc mt-4 text-center">
|
||||
<p class="text-xs text-gray-400">
|
||||
下载遇到问题?去
|
||||
<a
|
||||
@@ -45,7 +69,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||
@@ -59,7 +83,6 @@ marked.setOptions({
|
||||
});
|
||||
|
||||
const showModal = ref(false);
|
||||
const noPrompt = ref(false);
|
||||
const updateInfo = ref<UpdateResult>({
|
||||
hasUpdate: false,
|
||||
latestVersion: '',
|
||||
@@ -102,9 +125,6 @@ const parsedReleaseNotes = computed(() => {
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
if (noPrompt.value) {
|
||||
localStorage.setItem('updatePromptDismissed', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
@@ -112,21 +132,54 @@ const checkForUpdates = async () => {
|
||||
const result = await checkUpdate(config.version);
|
||||
if (result) {
|
||||
updateInfo.value = result;
|
||||
if (localStorage.getItem('updatePromptDismissed') !== 'true') {
|
||||
showModal.value = true;
|
||||
}
|
||||
showModal.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const downloading = ref(false);
|
||||
const downloadProgress = ref(0);
|
||||
const downloadStatus = ref('准备下载...');
|
||||
const downloadBtnText = computed(() => {
|
||||
if (downloading.value) return '下载中...';
|
||||
return '立即更新';
|
||||
});
|
||||
|
||||
// 处理下载状态更新
|
||||
const handleDownloadProgress = (_event: any, progress: number, status: string) => {
|
||||
downloadProgress.value = progress;
|
||||
downloadStatus.value = status;
|
||||
};
|
||||
|
||||
// 处理下载完成
|
||||
const handleDownloadComplete = (_event: any, success: boolean, filePath: string) => {
|
||||
downloading.value = false;
|
||||
if (success) {
|
||||
window.electron.ipcRenderer.send('install-update', filePath);
|
||||
} else {
|
||||
window.$message.error('下载失败,请重试或手动下载');
|
||||
}
|
||||
};
|
||||
|
||||
// 监听下载事件
|
||||
onMounted(() => {
|
||||
checkForUpdates();
|
||||
window.electron.ipcRenderer.on('download-progress', handleDownloadProgress);
|
||||
window.electron.ipcRenderer.on('download-complete', handleDownloadComplete);
|
||||
});
|
||||
|
||||
// 清理事件监听
|
||||
onUnmounted(() => {
|
||||
window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);
|
||||
window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);
|
||||
});
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const assets = updateInfo.value.releaseInfo?.assets || [];
|
||||
const { platform } = window.electron.process;
|
||||
const arch = window.electron.ipcRenderer.sendSync('get-arch');
|
||||
console.log('arch', arch);
|
||||
console.log('platform', platform);
|
||||
const version = updateInfo.value.latestVersion;
|
||||
const downUrls = {
|
||||
win32: {
|
||||
@@ -170,16 +223,20 @@ const handleUpdate = async () => {
|
||||
}
|
||||
|
||||
if (downloadUrl) {
|
||||
window.open(`https://www.ghproxy.cn/${downloadUrl}`, '_blank');
|
||||
try {
|
||||
downloading.value = true;
|
||||
downloadStatus.value = '准备下载...';
|
||||
window.electron.ipcRenderer.send('start-download', downloadUrl);
|
||||
} catch (error) {
|
||||
downloading.value = false;
|
||||
window.$message.error('启动下载失败,请重试或手动下载');
|
||||
console.error('下载失败:', error);
|
||||
}
|
||||
} else {
|
||||
// 如果没有找到对应的安装包,跳转到 release 页面
|
||||
window.$message.error('未找到适合当前系统的安装包,请手动下载');
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkForUpdates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -266,8 +323,18 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.download-status {
|
||||
@apply p-2;
|
||||
.progress-bar-wrapper {
|
||||
@apply w-full h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden;
|
||||
.progress-bar {
|
||||
@apply h-full bg-green-500 rounded-full transition-all duration-300 ease-out;
|
||||
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-actions {
|
||||
@apply flex gap-4 mt-6;
|
||||
@apply flex gap-4;
|
||||
.n-button {
|
||||
@apply flex-1 text-base py-2;
|
||||
}
|
||||
@@ -276,12 +343,18 @@ onMounted(() => {
|
||||
&:hover {
|
||||
@apply bg-gray-700;
|
||||
}
|
||||
&:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
.update-btn {
|
||||
@apply bg-green-600 border-none;
|
||||
&:hover {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
&:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,43 @@ const handleArtistClick = (id: number) => {
|
||||
store.commit('setCurrentArtistId', id);
|
||||
};
|
||||
|
||||
const setData = computed(() => store.state.setData);
|
||||
|
||||
// 监听字体变化并更新 CSS 变量
|
||||
watch(
|
||||
() => [setData.value.fontFamily, setData.value.fontScope],
|
||||
([newFont, fontScope]) => {
|
||||
const defaultFonts =
|
||||
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
||||
|
||||
// 如果不是歌词模式或全局模式,使用默认字体
|
||||
if (fontScope !== 'lyric' && fontScope !== 'global') {
|
||||
document.documentElement.style.setProperty('--current-font-family', defaultFonts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newFont === 'system-ui') {
|
||||
document.documentElement.style.setProperty('--current-font-family', defaultFonts);
|
||||
} else {
|
||||
// 处理多个字体,确保每个字体名都被正确引用
|
||||
const fontList = newFont.split(',').map((font) => {
|
||||
const trimmedFont = font.trim();
|
||||
// 如果字体名包含空格或特殊字符,添加引号(如果还没有引号的话)
|
||||
return /[\s'"()]/.test(trimmedFont) && !/^['"].*['"]$/.test(trimmedFont)
|
||||
? `"${trimmedFont}"`
|
||||
: trimmedFont;
|
||||
});
|
||||
|
||||
// 将选择的字体和默认字体组合
|
||||
document.documentElement.style.setProperty(
|
||||
'--current-font-family',
|
||||
`${fontList.join(', ')}, ${defaultFonts}`
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
lrcScroll
|
||||
});
|
||||
@@ -370,4 +407,19 @@ defineExpose({
|
||||
.music-drawer {
|
||||
transition: none; // 移除之前的过渡效果,现在使用 JS 动画
|
||||
}
|
||||
|
||||
// 添加全局字体样式
|
||||
:root {
|
||||
--current-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#drawer-target {
|
||||
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full pb-8;
|
||||
animation-duration: 300ms;
|
||||
|
||||
.music-lrc-text {
|
||||
font-family: var(--current-font-family);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface State {
|
||||
showUpdateModal: boolean;
|
||||
showArtistDrawer: boolean;
|
||||
currentArtistId: number | null;
|
||||
systemFonts: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
@@ -59,7 +60,8 @@ const state: State = {
|
||||
musicFull: false,
|
||||
showUpdateModal: false,
|
||||
showArtistDrawer: false,
|
||||
currentArtistId: null
|
||||
currentArtistId: null,
|
||||
systemFonts: [{ label: '系统默认', value: 'system-ui' }]
|
||||
};
|
||||
|
||||
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||
@@ -160,6 +162,15 @@ const mutations = {
|
||||
},
|
||||
setCurrentArtistId(state, id: number) {
|
||||
state.currentArtistId = id;
|
||||
},
|
||||
setSystemFonts(state, fonts: string[]) {
|
||||
state.systemFonts = [
|
||||
{ label: '系统默认', value: 'system-ui' },
|
||||
...fonts.map((font) => ({
|
||||
label: font,
|
||||
value: font
|
||||
}))
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,7 +178,10 @@ const actions = {
|
||||
initializeSettings({ commit }: { commit: any }) {
|
||||
if (isElectron) {
|
||||
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
||||
commit('setSetData', setData || defaultSettings);
|
||||
commit('setSetData', {
|
||||
...defaultSettings,
|
||||
...setData
|
||||
});
|
||||
} else {
|
||||
const savedSettings = localStorage.getItem('appSettings');
|
||||
if (savedSettings) {
|
||||
@@ -213,6 +227,17 @@ const actions = {
|
||||
},
|
||||
showArtist({ commit }, id: number) {
|
||||
commit('setCurrentArtistId', id);
|
||||
},
|
||||
async initializeSystemFonts({ commit, state }) {
|
||||
// 如果已经有字体列表(不只是默认字体),则不重复获取
|
||||
if (state.systemFonts.length > 1) return;
|
||||
|
||||
try {
|
||||
const fonts = await window.api.invoke('get-system-fonts');
|
||||
commit('setSystemFonts', fonts);
|
||||
} catch (error) {
|
||||
console.error('获取系统字体失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,51 @@
|
||||
</n-switch>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">字体设置</div>
|
||||
<div class="set-item-content">选择字体,优先使用排在前面的字体</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<n-radio-group v-model:value="setData.fontScope" class="mt-2">
|
||||
<n-radio key="global" value="global">全局</n-radio>
|
||||
<n-radio key="lyric" value="lyric">仅歌词</n-radio>
|
||||
</n-radio-group>
|
||||
<n-select
|
||||
v-model:value="selectedFonts"
|
||||
:options="systemFonts"
|
||||
filterable
|
||||
multiple
|
||||
placeholder="选择字体"
|
||||
style="width: 300px"
|
||||
:render-label="renderFontLabel"
|
||||
>
|
||||
</n-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFonts.length > 0" class="font-preview-container">
|
||||
<div class="font-preview-title">字体预览</div>
|
||||
<div class="font-preview" :style="{ fontFamily: setData.fontFamily }">
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">中文</div>
|
||||
<div class="preview-text">静夜思 床前明月光 疑是地上霜</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">English</div>
|
||||
<div class="preview-text">The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">日本語</div>
|
||||
<div class="preview-text">あいうえお かきくけこ さしすせそ</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">한국어</div>
|
||||
<div class="preview-text">가나다라마 바사아자차 카타파하</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">动画速度</div>
|
||||
@@ -366,7 +411,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormRules } from 'naive-ui';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { computed, h, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import localData from '@/../main/set.json';
|
||||
@@ -510,8 +555,55 @@ const proxyRules: FormRules = {
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化时从store获取代理配置
|
||||
onMounted(() => {
|
||||
// 使用 store 中的字体列表
|
||||
const systemFonts = computed(() => store.state.systemFonts);
|
||||
|
||||
// 已选择的字体列表
|
||||
const selectedFonts = ref<string[]>([]);
|
||||
|
||||
// 自定义渲染函数
|
||||
const renderFontLabel = (option: { label: string; value: string }) => {
|
||||
return h('span', { style: { fontFamily: option.value } }, option.label);
|
||||
};
|
||||
|
||||
// 监听字体选择变化
|
||||
watch(
|
||||
selectedFonts,
|
||||
(newFonts) => {
|
||||
// 如果没有选择任何字体,使用系统默认字体
|
||||
if (newFonts.length === 0) {
|
||||
store.commit('setSetData', {
|
||||
...setData.value,
|
||||
fontFamily: 'system-ui'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 将选择的字体组合成字体列表
|
||||
store.commit('setSetData', {
|
||||
...setData.value,
|
||||
fontFamily: newFonts.join(',')
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 初始化已选择的字体
|
||||
watch(
|
||||
() => setData.value.fontFamily,
|
||||
(newFont) => {
|
||||
if (newFont) {
|
||||
if (newFont === 'system-ui') {
|
||||
selectedFonts.value = [];
|
||||
} else {
|
||||
selectedFonts.value = newFont.split(',');
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 初始化时从store获取配置
|
||||
onMounted(async () => {
|
||||
checkForUpdates();
|
||||
if (setData.value.proxyConfig) {
|
||||
proxyForm.value = { ...setData.value.proxyConfig };
|
||||
@@ -734,8 +826,9 @@ const scrollToSection = async (sectionId: string) => {
|
||||
};
|
||||
|
||||
// 处理滚动,更新当前激活的分类
|
||||
const handleScroll = () => {
|
||||
const scrollTop = scrollbarRef.value?.containerRef.scrollTop;
|
||||
const handleScroll = (e: any) => {
|
||||
const { scrollTop } = e.target;
|
||||
|
||||
const sections = [
|
||||
{ id: 'basic', ref: basicRef },
|
||||
{ id: 'playback', ref: playbackRef },
|
||||
@@ -746,18 +839,30 @@ const handleScroll = () => {
|
||||
{ id: 'donation', ref: donationRef }
|
||||
];
|
||||
|
||||
const activeSection = sections[0].id;
|
||||
let lastValidSection = activeSection;
|
||||
|
||||
for (const section of sections) {
|
||||
if (section.ref?.value) {
|
||||
const { offsetTop } = section.ref.value;
|
||||
const offsetBottom = offsetTop + section.ref.value.offsetHeight;
|
||||
|
||||
if (scrollTop >= offsetTop - 100 && scrollTop < offsetBottom) {
|
||||
currentSection.value = section.id;
|
||||
break;
|
||||
if (scrollTop >= offsetTop - 100) {
|
||||
lastValidSection = section.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastValidSection !== currentSection.value) {
|
||||
currentSection.value = lastValidSection;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化时设置当前激活的分类
|
||||
onMounted(() => {
|
||||
// 延迟一帧等待 DOM 完全渲染
|
||||
nextTick(() => {
|
||||
handleScroll({ target: { scrollTop: 0 } });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -830,4 +935,34 @@ const handleScroll = () => {
|
||||
@apply text-green-500 bg-green-50 dark:bg-green-900;
|
||||
}
|
||||
}
|
||||
|
||||
.font-preview-container {
|
||||
@apply mt-4 p-4 rounded-lg;
|
||||
@apply bg-gray-50 dark:bg-dark-100;
|
||||
@apply border border-gray-200 dark:border-gray-700;
|
||||
|
||||
.font-preview-title {
|
||||
@apply text-sm font-medium mb-3;
|
||||
@apply text-gray-600 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.font-preview {
|
||||
@apply space-y-3;
|
||||
|
||||
.preview-item {
|
||||
@apply flex flex-col gap-1;
|
||||
|
||||
.preview-label {
|
||||
@apply text-xs text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
@apply text-base text-gray-900 dark:text-gray-100;
|
||||
@apply p-2 rounded;
|
||||
@apply bg-white dark:bg-dark;
|
||||
@apply border border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user