mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +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,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.23.0",
|
"version": "0.24.0",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
|
|
|
||||||
|
|
@ -421,11 +421,14 @@
|
||||||
"title": "commands"
|
"title": "commands"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"explore": "explore from your library",
|
"explore": "discovery",
|
||||||
|
"flashback": "flashback",
|
||||||
"mostPlayed": "most played",
|
"mostPlayed": "most played",
|
||||||
"newlyAdded": "newly added releases",
|
"newlyAdded": "recently added",
|
||||||
"recentlyPlayed": "recently played",
|
"recentlyPlayed": "recently played",
|
||||||
"recentlyReleased": "recently released",
|
"recentlyReleased": "recently released",
|
||||||
|
"starredAlbums": "starred albums",
|
||||||
|
"starredTracks": "starred tracks",
|
||||||
"title": "$t(common.home)"
|
"title": "$t(common.home)"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
|
|
@ -600,6 +603,10 @@
|
||||||
"enableAutoTranslation": "enable auto translation",
|
"enableAutoTranslation": "enable auto translation",
|
||||||
"enableRemote_description": "enables the remote control server to allow other devices to control the application",
|
"enableRemote_description": "enables the remote control server to allow other devices to control the application",
|
||||||
"enableRemote": "enable remote control server",
|
"enableRemote": "enable remote control server",
|
||||||
|
"enableScanlineEffect_description": "enable the animated scanline visual effect across the application",
|
||||||
|
"enableScanlineEffect": "enable scanline effect",
|
||||||
|
"enableShimmerEffect_description": "enable the animated shimmer visual effect across the application",
|
||||||
|
"enableShimmerEffect": "enable shimmer effect",
|
||||||
"exitToTray_description": "exit the application to the system tray",
|
"exitToTray_description": "exit the application to the system tray",
|
||||||
"exitToTray": "exit to tray",
|
"exitToTray": "exit to tray",
|
||||||
"exportImportSettings_control_description": "export and import settings via JSON",
|
"exportImportSettings_control_description": "export and import settings via JSON",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ const utils = isElectron() ? window.api.utils : null;
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { mode, theme } = useAppTheme();
|
const { mode, theme } = useAppTheme();
|
||||||
const language = useSettingsStore((store) => store.general.language);
|
const language = useSettingsStore((store) => store.general.language);
|
||||||
|
const enableScanlineEffect = useSettingsStore((store) => store.general.enableScanlineEffect);
|
||||||
|
const enableShimmerEffect = useSettingsStore((store) => store.general.enableShimmerEffect);
|
||||||
|
|
||||||
const { content, enabled } = useCssSettings();
|
const { content, enabled } = useCssSettings();
|
||||||
const { type: playbackType } = usePlaybackSettings();
|
const { type: playbackType } = usePlaybackSettings();
|
||||||
|
|
@ -188,6 +190,24 @@ export const App = () => {
|
||||||
}
|
}
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enableScanlineEffect) {
|
||||||
|
document.body.classList.add('enable-scanline');
|
||||||
|
document.documentElement.classList.add('enable-scanline');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('enable-scanline');
|
||||||
|
document.documentElement.classList.remove('enable-scanline');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableShimmerEffect) {
|
||||||
|
document.body.classList.add('enable-shimmer');
|
||||||
|
document.documentElement.classList.add('enable-shimmer');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('enable-shimmer');
|
||||||
|
document.documentElement.classList.remove('enable-shimmer');
|
||||||
|
}
|
||||||
|
}, [enableScanlineEffect, enableShimmerEffect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider forceColorScheme={mode} theme={theme}>
|
<MantineProvider forceColorScheme={mode} theme={theme}>
|
||||||
<Notifications
|
<Notifications
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--theme-card-default-bg);
|
background: var(--theme-card-default-bg);
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
border: 2px solid var(--theme-orange-transparent-40);
|
border: 2px solid var(--theme-orange-transparent-40);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
border-radius: var(--theme-radius-md);
|
||||||
box-shadow: var(--theme-shadow-orange-glow-soft);
|
box-shadow: var(--theme-shadow-orange-glow-soft);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -47,7 +47,6 @@
|
||||||
var(--theme-shadow-orange-glow-medium),
|
var(--theme-shadow-orange-glow-medium),
|
||||||
0 0 5px var(--theme-orange-transparent-30),
|
0 0 5px var(--theme-orange-transparent-30),
|
||||||
0 0 5px var(--theme-orange-transparent-20);
|
0 0 5px var(--theme-orange-transparent-20);
|
||||||
transform: scale(1.03);
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
background: var(--theme-card-default-bg);
|
background: var(--theme-card-default-bg);
|
||||||
border: 2px solid var(--theme-orange-transparent-40);
|
border: 2px solid var(--theme-orange-transparent-40);
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
box-shadow: var(--theme-shadow-orange-glow-soft);
|
box-shadow: var(--theme-shadow-orange-glow-soft);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -51,7 +51,6 @@
|
||||||
var(--theme-shadow-orange-glow-medium),
|
var(--theme-shadow-orange-glow-medium),
|
||||||
0 0 5px var(--theme-orange-transparent-30),
|
0 0 5px var(--theme-orange-transparent-30),
|
||||||
0 0 5px var(--theme-orange-transparent-20);
|
0 0 5px var(--theme-orange-transparent-20);
|
||||||
transform: scale(1.03);
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
|
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
|
||||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
||||||
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
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 { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
import {
|
import {
|
||||||
|
Album,
|
||||||
|
AlbumListResponse,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
|
|
@ -33,7 +36,7 @@ import {
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
const BASE_QUERY_ARGS = {
|
const BASE_QUERY_ARGS = {
|
||||||
limit: 15,
|
limit: 30,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -173,20 +176,255 @@ const HomeRoute = () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading =
|
const starredAlbums = useQuery(
|
||||||
(random.isLoading && queriesEnabled[HomeItem.RANDOM]) ||
|
albumQueries.list({
|
||||||
(recentlyPlayed.isLoading && queriesEnabled[HomeItem.RECENTLY_PLAYED] && !isJellyfin) ||
|
options: {
|
||||||
(recentlyAdded.isLoading && queriesEnabled[HomeItem.RECENTLY_ADDED]) ||
|
enabled: queriesEnabled[HomeItem.STARRED_ALBUMS],
|
||||||
(recentlyReleased.isLoading && queriesEnabled[HomeItem.RECENTLY_RELEASED]) ||
|
staleTime: 1000 * 60 * 5,
|
||||||
(((isJellyfin && mostPlayedSongs.isLoading) ||
|
},
|
||||||
(!isJellyfin && mostPlayedAlbums.isLoading)) &&
|
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]);
|
queriesEnabled[HomeItem.MOST_PLAYED]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isInitialLoading) {
|
||||||
return <Spinner container />;
|
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]: {
|
[HomeItem.MOST_PLAYED]: {
|
||||||
data: isJellyfin ? mostPlayedSongs?.data?.items : mostPlayedAlbums?.data?.items,
|
data: isJellyfin ? mostPlayedSongs?.data?.items : mostPlayedAlbums?.data?.items,
|
||||||
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
|
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
|
||||||
|
|
@ -217,6 +455,18 @@ const HomeRoute = () => {
|
||||||
query: recentlyReleased,
|
query: recentlyReleased,
|
||||||
title: t('page.home.recentlyReleased', { postProcess: 'sentenceCase' }),
|
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
|
const sortedCarousel = homeItems
|
||||||
|
|
@ -227,6 +477,10 @@ const HomeRoute = () => {
|
||||||
if (isJellyfin && item.id === HomeItem.RECENTLY_PLAYED) {
|
if (isJellyfin && item.id === HomeItem.RECENTLY_PLAYED) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Don't show flashback carousel if it has no data
|
||||||
|
if (item.id === HomeItem.FLASHBACK && !shouldShowFlashback) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
|
@ -310,7 +564,11 @@ const HomeRoute = () => {
|
||||||
<Group>
|
<Group>
|
||||||
<TextTitle order={3}>{carousel.title}</TextTitle>
|
<TextTitle order={3}>{carousel.title}</TextTitle>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => carousel.query.refetch()}
|
onClick={() =>
|
||||||
|
'onRefresh' in carousel
|
||||||
|
? carousel.onRefresh()
|
||||||
|
: carousel.query.refetch()
|
||||||
|
}
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
>
|
>
|
||||||
<Icon icon="refresh" />
|
<Icon icon="refresh" />
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,15 @@ import { useMergedRef } from '@mantine/hooks';
|
||||||
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import debounce from 'lodash/debounce';
|
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 { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||||
|
|
@ -28,6 +36,7 @@ import {
|
||||||
useCurrentStatus,
|
useCurrentStatus,
|
||||||
useDefaultQueue,
|
useDefaultQueue,
|
||||||
usePlayerControls,
|
usePlayerControls,
|
||||||
|
usePlayerStore,
|
||||||
usePreviousSong,
|
usePreviousSong,
|
||||||
useQueueControls,
|
useQueueControls,
|
||||||
useVolume,
|
useVolume,
|
||||||
|
|
@ -53,9 +62,10 @@ type QueueProps = {
|
||||||
|
|
||||||
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
|
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mergedRef = useMergedRef(ref, tableRef);
|
const mergedRef = useMergedRef(ref, tableRef);
|
||||||
const queue = useDefaultQueue();
|
const queue = useDefaultQueue();
|
||||||
const { reorderQueue, setCurrentTrack } = useQueueControls();
|
const { removeFromQueue, reorderQueue, setCurrentTrack } = useQueueControls();
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const previousSong = usePreviousSong();
|
const previousSong = usePreviousSong();
|
||||||
const status = useCurrentStatus();
|
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 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 (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<VirtualGridAutoSizerContainer>
|
<div
|
||||||
<VirtualTable
|
ref={containerRef}
|
||||||
alwaysShowHorizontalScroll
|
style={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}
|
||||||
autoFitColumns={tableConfig.autoFit}
|
tabIndex={0}
|
||||||
columnDefs={columnDefs}
|
>
|
||||||
context={{
|
<VirtualGridAutoSizerContainer>
|
||||||
currentSong,
|
<VirtualTable
|
||||||
handleDoubleClick,
|
alwaysShowHorizontalScroll
|
||||||
isFocused,
|
autoFitColumns={tableConfig.autoFit}
|
||||||
isQueue: true,
|
columnDefs={columnDefs}
|
||||||
itemType: LibraryItem.SONG,
|
context={{
|
||||||
onCellContextMenu,
|
currentSong,
|
||||||
status,
|
handleDoubleClick,
|
||||||
}}
|
isFocused,
|
||||||
deselectOnClickOutside={type === 'fullScreen'}
|
isQueue: true,
|
||||||
getRowId={(data) => data.data.uniqueId}
|
itemType: LibraryItem.SONG,
|
||||||
onCellContextMenu={onCellContextMenu}
|
onCellContextMenu,
|
||||||
onCellDoubleClicked={handleDoubleClick}
|
status,
|
||||||
onColumnMoved={handleColumnChange}
|
}}
|
||||||
onColumnResized={debouncedColumnChange}
|
deselectOnClickOutside={type === 'fullScreen'}
|
||||||
onDragStarted={handleDragStart}
|
getRowId={(data) => data.data.uniqueId}
|
||||||
onGridReady={handleGridReady}
|
onCellContextMenu={onCellContextMenu}
|
||||||
onGridSizeChanged={handleGridSizeChange}
|
onCellDoubleClicked={handleDoubleClick}
|
||||||
onRowDragEnd={handleDragEnd}
|
onColumnMoved={handleColumnChange}
|
||||||
ref={mergedRef}
|
onColumnResized={debouncedColumnChange}
|
||||||
rowBuffer={50}
|
onDragStarted={handleDragStart}
|
||||||
rowClassRules={rowClassRules}
|
onGridReady={handleGridReady}
|
||||||
rowData={songs}
|
onGridSizeChanged={handleGridSizeChange}
|
||||||
rowDragEntireRow
|
onRowDragEnd={handleDragEnd}
|
||||||
rowDragMultiRow
|
ref={mergedRef}
|
||||||
rowHeight={tableConfig.rowHeight || 40}
|
rowBuffer={50}
|
||||||
suppressCellFocus={type === 'fullScreen'}
|
rowClassRules={rowClassRules}
|
||||||
/>
|
rowData={songs}
|
||||||
</VirtualGridAutoSizerContainer>
|
rowDragEntireRow
|
||||||
|
rowDragMultiRow
|
||||||
|
rowHeight={tableConfig.rowHeight || 40}
|
||||||
|
suppressCellFocus={type === 'fullScreen'}
|
||||||
|
/>
|
||||||
|
</VirtualGridAutoSizerContainer>
|
||||||
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -46,21 +46,22 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
0deg,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
var(--album-color, rgba(0, 255, 255, 0.15)) 50%,
|
var(--album-color, rgb(0 255 255 / 15%)) 50%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
background-size: 100% 200%;
|
background-size: 100% 200%;
|
||||||
animation: scanline 6s linear infinite;
|
animation: scanline 6s linear infinite;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scanline {
|
@keyframes scanline {
|
||||||
0% {
|
0% {
|
||||||
background-position: 0 -100vh;
|
background-position: 0 -100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0 100vh;
|
background-position: 0 100vh;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,9 @@ export const FullScreenPlayer = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert RGB to RGB with opacity for scanline effect
|
// 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 imageUrl = currentSong?.imageUrl && currentSong.imageUrl.replace(/size=\d+/g, 'size=500');
|
||||||
const backgroundImage =
|
const backgroundImage =
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.1);
|
|
||||||
filter: drop-shadow(0 0 8px var(--theme-orange-transparent-40));
|
filter: drop-shadow(0 0 8px var(--theme-orange-transparent-40));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,20 +38,23 @@
|
||||||
|
|
||||||
.player-button.active {
|
.player-button.active {
|
||||||
svg {
|
svg {
|
||||||
fill: var(--theme-orange-base);
|
|
||||||
filter: drop-shadow(0 0 6px var(--theme-orange-transparent-50));
|
filter: drop-shadow(0 0 6px var(--theme-orange-transparent-50));
|
||||||
|
fill: var(--theme-orange-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.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%;
|
border-radius: 50%;
|
||||||
box-shadow: var(--theme-shadow-orange-glow-soft);
|
box-shadow: var(--theme-shadow-orange-glow-soft);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--theme-shadow-orange-glow-medium);
|
box-shadow: var(--theme-shadow-orange-glow-medium);
|
||||||
transform: scale(1.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
.container {
|
.container {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-top: 2px solid var(--theme-orange-transparent-40);
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
var(--theme-colors-background) 0%,
|
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);
|
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';
|
import { HomeItem, useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||||
|
|
||||||
const HOME_ITEMS: Array<[string, string]> = [
|
const HOME_ITEMS: Array<[string, string]> = [
|
||||||
[HomeItem.RANDOM, 'page.home.explore'],
|
[HomeItem.FLASHBACK, 'page.home.flashback'],
|
||||||
[HomeItem.RECENTLY_PLAYED, 'page.home.recentlyPlayed'],
|
[HomeItem.RECENTLY_PLAYED, 'page.home.recentlyPlayed'],
|
||||||
|
[HomeItem.RANDOM, 'page.home.explore'],
|
||||||
[HomeItem.RECENTLY_ADDED, 'page.home.newlyAdded'],
|
[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.MOST_PLAYED, 'page.home.mostPlayed'],
|
||||||
|
[HomeItem.RECENTLY_RELEASED, 'page.home.recentlyReleased'],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const HomeSettings = () => {
|
export const HomeSettings = () => {
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,48 @@ export const ThemeSettings = () => {
|
||||||
}),
|
}),
|
||||||
title: t('setting.accentColor', { postProcess: 'sentenceCase' }),
|
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} />;
|
return <SettingsSection options={themeOptions} />;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function IsUpdatedDialog() {
|
||||||
<Group justify="flex-end" wrap="nowrap">
|
<Group justify="flex-end" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`}
|
href={`https://github.com/antebudimir/feishin/releases/tag/v${version}`}
|
||||||
onClick={handleDismiss}
|
onClick={handleDismiss}
|
||||||
rightSection={<Icon icon="externalLink" />}
|
rightSection={<Icon icon="externalLink" />}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,14 @@ import {
|
||||||
} from '/@/shared/types/types';
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
const HomeItemSchema = z.enum([
|
const HomeItemSchema = z.enum([
|
||||||
|
'flashback',
|
||||||
'mostPlayed',
|
'mostPlayed',
|
||||||
'random',
|
'random',
|
||||||
'recentlyAdded',
|
'recentlyAdded',
|
||||||
'recentlyPlayed',
|
'recentlyPlayed',
|
||||||
'recentlyReleased',
|
'recentlyReleased',
|
||||||
|
'starredAlbums',
|
||||||
|
'starredTracks',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ArtistItemSchema = z.enum([
|
const ArtistItemSchema = z.enum([
|
||||||
|
|
@ -177,6 +180,8 @@ const GeneralSettingsSchema = z.object({
|
||||||
buttonSize: z.number(),
|
buttonSize: z.number(),
|
||||||
disabledContextMenu: z.record(z.boolean()),
|
disabledContextMenu: z.record(z.boolean()),
|
||||||
doubleClickQueueAll: z.boolean(),
|
doubleClickQueueAll: z.boolean(),
|
||||||
|
enableScanlineEffect: z.boolean(),
|
||||||
|
enableShimmerEffect: z.boolean(),
|
||||||
externalLinks: z.boolean(),
|
externalLinks: z.boolean(),
|
||||||
followSystemTheme: z.boolean(),
|
followSystemTheme: z.boolean(),
|
||||||
genreTarget: GenreTargetSchema,
|
genreTarget: GenreTargetSchema,
|
||||||
|
|
@ -388,11 +393,14 @@ export enum GenreTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HomeItem {
|
export enum HomeItem {
|
||||||
|
FLASHBACK = 'flashback',
|
||||||
MOST_PLAYED = 'mostPlayed',
|
MOST_PLAYED = 'mostPlayed',
|
||||||
RANDOM = 'random',
|
RANDOM = 'random',
|
||||||
RECENTLY_ADDED = 'recentlyAdded',
|
RECENTLY_ADDED = 'recentlyAdded',
|
||||||
RECENTLY_PLAYED = 'recentlyPlayed',
|
RECENTLY_PLAYED = 'recentlyPlayed',
|
||||||
RECENTLY_RELEASED = 'recentlyReleased',
|
RECENTLY_RELEASED = 'recentlyReleased',
|
||||||
|
STARRED_ALBUMS = 'starredAlbums',
|
||||||
|
STARRED_TRACKS = 'starredTracks',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataTableProps = z.infer<typeof DataTablePropsSchema>;
|
export type DataTableProps = z.infer<typeof DataTablePropsSchema>;
|
||||||
|
|
@ -495,10 +503,16 @@ export const sidebarItems: SidebarItemType[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const homeItems = Object.values(HomeItem).map((item) => ({
|
const homeItems: SortableItem<HomeItem>[] = [
|
||||||
disabled: false,
|
{ disabled: false, id: HomeItem.FLASHBACK },
|
||||||
id: item,
|
{ disabled: false, id: HomeItem.RECENTLY_PLAYED },
|
||||||
}));
|
{ disabled: false, id: HomeItem.RANDOM },
|
||||||
|
{ disabled: false, id: HomeItem.RECENTLY_ADDED },
|
||||||
|
{ disabled: false, id: HomeItem.STARRED_ALBUMS },
|
||||||
|
{ disabled: false, id: HomeItem.STARRED_TRACKS },
|
||||||
|
{ disabled: false, id: HomeItem.MOST_PLAYED },
|
||||||
|
{ disabled: true, id: HomeItem.RECENTLY_RELEASED },
|
||||||
|
];
|
||||||
|
|
||||||
const artistItems = Object.values(ArtistItem).map((item) => ({
|
const artistItems = Object.values(ArtistItem).map((item) => ({
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
@ -534,7 +548,7 @@ const initialState: SettingsState = {
|
||||||
type: FontType.SYSTEM,
|
type: FontType.SYSTEM,
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
accent: 'rgb(214, 46, 83)',
|
accent: 'rgb(255, 142, 83)',
|
||||||
albumArtRes: undefined,
|
albumArtRes: undefined,
|
||||||
albumBackground: true,
|
albumBackground: true,
|
||||||
albumBackgroundBlur: 50,
|
albumBackgroundBlur: 50,
|
||||||
|
|
@ -544,6 +558,8 @@ const initialState: SettingsState = {
|
||||||
buttonSize: 25,
|
buttonSize: 25,
|
||||||
disabledContextMenu: {},
|
disabledContextMenu: {},
|
||||||
doubleClickQueueAll: false,
|
doubleClickQueueAll: false,
|
||||||
|
enableScanlineEffect: true,
|
||||||
|
enableShimmerEffect: true,
|
||||||
externalLinks: true,
|
externalLinks: true,
|
||||||
followSystemTheme: false,
|
followSystemTheme: false,
|
||||||
genreTarget: GenreTarget.ALBUM,
|
genreTarget: GenreTarget.ALBUM,
|
||||||
|
|
@ -558,7 +574,7 @@ const initialState: SettingsState = {
|
||||||
playButtonBehavior: Play.NOW,
|
playButtonBehavior: Play.NOW,
|
||||||
playerbarOpenDrawer: false,
|
playerbarOpenDrawer: false,
|
||||||
resume: true,
|
resume: true,
|
||||||
showQueueDrawerButton: true,
|
showQueueDrawerButton: false,
|
||||||
sidebarCollapsedNavigation: true,
|
sidebarCollapsedNavigation: true,
|
||||||
sidebarCollapseShared: false,
|
sidebarCollapseShared: false,
|
||||||
sidebarItems,
|
sidebarItems,
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,6 @@ import merge from 'lodash/merge';
|
||||||
|
|
||||||
import { AppThemeConfiguration } from '/@/shared/themes/app-theme-types';
|
import { AppThemeConfiguration } from '/@/shared/themes/app-theme-types';
|
||||||
|
|
||||||
// const lightColors: MantineColorsTuple = [
|
|
||||||
// '#f5f5f5',
|
|
||||||
// '#e7e7e7',
|
|
||||||
// '#cdcdcd',
|
|
||||||
// '#b2b2b2',
|
|
||||||
// '#9a9a9a',
|
|
||||||
// '#8b8b8b',
|
|
||||||
// '#848484',
|
|
||||||
// '#717171',
|
|
||||||
// '#656565',
|
|
||||||
// '#575757',
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const darkColors: MantineColorsTuple = [
|
const darkColors: MantineColorsTuple = [
|
||||||
'#C9C9C9',
|
'#C9C9C9',
|
||||||
'#b8b8b8',
|
'#b8b8b8',
|
||||||
|
|
|
||||||
|
|
@ -33,46 +33,48 @@ html {
|
||||||
background: var(--theme-colors-background);
|
background: var(--theme-colors-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before,
|
body.enable-shimmer::before,
|
||||||
html::before {
|
html.enable-shimmer::before {
|
||||||
content: '';
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
105deg,
|
105deg,
|
||||||
transparent 40%,
|
transparent 40%,
|
||||||
rgba(0, 183, 255, 0.1) 45%,
|
rgb(0 183 255 / 10%) 45%,
|
||||||
rgba(0, 183, 255, 0.2) 50%,
|
rgb(0 183 255 / 20%) 50%,
|
||||||
rgba(0, 183, 255, 0.1) 55%,
|
rgb(0 183 255 / 10%) 55%,
|
||||||
transparent 60%
|
transparent 60%
|
||||||
);
|
);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 8s infinite;
|
animation: shimmer 8s infinite;
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body::after,
|
body.enable-scanline::after,
|
||||||
html::after {
|
html.enable-scanline::after {
|
||||||
content: '';
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
0deg,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
rgba(0, 255, 255, 0.03) 50%,
|
rgb(0 255 255 / 3%) 25%,
|
||||||
|
transparent 50%,
|
||||||
|
rgb(0 255 255 / 3%) 75%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
background-size: 100% 200%;
|
background-size: 100% 200%;
|
||||||
animation: scanline 6s linear infinite;
|
animation: scanline 6s linear infinite;
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
|
|
@ -100,9 +102,9 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
height: inherit;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
|
@ -164,27 +166,13 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes hologramGlow {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow:
|
|
||||||
0 0 10px rgba(0, 183, 255, 0.2),
|
|
||||||
0 0 20px rgba(0, 183, 255, 0.1),
|
|
||||||
0 0 40px rgba(0, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow:
|
|
||||||
0 0 20px rgba(0, 183, 255, 0.4),
|
|
||||||
0 0 40px rgba(0, 183, 255, 0.2),
|
|
||||||
0 0 80px rgba(0, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
@ -192,8 +180,9 @@ button {
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
background-position: -100% 0;
|
background-position: 0% 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 200% 0;
|
background-position: 200% 0;
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +192,7 @@ button {
|
||||||
0% {
|
0% {
|
||||||
background-position: 0 -100vh;
|
background-position: 0 -100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0 100vh;
|
background-position: 0 100vh;
|
||||||
}
|
}
|
||||||
|
|
@ -312,30 +302,27 @@ button {
|
||||||
:root {
|
:root {
|
||||||
--theme-background-noise: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4=');
|
--theme-background-noise: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4=');
|
||||||
--theme-fullscreen-player-text-shadow: black 0px 0px 10px;
|
--theme-fullscreen-player-text-shadow: black 0px 0px 10px;
|
||||||
|
--theme-orange-base: rgb(255 142 83);
|
||||||
/* Intro-inspired color palette */
|
--theme-orange-medium: rgb(255 123 52);
|
||||||
--theme-orange-base: rgb(255, 142, 83);
|
--theme-orange-dark: rgb(255 89 0);
|
||||||
--theme-orange-medium: rgb(255, 123, 52);
|
--theme-orange-transparent-70: rgb(255 142 83 / 70%);
|
||||||
--theme-orange-dark: rgb(255, 89, 0);
|
--theme-orange-transparent-40: rgb(255 142 83 / 40%);
|
||||||
--theme-orange-transparent-70: rgba(255, 142, 83, 0.7);
|
--theme-orange-transparent-30: rgb(255 142 83 / 30%);
|
||||||
--theme-orange-transparent-40: rgba(255, 142, 83, 0.4);
|
--theme-orange-transparent-20: rgb(255 142 83 / 20%);
|
||||||
--theme-orange-transparent-30: rgba(255, 142, 83, 0.3);
|
--theme-orange-transparent-15: rgb(255 142 83 / 15%);
|
||||||
--theme-orange-transparent-20: rgba(255, 142, 83, 0.2);
|
--theme-orange-transparent-10: rgb(255 142 83 / 10%);
|
||||||
--theme-orange-transparent-15: rgba(255, 142, 83, 0.15);
|
--theme-cyan-primary: rgb(0 183 255);
|
||||||
--theme-orange-transparent-10: rgba(255, 142, 83, 0.1);
|
--theme-cyan-secondary: rgb(0 255 255);
|
||||||
|
--theme-cyan-transparent-80: rgb(0 183 255 / 80%);
|
||||||
--theme-cyan-primary: rgb(0, 183, 255);
|
--theme-cyan-transparent-70: rgb(0 183 255 / 70%);
|
||||||
--theme-cyan-secondary: rgb(0, 255, 255);
|
--theme-cyan-transparent-60: rgb(0 183 255 / 60%);
|
||||||
--theme-cyan-transparent-80: rgba(0, 183, 255, 0.8);
|
--theme-cyan-transparent-50: rgb(0 183 255 / 50%);
|
||||||
--theme-cyan-transparent-70: rgba(0, 183, 255, 0.7);
|
--theme-cyan-transparent-40: rgb(0 183 255 / 40%);
|
||||||
--theme-cyan-transparent-60: rgba(0, 183, 255, 0.6);
|
--theme-cyan-transparent-30: rgb(0 183 255 / 30%);
|
||||||
--theme-cyan-transparent-50: rgba(0, 183, 255, 0.5);
|
--theme-cyan-transparent-20: rgb(0 183 255 / 20%);
|
||||||
--theme-cyan-transparent-40: rgba(0, 183, 255, 0.4);
|
--theme-cyan-transparent-10: rgb(0 183 255 / 10%);
|
||||||
--theme-cyan-transparent-30: rgba(0, 183, 255, 0.3);
|
--theme-cyan-transparent-05: rgb(0 183 255 / 5%);
|
||||||
--theme-cyan-transparent-20: rgba(0, 183, 255, 0.2);
|
--theme-cyan-transparent-03: rgb(0 183 255 / 3%);
|
||||||
--theme-cyan-transparent-10: rgba(0, 183, 255, 0.1);
|
|
||||||
--theme-cyan-transparent-05: rgba(0, 183, 255, 0.05);
|
|
||||||
--theme-cyan-transparent-03: rgba(0, 183, 255, 0.03);
|
|
||||||
|
|
||||||
/* Gradients */
|
/* Gradients */
|
||||||
--theme-primary-gradient: linear-gradient(
|
--theme-primary-gradient: linear-gradient(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue