diff --git a/electron/lyric.js b/electron/lyric.js index 5171324..0815cc9 100644 --- a/electron/lyric.js +++ b/electron/lyric.js @@ -1,17 +1,19 @@ -const { BrowserWindow } = require('electron'); +const { BrowserWindow, screen } = require('electron'); const path = require('path'); const config = require('./config'); let lyricWindow = null; +let isDragging = false; const createWin = () => { lyricWindow = new BrowserWindow({ width: 800, - height: 300, + height: 200, frame: false, show: false, transparent: true, hasShadow: false, + alwaysOnTop: true, webPreferences: { nodeIntegration: false, contextIsolation: true, @@ -68,6 +70,44 @@ const loadLyricWindow = (ipcMain) => { ipcMain.on('mouseleave-lyric', () => { lyricWindow.setIgnoreMouseEvents(false); }); + + // 开始拖动 + ipcMain.on('lyric-drag-start', () => { + isDragging = true; + }); + + // 处理拖动移动 + ipcMain.on('lyric-drag-move', (e, { deltaX, deltaY }) => { + if (!lyricWindow) return; + + const [currentX, currentY] = lyricWindow.getPosition(); + const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize; + const [windowWidth, windowHeight] = lyricWindow.getSize(); + + // 计算新位置,确保窗口不会移出屏幕 + const newX = Math.max(0, Math.min(currentX + deltaX, screenWidth - windowWidth)); + const newY = Math.max(0, Math.min(currentY + deltaY, screenHeight - windowHeight)); + + lyricWindow.setPosition(newX, newY); + }); + + // 结束拖动 + ipcMain.on('lyric-drag-end', () => { + isDragging = false; + }); + + // 添加鼠标穿透事件处理 + ipcMain.on('set-ignore-mouse', (e, shouldIgnore) => { + if (!lyricWindow) return; + + if (shouldIgnore) { + // 设置鼠标穿透,但保留拖动区域可交互 + lyricWindow.setIgnoreMouseEvents(true, { forward: true }); + } else { + // 取消鼠标穿透 + lyricWindow.setIgnoreMouseEvents(false); + } + }); }; module.exports = { diff --git a/src/hooks/MusicHook.ts b/src/hooks/MusicHook.ts index f3b56fb..652719a 100644 --- a/src/hooks/MusicHook.ts +++ b/src/hooks/MusicHook.ts @@ -92,7 +92,13 @@ export const audioServiceOn = (audio: typeof audioService) => { // 监听结束 audio.onEnd(() => { handleEnded(); - store.commit('nextPlay'); + if (store.state.playMode === 1) { + // 单曲循环模式 + audio.getCurrentSound()?.play(); + } else { + // 列表循环模式 + store.commit('nextPlay'); + } }); }; diff --git a/src/index.css b/src/index.css index 612605c..22c36a1 100644 --- a/src/index.css +++ b/src/index.css @@ -10,6 +10,10 @@ width: 100%; } +.n-slider-handle-indicator--top { + @apply bg-transparent text-[#ffffffdd] text-2xl px-2 py-1 shadow-none mb-0 !important; +} + .text-el { @apply overflow-ellipsis overflow-hidden whitespace-nowrap; } diff --git a/src/layout/components/PlayBar.vue b/src/layout/components/PlayBar.vue index 9120b02..e90973c 100644 --- a/src/layout/components/PlayBar.vue +++ b/src/layout/components/PlayBar.vue @@ -2,10 +2,14 @@ +
+
+ +
@@ -38,23 +42,26 @@
-
+
-
-
{{ getNowTime }}
- -
{{ getAllTime }}
-
-
-
- -
- -
- +
+
+ +
+
+ +
+
+ + + {{ playModeText }} + + @@ -133,17 +140,33 @@ watch( // 使用 useThrottleFn 创建节流版本的 seek 函数 const throttledSeek = useThrottleFn((value: number) => { if (!sound.value) return; - sound.value.seek((value * allTime.value) / 100); + sound.value.seek(value); + nowTime.value = value; }, 50); // 50ms 的节流延迟 // 修改 timeSlider 计算属性 const timeSlider = computed({ - get: () => (nowTime.value / allTime.value) * 100, + get: () => nowTime.value, set: throttledSeek, }); +const formatTooltip = (value: number) => { + return `${secondToMinute(value)} / ${secondToMinute(allTime.value)}`; +}; + // 音量条 const audioVolume = ref(localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1); +const getVolumeIcon = computed(() => { + // 0 静音 ri-volume-mute-line 0.5 ri-volume-down-line 1 ri-volume-up-line + if (audioVolume.value === 0) { + return 'ri-volume-mute-line'; + } + if (audioVolume.value <= 0.5) { + return 'ri-volume-down-line'; + } + return 'ri-volume-up-line'; +}); + const volumeSlider = computed({ get: () => audioVolume.value * 100, set: (value) => { @@ -153,17 +176,31 @@ const volumeSlider = computed({ audioVolume.value = value / 100; }, }); -// 获取当前播放时间 -const getNowTime = computed(() => { - return secondToMinute(nowTime.value); + +// 静音 +const mute = () => { + if (volumeSlider.value === 0) { + volumeSlider.value = 30; + } else { + volumeSlider.value = 0; + } +}; + +// 播放模式 +const playMode = computed(() => store.state.playMode); +const playModeIcon = computed(() => { + return playMode.value === 0 ? 'ri-repeat-2-line' : 'ri-repeat-one-line'; +}); +const playModeText = computed(() => { + return playMode.value === 0 ? '列表循环' : '单曲循环'; }); -// 获取总时间 -const getAllTime = computed(() => { - return secondToMinute(allTime.value); -}); +// 切换播放模式 +const togglePlayMode = () => { + store.commit('togglePlayMode'); +}; -function handleEnded() { +function handleNext() { store.commit('nextPlay'); } @@ -224,13 +261,13 @@ const toggleFavorite = async (e: Event) => { } .music-play-bar { - @apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2; + @apply h-20 w-full absolute bottom-0 left-0 flex items-center box-border px-6 py-2 pt-3; z-index: 9999; box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034); background-color: #212121; animation-duration: 0.5s !important; .music-content { - width: 140px; + width: 160px; @apply ml-4; &-title { @@ -253,14 +290,14 @@ const toggleFavorite = async (e: Event) => { } .music-buttons { - @apply mx-6; + @apply mx-6 flex-1 flex justify-center; .iconfont { @apply text-2xl hover:text-green-500 transition; } .icon { - @apply text-xl hover:text-white; + @apply text-3xl hover:text-white; } @apply flex items-center; @@ -270,25 +307,28 @@ const toggleFavorite = async (e: Event) => { } &-play { - background: #383838; - @apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition bg-opacity-40; - } -} - -.music-time { - @apply flex flex-1 items-center; - - .time { - @apply mx-4 mt-1; + background-color: #ffffff20; + @apply flex justify-center items-center w-20 h-12 rounded-full mx-4 hover:bg-[#ffffff40] transition; } } .audio-volume { - width: 140px; - @apply flex items-center mx-4; + @apply flex items-center relative; + &:hover { + .volume-slider { + @apply opacity-100 visible; + } + } + .volume-icon { + @apply cursor-pointer; - .iconfont { - @apply text-2xl hover:text-green-500 transition cursor-pointer mr-4; + .iconfont { + @apply text-2xl hover:text-green-500 transition; + } + } + + .volume-slider { + @apply absolute opacity-0 invisible transition-all duration-300 bottom-[30px] left-1/2 -translate-x-1/2 h-[180px] px-2 py-4 bg-gray-800 bg-opacity-80 rounded-xl; } } @@ -356,17 +396,31 @@ const toggleFavorite = async (e: Event) => { --n-handle-size: 12px; --n-handle-color: var(--primary-color); - &:hover { - --n-rail-height: 6px; - --n-handle-size: 14px; + &.n-slider--vertical { + height: 100%; + + .n-slider-rail { + width: 4px; + } + + &:hover { + .n-slider-rail { + width: 6px; + } + + .n-slider-handle { + width: 14px; + height: 14px; + } + } } .n-slider-rail { - @apply overflow-hidden; + @apply overflow-hidden transition-all duration-200; } .n-slider-handle { - @apply transition-opacity duration-200; + @apply transition-all duration-200; opacity: 0; } @@ -418,4 +472,17 @@ const toggleFavorite = async (e: Event) => { .like-active { @apply text-red-600; } + +.icon-loop, +.icon-single-loop { + font-size: 1.5rem; +} + +.music-time .n-slider { + position: absolute; + top: 0; + left: 0; + padding: 0; + border-radius: 0; +} diff --git a/src/store/index.ts b/src/store/index.ts index 210a8fd..22d4391 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -13,6 +13,11 @@ const defaultSettings = { authorUrl: 'https://github.com/algerkong', }; +function getLocalStorageItem(key: string, defaultValue: T): T { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; +} + interface State { menus: any[]; play: boolean; @@ -28,6 +33,7 @@ interface State { searchValue: string; searchType: number; favoriteList: number[]; + playMode: number; } const state: State = { @@ -36,7 +42,7 @@ const state: State = { isPlay: false, playMusic: {} as SongResult, playMusicUrl: '', - user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') as string) : null, + user: getLocalStorageItem('user', null), playList: [], playListIndex: 0, setData: defaultSettings, @@ -44,7 +50,8 @@ const state: State = { isMobile: false, searchValue: '', searchType: 1, - favoriteList: localStorage.getItem('favoriteList') ? JSON.parse(localStorage.getItem('favoriteList') || '[]') : [], + favoriteList: getLocalStorageItem('favoriteList', []), + playMode: getLocalStorageItem('playMode', 0), }; const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook(); @@ -91,6 +98,10 @@ const mutations = { state.favoriteList = state.favoriteList.filter((id) => id !== songId); localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList)); }, + togglePlayMode(state: State) { + state.playMode = state.playMode === 0 ? 1 : 0; + localStorage.setItem('playMode', JSON.stringify(state.playMode)); + }, }; const actions = { diff --git a/src/views/lyric/index.vue b/src/views/lyric/index.vue index b1bf2bb..77fbe17 100644 --- a/src/views/lyric/index.vue +++ b/src/views/lyric/index.vue @@ -2,9 +2,11 @@
+
@@ -25,7 +27,7 @@
-
+
@@ -61,7 +63,7 @@
-
暂无歌词
+
无歌词
@@ -84,6 +86,8 @@ const isInitialized = ref(false); // 字体大小控制 const fontSize = ref(24); // 默认字体大小 const fontSizeStep = 2; // 每次整的步长 +const animationFrameId = ref(null); +const lastUpdateTime = ref(performance.now()); // 静态数据 const staticData = ref<{ @@ -136,14 +140,21 @@ const clearHideTimer = () => { // 处理鼠标进入窗口 const handleMouseEnter = () => { - if (!lyricSetting.value.isLock) return; - isHovering.value = true; + console.log('handleMouseEnter'); + if (lyricSetting.value.isLock) { + isHovering.value = true; + windowData.electron.ipcRenderer.send('set-ignore-mouse', true); + } else { + windowData.electron.ipcRenderer.send('set-ignore-mouse', false); + } }; // 处理鼠标离开窗口 const handleMouseLeave = () => { + console.log('handleMouseLeave'); if (!lyricSetting.value.isLock) return; isHovering.value = false; + windowData.electron.ipcRenderer.send('set-ignore-mouse', false); }; // 监听锁定状态变化 @@ -180,7 +191,7 @@ const wrapperStyle = computed(() => { const containerCenter = containerHeight.value / 2; // 计算当前行到顶部的距离(包含padding) - const currentLineTop = currentIndex.value * lineHeight.value + containerHeight.value * 0.2; // 加上顶部padding + const currentLineTop = currentIndex.value * lineHeight.value + containerHeight.value * 0.2 + lineHeight.value; // 加上顶部padding // 计算偏移量,使当前行居中 const targetOffset = containerCenter - currentLineTop; @@ -265,10 +276,6 @@ onMounted(() => { resizeObserver.disconnect(); }); }); - -// 动画帧ID -const animationFrameId = ref(null); - // 实际播放时间 const actualTime = ref(0); @@ -317,9 +324,8 @@ const updateProgress = () => { }; // 记录上次更新时间 -const lastUpdateTime = ref(performance.now()); -// 监听数据更新 +// 监听据更新 watch( () => dynamicData.value, (newData: any) => { @@ -351,7 +357,7 @@ watch( }, ); -// 修改数据更新处理 +// 修改数据更新处 const handleDataUpdate = (parsedData: { nowTime: number; startCurrentTime: number; @@ -405,7 +411,7 @@ onMounted(() => { animationFrameId.value = null; } - // 保据格式正确 + // 确保数据格式正确 if (Array.isArray(parsedData.lrcArray)) { staticData.value = { lrcArray: parsedData.lrcArray, @@ -446,6 +452,7 @@ const handleTop = () => { const handleLock = () => { lyricSetting.value.isLock = !lyricSetting.value.isLock; + windowData.electron.ipcRenderer.send('set-ignore-mouse', lyricSetting.value.isLock); }; const handleClose = () => { @@ -459,6 +466,74 @@ watch( }, { deep: true }, ); + +// 添加拖动相关变量 +const isDragging = ref(false); +const startPosition = ref({ x: 0, y: 0 }); + +// 处理鼠标按下事件 +const handleMouseDown = (e: MouseEvent) => { + // 如果点击的是控制按钮区域或窗口被锁定,不处理拖动 + if ( + lyricSetting.value.isLock || + (e.target as HTMLElement).closest('.control-buttons') || + (e.target as HTMLElement).closest('.font-size-controls') + ) { + return; + } + + // 只响应鼠标左键 + if (e.button !== 0) return; + + isDragging.value = true; + startPosition.value = { x: e.screenX, y: e.screenY }; + + // 添加全局鼠标事件监听 + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.value) return; + + const deltaX = e.screenX - startPosition.value.x; + const deltaY = e.screenY - startPosition.value.y; + + // 发送移动事件到主进程 + windowData.electron.ipcRenderer.send('lyric-drag-move', { deltaX, deltaY }); + startPosition.value = { x: e.screenX, y: e.screenY }; + }; + + const handleMouseUp = () => { + if (!isDragging.value) return; + isDragging.value = false; + + // 移除事件监听 + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + // 添加全局事件监听 + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); +}; + +// 组件卸载时清理 +onUnmounted(() => { + isDragging.value = false; +}); + +onMounted(() => { + const lyricLock = document.getElementById('lyric-lock'); + if (lyricLock) { + lyricLock.onmouseenter = () => { + if (lyricSetting.value.isLock) { + windowData.electron.ipcRenderer.send('set-ignore-mouse', false); + } + }; + lyricLock.onmouseleave = () => { + if (lyricSetting.value.isLock) { + windowData.electron.ipcRenderer.send('set-ignore-mouse', true); + } + }; + } +}); diff --git a/tailwind.config.js b/tailwind.config.js index 82cd6a0..5894c6c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,14 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + content: ['./src/**/*.{vue,js,ts,jsx,tsx}'], theme: { - extend: {}, + extend: { + colors: { + highlight: 'var(--highlight-color)', + text: 'var(--text-color)', + secondary: 'var(--text-secondary)', + }, + }, }, plugins: [], };