mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
Merge pull request #323 from Psychotoxical/feat/song-info-double-click-copy
feat(ui): copy song fields from song info via double-click
This commit is contained in:
@@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user