mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
feat: "open in spotify" button (#1839)
* feat: open in spotify * fix: disable native spotify URI by default
This commit is contained in:
@@ -37,7 +37,8 @@
|
|||||||
"openApplicationDirectory": "open application directory",
|
"openApplicationDirectory": "open application directory",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Open in Last.fm",
|
"lastfm": "Open in Last.fm",
|
||||||
"musicbrainz": "Open in MusicBrainz"
|
"musicbrainz": "Open in MusicBrainz",
|
||||||
|
"spotify": "Open in Spotify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -924,6 +925,10 @@
|
|||||||
"mpvExtraParameters_help": "one per line",
|
"mpvExtraParameters_help": "one per line",
|
||||||
"musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists",
|
"musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists",
|
||||||
"musicbrainz": "show MusicBrainz links",
|
"musicbrainz": "show MusicBrainz links",
|
||||||
|
"spotify_description": "show links to Spotify on artist/album pages",
|
||||||
|
"spotify": "show Spotify links",
|
||||||
|
"nativeSpotify_description": "open in the Spotify app instead of your browser",
|
||||||
|
"nativeSpotify": "use Spotify app",
|
||||||
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available",
|
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available",
|
||||||
"neteaseTranslation": "Enable NetEase translations",
|
"neteaseTranslation": "Enable NetEase translations",
|
||||||
"notify": "enable song notifications",
|
"notify": "enable song notifications",
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ interface AlbumMetadataExternalLinksProps {
|
|||||||
lastFM: boolean;
|
lastFM: boolean;
|
||||||
mbzId?: null | string;
|
mbzId?: null | string;
|
||||||
musicBrainz: boolean;
|
musicBrainz: boolean;
|
||||||
|
nativeSpotify: boolean;
|
||||||
|
spotify: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumMetadataExternalLinks = ({
|
const AlbumMetadataExternalLinks = ({
|
||||||
@@ -307,10 +309,12 @@ const AlbumMetadataExternalLinks = ({
|
|||||||
lastFM,
|
lastFM,
|
||||||
mbzId,
|
mbzId,
|
||||||
musicBrainz,
|
musicBrainz,
|
||||||
|
nativeSpotify,
|
||||||
|
spotify,
|
||||||
}: AlbumMetadataExternalLinksProps) => {
|
}: AlbumMetadataExternalLinksProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!externalLinks || (!lastFM && !musicBrainz)) return null;
|
if (!externalLinks || (!lastFM && !musicBrainz && !spotify)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
@@ -358,6 +362,28 @@ const AlbumMetadataExternalLinks = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{spotify && (
|
||||||
|
<ActionIcon
|
||||||
|
component="a"
|
||||||
|
href={
|
||||||
|
nativeSpotify
|
||||||
|
? `spotify:search:${encodeURIComponent(albumArtist || '')}%20${encodeURIComponent(albumName || '')}`
|
||||||
|
: `https://open.spotify.com/search/${encodeURIComponent(albumArtist || '')}%20${encodeURIComponent(albumName || '')}`
|
||||||
|
}
|
||||||
|
icon="brandSpotify"
|
||||||
|
iconProps={{
|
||||||
|
fill: 'default',
|
||||||
|
size: 'xl',
|
||||||
|
}}
|
||||||
|
radius="md"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target={nativeSpotify ? undefined : '_blank'}
|
||||||
|
tooltip={{
|
||||||
|
label: t('action.openIn.spotify'),
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -370,7 +396,7 @@ export const AlbumDetailContent = () => {
|
|||||||
albumQueries.detail({ query: { id: albumId }, serverId: server.id }),
|
albumQueries.detail({ query: { id: albumId }, serverId: server.id }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { externalLinks, lastFM, musicBrainz } = useExternalLinks();
|
const { externalLinks, lastFM, musicBrainz, nativeSpotify, spotify } = useExternalLinks();
|
||||||
|
|
||||||
const comment = detailQuery?.data?.comment;
|
const comment = detailQuery?.data?.comment;
|
||||||
|
|
||||||
@@ -403,6 +429,8 @@ export const AlbumDetailContent = () => {
|
|||||||
lastFM={lastFM}
|
lastFM={lastFM}
|
||||||
mbzId={mbzId || undefined}
|
mbzId={mbzId || undefined}
|
||||||
musicBrainz={musicBrainz}
|
musicBrainz={musicBrainz}
|
||||||
|
nativeSpotify={nativeSpotify}
|
||||||
|
spotify={spotify}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -890,7 +890,9 @@ interface AlbumArtistMetadataExternalLinksProps {
|
|||||||
lastFM: boolean;
|
lastFM: boolean;
|
||||||
mbzId?: null | string;
|
mbzId?: null | string;
|
||||||
musicBrainz: boolean;
|
musicBrainz: boolean;
|
||||||
|
nativeSpotify: boolean;
|
||||||
order?: number;
|
order?: number;
|
||||||
|
spotify: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumArtistMetadataExternalLinks = ({
|
const AlbumArtistMetadataExternalLinks = ({
|
||||||
@@ -899,11 +901,13 @@ const AlbumArtistMetadataExternalLinks = ({
|
|||||||
lastFM,
|
lastFM,
|
||||||
mbzId,
|
mbzId,
|
||||||
musicBrainz,
|
musicBrainz,
|
||||||
|
nativeSpotify,
|
||||||
order,
|
order,
|
||||||
|
spotify,
|
||||||
}: AlbumArtistMetadataExternalLinksProps) => {
|
}: AlbumArtistMetadataExternalLinksProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!externalLinks || (!lastFM && !musicBrainz)) return null;
|
if (!externalLinks || (!lastFM && !musicBrainz && !spotify)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid.Col order={order} span={12}>
|
<Grid.Col order={order} span={12}>
|
||||||
@@ -948,6 +952,27 @@ const AlbumArtistMetadataExternalLinks = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{spotify && (
|
||||||
|
<ActionIcon
|
||||||
|
component="a"
|
||||||
|
href={
|
||||||
|
nativeSpotify
|
||||||
|
? `spotify:search:${encodeURIComponent(artistName || '')}`
|
||||||
|
: `https://open.spotify.com/search/${encodeURIComponent(artistName || '')}`
|
||||||
|
}
|
||||||
|
icon="brandSpotify"
|
||||||
|
iconProps={{
|
||||||
|
fill: 'default',
|
||||||
|
size: 'xl',
|
||||||
|
}}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target={nativeSpotify ? undefined : '_blank'}
|
||||||
|
tooltip={{
|
||||||
|
label: t('action.openIn.spotify'),
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -1050,7 +1075,7 @@ export const AlbumArtistDetailContent = ({
|
|||||||
}: AlbumArtistDetailContentProps) => {
|
}: AlbumArtistDetailContentProps) => {
|
||||||
const artistItems = useArtistItems();
|
const artistItems = useArtistItems();
|
||||||
const artistRadioCount = useArtistRadioCount();
|
const artistRadioCount = useArtistRadioCount();
|
||||||
const { externalLinks, lastFM, musicBrainz } = useExternalLinks();
|
const { externalLinks, lastFM, musicBrainz, nativeSpotify, spotify } = useExternalLinks();
|
||||||
const { albumArtistId, artistId } = useParams() as {
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
albumArtistId?: string;
|
albumArtistId?: string;
|
||||||
artistId?: string;
|
artistId?: string;
|
||||||
@@ -1136,14 +1161,16 @@ export const AlbumArtistDetailContent = ({
|
|||||||
genres={detailQuery.data?.genres}
|
genres={detailQuery.data?.genres}
|
||||||
order={genresOrder}
|
order={genresOrder}
|
||||||
/>
|
/>
|
||||||
{externalLinks && (lastFM || musicBrainz) && (
|
{externalLinks && (lastFM || musicBrainz || spotify) && (
|
||||||
<AlbumArtistMetadataExternalLinks
|
<AlbumArtistMetadataExternalLinks
|
||||||
artistName={detailQuery.data?.name}
|
artistName={detailQuery.data?.name}
|
||||||
externalLinks={externalLinks}
|
externalLinks={externalLinks}
|
||||||
lastFM={lastFM}
|
lastFM={lastFM}
|
||||||
mbzId={mbzId}
|
mbzId={mbzId}
|
||||||
musicBrainz={musicBrainz}
|
musicBrainz={musicBrainz}
|
||||||
|
nativeSpotify={nativeSpotify}
|
||||||
order={externalLinksOrder}
|
order={externalLinksOrder}
|
||||||
|
spotify={spotify}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enabledItem.biography && (
|
{enabledItem.biography && (
|
||||||
|
|||||||
@@ -601,6 +601,48 @@ export const ApplicationSettings = memo(() => {
|
|||||||
isHidden: !settings.externalLinks,
|
isHidden: !settings.externalLinks,
|
||||||
title: t('setting.musicbrainz', { postProcess: 'sentenceCase' }),
|
title: t('setting.musicbrainz', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.spotify}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
spotify: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.spotify', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !settings.externalLinks,
|
||||||
|
title: t('setting.spotify', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.nativeSpotify}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
nativeSpotify: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.nativeSpotify', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !settings.externalLinks || !settings.spotify,
|
||||||
|
title: t('setting.nativeSpotify', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
|
|||||||
{ key: 'FS_GENERAL_LASTFM_API_KEY', path: ['general', 'lastfmApiKey'], type: 'string' },
|
{ key: 'FS_GENERAL_LASTFM_API_KEY', path: ['general', 'lastfmApiKey'], type: 'string' },
|
||||||
{ key: 'FS_GENERAL_LAST_FM', path: ['general', 'lastFM'], type: 'bool' },
|
{ key: 'FS_GENERAL_LAST_FM', path: ['general', 'lastFM'], type: 'bool' },
|
||||||
{ key: 'FS_GENERAL_MUSIC_BRAINZ', path: ['general', 'musicBrainz'], type: 'bool' },
|
{ key: 'FS_GENERAL_MUSIC_BRAINZ', path: ['general', 'musicBrainz'], type: 'bool' },
|
||||||
|
{ key: 'FS_GENERAL_SPOTIFY', path: ['general', 'spotify'], type: 'bool' },
|
||||||
|
{ key: 'FS_GENERAL_SPOTIFY_NATIVE_APP', path: ['general', 'nativeSpotify'], type: 'bool' },
|
||||||
{ key: 'FS_GENERAL_NATIVE_ASPECT_RATIO', path: ['general', 'nativeAspectRatio'], type: 'bool' },
|
{ key: 'FS_GENERAL_NATIVE_ASPECT_RATIO', path: ['general', 'nativeAspectRatio'], type: 'bool' },
|
||||||
{
|
{
|
||||||
key: 'FS_GENERAL_PLAYERBAR_OPEN_DRAWER',
|
key: 'FS_GENERAL_PLAYERBAR_OPEN_DRAWER',
|
||||||
|
|||||||
@@ -477,6 +477,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
lastfmApiKey: z.string(),
|
lastfmApiKey: z.string(),
|
||||||
musicBrainz: z.boolean(),
|
musicBrainz: z.boolean(),
|
||||||
nativeAspectRatio: z.boolean(),
|
nativeAspectRatio: z.boolean(),
|
||||||
|
nativeSpotify: z.boolean(),
|
||||||
passwordStore: z.string().optional(),
|
passwordStore: z.string().optional(),
|
||||||
pathReplace: z.string(),
|
pathReplace: z.string(),
|
||||||
pathReplaceWith: z.string(),
|
pathReplaceWith: z.string(),
|
||||||
@@ -499,6 +500,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
sidebarPlaylistSorting: z.boolean(),
|
sidebarPlaylistSorting: z.boolean(),
|
||||||
sideQueueType: SideQueueTypeSchema,
|
sideQueueType: SideQueueTypeSchema,
|
||||||
skipButtons: SkipButtonsSchema,
|
skipButtons: SkipButtonsSchema,
|
||||||
|
spotify: z.boolean(),
|
||||||
theme: z.nativeEnum(AppTheme),
|
theme: z.nativeEnum(AppTheme),
|
||||||
themeDark: z.nativeEnum(AppTheme),
|
themeDark: z.nativeEnum(AppTheme),
|
||||||
themeLight: z.nativeEnum(AppTheme),
|
themeLight: z.nativeEnum(AppTheme),
|
||||||
@@ -1129,6 +1131,7 @@ const initialState: SettingsState = {
|
|||||||
lastfmApiKey: '',
|
lastfmApiKey: '',
|
||||||
musicBrainz: true,
|
musicBrainz: true,
|
||||||
nativeAspectRatio: false,
|
nativeAspectRatio: false,
|
||||||
|
nativeSpotify: false,
|
||||||
passwordStore: undefined,
|
passwordStore: undefined,
|
||||||
pathReplace: '',
|
pathReplace: '',
|
||||||
pathReplaceWith: '',
|
pathReplaceWith: '',
|
||||||
@@ -1161,6 +1164,7 @@ const initialState: SettingsState = {
|
|||||||
skipBackwardSeconds: 5,
|
skipBackwardSeconds: 5,
|
||||||
skipForwardSeconds: 10,
|
skipForwardSeconds: 10,
|
||||||
},
|
},
|
||||||
|
spotify: true,
|
||||||
theme: AppTheme.DEFAULT_DARK,
|
theme: AppTheme.DEFAULT_DARK,
|
||||||
themeDark: AppTheme.DEFAULT_DARK,
|
themeDark: AppTheme.DEFAULT_DARK,
|
||||||
themeLight: AppTheme.DEFAULT_LIGHT,
|
themeLight: AppTheme.DEFAULT_LIGHT,
|
||||||
@@ -2549,6 +2553,8 @@ export const useExternalLinks = () =>
|
|||||||
externalLinks: state.general.externalLinks,
|
externalLinks: state.general.externalLinks,
|
||||||
lastFM: state.general.lastFM,
|
lastFM: state.general.lastFM,
|
||||||
musicBrainz: state.general.musicBrainz,
|
musicBrainz: state.general.musicBrainz,
|
||||||
|
nativeSpotify: state.general.nativeSpotify,
|
||||||
|
spotify: state.general.spotify,
|
||||||
}),
|
}),
|
||||||
shallow,
|
shallow,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ import {
|
|||||||
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
||||||
import { PiMouseLeftClickFill, PiMouseRightClickFill } from 'react-icons/pi';
|
import { PiMouseLeftClickFill, PiMouseRightClickFill } from 'react-icons/pi';
|
||||||
import { RiPlayListAddLine, RiRepeat2Line, RiRepeatOneLine } from 'react-icons/ri';
|
import { RiPlayListAddLine, RiRepeat2Line, RiRepeatOneLine } from 'react-icons/ri';
|
||||||
import { SiMusicbrainz } from 'react-icons/si';
|
import { SiMusicbrainz, SiSpotify } from 'react-icons/si';
|
||||||
|
|
||||||
import styles from './icon.module.css';
|
import styles from './icon.module.css';
|
||||||
|
|
||||||
@@ -156,6 +156,7 @@ export const AppIcon = {
|
|||||||
brandGitHub: LuGithub,
|
brandGitHub: LuGithub,
|
||||||
brandLastfm: FaLastfmSquare,
|
brandLastfm: FaLastfmSquare,
|
||||||
brandMusicBrainz: SiMusicbrainz,
|
brandMusicBrainz: SiMusicbrainz,
|
||||||
|
brandSpotify: SiSpotify,
|
||||||
cache: LuCloudDownload,
|
cache: LuCloudDownload,
|
||||||
check: LuCheck,
|
check: LuCheck,
|
||||||
clipboardCopy: LuClipboardCopy,
|
clipboardCopy: LuClipboardCopy,
|
||||||
|
|||||||
Reference in New Issue
Block a user