1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Convert ItemsContainer to react

This commit is contained in:
grafixeyehero 2023-10-13 02:07:49 +03:00
parent 71e431d562
commit c767dba439
7 changed files with 568 additions and 39 deletions

13
package-lock.json generated
View file

@ -74,6 +74,7 @@
"@types/lodash-es": "4.17.7",
"@types/react": "17.0.59",
"@types/react-dom": "17.0.20",
"@types/sortablejs": "1.15.3",
"@typescript-eslint/eslint-plugin": "5.59.7",
"@typescript-eslint/parser": "5.59.7",
"@uupaa/dynamic-import-polyfill": "1.0.2",
@ -4317,6 +4318,12 @@
"@types/node": "*"
}
},
"node_modules/@types/sortablejs": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.3.tgz",
"integrity": "sha512-v+zh6TZP/cLeMUK0MDx1onp8e7Jk2/4iTQ7sb/n80rTAvBm14yJkpOEfJdrTCkHiF7IZbPjxGX2NRJfogRoYIg==",
"dev": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
@ -24923,6 +24930,12 @@
"@types/node": "*"
}
},
"@types/sortablejs": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.3.tgz",
"integrity": "sha512-v+zh6TZP/cLeMUK0MDx1onp8e7Jk2/4iTQ7sb/n80rTAvBm14yJkpOEfJdrTCkHiF7IZbPjxGX2NRJfogRoYIg==",
"dev": true
},
"@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",

View file

@ -16,6 +16,7 @@
"@types/lodash-es": "4.17.7",
"@types/react": "17.0.59",
"@types/react-dom": "17.0.20",
"@types/sortablejs": "1.15.3",
"@typescript-eslint/eslint-plugin": "5.59.7",
"@typescript-eslint/parser": "5.59.7",
"@uupaa/dynamic-import-polyfill": "1.0.2",

View file

@ -1,33 +0,0 @@
import React, { FC, useEffect, useRef } from 'react';
import ItemsContainerElement from 'elements/ItemsContainerElement';
import imageLoader from 'components/images/imageLoader';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import { LibraryViewSettings, ViewMode } from 'types/library';
interface ItemsContainerI {
libraryViewSettings: LibraryViewSettings;
getItemsHtml: () => string
}
const ItemsContainer: FC<ItemsContainerI> = ({ libraryViewSettings, getItemsHtml }) => {
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement;
itemsContainer.innerHTML = getItemsHtml();
imageLoader.lazyChildren(itemsContainer);
}, [getItemsHtml]);
const cssClass = libraryViewSettings.ViewMode === ViewMode.ListView ? 'vertical-list' : 'vertical-wrap';
return (
<div ref={element}>
<ItemsContainerElement
className={`itemsContainer ${cssClass} centered padded-left padded-right padded-right-withalphapicker`}
/>
</div>
);
};
export default ItemsContainer;

View file

@ -11,9 +11,9 @@ import listview from 'components/listview/listview';
import cardBuilder from 'components/cardbuilder/cardBuilder';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'scripts/globalize';
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
import AlphabetPicker from './AlphabetPicker';
import FilterButton from './filter/FilterButton';
import ItemsContainer from './ItemsContainer';
import NewCollectionButton from './NewCollectionButton';
import Pagination from './Pagination';
import PlayAllButton from './PlayAllButton';
@ -67,7 +67,8 @@ const ItemsView: FC<ItemsViewProps> = ({
const {
isLoading,
data: itemsResult,
isPreviousData
isPreviousData,
refetch
} = useGetItemsViewByType(
viewType,
parentId,
@ -252,7 +253,10 @@ const ItemsView: FC<ItemsViewProps> = ({
<Loading />
) : (
<ItemsContainer
className='centered padded-left padded-right padded-right-withalphapicker'
libraryViewSettings={libraryViewSettings}
parentId={parentId}
reloadItems={refetch}
getItemsHtml={getItemsHtml}
/>
)}

View file

@ -0,0 +1,522 @@
import type {
LibraryUpdateInfo,
SeriesTimerInfoDto,
TimerInfoDto,
UserItemDataDto
} from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef } from 'react';
import classNames from 'classnames';
import { Box } from '@mui/material';
import Sortable from 'sortablejs';
import { usePlaylistsMoveItemMutation } from 'hooks/useFetchItems';
import Events, { Event } from 'utils/events';
import serverNotifications from 'scripts/serverNotifications';
import inputManager from 'scripts/inputManager';
import dom from 'scripts/dom';
import browser from 'scripts/browser';
import imageLoader from 'components/images/imageLoader';
import layoutManager from 'components/layoutManager';
import { playbackManager } from 'components/playback/playbackmanager';
import itemShortcuts from 'components/shortcuts';
import MultiSelect from 'components/multiSelect/multiSelect';
import loading from 'components/loading/loading';
import focusManager from 'components/focusManager';
import { LibraryViewSettings, ParentId, ViewMode } from 'types/library';
function disableEvent(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
return false;
}
function getShortcutOptions() {
return {
click: false
};
}
interface ItemsContainerProps {
className?: string;
libraryViewSettings: LibraryViewSettings;
isContextMenuEnabled?: boolean;
isMultiSelectEnabled?: boolean;
isDragreOrderEnabled?: boolean;
dataMonitor?: string;
parentId?: ParentId;
reloadItems: () => void;
getItemsHtml?: () => string;
children?: React.ReactNode;
}
const ItemsContainer: FC<ItemsContainerProps> = ({
className,
libraryViewSettings,
isContextMenuEnabled,
isMultiSelectEnabled,
isDragreOrderEnabled,
dataMonitor,
parentId,
reloadItems,
getItemsHtml,
children
}) => {
const { mutateAsync: playlistsMoveItemMutation } = usePlaylistsMoveItemMutation();
const itemsContainerRef = useRef<HTMLDivElement>(null);
const multiSelectref = useRef<MultiSelect | null>(null);
const sortableref = useRef<Sortable | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const onClick = useCallback((e: MouseEvent) => {
const itemsContainer = itemsContainerRef.current as HTMLDivElement;
const multiSelect = multiSelectref.current;
if (
multiSelect
&& multiSelect.onContainerClick.call(itemsContainer, e) === false
) {
return;
}
itemShortcuts.onClick.call(itemsContainer, e);
}, []);
const onContextMenu = useCallback((e: MouseEvent) => {
const target = e.target as HTMLElement;
const card = dom.parentWithAttribute(target, 'data-id');
// check for serverId, it won't be present on selectserver
if (card?.getAttribute('data-serverid')) {
inputManager.handleCommand('menu', {
sourceElement: card
});
e.preventDefault();
e.stopPropagation();
return false;
}
}, []);
const initMultiSelect = useCallback((itemsContainer: HTMLDivElement) => {
multiSelectref.current = new MultiSelect({
container: itemsContainer,
bindOnClick: false
});
}, []);
const destroyMultiSelect = useCallback(() => {
if (multiSelectref.current) {
multiSelectref.current.destroy();
multiSelectref.current = null;
}
}, []);
const onDrop = useCallback(
async (evt: Sortable.SortableEvent) => {
const el = evt.item;
const newIndex = evt.newIndex;
const itemId = el.getAttribute('data-playlistitemid');
const playlistId = el.getAttribute('data-playlistid');
if (!playlistId) {
const oldIndex = evt.oldIndex;
el.dispatchEvent(
new CustomEvent('itemdrop', {
detail: {
oldIndex: oldIndex,
newIndex: newIndex,
playlistItemId: itemId
},
bubbles: true,
cancelable: false
})
);
return;
}
if (!itemId) throw new Error('null itemId');
if (!newIndex) throw new Error('null newIndex');
try {
loading.show();
await playlistsMoveItemMutation({
playlistId,
itemId,
newIndex
});
loading.hide();
} catch (error) {
loading.hide();
reloadItems();
}
},
[playlistsMoveItemMutation, reloadItems]
);
const initDragReordering = useCallback((itemsContainer: HTMLDivElement) => {
sortableref.current = Sortable.create(itemsContainer, {
draggable: '.listItem',
handle: '.listViewDragHandle',
// dragging ended
onEnd: (evt: Sortable.SortableEvent) => {
return onDrop(evt);
}
});
}, [onDrop]);
const destroyDragReordering = useCallback(() => {
if (sortableref.current) {
sortableref.current.destroy();
sortableref.current = null;
}
}, []);
const notifyRefreshNeeded = useCallback(
(isInForeground: boolean) => {
if (isInForeground === true) {
reloadItems();
} else {
timerRef.current = setTimeout(() => reloadItems(), 10000);
}
},
[reloadItems]
);
const getEventsToMonitor = useCallback(() => {
const monitor = dataMonitor;
if (monitor) {
return monitor.split(',');
}
return [];
}, [dataMonitor]);
const onUserDataChanged = useCallback(
(_e: Event, userData: UserItemDataDto) => {
const itemsContainer = itemsContainerRef.current as HTMLDivElement;
import('../../components/cardbuilder/cardBuilder')
.then((cardBuilder) => {
cardBuilder.onUserDataChanged(userData, itemsContainer);
})
.catch((err) => {
console.error(
'[onUserDataChanged] failed to load onUserData Changed',
err
);
});
const eventsToMonitor = getEventsToMonitor();
if (
eventsToMonitor.indexOf('markfavorite') !== -1
|| eventsToMonitor.indexOf('markplayed') !== -1
) {
notifyRefreshNeeded(false);
}
},
[getEventsToMonitor, notifyRefreshNeeded]
);
const onTimerCreated = useCallback(
(_e: Event, data: TimerInfoDto) => {
const itemsContainer = itemsContainerRef.current as HTMLDivElement;
const eventsToMonitor = getEventsToMonitor();
if (eventsToMonitor.indexOf('timers') !== -1) {
notifyRefreshNeeded(false);
return;
}
const programId = data.ProgramId;
// This could be null, not supported by all tv providers
const newTimerId = data.Id;
if (programId && newTimerId) {
import('../../components/cardbuilder/cardBuilder')
.then((cardBuilder) => {
cardBuilder.onTimerCreated(
programId,
newTimerId,
itemsContainer
);
})
.catch((err) => {
console.error(
'[onTimerCreated] failed to load onTimer Created',
err
);
});
}
},
[getEventsToMonitor, notifyRefreshNeeded]
);
const onSeriesTimerCreated = useCallback(() => {
const eventsToMonitor = getEventsToMonitor();
if (eventsToMonitor.indexOf('seriestimers') !== -1) {
notifyRefreshNeeded(false);
}
}, [getEventsToMonitor, notifyRefreshNeeded]);
const onTimerCancelled = useCallback(
(_e: Event, data: TimerInfoDto) => {
const itemsContainer = itemsContainerRef.current as HTMLDivElement;
const eventsToMonitor = getEventsToMonitor();
if (eventsToMonitor.indexOf('timers') !== -1) {
notifyRefreshNeeded(false);
return;
}
const timerId = data.Id;
if (timerId) {
import('../../components/cardbuilder/cardBuilder')
.then((cardBuilder) => {
cardBuilder.onTimerCancelled(timerId, itemsContainer);
})
.catch((err) => {
console.error(
'[onTimerCancelled] failed to load onTimer Cancelled',
err
);
});
}
},
[getEventsToMonitor, notifyRefreshNeeded]
);
const onSeriesTimerCancelled = useCallback(
(_e: Event, data: SeriesTimerInfoDto) => {
const itemsContainer = itemsContainerRef.current as HTMLDivElement;
const eventsToMonitor = getEventsToMonitor();
if (eventsToMonitor.indexOf('seriestimers') !== -1) {
notifyRefreshNeeded(false);
return;
}
const cancelledTimerId = data.Id;
if (cancelledTimerId) {
import('../../components/cardbuilder/cardBuilder')
.then((cardBuilder) => {
cardBuilder.onSeriesTimerCancelled(
cancelledTimerId,
itemsContainer
);
})
.catch((err) => {
console.error(
'[onSeriesTimerCancelled] failed to load onSeriesTimer Cancelled',
err
);
});
}
},
[getEventsToMonitor, notifyRefreshNeeded]
);
const onLibraryChanged = useCallback(
(_e: Event, data: LibraryUpdateInfo) => {
const eventsToMonitor = getEventsToMonitor();
if (
eventsToMonitor.indexOf('seriestimers') !== -1
|| eventsToMonitor.indexOf('timers') !== -1
) {
// yes this is an assumption
return;
}
const itemsAdded = data.ItemsAdded ?? [];
const itemsRemoved = data.ItemsRemoved ?? [];
if (!itemsAdded.length && !itemsRemoved.length) {
return;
}
if (parentId) {
const foldersAddedTo = data.FoldersAddedTo ?? [];
const foldersRemovedFrom = data.FoldersRemovedFrom ?? [];
const collectionFolders = data.CollectionFolders ?? [];
if (
foldersAddedTo.indexOf(parentId) === -1
&& foldersRemovedFrom.indexOf(parentId) === -1
&& collectionFolders.indexOf(parentId) === -1
) {
return;
}
}
notifyRefreshNeeded(false);
},
[getEventsToMonitor, notifyRefreshNeeded, parentId]
);
const onPlaybackStopped = useCallback(
(_e: Event, stopInfo) => {
const state = stopInfo.state;
const eventsToMonitor = getEventsToMonitor();
if (
state.NowPlayingItem
&& state.NowPlayingItem.MediaType === 'Video'
) {
if (eventsToMonitor.indexOf('videoplayback') !== -1) {
notifyRefreshNeeded(true);
return;
}
} else if (
state.NowPlayingItem
&& state.NowPlayingItem.MediaType === 'Audio'
&& eventsToMonitor.indexOf('audioplayback') !== -1
) {
notifyRefreshNeeded(true);
return;
}
},
[getEventsToMonitor, notifyRefreshNeeded]
);
const setFocus = useCallback(
(
itemsContainer: HTMLDivElement,
focusId: string | null | undefined
) => {
if (focusId) {
const newElement = itemsContainer.querySelector(
'[data-id="' + focusId + '"]'
);
if (newElement) {
try {
focusManager.focus(newElement);
return;
} catch (err) {
console.error(err);
}
}
}
focusManager.autoFocus(itemsContainer);
},
[]
);
useEffect(() => {
const itemsContainer = itemsContainerRef.current;
if (!itemsContainer) {
console.error('Unexpected null reference');
return;
}
const activeElement = document.activeElement;
let focusId;
let hasActiveElement;
if (itemsContainer?.contains(activeElement)) {
hasActiveElement = true;
focusId = activeElement?.getAttribute('data-id');
}
if (getItemsHtml) {
itemsContainer.innerHTML = getItemsHtml();
}
imageLoader.lazyChildren(itemsContainer);
if (hasActiveElement) {
setFocus(itemsContainer, focusId);
}
}, [getItemsHtml, setFocus]);
useEffect(() => {
const itemsContainer = itemsContainerRef.current;
if (!itemsContainer) {
console.error('Unexpected null reference');
return;
}
if (
layoutManager.desktop
|| (layoutManager.mobile && isMultiSelectEnabled !== false)
) {
initMultiSelect(itemsContainer);
}
if (isDragreOrderEnabled === true) {
initDragReordering(itemsContainer);
}
itemsContainer.addEventListener('click', onClick);
if (browser.touch) {
itemsContainer.addEventListener('contextmenu', disableEvent);
} else if (isContextMenuEnabled !== false) {
itemsContainer.addEventListener('contextmenu', onContextMenu);
}
itemShortcuts.on(itemsContainer, getShortcutOptions());
Events.on(serverNotifications, 'UserDataChanged', onUserDataChanged);
Events.on(serverNotifications, 'TimerCreated', onTimerCreated);
Events.on(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.on(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);
Events.on(serverNotifications, 'LibraryChanged', onLibraryChanged);
Events.on(playbackManager, 'playbackstop', onPlaybackStopped);
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
destroyMultiSelect();
destroyDragReordering();
itemsContainer.removeEventListener('click', onClick);
itemsContainer.removeEventListener('contextmenu', onContextMenu);
itemsContainer.removeEventListener('contextmenu', disableEvent);
itemShortcuts.off(itemsContainer, getShortcutOptions());
Events.off(serverNotifications, 'UserDataChanged', onUserDataChanged);
Events.off(serverNotifications, 'TimerCreated', onTimerCreated);
Events.off(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.off( serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);
Events.off(serverNotifications, 'LibraryChanged', onLibraryChanged);
Events.off(playbackManager, 'playbackstop', onPlaybackStopped);
};
}, [
destroyDragReordering,
destroyMultiSelect,
initDragReordering,
initMultiSelect,
isContextMenuEnabled,
isDragreOrderEnabled,
isMultiSelectEnabled,
onClick,
onContextMenu,
onLibraryChanged,
onPlaybackStopped,
onSeriesTimerCancelled,
onSeriesTimerCreated,
onTimerCancelled,
onTimerCreated,
onUserDataChanged
]);
const itemsContainerClass = classNames(
'itemsContainer',
{ 'itemsContainer-tv': layoutManager.tv },
libraryViewSettings.ViewMode === ViewMode.ListView ?
'vertical-list' :
'vertical-wrap',
className
);
return (
<Box ref={itemsContainerRef} className={itemsContainerClass}>
{children}
</Box>
);
};
export default ItemsContainer;

View file

@ -1,4 +1,5 @@
import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client';
import { AxiosRequestConfig } from 'axios';
import type { ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest } from '@jellyfin/sdk/lib/generated-client';
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
@ -13,8 +14,8 @@ import { getMoviesApi } from '@jellyfin/sdk/lib/utils/api/movies-api';
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
import { AxiosRequestConfig } from 'axios';
import { useQuery } from '@tanstack/react-query';
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
import { useMutation, useQuery } from '@tanstack/react-query';
import { JellyfinApiContext, useApi } from './useApi';
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
@ -509,3 +510,24 @@ export const useGetItemsViewByType = (
].includes(viewType) && !!parentId
});
};
const fetchPlaylistsMoveItem = async (
currentApi: JellyfinApiContext,
requestParameters: PlaylistsApiMoveItemRequest
) => {
const { api, user } = currentApi;
if (api && user?.Id) {
const response = await getPlaylistsApi(api).moveItem({
...requestParameters
});
return response.data;
}
};
export const usePlaylistsMoveItemMutation = () => {
const currentApi = useApi();
return useMutation({
mutationFn: (requestParameters: PlaylistsApiMoveItemRequest) =>
fetchPlaylistsMoveItem(currentApi, requestParameters )
});
};

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
interface Event {
export interface Event {
type: string;
}