Skip to content

Commit

Permalink
Add Library View
Browse files Browse the repository at this point in the history
  • Loading branch information
grafixeyehero committed Jun 14, 2023
1 parent b13b1ff commit 3e50a44
Show file tree
Hide file tree
Showing 69 changed files with 5,471 additions and 1,744 deletions.
404 changes: 306 additions & 98 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
"@loadable/component": "5.15.3",
"@mui/icons-material": "5.11.16",
"@mui/material": "5.13.3",
"@tanstack/react-query": "4.29.12",
"@tanstack/react-query-devtools": "4.29.12",
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.3.2",
Expand Down Expand Up @@ -109,6 +111,7 @@
"screenfull": "6.0.2",
"sortablejs": "1.15.0",
"swiper": "9.3.2",
"use-local-storage-state": "17.3.0",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.2",
"workbox-core": "6.5.4",
Expand Down
35 changes: 24 additions & 11 deletions src/RootApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import loadable from '@loadable/component';
import { History } from '@remix-run/router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';

import StableApp from './apps/stable/App';
Expand All @@ -9,21 +11,32 @@ import { WebConfigProvider } from './hooks/useWebConfig';

const ExperimentalApp = loadable(() => import('./apps/experimental/App'));

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false
}
}
});

const RootApp = ({ history }: { history: History }) => {
const layoutMode = localStorage.getItem('layout');

return (
<ApiProvider>
<WebConfigProvider>
<HistoryRouter history={history}>
{
layoutMode === 'experimental' ?
<ExperimentalApp /> :
<StableApp />
}
</HistoryRouter>
</WebConfigProvider>
</ApiProvider>
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<HistoryRouter history={history}>
{
layoutMode === 'experimental' ?
<ExperimentalApp /> :
<StableApp />
}
</HistoryRouter>
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};

Expand Down
4 changes: 2 additions & 2 deletions src/apps/experimental/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Outlet, useLocation } from 'react-router-dom';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage';
import useLocalStorageState from 'use-local-storage-state';

import AppToolbar from './components/AppToolbar';
import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
Expand All @@ -25,7 +25,7 @@ const DEFAULT_EXPERIMENTAL_APP_SETTINGS: ExperimentalAppSettings = {
};

const AppLayout = () => {
const [ appSettings, setAppSettings ] = useLocalStorage<ExperimentalAppSettings>('ExperimentalAppSettings', DEFAULT_EXPERIMENTAL_APP_SETTINGS);
const [ appSettings, setAppSettings ] = useLocalStorageState<ExperimentalAppSettings>('ExperimentalAppSettings', { defaultValue: DEFAULT_EXPERIMENTAL_APP_SETTINGS });
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
const { user } = useApi();
const location = useLocation();
Expand Down
244 changes: 244 additions & 0 deletions src/apps/experimental/components/library/LibraryHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';

import {
Chip,
CircularProgress,
Divider,
MenuItem,
TextField,
Typography,
useScrollTrigger
} from '@mui/material';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Stack from '@mui/material/Stack';

import { useLibrarySettings } from 'hooks/useLibrarySettings';
import { useGetViewItemsByType } from 'hooks/useFetchItems';
import { getItemTypesEnum } from 'utils/items';

import FilterButton from './filter/FilterButton';
import NewCollectionButton from './NewCollectionButton';
import PlayAllButton from './PlayAllButton';
import ShuffleButton from './ShuffleButton';
import SortButton from './SortButton';
import ViewSettingsButton from './ViewSettingsButton';

import { LibraryViewSelectOptions } from 'types/library';

const visibleBtn = [
'movies',
'favorites',
'trailers',
'collections',
'series',
'episodes',
'albums',
'albumArtists',
'artists',
'songs',
'photos',
'videos'
];

interface LibraryHeaderProps {
collectionType?: string | null;
parentId: string | null;
item?: BaseItemDto;
viewSelectOptions: LibraryViewSelectOptions[];
viewType: string;
setViewType: React.Dispatch<React.SetStateAction<string | null>>
}

const LibraryHeader: FC<LibraryHeaderProps> = ({
viewSelectOptions,
viewType,
setViewType,
collectionType,
parentId
}) => {
const { libraryViewSettings, setLibraryViewSettings } =
useLibrarySettings();
const {
isLoading: isLoading,
data: itemsResult,
isFetching
} = useGetViewItemsByType(viewType, parentId);

const handleViewType = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setViewType(e.target.value);
},
[setViewType]
);

const isBtnPlayAllEnabled = useCallback(() => {
return (
viewType !== 'collections'
&& viewType !== 'trailers'
&& viewType !== 'albumArtists'
&& viewType !== 'artists'
&& viewType !== 'photos'
);
}, [viewType]);

const isBtnShuffleEnabled = useCallback(() => {
return (
viewType !== 'collections'
&& viewType !== 'trailers'
&& viewType !== 'albumArtists'
&& viewType !== 'artists'
&& viewType !== 'photos'
);
}, [viewType]);

const isBtnViewSettingsEnabled = useCallback(() => {
return viewType !== 'songs' && viewType !== 'trailers';
}, [viewType]);

const isBtnSortEnabled = useCallback(() => {
return viewType !== 'collections';
}, [viewType]);

const isBtnFilterEnabled = useCallback(() => {
return viewType !== 'collections';
}, [viewType]);

const isBtnNewCollectionEnabled = useCallback(() => {
return viewType === 'collections';
}, [viewType]);

const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 1
});

return (
<AppBar
position='sticky'
elevation={trigger ? 1 : 0}
sx={{
margin: 0,
top: 48,
background: trigger ? 'primary' : 'transparent'
}}
>
<Toolbar
variant='dense'
sx={{
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: 'center',
px: { xs: 2, sm: 5 }
}}
>
<Stack
display='flex'
alignItems='center'
direction='row'
divider={<Divider orientation='vertical' flexItem />}
spacing={2}
sx={{
py: { xs: 1, sm: 0 }
}}
>
<TextField
select
hiddenLabel
value={viewType}
size='small'
variant='filled'
onChange={handleViewType}
>
{viewSelectOptions.map((option) => {
return (
<MenuItem
key={option.value}
value={option.value}
>
{option.title}
</MenuItem>
);
})}
</TextField>

{visibleBtn.includes(viewType) && (
<Chip
label={
isLoading || isFetching ? (
<CircularProgress
color='inherit'
size={18}
/>
) : (
<Typography variant='h6'>
{itemsResult?.TotalRecordCount}
</Typography>
)
}
/>
)}
</Stack>

{visibleBtn.includes(viewType) && (
<Stack
display='flex'
alignItems='center'
direction='row'
spacing={2}
divider={
<Divider
orientation='vertical'
flexItem
variant='inset'
/>
}
sx={{
py: { xs: 1, sm: 0 }
}}
>
{isBtnPlayAllEnabled() && (
<PlayAllButton parentId={parentId} />
)}

{isBtnShuffleEnabled() && (
<ShuffleButton parentId={parentId} />
)}

{isBtnViewSettingsEnabled() && (
<ViewSettingsButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}

{isBtnSortEnabled() && (
<SortButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}

{isBtnFilterEnabled() && (
<FilterButton
context={collectionType}
parentId={parentId}
itemType={getItemTypesEnum(viewType)}
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}

{isBtnNewCollectionEnabled() && <NewCollectionButton />}
</Stack>
)}
</Toolbar>
</AppBar>
);
};

export default LibraryHeader;
34 changes: 34 additions & 0 deletions src/apps/experimental/components/library/NewCollectionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import globalize from 'scripts/globalize';

const NewCollectionButton: FC = () => {
const showCollectionEditor = useCallback(() => {
import('components/collectionEditor/collectionEditor').then(
({ default: CollectionEditor }) => {
const serverId = window.ApiClient.serverId();
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: [],
serverId: serverId
}).catch(() => {
// closed collection editor
});
}).catch(err => {
console.error('[NewCollection] failed to load collection editor', err);
});
}, []);

return (
<IconButton
title={globalize.translate('Add')}
className='paper-icon-button-light btnNewCollection autoSize'
onClick={showCollectionEditor}
>
<AddIcon />
</IconButton>
);
};

export default NewCollectionButton;
Loading

0 comments on commit 3e50a44

Please sign in to comment.