Skip to content

Commit bae8700

Browse files
committed
feat: 支持选择歌词 (#383)
1 parent 12874a5 commit bae8700

6 files changed

Lines changed: 56 additions & 11 deletions

File tree

res/lang/en-US.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@
168168
"download_lyric": "Download Lyrics",
169169
"more_actions": "More Actions",
170170
"search_and_replace": "Search & Replace Lyrics",
171-
"source_info": "Lyrics from {{platform}} - {{title}}{{artist}}"
171+
"source_info": "Lyrics from {{platform}} - {{title}}{{artist}}",
172+
"select_mode": "Select Mode"
172173
},
173174
"media": {
174175
"unknown_title": "Untitled",

res/lang/zh-CN.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@
168168
"download_lyric": "下载歌词",
169169
"more_actions": "更多操作",
170170
"search_and_replace": "搜索并替换歌词",
171-
"source_info": "歌词来自 {{platform}} - {{title}}{{artist}}"
171+
"source_info": "歌词来自 {{platform}} - {{title}}{{artist}}",
172+
"select_mode": "选择模式"
172173
},
173174
"media": {
174175
"unknown_title": "未命名",

res/lang/zh-TW.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@
168168
"download_lyric": "下載歌詞",
169169
"more_actions": "更多操作",
170170
"search_and_replace": "搜尋並替換歌詞",
171-
"source_info": "歌詞來自 {{platform}} - {{title}}{{artist}}"
171+
"source_info": "歌詞來自 {{platform}} - {{title}}{{artist}}",
172+
"select_mode": "選擇模式"
172173
},
173174
"media": {
174175
"unknown_title": "無標題",

src/renderer/mainWindow/components/layout/FullscreenPlayer/LyricPanel.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface LyricPanelProps {
1616
fontScale: number;
1717
/** 是否显示翻译 */
1818
showTranslation: boolean;
19+
/** 是否处于文本选择模式 */
20+
isSelectable: boolean;
1921
}
2022

2123
/** 活跃行基准字号 (px) — 对标设计稿 32px */
@@ -32,13 +34,20 @@ const USER_SCROLL_PAUSE = 4000;
3234
const prefersReducedMotion =
3335
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3436

35-
const LyricPanel = memo(function LyricPanel({ fontScale, showTranslation }: LyricPanelProps) {
37+
const LyricPanel = memo(function LyricPanel({
38+
fontScale,
39+
showTranslation,
40+
isSelectable,
41+
}: LyricPanelProps) {
3642
const { t } = useTranslation();
3743
const lyricState = useLyric();
3844
const containerRef = useRef<HTMLDivElement>(null);
3945
const activeIndexRef = useRef<number>(-1);
4046
const isInitialRef = useRef(true);
4147

48+
const isSelectableRef = useRef(false);
49+
isSelectableRef.current = isSelectable;
50+
4251
// 拖拽滚动状态
4352
const dragState = useRef({
4453
isDragging: false,
@@ -96,8 +105,10 @@ const LyricPanel = memo(function LyricPanel({ fontScale, showTranslation }: Lyri
96105
});
97106
}, [activeIndex]);
98107

99-
/** 拖拽开始 */
108+
/** 拖拽开始(文本选择模式下跳过) */
100109
const handlePointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
110+
if (isSelectableRef.current) return;
111+
101112
const container = containerRef.current;
102113
if (!container) return;
103114

@@ -111,8 +122,10 @@ const LyricPanel = memo(function LyricPanel({ fontScale, showTranslation }: Lyri
111122
wasDraggedRef.current = false;
112123
}, []);
113124

114-
/** 拖拽移动 */
125+
/** 拖拽移动(文本选择模式下跳过) */
115126
const handlePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
127+
if (isSelectableRef.current) return;
128+
116129
const state = dragState.current;
117130
if (!state.isDragging) return;
118131

@@ -159,10 +172,9 @@ const LyricPanel = memo(function LyricPanel({ fontScale, showTranslation }: Lyri
159172
[pauseAutoScroll],
160173
);
161174

162-
/** 点击歌词行跳转播放位置(拖拽时不触发) */
175+
/** 双击歌词行跳转播放位置(拖拽或文本选择模式下不触发) */
163176
const handleLyricClick = useCallback((item: IParsedLrcItem) => {
164-
console.log('点击歌词行', item);
165-
if (wasDraggedRef.current) return;
177+
if (wasDraggedRef.current || isSelectableRef.current) return;
166178
trackPlayer.seekTo(item.time);
167179
}, []);
168180

@@ -191,7 +203,7 @@ const LyricPanel = memo(function LyricPanel({ fontScale, showTranslation }: Lyri
191203
return (
192204
<div
193205
ref={containerRef}
194-
className="l-fullscreen-player__lyric-scroll"
206+
className={`l-fullscreen-player__lyric-scroll${isSelectable ? ' is-selectable' : ''}`}
195207
onPointerDown={handlePointerDown}
196208
onPointerMove={handlePointerMove}
197209
onPointerUp={handlePointerUp}

src/renderer/mainWindow/components/layout/FullscreenPlayer/index.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@
220220
&.is-dragging {
221221
cursor: grabbing;
222222
}
223+
224+
&.is-selectable {
225+
user-select: text;
226+
cursor: text;
227+
228+
::selection {
229+
background-color: var(--color-fill-brand);
230+
color: var(--color-text-on-brand);
231+
}
232+
}
223233
// 顶部/底部渐变遮罩
224234
mask-image: linear-gradient(
225235
to bottom,

src/renderer/mainWindow/components/layout/FullscreenPlayer/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { useState, useEffect, useCallback, useRef, memo } from 'react';
66
import { createPortal } from 'react-dom';
77
import { motion, AnimatePresence } from 'framer-motion';
8-
import { ChevronDown, MoreHorizontal, Languages } from 'lucide-react';
8+
import { ChevronDown, MoreHorizontal, Languages, TextCursorInput } from 'lucide-react';
99
import { DesktopLyric } from '@renderer/common/icons';
1010
import { useAtomValue } from 'jotai/react';
1111
import { useTranslation } from 'react-i18next';
@@ -45,6 +45,7 @@ const FullscreenPlayer = memo(function FullscreenPlayer() {
4545
const [showTranslation, setShowTranslation] = useState(
4646
() => syncKV.get('player.showLyricTranslation') ?? true,
4747
);
48+
const [isSelectable, setIsSelectable] = useState(false);
4849

4950
// Escape 关闭
5051
useEffect(() => {
@@ -63,6 +64,7 @@ const FullscreenPlayer = memo(function FullscreenPlayer() {
6364
useEffect(() => {
6465
if (!open) {
6566
setSettingsOpen(false);
67+
setIsSelectable(false);
6668
}
6769
}, [open]);
6870

@@ -104,6 +106,10 @@ const FullscreenPlayer = memo(function FullscreenPlayer() {
104106
handleShowTranslationChange(!showTranslation);
105107
}, [showTranslation, handleShowTranslationChange]);
106108

109+
const handleSelectableToggle = useCallback(() => {
110+
setIsSelectable((prev) => !prev);
111+
}, []);
112+
107113
return createPortal(
108114
<AnimatePresence>
109115
{open && (
@@ -193,10 +199,24 @@ const FullscreenPlayer = memo(function FullscreenPlayer() {
193199
<LyricPanel
194200
fontScale={fontScale}
195201
showTranslation={showTranslation}
202+
isSelectable={isSelectable}
196203
/>
197204

198205
{/* 右下角工具栏:翻译 / 桌面歌词 / 设置 */}
199206
<div className="l-fullscreen-player__settings-anchor">
207+
<button
208+
className={cn(
209+
'l-fullscreen-player__tool-btn',
210+
isSelectable && 'is-active',
211+
)}
212+
type="button"
213+
title={t('lyric.select_mode')}
214+
aria-label={t('lyric.select_mode')}
215+
aria-pressed={isSelectable}
216+
onClick={handleSelectableToggle}
217+
>
218+
<TextCursorInput size={16} />
219+
</button>
200220
<button
201221
className={cn(
202222
'l-fullscreen-player__tool-btn',

0 commit comments

Comments
 (0)