feat: 添加字体配置功能 可配置歌词页面 或全局字体

This commit is contained in:
alger
2025-01-17 22:45:59 +08:00
parent 914e043502
commit 1bdb8fcb4a
13 changed files with 525 additions and 42 deletions
+38 -2
View File
@@ -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
View File
@@ -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']
+96 -23
View File
@@ -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>
+27 -2
View File
@@ -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);
}
}
};
+145 -10
View File
@@ -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>