feat(ui): copy song fields from song info via double-click

Double-click the Title, Artist, or Album value to copy plain text to the
clipboard with toast feedback. Apply user-select: none on those cells so
double-click does not trigger word selection.
This commit is contained in:
Maxim Isaev
2026-04-26 17:33:58 +03:00
parent 1dcc1e101c
commit f92cfa183d
2 changed files with 31 additions and 3 deletions
+26 -3
View File
@@ -5,6 +5,8 @@ import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { getSong, SubsonicSong } from '../api/subsonic'; import { getSong, SubsonicSong } from '../api/subsonic';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { copyTextToClipboard } from '../utils/serverMagicString';
import { showToast } from '../utils/toast';
function formatDuration(s: number): string { function formatDuration(s: number): string {
const m = Math.floor(s / 60); const m = Math.floor(s / 60);
@@ -28,6 +30,27 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) {
); );
} }
/** Title / Artist / Album: double-click the value cell to copy plain text. */
function CopyableFieldRow({ label, text }: { label: string; text: string | null | undefined }) {
const { t } = useTranslation();
if (!text || text === '—') return null;
const onDoubleClick = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const ok = await copyTextToClipboard(text);
if (ok) showToast(t('orbit.tooltipCopied'), 2000, 'info');
else showToast(t('contextMenu.shareCopyFailed'), 3500, 'error');
};
return (
<tr>
<td className="song-info-label">{label}</td>
<td className="song-info-value song-info-value--no-select" onDoubleClick={onDoubleClick}>
{text}
</td>
</tr>
);
}
function Divider() { function Divider() {
return <tr><td colSpan={2} className="song-info-divider" /></tr>; return <tr><td colSpan={2} className="song-info-divider" /></tr>;
} }
@@ -96,9 +119,9 @@ export default function SongInfoModal() {
{!loading && song && ( {!loading && song && (
<table className="song-info-table"> <table className="song-info-table">
<tbody> <tbody>
<Row label={t('songInfo.songTitle')} value={song.title} /> <CopyableFieldRow label={t('songInfo.songTitle')} text={song.title} />
<Row label={t('songInfo.artist')} value={song.artist} /> <CopyableFieldRow label={t('songInfo.artist')} text={song.artist} />
<Row label={t('songInfo.album')} value={song.album} /> <CopyableFieldRow label={t('songInfo.album')} text={song.album} />
{song.albumArtist && song.albumArtist !== song.artist && ( {song.albumArtist && song.albumArtist !== song.artist && (
<Row label={t('songInfo.albumArtist')} value={song.albumArtist} /> <Row label={t('songInfo.albumArtist')} value={song.albumArtist} />
)} )}
+5
View File
@@ -1394,6 +1394,11 @@
padding: 4px 16px 4px 8px; padding: 4px 16px 4px 8px;
word-break: break-all; word-break: break-all;
} }
/* Double-click copies; avoid browser selecting word on dblclick */
.song-info-value--no-select {
user-select: none;
-webkit-user-select: none;
}
.song-info-divider { .song-info-divider {
padding: 6px 0; padding: 6px 0;
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);