diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8e6f5f9c4..8a5012a5e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -37,6 +37,7 @@ "channel_one": "channel", "channel_other": "channels", "clear": "clear", + "close": "close", "collapse": "collapse", "comingSoon": "coming soon…", "configure": "configure", @@ -458,6 +459,9 @@ "gaplessAudio_optionWeak": "weak (recommended)", "globalMediaHotkeys": "global media hotkeys", "globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback", + "homeConfiguration": "home page configuration", + "homeConfiguration_description": "configure what items are shown, and in what order, on the home page", + "homeConfigurationNote": "Recently played not available for Jellyfin", "hotkey_browserBack": "browser back", "hotkey_browserForward": "browser forward", "hotkey_favoriteCurrentSong": "favorite $t(common.currentSong)", diff --git a/src/renderer/features/home/routes/home-route.tsx b/src/renderer/features/home/routes/home-route.tsx index 8f67cd0e3..564440dba 100644 --- a/src/renderer/features/home/routes/home-route.tsx +++ b/src/renderer/features/home/routes/home-route.tsx @@ -12,7 +12,12 @@ import { useAlbumList } from '/@/renderer/features/albums'; import { useRecentlyPlayed } from '/@/renderer/features/home/queries/recently-played-query'; import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared'; import { AppRoute } from '/@/renderer/router/routes'; -import { useCurrentServer, useWindowSettings } from '/@/renderer/store'; +import { + HomeItem, + useCurrentServer, + useGeneralSettings, + useWindowSettings, +} from '/@/renderer/store'; import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel'; import { Platform } from '/@/renderer/types'; import { useQueryClient } from '@tanstack/react-query'; @@ -28,6 +33,7 @@ const HomeRoute = () => { const server = useCurrentServer(); const itemsPerPage = 15; const { windowBarStyle } = useWindowSettings(); + const { homeItems } = useGeneralSettings(); const feature = useAlbumList({ options: { @@ -129,16 +135,15 @@ const HomeRoute = () => { return ; } - const carousels = [ - { + const carousels = { + [HomeItem.RANDOM]: { data: random?.data?.items, itemType: LibraryItem.ALBUM, sortBy: AlbumListSort.RANDOM, sortOrder: SortOrder.ASC, title: t('page.home.explore', { postProcess: 'sentenceCase' }), - uniqueId: 'random', }, - { + [HomeItem.RECENTLY_PLAYED]: { data: recentlyPlayed?.data?.items, itemType: LibraryItem.ALBUM, pagination: { @@ -147,9 +152,8 @@ const HomeRoute = () => { sortBy: AlbumListSort.RECENTLY_PLAYED, sortOrder: SortOrder.DESC, title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }), - uniqueId: 'recentlyPlayed', }, - { + [HomeItem.RECENTLY_ADDED]: { data: recentlyAdded?.data?.items, itemType: LibraryItem.ALBUM, pagination: { @@ -158,9 +162,8 @@ const HomeRoute = () => { sortBy: AlbumListSort.RECENTLY_ADDED, sortOrder: SortOrder.DESC, title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }), - uniqueId: 'recentlyAdded', }, - { + [HomeItem.MOST_PLAYED]: { data: server?.type === ServerType.JELLYFIN ? mostPlayedSongs?.data?.items @@ -175,9 +178,24 @@ const HomeRoute = () => { : AlbumListSort.PLAY_COUNT, sortOrder: SortOrder.DESC, title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }), - uniqueId: 'mostPlayed', }, - ]; + }; + + const sortedCarousel = homeItems + .filter((item) => { + if (item.disabled) { + return false; + } + if (server?.type === ServerType.JELLYFIN && item.id === HomeItem.RECENTLY_PLAYED) { + return false; + } + + return true; + }) + .map((item) => ({ + ...carousels[item.id], + uniqueId: item.id, + })); const invalidateCarouselQuery = (carousel: { itemType: LibraryItem; @@ -232,87 +250,76 @@ const HomeRoute = () => { spacing="lg" > - {carousels - .filter((carousel) => { - if ( - server?.type === ServerType.JELLYFIN && - carousel.uniqueId === 'recentlyPlayed' - ) { - return null; - } - - return carousel; - }) - .map((carousel) => ( - ( + - - {carousel.title} - + ], + }} + title={{ + label: ( + + + {carousel.title} + - invalidateCarouselQuery(carousel)} - > - - - - ), - }} - uniqueId={carousel.uniqueId} - /> - ))} + invalidateCarouselQuery(carousel)} + > + + + + ), + }} + uniqueId={carousel.uniqueId} + /> + ))} diff --git a/src/renderer/features/settings/components/general/draggable-item.tsx b/src/renderer/features/settings/components/general/draggable-item.tsx new file mode 100644 index 000000000..57c947940 --- /dev/null +++ b/src/renderer/features/settings/components/general/draggable-item.tsx @@ -0,0 +1,50 @@ +import { Group, Checkbox } from '@mantine/core'; +import { useDragControls, Reorder } from 'framer-motion'; +import { MdDragIndicator } from 'react-icons/md'; + +const DragHandle = ({ dragControls }: any) => { + return ( + dragControls.start(event)} + /> + ); +}; + +interface SidebarItem { + disabled: boolean; + id: string; +} + +export interface DraggableItemProps { + handleChangeDisabled: (id: string, e: boolean) => void; + item: SidebarItem; + value: string; +} + +export const DraggableItem = ({ item, value, handleChangeDisabled }: DraggableItemProps) => { + const dragControls = useDragControls(); + + return ( + + + handleChangeDisabled(item.id, e.target.checked)} + /> + + {value} + + + ); +}; diff --git a/src/renderer/features/settings/components/general/general-tab.tsx b/src/renderer/features/settings/components/general/general-tab.tsx index c0caaf5eb..bae90101e 100644 --- a/src/renderer/features/settings/components/general/general-tab.tsx +++ b/src/renderer/features/settings/components/general/general-tab.tsx @@ -6,6 +6,7 @@ import { ThemeSettings } from '/@/renderer/features/settings/components/general/ import { RemoteSettings } from '/@/renderer/features/settings/components/general/remote-settings'; import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs'; import isElectron from 'is-electron'; +import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings'; export const GeneralTab = () => { return ( @@ -16,6 +17,8 @@ export const GeneralTab = () => { + + {isElectron() && ( <> diff --git a/src/renderer/features/settings/components/general/home-settings.tsx b/src/renderer/features/settings/components/general/home-settings.tsx new file mode 100644 index 000000000..c9386bdec --- /dev/null +++ b/src/renderer/features/settings/components/general/home-settings.tsx @@ -0,0 +1,108 @@ +import { useCallback, useMemo, useState } from 'react'; +import { Reorder } from 'framer-motion'; +import isEqual from 'lodash/isEqual'; +import { useTranslation } from 'react-i18next'; +import { Button } from '/@/renderer/components'; +import { + useSettingsStoreActions, + useGeneralSettings, + HomeItem, +} from '../../../../store/settings.store'; +import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; +import { DraggableItem } from '/@/renderer/features/settings/components/general/draggable-item'; + +export const HomeSettings = () => { + const { t } = useTranslation(); + const { homeItems } = useGeneralSettings(); + const { setHomeItems } = useSettingsStoreActions(); + const [open, setOpen] = useState(false); + + const translatedSidebarItemMap = useMemo( + () => ({ + [HomeItem.RANDOM]: t('page.home.explore', { postProcess: 'sentenceCase' }), + [HomeItem.RECENTLY_PLAYED]: t('page.home.recentlyPlayed', { + postProcess: 'sentenceCase', + }), + [HomeItem.RECENTLY_ADDED]: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }), + [HomeItem.MOST_PLAYED]: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }), + }), + [t], + ); + + const [localHomeItems, setLocalHomeItems] = useState(homeItems); + + const handleSave = () => { + setHomeItems(localHomeItems); + }; + + const handleChangeDisabled = useCallback((id: string, e: boolean) => { + setLocalHomeItems((items) => + items.map((item) => { + if (item.id === id) { + return { + ...item, + disabled: !e, + }; + } + + return item; + }), + ); + }, []); + + const isSaveButtonDisabled = isEqual(homeItems, localHomeItems); + + return ( + <> + + {open && ( + + )} + + + } + description={t('setting.homeConfiguration', { + context: 'description', + postProcess: 'sentenceCase', + })} + note={t('setting.homeConfigurationNote')} + title={t('setting.homeConfiguration', { postProcess: 'sentenceCase' })} + /> + {open && ( + + {localHomeItems.map((item) => ( + + ))} + + )} + + ); +}; diff --git a/src/renderer/features/settings/components/general/sidebar-settings.tsx b/src/renderer/features/settings/components/general/sidebar-settings.tsx index 439e3be49..2529286ba 100644 --- a/src/renderer/features/settings/components/general/sidebar-settings.tsx +++ b/src/renderer/features/settings/components/general/sidebar-settings.tsx @@ -1,77 +1,33 @@ -import { ChangeEvent, useCallback, useState } from 'react'; -import { Group } from '@mantine/core'; -import { Reorder, useDragControls } from 'framer-motion'; +import { ChangeEvent, useCallback, useMemo, useState } from 'react'; +import { Reorder } from 'framer-motion'; import isEqual from 'lodash/isEqual'; import { useTranslation } from 'react-i18next'; -import { MdDragIndicator } from 'react-icons/md'; -import { Button, Checkbox, Switch } from '/@/renderer/components'; +import { Button, Switch } from '/@/renderer/components'; import { useSettingsStoreActions, useGeneralSettings } from '../../../../store/settings.store'; import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; -import i18n from '/@/i18n/i18n'; - -const translatedSidebarItemMap = { - Albums: i18n.t('page.sidebar.albums', { postProcess: 'titleCase' }), - Artists: i18n.t('page.sidebar.artists', { postProcess: 'titleCase' }), - Folders: i18n.t('page.sidebar.folders', { postProcess: 'titleCase' }), - Genres: i18n.t('page.sidebar.genres', { postProcess: 'titleCase' }), - Home: i18n.t('page.sidebar.home', { postProcess: 'titleCase' }), - 'Now Playing': i18n.t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }), - Playlists: i18n.t('page.sidebar.playlists', { postProcess: 'titleCase' }), - Search: i18n.t('page.sidebar.search', { postProcess: 'titleCase' }), - Settings: i18n.t('page.sidebar.settings', { postProcess: 'titleCase' }), - Tracks: i18n.t('page.sidebar.tracks', { postProcess: 'titleCase' }), -}; - -const DragHandle = ({ dragControls }: any) => { - return ( - dragControls.start(event)} - /> - ); -}; - -interface SidebarItem { - disabled: boolean; - id: string; -} - -interface DraggableSidebarItemProps { - handleChangeDisabled: (id: string, e: boolean) => void; - item: SidebarItem; -} - -const DraggableSidebarItem = ({ item, handleChangeDisabled }: DraggableSidebarItemProps) => { - const dragControls = useDragControls(); - - return ( - - - handleChangeDisabled(item.id, e.target.checked)} - /> - - {translatedSidebarItemMap[item.id as keyof typeof translatedSidebarItemMap]} - - - ); -}; +import { DraggableItem } from '/@/renderer/features/settings/components/general/draggable-item'; export const SidebarSettings = () => { const { t } = useTranslation(); const settings = useGeneralSettings(); const { setSidebarItems, setSettings } = useSettingsStoreActions(); + const [open, setOpen] = useState(false); + + const translatedSidebarItemMap = useMemo( + () => ({ + Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }), + Artists: t('page.sidebar.artists', { postProcess: 'titleCase' }), + Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }), + Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }), + Home: t('page.sidebar.home', { postProcess: 'titleCase' }), + 'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }), + Playlists: t('page.sidebar.playlists', { postProcess: 'titleCase' }), + Search: t('page.sidebar.search', { postProcess: 'titleCase' }), + Settings: t('page.sidebar.settings', { postProcess: 'titleCase' }), + Tracks: t('page.sidebar.tracks', { postProcess: 'titleCase' }), + }), + [t], + ); const [localSidebarItems, setLocalSidebarItems] = useState(settings.sidebarItems); @@ -144,14 +100,25 @@ export const SidebarSettings = () => { /> - {t('common.save', { postProcess: 'titleCase' })} - + <> + {open && ( + + )} + + } description={t('setting.sidebarCollapsedNavigation', { context: 'description', @@ -159,19 +126,26 @@ export const SidebarSettings = () => { })} title={t('setting.sidebarConfiguration', { postProcess: 'sentenceCase' })} /> - - {localSidebarItems.map((item) => ( - - ))} - + {open && ( + + {localSidebarItems.map((item) => ( + + ))} + + )} ); }; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 2449579cc..56d1ce231 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -91,6 +91,23 @@ export const sidebarItems = [ }, ]; +export type SortableItem = { + disabled: boolean; + id: T; +}; + +export enum HomeItem { + MOST_PLAYED = 'mostPlayed', + RANDOM = 'random', + RECENTLY_ADDED = 'recentlyAdded', + RECENTLY_PLAYED = 'recentlyPlayed', +} + +export const homeItems = Object.values(HomeItem).map((item) => ({ + disabled: false, + id: item, +})); + export type PersistedTableColumn = { column: TableColumn; extraProps?: Partial; @@ -173,6 +190,7 @@ export interface SettingsState { defaultFullPlaylist: boolean; externalLinks: boolean; followSystemTheme: boolean; + homeItems: SortableItem[]; language: string; playButtonBehavior: Play; resume: boolean; @@ -254,6 +272,7 @@ export interface SettingsSlice extends SettingsState { actions: { reset: () => void; resetSampleRate: () => void; + setHomeItems: (item: SortableItem[]) => void; setSettings: (data: Partial) => void; setSidebarItems: (items: SidebarItemType[]) => void; setTable: (type: TableType, data: DataTableProps) => void; @@ -287,6 +306,7 @@ const initialState: SettingsState = { defaultFullPlaylist: true, externalLinks: true, followSystemTheme: false, + homeItems, language: 'en', playButtonBehavior: Play.NOW, resume: false, @@ -577,6 +597,11 @@ export const useSettingsStore = create()( state.playback.mpvProperties.audioSampleRateHz = 0; }); }, + setHomeItems: (items: SortableItem[]) => { + set((state) => { + state.general.homeItems = items; + }); + }, setSettings: (data) => { set({ ...get(), ...data }); }, @@ -600,7 +625,7 @@ export const useSettingsStore = create()( return merge(currentState, persistedState); }, name: 'store_settings', - version: 7, + version: 8, }, ), );