Compare commits

..

18 Commits

Author SHA1 Message Date
jeffvli 2c5671cf38 update to v0.16.0 2025-06-26 01:39:47 -07:00
Hosted Weblate bd12fbecac Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (674 of 674 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2025-06-26 10:37:11 +02:00
Hosted Weblate c1d88ada91 Translated using Weblate (Finnish)
Currently translated at 100.0% (676 of 676 strings)

Co-authored-by: jonoafi <joona@jonottaa.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2025-06-26 10:37:11 +02:00
Hosted Weblate d6a3e1d90b Translated using Weblate (French)
Currently translated at 100.0% (674 of 674 strings)

Translated using Weblate (French)

Currently translated at 99.2% (669 of 674 strings)

Translated using Weblate (French)

Currently translated at 99.2% (669 of 674 strings)

Co-authored-by: Dylan MONTIGAUD <dylanmontigaud17@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <kosmomoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-06-26 10:37:11 +02:00
Hosted Weblate 789c7f3d81 Translated using Weblate (Spanish)
Currently translated at 100.0% (674 of 674 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2025-06-26 10:37:11 +02:00
Hosted Weblate f3c785d0fa Translated using Weblate (German)
Currently translated at 86.7% (585 of 674 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Slincess <kisacikdevran0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2025-06-26 10:37:10 +02:00
jeffvli 062c1c2b61 decrease brightness of header overlay on dark 2025-06-26 01:36:54 -07:00
jeffvli eb078d62cd more adjustments to styles on the fullscreen player 2025-06-26 01:36:42 -07:00
jeffvli c429ac9223 move fonts to assets folder 2025-06-26 01:36:16 -07:00
jeffvli bd26967ff2 fix word breaks on lyrics (#969) 2025-06-26 01:11:46 -07:00
jeffvli 620b810191 add option to set local lyric priority 2025-06-25 21:25:29 -07:00
jeffvli 64866c59bd adjust styles on fullscreen player image section
- fix image transition
- fix image aspect ratio
- adjust text sizes and shadow
2025-06-25 20:40:45 -07:00
jeffvli 0afbe4c0a2 improve visibility of fullscreen player buttons 2025-06-25 19:53:49 -07:00
jeffvli 6782cd0dcc re-add presence animation when collapsing sidebar image 2025-06-25 19:48:59 -07:00
jeffvli 8f585a5be9 adjust styles to better support light theme 2025-06-25 19:44:28 -07:00
jeffvli ac0c396712 fix sidebar image height when using Windows or macOS window bar 2025-06-25 09:02:22 -07:00
Kendall Garner b989a66991 only show owned playlists on playlist sidebar 2025-06-25 08:19:22 -07:00
Kendall Garner 2814b623e7 fix player button light theme and tooltip 2025-06-25 08:05:57 -07:00
46 changed files with 318 additions and 144 deletions
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "0.15.1",
"version": "0.16.0",
"description": "A modern self-hosted music player.",
"keywords": [
"subsonic",
+19 -4
View File
@@ -113,7 +113,9 @@
"trackPeak": "Track-Spitzenpegel",
"codec": "Codec",
"albumPeak": "Album-Spitzenpegel",
"albumGain": "Album-Pegelverstärkung"
"albumGain": "Album-Pegelverstärkung",
"tags": "tags",
"viewReleaseNotes": "Release Notes anzeigen"
},
"error": {
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
@@ -237,7 +239,8 @@
"description": "Beschreibung",
"setExpiration": "Ablaufdatum setzen",
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
"allowDownloading": "Herunterladen zulassen"
"allowDownloading": "Herunterladen zulassen",
"success": "Link in die Zwischenablage kopiert (oder hier klicken um zu öffnen)"
}
},
"entity": {
@@ -429,7 +432,8 @@
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)",
"shared": "$t(entity.playlist_other) geteilt"
"shared": "$t(entity.playlist_other) geteilt",
"myLibrary": "meine bibliothek"
},
"setting": {
"playbackTab": "Wiedergabe",
@@ -676,6 +680,17 @@
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
"zoom_description": "Setzt den Zoom (in %) für das Programm",
"zoom": "Zoom"
"zoom": "Zoom",
"albumBackground": "Album Hintergrund",
"customCss": "Benutzerdefiniert css",
"homeConfiguration": "Startseite Konfiguration",
"lastfmApiKey": "{{lastfm}} API-Schlüssel",
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für benötigt",
"discordListening": "Status als hört zu anzeigen",
"discordListening_description": "Status als hört zu statt als spielt anzeigen",
"lastfm": "zeige last.fm links",
"lastfm_description": "zeige links zu last.fm auf dem Künstler/Album-Seiten",
"musicbrainz": "Zeig musicbrainz links",
"customCssEnable": "aktiviere Benutzerdefinierte css"
}
}
+2
View File
@@ -536,6 +536,8 @@
"floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue",
"followLyric": "follow current lyric",
"followLyric_description": "scroll the lyric to the current playing position",
"preferLocalLyrics": "prefer local lyrics",
"preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available",
"font": "font",
"font_description": "sets the font to use for the application",
"fontType": "font type",
+10 -4
View File
@@ -385,7 +385,9 @@
"preview": "Vista previa",
"translation": "traducción",
"additionalParticipants": "Participantes adicionales",
"tags": "Etiquetas"
"tags": "Etiquetas",
"newVersion": "Una nueva versión ha sido instalada ({{version}})",
"viewReleaseNotes": "Ver notas de lanzamiento"
},
"error": {
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
@@ -469,7 +471,8 @@
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)",
"shared": "compartido $t(entity.playlist_other)"
"shared": "compartido $t(entity.playlist_other)",
"myLibrary": "Mi biblioteca"
},
"appMenu": {
"selectServer": "seleccionar servidor",
@@ -655,7 +658,8 @@
},
"queryEditor": {
"input_optionMatchAll": "coincidir todos",
"input_optionMatchAny": "coincidir cualquiera"
"input_optionMatchAny": "coincidir cualquiera",
"title": "Editor de consultas"
},
"shareItem": {
"createFailed": "No se pudo crear el recurso compartido (¿está habilitado el uso compartido?)",
@@ -737,7 +741,9 @@
"view": {
"card": "tarjeta",
"table": "tabla",
"poster": "cartel"
"poster": "cartel",
"list": "Lista",
"grid": "Cuadrícula"
}
}
},
+13 -5
View File
@@ -90,7 +90,9 @@
"trackGain": "raidan vahvistus (gain)",
"trackPeak": "kappaleen huippu (peak)",
"additionalParticipants": "muut osallistujat",
"tags": "tägit"
"tags": "tägit",
"newVersion": "uusi versio on asennettu ({{version}})",
"viewReleaseNotes": "katsele julkaisutietoja"
},
"entity": {
"album_one": "albumi",
@@ -279,7 +281,8 @@
},
"queryEditor": {
"input_optionMatchAny": "sovita joku",
"input_optionMatchAll": "sovita kaikki"
"input_optionMatchAll": "sovita kaikki",
"title": "kyselyeditori"
}
},
"setting": {
@@ -515,7 +518,9 @@
"lastfm_description": "näytä linkit last.fm sivulle artistin/albumin sivuilla",
"musicbrainz": "näytä musicbrainz linkit",
"neteaseTranslation": "Ota NetEasen käännökset käyttöön",
"neteaseTranslation_description": "Käytöss ollessa noutaa ja näyttää käännetyt sanat NetEasesta, jos ne ovat saatavilla."
"neteaseTranslation_description": "Käytöss ollessa noutaa ja näyttää käännetyt sanat NetEasesta, jos ne ovat saatavilla.",
"preferLocalLyrics_description": "suosi paikallisia sanoituksia ulkoisten sijasta, kun saatavilla",
"preferLocalLyrics": "suosi paikallisia sanoituksia"
},
"page": {
"itemDetail": {
@@ -584,7 +589,8 @@
"home": "$t(common.home)",
"nowPlaying": "nyt soi",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)"
"search": "$t(common.search)",
"myLibrary": "oma kirjasto"
},
"setting": {
"generalTab": "yleinen",
@@ -745,7 +751,9 @@
"view": {
"table": "taulukko",
"card": "kortti",
"poster": "juliste"
"poster": "juliste",
"grid": "ruudukko",
"list": "lista"
}
},
"column": {
+13 -5
View File
@@ -150,7 +150,9 @@
"codec": "codec",
"translation": "traduction",
"additionalParticipants": "participants additionnels",
"tags": "tags"
"tags": "tags",
"newVersion": "une nouvelle version vient d'être installé ({{version}})",
"viewReleaseNotes": "voir la note de version"
},
"error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
@@ -234,7 +236,8 @@
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)",
"shared": "partagé $t(entity.playlist_other)"
"shared": "partagé $t(entity.playlist_other)",
"myLibrary": "ma bibliothèque"
},
"fullscreenPlayer": {
"config": {
@@ -602,7 +605,9 @@
"lastfm": "affiche les liens de last.fm",
"musicbrainz_description": "affiches les liens vers musicbrainz sur les pages des artistes/albums, quand mbid existes",
"lastfm_description": "affiche les liens vers last.fm sur les pages des artistes/albums",
"musicbrainz": "affiches les liens musicbrainz"
"musicbrainz": "affiches les liens musicbrainz",
"neteaseTranslation": "Activer les traductions NetEase",
"neteaseTranslation_description": "Lorsque cette option est activée, récupère et affiche les paroles traduites de NetEase si elles sont disponibles."
},
"form": {
"deletePlaylist": {
@@ -643,7 +648,8 @@
},
"queryEditor": {
"input_optionMatchAll": "correspondre à tous",
"input_optionMatchAny": "correspondre à n'importe quel"
"input_optionMatchAny": "correspondre à n'importe quel",
"title": "éditeur de requête"
},
"editPlaylist": {
"title": "modifier $t(entity.playlist_one)",
@@ -733,7 +739,9 @@
"view": {
"table": "liste",
"poster": "poster",
"card": "Carte"
"card": "Carte",
"grid": "grille",
"list": "liste"
},
"label": {
"releaseDate": "date de sortie",
+10 -4
View File
@@ -111,7 +111,9 @@
"preview": "预览",
"translation": "翻译",
"additionalParticipants": "其他参与者",
"tags": "标签"
"tags": "标签",
"viewReleaseNotes": "查看发行说明",
"newVersion": "已安装新版本 ({{version}})"
},
"entity": {
"albumArtist_other": "专辑艺术家",
@@ -483,7 +485,8 @@
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)",
"shared": "共享$t(entity.playlist_other)"
"shared": "共享$t(entity.playlist_other)",
"myLibrary": "我的媒体库"
},
"fullscreenPlayer": {
"config": {
@@ -659,7 +662,8 @@
},
"queryEditor": {
"input_optionMatchAll": "匹配全部",
"input_optionMatchAny": "匹配任何"
"input_optionMatchAny": "匹配任何",
"title": "查询编辑器"
},
"editPlaylist": {
"title": "编辑$t(entity.playlist_one)",
@@ -695,7 +699,9 @@
"view": {
"table": "表格",
"poster": "海报",
"card": "卡片"
"card": "卡片",
"grid": "网格",
"list": "列表"
},
"label": {
"releaseDate": "发布日期",
@@ -1,7 +1,9 @@
.lyric-line {
padding: 0 1rem;
font-weight: 600;
line-height: 1.2;
color: var(--theme-colors-foreground);
word-break: keep-all;
opacity: 0.5;
transition:
opacity 0.3s ease-in-out,
@@ -86,7 +86,7 @@ export const useSongLyricsBySong = (
song: QueueSong | undefined,
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
const { query } = args;
const { fetch } = useLyricsSettings();
const { fetch, preferLocalLyrics } = useLyricsSettings();
const server = getServerById(song?.serverId);
return useQuery({
@@ -97,6 +97,9 @@ export const useSongLyricsBySong = (
if (!server) throw new Error('Server not found');
if (!song) return null;
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
let remoteLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
const subsonicLyrics = await api.controller
.getStructuredLyrics({
@@ -106,7 +109,7 @@ export const useSongLyricsBySong = (
.catch(console.error);
if (subsonicLyrics?.length) {
return subsonicLyrics;
localLyrics = subsonicLyrics;
}
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
const jfLyrics = await api.controller
@@ -117,7 +120,7 @@ export const useSongLyricsBySong = (
.catch((err) => console.log(err));
if (jfLyrics) {
return {
localLyrics = {
artist: song.artists?.[0]?.name,
lyrics: jfLyrics,
name: song.name,
@@ -126,7 +129,7 @@ export const useSongLyricsBySong = (
};
}
} else if (song.lyrics) {
return {
localLyrics = {
artist: song.artists?.[0]?.name,
lyrics: formatLyrics(song.lyrics),
name: song.name,
@@ -135,12 +138,16 @@ export const useSongLyricsBySong = (
};
}
if (preferLocalLyrics && localLyrics) {
return localLyrics;
}
if (fetch) {
const remoteLyricsResult: InternetProviderLyricResponse | null =
await lyricsIpc?.getRemoteLyricsBySong(song);
if (remoteLyricsResult) {
return {
remoteLyrics = {
...remoteLyricsResult,
lyrics: formatLyrics(remoteLyricsResult.lyrics),
remote: true,
@@ -148,6 +155,14 @@ export const useSongLyricsBySong = (
}
}
if (remoteLyrics) {
return remoteLyrics;
}
if (localLyrics) {
return localLyrics;
}
return null;
},
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
@@ -183,9 +198,7 @@ export const useSongLyricsByRemoteId = (
);
},
queryFn: async () => {
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
query as LyricGetQuery,
);
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(query as any);
if (remoteLyricsResult) {
return formatLyrics(remoteLyricsResult);
@@ -2,8 +2,6 @@
position: absolute;
max-width: 100%;
height: 100%;
object-fit: var(--theme-image-fit);
object-position: 50% 100%;
border-radius: 5px;
filter: drop-shadow(0 0 5px rgb(0 0 0 / 40%)) drop-shadow(0 0 5px rgb(0 0 0 / 40%));
}
@@ -24,8 +22,13 @@
justify-content: center;
padding: 1rem;
text-align: center;
cursor: default;
border-radius: 5px;
a {
cursor: pointer;
}
h1 {
font-size: 3.5vh;
}
@@ -16,9 +16,7 @@ import { Center } from '/@/shared/components/center/center';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Image } from '/@/shared/components/image/image';
import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text';
import { PlayerData, QueueSong } from '/@/shared/types/domain-types';
@@ -52,9 +50,14 @@ const scaleImageUrl = (imageSize: number, url?: null | string) => {
.replace(/&height=\d+/, `&height=${imageSize}`);
};
const MotionImage = motion.create(Image);
const MotionImage = motion.img;
const ImageWithPlaceholder = ({
className,
...props
}: HTMLMotionProps<'img'> & { placeholder?: string }) => {
const nativeAspectRatio = useSettingsStore((store) => store.general.nativeAspectRatio);
const ImageWithPlaceholder = ({ ...props }: HTMLMotionProps<'img'> & { placeholder?: string }) => {
if (!props.src) {
return (
<Center
@@ -76,7 +79,11 @@ const ImageWithPlaceholder = ({ ...props }: HTMLMotionProps<'img'> & { placehold
return (
<MotionImage
className={styles.image}
className={clsx(styles.image, className)}
style={{
objectFit: nativeAspectRatio ? 'contain' : 'cover',
width: nativeAspectRatio ? 'auto' : '100%',
}}
{...props}
/>
);
@@ -201,45 +208,35 @@ export const FullScreenPlayerImage = () => {
</div>
<Stack
className={styles.metadataContainer}
gap="xs"
gap="md"
maw="100%"
>
<TextTitle
<Text
fw={900}
order={1}
lh="1.2"
overflow="hidden"
size="4xl"
w="100%"
>
{currentSong?.name}
</TextTitle>
<TextTitle
</Text>
<Text
component={Link}
fw={600}
isLink
order={3}
overflow="hidden"
style={{
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
}}
size="xl"
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: currentSong?.albumId || '',
})}
w="100%"
>
{currentSong?.album}{' '}
</TextTitle>
<TextTitle
key="fs-artists"
order={3}
style={{
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
}}
>
{currentSong?.album}
</Text>
<Text key="fs-artists">
{currentSong?.artists?.map((artist, index) => (
<Fragment key={`fs-artist-${artist.id}`}>
{index > 0 && (
<Text
isMuted
style={{
display: 'inline-block',
padding: '0 0.5rem',
@@ -250,12 +247,7 @@ export const FullScreenPlayerImage = () => {
)}
<Text
component={Link}
fw={600}
isLink
isMuted
style={{
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
}}
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
})}
@@ -264,7 +256,7 @@ export const FullScreenPlayerImage = () => {
</Text>
</Fragment>
))}
</TextTitle>
</Text>
<Group
justify="center"
mt="sm"
@@ -1,6 +1,6 @@
import { useHotkeys } from '@mantine/hooks';
import { motion, Variants } from 'motion/react';
import { CSSProperties, useLayoutEffect, useRef } from 'react';
import { CSSProperties, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router';
@@ -32,7 +32,11 @@ import { Platform } from '/@/shared/types/types';
const mainBackground = 'var(--theme-colors-background)';
const Controls = () => {
interface ControlsProps {
isPageHovered: boolean;
}
const Controls = ({ isPageHovered }: ControlsProps) => {
const { t } = useTranslation();
const {
dynamicBackground,
@@ -77,7 +81,7 @@ const Controls = () => {
iconProps={{ size: 'lg' }}
onClick={handleToggleFullScreenPlayer}
tooltip={{ label: t('common.minimize', { postProcess: 'titleCase' }) }}
variant="subtle"
variant={isPageHovered ? 'default' : 'subtle'}
/>
<Popover position="bottom-start">
<Popover.Target>
@@ -85,7 +89,7 @@ const Controls = () => {
icon="settings"
iconProps={{ size: 'lg' }}
tooltip={{ label: t('common.configure', { postProcess: 'titleCase' }) }}
variant="subtle"
variant={isPageHovered ? 'default' : 'subtle'}
/>
</Popover.Target>
<Popover.Dropdown>
@@ -410,6 +414,8 @@ export const FullScreenPlayer = () => {
const { setStore } = useFullScreenPlayerStoreActions();
const { windowBarStyle } = useWindowSettings();
const [isPageHovered, setIsPageHovered] = useState(false);
const location = useLocation();
const isOpenedRef = useRef<boolean | null>(null);
@@ -441,10 +447,12 @@ export const FullScreenPlayer = () => {
custom={{ background, backgroundImage, dynamicBackground, windowBarStyle }}
exit="closed"
initial="closed"
onMouseEnter={() => setIsPageHovered(true)}
onMouseLeave={() => setIsPageHovered(false)}
transition={{ duration: 2 }}
variants={containerVariants}
>
<Controls />
<Controls isPageHovered={isPageHovered} />
{dynamicBackground && (
<div
className={styles.backgroundImageOverlay}
@@ -71,7 +71,7 @@ export const LeftControls = () => {
<LayoutGroup>
<AnimatePresence
initial={false}
mode="wait"
mode="popLayout"
>
{!hideImage && (
<div className={styles.imageWrapper}>
@@ -83,7 +83,7 @@ export const LeftControls = () => {
key="playerbar-image"
onClick={handleToggleFullScreenPlayer}
role="button"
transition={{ duration: 0.3, ease: 'easeInOut' }}
transition={{ duration: 0.2, ease: 'easeIn' }}
>
<Tooltip
label={t('player.toggleFullscreenPlayer', {
@@ -37,6 +37,7 @@
}
.main {
background: var(--theme-colors-foreground) !important;
border-radius: 50%;
svg {
@@ -15,7 +15,7 @@ interface PlayerButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
}
export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
({ icon, isActive, tooltip, variant, ...rest }: PlayerButtonProps) => {
({ icon, isActive, tooltip, variant, ...rest }: PlayerButtonProps, ref) => {
if (tooltip) {
return (
<Tooltip {...tooltip}>
@@ -23,6 +23,7 @@ export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
className={clsx({
[styles.active]: isActive,
})}
ref={ref}
{...rest}
onClick={(e) => {
e.stopPropagation();
@@ -41,6 +42,7 @@ export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
className={clsx(styles.playerButton, styles[variant], {
[styles.active]: isActive,
})}
ref={ref}
{...rest}
onClick={(e) => {
e.stopPropagation();
@@ -58,21 +60,23 @@ interface PlayButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
isPaused?: boolean;
}
export const PlayButton = ({ isPaused, ...props }: PlayButtonProps) => {
return (
<ActionIcon
className={styles.main}
icon={isPaused ? 'mediaPlay' : 'mediaPause'}
iconProps={{
size: 'lg',
}}
tooltip={
isPaused
? t('player.play', { postProcess: 'sentenceCase' })
: t('player.pause', { postProcess: 'sentenceCase' })
}
variant="white"
{...props}
/>
);
};
export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>(
({ isPaused, ...props }: PlayButtonProps, ref) => {
return (
<ActionIcon
className={styles.main}
icon={isPaused ? 'mediaPlay' : 'mediaPause'}
iconProps={{
size: 'lg',
}}
ref={ref}
tooltip={{
label: isPaused
? (t('player.play', { postProcess: 'sentenceCase' }) as string)
: (t('player.pause', { postProcess: 'sentenceCase' }) as string),
}}
{...props}
/>
);
},
);
@@ -43,6 +43,28 @@ export const LyricSettings = () => {
}),
title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Prefer local lyrics"
defaultChecked={settings.preferLocalLyrics}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
preferLocalLyrics: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.preferLocalLyrics', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.preferLocalLyrics', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
@@ -14,8 +14,7 @@ export const PlayButton = ({ className, ...props }: PlayButtonProps) => {
className={clsx(styles.button, className)}
icon="mediaPlay"
iconProps={{
fill: 'default',
size: 'lg',
size: 'xl',
}}
variant="filled"
{...props}
@@ -181,7 +181,9 @@ export const SidebarPlaylistList = () => {
const owned: Array<[boolean, () => void] | Playlist> = [];
for (const playlist of data.items) {
owned.push(playlist);
if (!playlist.owner || playlist.owner === server.username) {
owned.push(playlist);
}
}
return { ...base, items: owned };
@@ -24,6 +24,7 @@
.image-container {
position: relative;
width: var(--sidebar-image-height);
height: var(--sidebar-image-height);
cursor: pointer;
animation: fade-in 0.2s ease-in-out;
@@ -19,13 +19,18 @@ import {
useSetFullScreenPlayerStore,
useSidebarStore,
} from '/@/renderer/store';
import { SidebarItemType, useGeneralSettings } from '/@/renderer/store/settings.store';
import {
SidebarItemType,
useGeneralSettings,
useWindowSettings,
} from '/@/renderer/store/settings.store';
import { Accordion } from '/@/shared/components/accordion/accordion';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { Platform } from '/@/shared/types/types';
export const Sidebar = () => {
const { t } = useTranslation();
@@ -64,6 +69,7 @@ export const Sidebar = () => {
};
const { sidebarItems } = useGeneralSettings();
const { windowBarStyle } = useWindowSettings();
const sidebarItemsWithRoute: SidebarItemType[] = useMemo(() => {
if (!sidebarItems) return [];
@@ -80,6 +86,18 @@ export const Sidebar = () => {
return items;
}, [sidebarItems, translatedSidebarItemMap]);
const scrollAreaHeight = useMemo(() => {
if (showImage) {
if (windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS) {
return `calc(100% - 105px - ${sidebar.leftWidth})`;
}
return `calc(100% - ${sidebar.leftWidth})`;
}
return '100%';
}, [showImage, sidebar.leftWidth, windowBarStyle]);
return (
<div
className={styles.container}
@@ -95,7 +113,7 @@ export const Sidebar = () => {
allowDragScroll
className={styles.scrollArea}
style={{
maxHeight: showImage ? `calc(100vh - 90px - ${sidebar.leftWidth})` : '100%',
height: scrollAreaHeight,
}}
>
<Accordion
+2
View File
@@ -266,6 +266,7 @@ export interface SettingsState {
fontSizeUnsync: number;
gap: number;
gapUnsync: number;
preferLocalLyrics: boolean;
showMatch: boolean;
showProvider: boolean;
sources: LyricSource[];
@@ -448,6 +449,7 @@ const initialState: SettingsState = {
fontSizeUnsync: 24,
gap: 24,
gapUnsync: 24,
preferLocalLyrics: true,
showMatch: true,
showProvider: true,
sources: [LyricSource.NETEASE, LyricSource.LRCLIB],
+12 -3
View File
@@ -50,7 +50,10 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
useEffect(() => {
if (type === FontType.SYSTEM && system) {
const root = document.documentElement;
root.style.setProperty('--theme-content-font-family', 'dynamic-font');
root.style.setProperty(
'--theme-content-font-family',
'dynamic-font, "Noto Sans JP", sans-serif',
);
if (!textStyleRef.current) {
textStyleRef.current = document.createElement('style');
@@ -64,7 +67,10 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
}`;
} else if (type === FontType.CUSTOM && custom) {
const root = document.documentElement;
root.style.setProperty('--theme-content-font-family', 'dynamic-font');
root.style.setProperty(
'--theme-content-font-family',
'dynamic-font, "Noto Sans JP", sans-serif',
);
if (!textStyleRef.current) {
textStyleRef.current = document.createElement('style');
@@ -78,7 +84,10 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
}`;
} else {
const root = document.documentElement;
root.style.setProperty('--theme-content-font-family', builtIn);
root.style.setProperty(
'--theme-content-font-family',
`${builtIn}, "Noto Sans JP", sans-serif`,
);
}
}, [builtIn, custom, system, type]);
@@ -53,6 +53,11 @@
&:focus-visible {
background: darken(var(--theme-colors-primary-filled), 10%);
}
svg {
color: var(--theme-colors-primary-contrast);
fill: var(--theme-colors-primary-contrast);
}
}
&[data-variant='subtle'] {
@@ -60,8 +65,15 @@
background: transparent;
&:hover,
&:active,
&:focus-visible {
background: lighten(var(--theme-colors-surface), 10%);
@mixin dark {
background: lighten(var(--theme-colors-background), 5%);
}
@mixin light {
background: darken(var(--theme-colors-background), 5%);
}
}
}
@@ -92,7 +92,13 @@
&:hover,
&:active,
&:focus-visible {
background-color: lighten(var(--button-bg), 10%);
@mixin dark {
background-color: lighten(var(--theme-colors-background), 10%);
}
@mixin light {
background-color: darken(var(--theme-colors-background), 5%);
}
}
}
@@ -100,11 +106,11 @@
border: 1px solid transparent;
&:hover {
background-color: darken(var(--button-bg), 5%);
background-color: darken(var(--theme-colors-background), 5%);
}
&:focus-visible {
background-color: darken(var(--button-bg), 10%);
background-color: darken(var(--theme-colors-background), 10%);
}
}
@@ -70,6 +70,10 @@
fill: var(--theme-colors-foreground);
}
.fill-contrast {
fill: var(--theme-colors-primary-contrast);
}
.fill-inherit {
fill: inherit;
}
+12 -2
View File
@@ -224,11 +224,21 @@ export const AppIcon = {
export interface IconProps extends Omit<IconBaseProps, 'color' | 'fill' | 'size'> {
animate?: 'pulse' | 'spin';
color?: 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn';
fill?: 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn';
color?: IconColor;
fill?: IconColor;
icon: keyof typeof AppIcon;
size?: '2xl' | '3xl' | '4xl' | '5xl' | 'lg' | 'md' | 'sm' | 'xl' | 'xs' | number | string;
}
type IconColor =
| 'contrast'
| 'default'
| 'error'
| 'info'
| 'inherit'
| 'muted'
| 'primary'
| 'success'
| 'warn';
export const Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
const { animate, className, color, fill, icon, size = 'md' } = props;
+37 -7
View File
@@ -1,16 +1,18 @@
import type { ImgHTMLAttributes } from 'react';
import clsx from 'clsx';
import { motion, MotionConfigProps } from 'motion/react';
import { type ImgHTMLAttributes } from 'react';
import { Img } from 'react-image';
import styles from './image.module.css';
import { animationProps } from '/@/shared/components/animations/animation-props';
import { Icon } from '/@/shared/components/icon/icon';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
interface ImageContainerProps {
interface ImageContainerProps extends MotionConfigProps {
children: React.ReactNode;
className?: string;
enableAnimation?: boolean;
}
interface ImageLoaderProps {
@@ -19,6 +21,8 @@ interface ImageLoaderProps {
interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
containerClassName?: string;
enableAnimation?: boolean;
imageContainerProps?: Omit<ImageContainerProps, 'children'>;
includeLoader?: boolean;
includeUnloader?: boolean;
src: string | string[] | undefined;
@@ -32,6 +36,8 @@ interface ImageUnloaderProps {
export function Image({
className,
containerClassName,
enableAnimation,
imageContainerProps,
includeLoader = true,
includeUnloader = true,
src,
@@ -41,7 +47,13 @@ export function Image({
<Img
className={clsx(styles.image, className)}
container={(children) => (
<ImageContainer className={containerClassName}>{children}</ImageContainer>
<ImageContainer
className={containerClassName}
enableAnimation={enableAnimation}
{...imageContainerProps}
>
{children}
</ImageContainer>
)}
loader={
includeLoader ? (
@@ -50,7 +62,6 @@ export function Image({
</ImageContainer>
) : null
}
loading="lazy"
src={src}
unloader={
includeUnloader ? (
@@ -66,8 +77,27 @@ export function Image({
return <ImageUnloader />;
}
function ImageContainer({ children, className }: ImageContainerProps) {
return <div className={clsx(styles.imageContainer, className)}>{children}</div>;
function ImageContainer({ children, className, enableAnimation, ...props }: ImageContainerProps) {
if (!enableAnimation) {
return (
<div
className={clsx(styles.imageContainer, className)}
{...props}
>
{children}
</div>
);
}
return (
<motion.div
className={clsx(styles.imageContainer, className)}
{...animationProps.fadeIn}
{...props}
>
{children}
</motion.div>
);
}
function ImageLoader({ className }: ImageLoaderProps) {
+20 -29
View File
@@ -11,7 +11,8 @@
text-rendering: optimizelegibility;
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
text-size-adjust: none;
outline: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@@ -122,59 +123,56 @@ button {
@font-face {
font-family: Archivo;
font-weight: 100 1000;
src: url('../../renderer/fonts/Archivo-VariableFont_wdth,wght.ttf')
format('truetype-variations');
src: url('../../../assets/fonts/Archivo-VariableFont_wdth,wght.ttf');
}
@font-face {
font-family: Raleway;
font-weight: 100 1000;
src: url('../../renderer/fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations');
src: url('../../../assets/fonts/Raleway-VariableFont_wght.ttf');
}
@font-face {
font-family: Fredoka;
font-weight: 100 1000;
src: url('../../renderer/fonts/Fredoka-VariableFont_wdth,wght.ttf')
format('truetype-variations');
src: url('../../../assets/fonts/Fredoka-VariableFont_wdth,wght.ttf');
}
@font-face {
font-family: 'League Spartan';
font-weight: 100 1000;
src: url('../../renderer/fonts/LeagueSpartan-VariableFont_wght.ttf')
format('truetype-variations');
src: url('../../../assets/fonts/LeagueSpartan-VariableFont_wght.ttf');
}
@font-face {
font-family: Lexend;
font-weight: 100 1000;
src: url('../../renderer/fonts/Lexend-VariableFont_wght.ttf') format('truetype-variations');
src: url('../../../assets/fonts/Lexend-VariableFont_wght.ttf');
}
@font-face {
font-family: Inter;
font-weight: 100 1000;
src: url('../../renderer/fonts/Inter-VariableFont_slnt,wght.ttf') format('truetype-variations');
src: url('../../../assets/fonts/Inter-VariableFont_slnt,wght.ttf');
}
@font-face {
font-family: Sora;
font-weight: 100 1000;
src: url('../../renderer/fonts/Sora-VariableFont_wght.ttf') format('truetype-variations');
src: url('../../../assets/fonts/Sora-VariableFont_wght.ttf');
}
@font-face {
font-family: 'Work Sans';
font-weight: 100 1000;
src: url('../../renderer/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations');
src: url('../../../assets/fonts/WorkSans-VariableFont_wght.ttf');
}
@font-face {
font-family: Poppins;
font-style: normal;
font-weight: 400;
src: url('../../renderer/fonts/Poppins-Regular.ttf') format('truetype');
src: url('../../../assets/fonts/Poppins-Regular.ttf');
font-display: swap;
}
@@ -182,7 +180,7 @@ button {
font-family: Poppins;
font-style: normal;
font-weight: 600;
src: url('../../renderer/fonts/Poppins-SemiBold.ttf') format('truetype');
src: url('../../../assets/fonts/Poppins-SemiBold.ttf');
font-display: swap;
}
@@ -190,7 +188,7 @@ button {
font-family: Poppins;
font-style: normal;
font-weight: 700;
src: url('../../renderer/fonts/Poppins-Bold.ttf') format('truetype');
src: url('../../../assets/fonts/Poppins-Bold.ttf');
font-display: swap;
}
@@ -198,7 +196,7 @@ button {
font-family: Poppins;
font-style: normal;
font-weight: 800;
src: url('../../renderer/fonts/Poppins-ExtraBold.ttf') format('truetype');
src: url('../../../assets/fonts/Poppins-ExtraBold.ttf');
font-display: swap;
}
@@ -206,28 +204,21 @@ button {
font-family: Poppins;
font-style: normal;
font-weight: 900;
src: url('../../renderer/fonts/Poppins-Black.ttf') format('truetype');
src: url('../../../assets/fonts/Poppins-Black.ttf');
font-display: swap;
}
@font-face {
font-family: Raleway;
font-weight: 100 1000;
src: url('../../renderer/fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations');
src: url('../../../assets/fonts/Raleway-VariableFont_wght.ttf');
}
@font-face {
font-family: DroidSerif;
src: url('https://rawgit.com/google/fonts/master/ufl/ubuntumono/UbuntuMono-Italic.ttf')
format('truetype');
unicode-range: U+000-5FF; /* Latin glyphs */
}
@font-face {
font-family: DroidSerif;
src: url('https://fonts.gstatic.com/ea/notosansjp/v5/NotoSansJP-Regular.woff2')
format('truetype');
unicode-range: U+3000-9FFF, U+ff??; /* Japanese glyphs */
font-family: 'Noto Sans JP';
font-weight: 100 900;
src: url('../../../assets/fonts/NotoSansJP-VariableFont_wght.ttf');
unicode-range: U+3000-9FFF, U+FF00-FFEF; /* Japanese characters */
}
:root {
@@ -11,8 +11,8 @@ export const defaultLight: AppThemeConfiguration = {
'scrollbar-track-background': 'transparent',
},
colors: {
background: 'rgb(255, 255, 255)',
'background-alternate': 'rgb(245, 245, 245)',
background: 'rgb(235, 235, 235)',
'background-alternate': 'rgb(240, 240, 240)',
black: 'rgb(0, 0, 0)',
foreground: 'rgb(25, 25, 25)',
'foreground-muted': 'rgb(80, 80, 80)',
@@ -20,7 +20,7 @@ export const defaultLight: AppThemeConfiguration = {
'state-info': 'rgb(0, 122, 255)',
'state-success': 'rgb(48, 209, 88)',
'state-warning': 'rgb(255, 214, 0)',
surface: 'rgb(245, 245, 245)',
surface: 'rgb(225, 225, 225)',
'surface-foreground': 'rgb(0, 0, 0)',
white: 'rgb(255, 255, 255)',
},
+1 -1
View File
@@ -3,7 +3,7 @@ import { AppThemeConfiguration } from './app-theme-types';
export const defaultTheme: AppThemeConfiguration = {
app: {
'overlay-header':
'linear-gradient(transparent 0%, rgb(0 0 0 / 75%) 100%), var(--theme-background-noise)',
'linear-gradient(transparent 0%, rgb(0 0 0 / 85%) 100%), var(--theme-background-noise)',
'overlay-subheader':
'linear-gradient(180deg, rgb(0 0 0 / 5%) 0%, var(--theme-colors-background) 100%), var(--theme-background-noise)',
'root-font-size': '16px',