mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-06 20:51:39 +00:00
feat: add visual effects and enhance home screen functionality
- Add configurable shimmer and scanline visual effects with toggle settings - Introduce starred albums and tracks sections to home screen - Add flashback feature for album recommendations by decades - Enhance home screen with increased item limits (30) - Update default color scheme to orange-based theme - Implement Backspace/Delete key functionality for removing songs from queue in fullscreen mode
This commit is contained in:
parent
228fc8e82b
commit
bf5e7bc774
17 changed files with 533 additions and 144 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
|
||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
||||
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
||||
|
|
@ -24,6 +25,8 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
|
|||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||
import {
|
||||
Album,
|
||||
AlbumListResponse,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
ServerType,
|
||||
|
|
@ -33,7 +36,7 @@ import {
|
|||
import { Platform } from '/@/shared/types/types';
|
||||
|
||||
const BASE_QUERY_ARGS = {
|
||||
limit: 15,
|
||||
limit: 30,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
};
|
||||
|
|
@ -173,20 +176,255 @@ const HomeRoute = () => {
|
|||
}),
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
(random.isLoading && queriesEnabled[HomeItem.RANDOM]) ||
|
||||
(recentlyPlayed.isLoading && queriesEnabled[HomeItem.RECENTLY_PLAYED] && !isJellyfin) ||
|
||||
(recentlyAdded.isLoading && queriesEnabled[HomeItem.RECENTLY_ADDED]) ||
|
||||
(recentlyReleased.isLoading && queriesEnabled[HomeItem.RECENTLY_RELEASED]) ||
|
||||
(((isJellyfin && mostPlayedSongs.isLoading) ||
|
||||
(!isJellyfin && mostPlayedAlbums.isLoading)) &&
|
||||
const starredAlbums = useQuery(
|
||||
albumQueries.list({
|
||||
options: {
|
||||
enabled: queriesEnabled[HomeItem.STARRED_ALBUMS],
|
||||
staleTime: 1000 * 60 * 5,
|
||||
},
|
||||
query: {
|
||||
...BASE_QUERY_ARGS,
|
||||
favorite: true,
|
||||
sortBy: AlbumListSort.FAVORITED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const starredTracks = useQuery(
|
||||
songsQueries.list(
|
||||
{
|
||||
options: {
|
||||
enabled: queriesEnabled[HomeItem.STARRED_TRACKS],
|
||||
staleTime: 1000 * 60 * 5,
|
||||
},
|
||||
query: {
|
||||
...BASE_QUERY_ARGS,
|
||||
favorite: true,
|
||||
sortBy: SongListSort.FAVORITED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
serverId: server?.id,
|
||||
},
|
||||
300,
|
||||
),
|
||||
);
|
||||
|
||||
// Flashback: Get a random decade from the past
|
||||
// Pre-compute which decades have albums to avoid empty queries
|
||||
const [flashbackSeed, setFlashbackSeed] = useState(0);
|
||||
const [hasFlashbackLoaded, setHasFlashbackLoaded] = useState(false);
|
||||
|
||||
// Get all available decades with albums
|
||||
const availableDecades = useMemo(() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentDecade = Math.floor(currentYear / 10) * 10;
|
||||
const minDecade = 1920;
|
||||
const decades: Array<{ decade: number; maxYear: number; minYear: number }> = [];
|
||||
|
||||
for (let decade = minDecade; decade <= currentDecade; decade += 10) {
|
||||
decades.push({
|
||||
decade,
|
||||
maxYear: decade + 9,
|
||||
minYear: decade,
|
||||
});
|
||||
}
|
||||
|
||||
return decades;
|
||||
}, []);
|
||||
|
||||
// Get count for each decade
|
||||
const decadeQueries = useQuery({
|
||||
enabled: queriesEnabled[HomeItem.FLASHBACK] && !!server?.id,
|
||||
queryFn: async () => {
|
||||
if (!server?.id) return [];
|
||||
|
||||
const promises = availableDecades.map(async ({ decade, maxYear, minYear }) => {
|
||||
try {
|
||||
const count = await api.controller.getAlbumListCount({
|
||||
apiClientProps: { serverId: server.id },
|
||||
query: {
|
||||
maxYear,
|
||||
minYear,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
},
|
||||
});
|
||||
return { count, decade, hasAlbums: count > 0 };
|
||||
} catch (error) {
|
||||
console.error(`Error checking decade ${decade}s:`, error);
|
||||
return { count: 0, decade, hasAlbums: false };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
return results.filter((r) => r.hasAlbums);
|
||||
},
|
||||
queryKey: ['flashback-decades', server?.id],
|
||||
staleTime: 1000 * 60 * 30, // Cache for 30 minutes
|
||||
});
|
||||
|
||||
// Get a random decade from available ones
|
||||
const { flashbackDecade, flashbackMaxYear, flashbackMinYear } = useMemo(() => {
|
||||
if (decadeQueries.data && decadeQueries.data.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * decadeQueries.data.length);
|
||||
const selectedDecade = decadeQueries.data[randomIndex];
|
||||
return {
|
||||
flashbackDecade: selectedDecade.decade,
|
||||
flashbackMaxYear: selectedDecade.decade + 9,
|
||||
flashbackMinYear: selectedDecade.decade,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to current decade if no data yet
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentDecade = Math.floor(currentYear / 10) * 10;
|
||||
return {
|
||||
flashbackDecade: currentDecade,
|
||||
flashbackMaxYear: currentDecade + 9,
|
||||
flashbackMinYear: currentDecade,
|
||||
};
|
||||
// flashbackSeed is intentionally included to force re-computation on refresh
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [decadeQueries.data, flashbackSeed]);
|
||||
|
||||
const flashback = useQuery<AlbumListResponse>({
|
||||
enabled:
|
||||
queriesEnabled[HomeItem.FLASHBACK] &&
|
||||
decadeQueries.data &&
|
||||
decadeQueries.data.length > 0 &&
|
||||
!!server?.id,
|
||||
gcTime: 1000 * 60 * 5,
|
||||
placeholderData: (): AlbumListResponse => {
|
||||
// Create placeholder data with correct structure but empty items
|
||||
// This prevents UI jumping while avoiding cross-decade cache contamination
|
||||
return {
|
||||
items: Array(BASE_QUERY_ARGS.limit)
|
||||
.fill(null)
|
||||
.map((_, index) => ({
|
||||
albumArtist: '',
|
||||
albumArtists: [], // Required for card rows
|
||||
artists: [],
|
||||
backdropImageUrl: null,
|
||||
comment: null,
|
||||
createdAt: '',
|
||||
duration: null,
|
||||
explicitStatus: null,
|
||||
genres: [],
|
||||
id: `placeholder-${flashbackSeed}-${index}`,
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl: null,
|
||||
isCompilation: null,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
lastPlayedAt: null,
|
||||
mbzId: null,
|
||||
name: '',
|
||||
originalDate: null,
|
||||
participants: null,
|
||||
playCount: null,
|
||||
recordLabels: [],
|
||||
releaseDate: null,
|
||||
releaseTypes: [],
|
||||
releaseYear: null,
|
||||
serverId: server?.id || '',
|
||||
serverType: server?.type || ServerType.JELLYFIN,
|
||||
size: null,
|
||||
songCount: null,
|
||||
tags: null,
|
||||
uniqueId: `placeholder-${flashbackSeed}-${index}`,
|
||||
updatedAt: '',
|
||||
userFavorite: false,
|
||||
userRating: null,
|
||||
version: null,
|
||||
})) as Album[],
|
||||
startIndex: 0,
|
||||
totalRecordCount: 0,
|
||||
};
|
||||
},
|
||||
queryFn: ({ signal }) => {
|
||||
const result = api.controller.getAlbumList({
|
||||
apiClientProps: { serverId: server?.id, signal },
|
||||
query: {
|
||||
...BASE_QUERY_ARGS,
|
||||
maxYear: flashbackMaxYear,
|
||||
minYear: flashbackMinYear,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
},
|
||||
});
|
||||
result.then((data) => {
|
||||
console.log(
|
||||
`[Flashback] Fetched ${data.items?.length || 0} albums for ${flashbackDecade}s (seed: ${flashbackSeed})`,
|
||||
);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
queryKey: [
|
||||
'albums',
|
||||
'list',
|
||||
server?.id,
|
||||
{
|
||||
...BASE_QUERY_ARGS,
|
||||
_flashbackSeed: flashbackSeed, // Force unique cache key
|
||||
maxYear: flashbackMaxYear,
|
||||
minYear: flashbackMinYear,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
},
|
||||
],
|
||||
staleTime: 0, // Force fresh data every time to prevent cross-decade caching
|
||||
});
|
||||
|
||||
// Track when Flashback has successfully loaded at least once
|
||||
useEffect(() => {
|
||||
if (flashback.data && !hasFlashbackLoaded) {
|
||||
setHasFlashbackLoaded(true);
|
||||
}
|
||||
}, [flashback.data, hasFlashbackLoaded]);
|
||||
|
||||
// Only show flashback if we have decades with albums
|
||||
const shouldShowFlashback = decadeQueries.data && decadeQueries.data.length > 0;
|
||||
|
||||
// Only show full-page spinner on initial load, not on refetch
|
||||
const isInitialLoading =
|
||||
(random.isLoading && !random.data && queriesEnabled[HomeItem.RANDOM]) ||
|
||||
(recentlyPlayed.isLoading &&
|
||||
!recentlyPlayed.data &&
|
||||
queriesEnabled[HomeItem.RECENTLY_PLAYED] &&
|
||||
!isJellyfin) ||
|
||||
(recentlyAdded.isLoading &&
|
||||
!recentlyAdded.data &&
|
||||
queriesEnabled[HomeItem.RECENTLY_ADDED]) ||
|
||||
(recentlyReleased.isLoading &&
|
||||
!recentlyReleased.data &&
|
||||
queriesEnabled[HomeItem.RECENTLY_RELEASED]) ||
|
||||
(starredAlbums.isLoading &&
|
||||
!starredAlbums.data &&
|
||||
queriesEnabled[HomeItem.STARRED_ALBUMS]) ||
|
||||
(starredTracks.isLoading &&
|
||||
!starredTracks.data &&
|
||||
queriesEnabled[HomeItem.STARRED_TRACKS]) ||
|
||||
(flashback.isLoading && !hasFlashbackLoaded && queriesEnabled[HomeItem.FLASHBACK]) ||
|
||||
(((isJellyfin && mostPlayedSongs.isLoading && !mostPlayedSongs.data) ||
|
||||
(!isJellyfin && mostPlayedAlbums.isLoading && !mostPlayedAlbums.data)) &&
|
||||
queriesEnabled[HomeItem.MOST_PLAYED]);
|
||||
|
||||
if (isLoading) {
|
||||
if (isInitialLoading) {
|
||||
return <Spinner container />;
|
||||
}
|
||||
|
||||
const carousels = {
|
||||
const carousels: Record<HomeItem, any> = {
|
||||
[HomeItem.FLASHBACK]: {
|
||||
data: flashback?.data?.items,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
onRefresh: () => {
|
||||
// Incrementing seed changes query key, which automatically triggers a new fetch
|
||||
setFlashbackSeed((prev) => prev + 1);
|
||||
},
|
||||
query: flashback,
|
||||
title: `${t('page.home.flashback', { postProcess: 'sentenceCase' })} - ${flashbackDecade}s`,
|
||||
},
|
||||
[HomeItem.MOST_PLAYED]: {
|
||||
data: isJellyfin ? mostPlayedSongs?.data?.items : mostPlayedAlbums?.data?.items,
|
||||
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
|
||||
|
|
@ -217,6 +455,18 @@ const HomeRoute = () => {
|
|||
query: recentlyReleased,
|
||||
title: t('page.home.recentlyReleased', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
[HomeItem.STARRED_ALBUMS]: {
|
||||
data: starredAlbums?.data?.items,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
query: starredAlbums,
|
||||
title: t('page.home.starredAlbums', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
[HomeItem.STARRED_TRACKS]: {
|
||||
data: starredTracks?.data?.items,
|
||||
itemType: LibraryItem.SONG,
|
||||
query: starredTracks,
|
||||
title: t('page.home.starredTracks', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
};
|
||||
|
||||
const sortedCarousel = homeItems
|
||||
|
|
@ -227,6 +477,10 @@ const HomeRoute = () => {
|
|||
if (isJellyfin && item.id === HomeItem.RECENTLY_PLAYED) {
|
||||
return false;
|
||||
}
|
||||
// Don't show flashback carousel if it has no data
|
||||
if (item.id === HomeItem.FLASHBACK && !shouldShowFlashback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
|
|
@ -310,7 +564,11 @@ const HomeRoute = () => {
|
|||
<Group>
|
||||
<TextTitle order={3}>{carousel.title}</TextTitle>
|
||||
<ActionIcon
|
||||
onClick={() => carousel.query.refetch()}
|
||||
onClick={() =>
|
||||
'onRefresh' in carousel
|
||||
? carousel.onRefresh()
|
||||
: carousel.query.refetch()
|
||||
}
|
||||
variant="transparent"
|
||||
>
|
||||
<Icon icon="refresh" />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@ import { useMergedRef } from '@mantine/hooks';
|
|||
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import isElectron from 'is-electron';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||
|
|
@ -28,6 +36,7 @@ import {
|
|||
useCurrentStatus,
|
||||
useDefaultQueue,
|
||||
usePlayerControls,
|
||||
usePlayerStore,
|
||||
usePreviousSong,
|
||||
useQueueControls,
|
||||
useVolume,
|
||||
|
|
@ -53,9 +62,10 @@ type QueueProps = {
|
|||
|
||||
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMergedRef(ref, tableRef);
|
||||
const queue = useDefaultQueue();
|
||||
const { reorderQueue, setCurrentTrack } = useQueueControls();
|
||||
const { removeFromQueue, reorderQueue, setCurrentTrack } = useQueueControls();
|
||||
const currentSong = useCurrentSong();
|
||||
const previousSong = usePreviousSong();
|
||||
const status = useCurrentStatus();
|
||||
|
|
@ -257,42 +267,98 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
|||
|
||||
const onCellContextMenu = useHandleTableContextMenu(LibraryItem.SONG, QUEUE_CONTEXT_MENU_ITEMS);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// Check if Delete or Backspace key was pressed
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
const { api } = tableRef?.current || {};
|
||||
if (!api) return;
|
||||
|
||||
const selectedNodes = api.getSelectedNodes();
|
||||
if (!selectedNodes || selectedNodes.length === 0) return;
|
||||
|
||||
const uniqueIds = selectedNodes.map((node) => node.data?.uniqueId).filter(Boolean);
|
||||
if (!uniqueIds.length) return;
|
||||
|
||||
const currentSongState = usePlayerStore.getState().current.song;
|
||||
const playerData = removeFromQueue(uniqueIds as string[]);
|
||||
const isCurrentSongRemoved =
|
||||
currentSongState && uniqueIds.includes(currentSongState?.uniqueId);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
if (isCurrentSongRemoved) {
|
||||
setQueue(playerData, false);
|
||||
} else {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
}
|
||||
|
||||
api.redrawRows();
|
||||
|
||||
if (isCurrentSongRemoved) {
|
||||
updateSong(playerData.current.song);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
[playbackType, removeFromQueue],
|
||||
);
|
||||
|
||||
// Add keyboard event listener to container
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<VirtualTable
|
||||
alwaysShowHorizontalScroll
|
||||
autoFitColumns={tableConfig.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
context={{
|
||||
currentSong,
|
||||
handleDoubleClick,
|
||||
isFocused,
|
||||
isQueue: true,
|
||||
itemType: LibraryItem.SONG,
|
||||
onCellContextMenu,
|
||||
status,
|
||||
}}
|
||||
deselectOnClickOutside={type === 'fullScreen'}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
onCellContextMenu={onCellContextMenu}
|
||||
onCellDoubleClicked={handleDoubleClick}
|
||||
onColumnMoved={handleColumnChange}
|
||||
onColumnResized={debouncedColumnChange}
|
||||
onDragStarted={handleDragStart}
|
||||
onGridReady={handleGridReady}
|
||||
onGridSizeChanged={handleGridSizeChange}
|
||||
onRowDragEnd={handleDragEnd}
|
||||
ref={mergedRef}
|
||||
rowBuffer={50}
|
||||
rowClassRules={rowClassRules}
|
||||
rowData={songs}
|
||||
rowDragEntireRow
|
||||
rowDragMultiRow
|
||||
rowHeight={tableConfig.rowHeight || 40}
|
||||
suppressCellFocus={type === 'fullScreen'}
|
||||
/>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}
|
||||
tabIndex={0}
|
||||
>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<VirtualTable
|
||||
alwaysShowHorizontalScroll
|
||||
autoFitColumns={tableConfig.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
context={{
|
||||
currentSong,
|
||||
handleDoubleClick,
|
||||
isFocused,
|
||||
isQueue: true,
|
||||
itemType: LibraryItem.SONG,
|
||||
onCellContextMenu,
|
||||
status,
|
||||
}}
|
||||
deselectOnClickOutside={type === 'fullScreen'}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
onCellContextMenu={onCellContextMenu}
|
||||
onCellDoubleClicked={handleDoubleClick}
|
||||
onColumnMoved={handleColumnChange}
|
||||
onColumnResized={debouncedColumnChange}
|
||||
onDragStarted={handleDragStart}
|
||||
onGridReady={handleGridReady}
|
||||
onGridSizeChanged={handleGridSizeChange}
|
||||
onRowDragEnd={handleDragEnd}
|
||||
ref={mergedRef}
|
||||
rowBuffer={50}
|
||||
rowClassRules={rowClassRules}
|
||||
rowData={songs}
|
||||
rowDragEntireRow
|
||||
rowDragMultiRow
|
||||
rowHeight={tableConfig.rowHeight || 40}
|
||||
suppressCellFocus={type === 'fullScreen'}
|
||||
/>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,21 +46,22 @@
|
|||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
transparent 0%,
|
||||
var(--album-color, rgba(0, 255, 255, 0.15)) 50%,
|
||||
var(--album-color, rgb(0 255 255 / 15%)) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 100% 200%;
|
||||
animation: scanline 6s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% {
|
||||
background-position: 0 -100vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 100vh;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -429,7 +429,9 @@ export const FullScreenPlayer = () => {
|
|||
});
|
||||
|
||||
// Convert RGB to RGB with opacity for scanline effect
|
||||
const scanlineColor = background ? background.replace('rgb', 'rgba').replace(')', ', 0.15)') : 'rgba(0, 255, 255, 0.15)';
|
||||
const scanlineColor = background
|
||||
? background.replace('rgb', 'rgba').replace(')', ', 0.15)')
|
||||
: 'rgba(0, 255, 255, 0.15)';
|
||||
|
||||
const imageUrl = currentSong?.imageUrl && currentSong.imageUrl.replace(/size=\d+/g, 'size=500');
|
||||
const backgroundImage =
|
||||
|
|
@ -460,7 +462,7 @@ export const FullScreenPlayer = () => {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
<div
|
||||
className={styles.scanlineOverlay}
|
||||
style={
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
filter: drop-shadow(0 0 8px var(--theme-orange-transparent-40));
|
||||
}
|
||||
|
||||
|
|
@ -39,20 +38,23 @@
|
|||
|
||||
.player-button.active {
|
||||
svg {
|
||||
fill: var(--theme-orange-base);
|
||||
filter: drop-shadow(0 0 6px var(--theme-orange-transparent-50));
|
||||
fill: var(--theme-orange-base);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
background: linear-gradient(135deg, var(--theme-orange-base), var(--theme-orange-medium)) !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--theme-orange-base),
|
||||
var(--theme-orange-medium)
|
||||
) !important;
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--theme-shadow-orange-glow-soft);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--theme-shadow-orange-glow-medium);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.container {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
border-top: 2px solid var(--theme-orange-transparent-40);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--theme-colors-background) 0%,
|
||||
rgba(2, 26, 26, 0.95) 100%
|
||||
rgb(2 26 26 / 95%) 100%
|
||||
);
|
||||
border-top: 2px solid var(--theme-orange-transparent-40);
|
||||
box-shadow: 0 -4px 12px var(--theme-orange-transparent-15);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import { DraggableItems } from '/@/renderer/features/settings/components/general
|
|||
import { HomeItem, useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
|
||||
const HOME_ITEMS: Array<[string, string]> = [
|
||||
[HomeItem.RANDOM, 'page.home.explore'],
|
||||
[HomeItem.FLASHBACK, 'page.home.flashback'],
|
||||
[HomeItem.RECENTLY_PLAYED, 'page.home.recentlyPlayed'],
|
||||
[HomeItem.RANDOM, 'page.home.explore'],
|
||||
[HomeItem.RECENTLY_ADDED, 'page.home.newlyAdded'],
|
||||
[HomeItem.RECENTLY_RELEASED, 'page.home.recentlyReleased'],
|
||||
[HomeItem.STARRED_ALBUMS, 'page.home.starredAlbums'],
|
||||
[HomeItem.STARRED_TRACKS, 'page.home.starredTracks'],
|
||||
[HomeItem.MOST_PLAYED, 'page.home.mostPlayed'],
|
||||
[HomeItem.RECENTLY_RELEASED, 'page.home.recentlyReleased'],
|
||||
];
|
||||
|
||||
export const HomeSettings = () => {
|
||||
|
|
|
|||
|
|
@ -161,6 +161,48 @@ export const ThemeSettings = () => {
|
|||
}),
|
||||
title: t('setting.accentColor', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
checked={settings.enableShimmerEffect}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
enableShimmerEffect: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: t('setting.enableShimmerEffect', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: t('setting.enableShimmerEffect', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
checked={settings.enableScanlineEffect}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
enableScanlineEffect: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: t('setting.enableScanlineEffect', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: t('setting.enableScanlineEffect', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={themeOptions} />;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue