diff --git a/.eslintrc.js b/.eslintrc.js index a43399e96c..2b65ee767b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -261,7 +261,11 @@ module.exports = { 'ServerNotifications': 'writable', 'TaskButton': 'writable', 'UserParentalControlPage': 'writable', - 'Windows': 'readonly' + 'Windows': 'readonly', + // Build time definitions + __JF_BUILD_VERSION__: 'readonly', + __USE_SYSTEM_FONTS__: 'readonly', + __WEBPACK_SERVE__: 'readonly' }, rules: { '@typescript-eslint/prefer-string-starts-ends-with': ['error'] diff --git a/package-lock.json b/package-lock.json index cf80543d0a..c371ea7149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "screenfull": "6.0.2", "sortablejs": "1.15.2", "swiper": "11.0.5", + "usehooks-ts": "2.14.0", "webcomponents.js": "0.7.24", "whatwg-fetch": "3.6.20" }, @@ -12671,8 +12672,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -21900,6 +21900,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/usehooks-ts": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz", + "integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -31866,8 +31880,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.memoize": { "version": "4.1.2", @@ -38668,6 +38681,14 @@ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "requires": {} }, + "usehooks-ts": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz", + "integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==", + "requires": { + "lodash.debounce": "^4.0.8" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 91b87c28f3..ee026b811d 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "screenfull": "6.0.2", "sortablejs": "1.15.2", "swiper": "11.0.5", + "usehooks-ts": "2.14.0", "webcomponents.js": "0.7.24", "whatwg-fetch": "3.6.20" }, diff --git a/src/apps/experimental/components/drawers/DrawerHeaderLink.tsx b/src/apps/experimental/components/drawers/DrawerHeaderLink.tsx index 4515779763..a60dd2a6c7 100644 --- a/src/apps/experimental/components/drawers/DrawerHeaderLink.tsx +++ b/src/apps/experimental/components/drawers/DrawerHeaderLink.tsx @@ -3,15 +3,13 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import React from 'react'; -import { useApi } from 'hooks/useApi'; import { useSystemInfo } from 'hooks/useSystemInfo'; import ListItemLink from 'components/ListItemLink'; import appIcon from 'assets/img/icon-transparent.png'; const DrawerHeaderLink = () => { - const { api } = useApi(); - const { data: systemInfo } = useSystemInfo(api); + const { data: systemInfo } = useSystemInfo(); return ( diff --git a/src/apps/stable/routes/search.tsx b/src/apps/stable/routes/search.tsx index c9285f6075..5c483c9a8c 100644 --- a/src/apps/stable/routes/search.tsx +++ b/src/apps/stable/routes/search.tsx @@ -1,16 +1,46 @@ -import React, { FunctionComponent, useState } from 'react'; +import React, { type FC, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import Page from '../../../components/Page'; -import SearchFields from '../../../components/search/SearchFields'; -import SearchResults from '../../../components/search/SearchResults'; -import SearchSuggestions from '../../../components/search/SearchSuggestions'; -import LiveTVSearchResults from '../../../components/search/LiveTVSearchResults'; -import globalize from '../../../scripts/globalize'; +import Page from 'components/Page'; +import SearchFields from 'components/search/SearchFields'; +import SearchResults from 'components/search/SearchResults'; +import SearchSuggestions from 'components/search/SearchSuggestions'; +import LiveTVSearchResults from 'components/search/LiveTVSearchResults'; +import { usePrevious } from 'hooks/usePrevious'; +import globalize from 'scripts/globalize'; -const Search: FunctionComponent = () => { - const [ query, setQuery ] = useState(); - const [ searchParams ] = useSearchParams(); +const COLLECTION_TYPE_PARAM = 'collectionType'; +const PARENT_ID_PARAM = 'parentId'; +const QUERY_PARAM = 'query'; +const SERVER_ID_PARAM = 'serverId'; + +const Search: FC = () => { + const [ searchParams, setSearchParams ] = useSearchParams(); + const urlQuery = searchParams.get(QUERY_PARAM) || ''; + const [ query, setQuery ] = useState(urlQuery); + const prevQuery = usePrevious(query, ''); + + useEffect(() => { + if (query !== prevQuery) { + if (query === '' && urlQuery !== '') { + // The query input has been cleared; remove the url param + searchParams.delete(QUERY_PARAM); + setSearchParams(searchParams, { replace: true }); + } else if (query !== urlQuery) { + // Update the query url param value + searchParams.set(QUERY_PARAM, query); + setSearchParams(searchParams, { replace: true }); + } + } else if (query !== urlQuery) { + // Update the query if the query url param has changed + if (!urlQuery) { + searchParams.delete(QUERY_PARAM); + setSearchParams(searchParams, { replace: true }); + } + + setQuery(urlQuery); + } + }, [query, prevQuery, searchParams, setSearchParams, urlQuery]); return ( { title={globalize.translate('Search')} className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage' > - + {!query && } diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index 69caf9cbce..6d1c3c24f5 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -339,7 +339,8 @@ function executeCommand(item, id, options) { break; case 'addtoplaylist': import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => { - new PlaylistEditor({ + const playlistEditor = new PlaylistEditor(); + playlistEditor.show({ items: [itemId], serverId: serverId }).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); diff --git a/src/components/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js index 48cbce0e1f..d69c7b33bd 100644 --- a/src/components/multiSelect/multiSelect.js +++ b/src/components/multiSelect/multiSelect.js @@ -6,7 +6,6 @@ import dom from '../../scripts/dom'; import './multiSelect.scss'; import ServerConnections from '../ServerConnections'; import alert from '../alert'; -import PlaylistEditor from '../playlisteditor/playlisteditor'; import confirm from '../confirm/confirm'; import itemHelper from '../itemHelper'; import datetime from '../../scripts/datetime'; @@ -269,9 +268,16 @@ function showMenuForSelectedItems(e) { dispatchNeedsRefresh(); break; case 'playlist': - new PlaylistEditor({ - items: items, - serverId: serverId + import('../playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => { + const playlistEditor = new PlaylistEditor(); + playlistEditor.show({ + items: items, + serverId: serverId + }).catch(() => { + // Dialog closed + }); + }).catch(err => { + console.error('[AddToPlaylist] failed to load playlist editor', err); }); hideSelections(); dispatchNeedsRefresh(); diff --git a/src/components/playlisteditor/playlisteditor.js b/src/components/playlisteditor/playlisteditor.js index ee7cd2b41a..625135a472 100644 --- a/src/components/playlisteditor/playlisteditor.js +++ b/src/components/playlisteditor/playlisteditor.js @@ -222,7 +222,7 @@ function centerFocus(elem, horiz, on) { } export class PlaylistEditor { - constructor(options) { + show(options) { const items = options.items || {}; currentServerId = options.serverId; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 1120e03c02..9406e16e2b 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -704,15 +704,20 @@ export default function () { import('../playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => { getSaveablePlaylistItems().then(function (items) { const serverId = items.length ? items[0].ServerId : ApiClient.serverId(); - new PlaylistEditor({ + const playlistEditor = new PlaylistEditor(); + playlistEditor.show({ items: items.map(function (i) { return i.Id; }), serverId: serverId, enableAddToPlayQueue: false, defaultValue: 'new' + }).catch(() => { + // Dialog closed }); }); + }).catch(err => { + console.error('[savePlaylist] failed to load playlist editor', err); }); } diff --git a/src/components/search/LiveTVSearchResults.tsx b/src/components/search/LiveTVSearchResults.tsx index 0e115977ca..f26464ff22 100644 --- a/src/components/search/LiveTVSearchResults.tsx +++ b/src/components/search/LiveTVSearchResults.tsx @@ -2,7 +2,8 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; import type { ApiClient } from 'jellyfin-apiclient'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import classNames from 'classnames'; -import React, { FunctionComponent, useEffect, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useState } from 'react'; +import { useDebounceValue } from 'usehooks-ts'; import globalize from '../../scripts/globalize'; import ServerConnections from '../ServerConnections'; @@ -30,7 +31,7 @@ type LiveTVSearchResultsProps = { /* * React component to display search result rows for live tv library search */ -const LiveTVSearchResults: FunctionComponent = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => { +const LiveTVSearchResults: FC = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => { const [ movies, setMovies ] = useState([]); const [ episodes, setEpisodes ] = useState([]); const [ sports, setSports ] = useState([]); @@ -38,23 +39,24 @@ const LiveTVSearchResults: FunctionComponent = ({ serv const [ news, setNews ] = useState([]); const [ programs, setPrograms ] = useState([]); const [ channels, setChannels ] = useState([]); + const [ debouncedQuery ] = useDebounceValue(query, 500); + + const getDefaultParameters = useCallback(() => ({ + ParentId: parentId, + searchTerm: debouncedQuery, + Limit: 24, + Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount', + Recursive: true, + EnableTotalRecordCount: false, + ImageTypeLimit: 1, + IncludePeople: false, + IncludeMedia: false, + IncludeGenres: false, + IncludeStudios: false, + IncludeArtists: false + }), [ parentId, debouncedQuery ]); useEffect(() => { - const getDefaultParameters = () => ({ - ParentId: parentId, - searchTerm: query, - Limit: 24, - Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount', - Recursive: true, - EnableTotalRecordCount: false, - ImageTypeLimit: 1, - IncludePeople: false, - IncludeMedia: false, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false - }); - const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems( apiClient?.getCurrentUserId(), { @@ -73,65 +75,67 @@ const LiveTVSearchResults: FunctionComponent = ({ serv setPrograms([]); setChannels([]); - if (query && collectionType === CollectionType.Livetv) { - const apiClient = ServerConnections.getApiClient(serverId); - - // Movies row - fetchItems(apiClient, { - IncludeItemTypes: 'LiveTvProgram', - IsMovie: true - }) - .then(result => setMovies(result.Items || [])) - .catch(() => setMovies([])); - // Episodes row - fetchItems(apiClient, { - IncludeItemTypes: 'LiveTvProgram', - IsMovie: false, - IsSeries: true, - IsSports: false, - IsKids: false, - IsNews: false - }) - .then(result => setEpisodes(result.Items || [])) - .catch(() => setEpisodes([])); - // Sports row - fetchItems(apiClient, { - IncludeItemTypes: 'LiveTvProgram', - IsSports: true - }) - .then(result => setSports(result.Items || [])) - .catch(() => setSports([])); - // Kids row - fetchItems(apiClient, { - IncludeItemTypes: 'LiveTvProgram', - IsKids: true - }) - .then(result => setKids(result.Items || [])) - .catch(() => setKids([])); - // News row - fetchItems(apiClient, { - IncludeItemTypes: 'LiveTvProgram', - IsNews: true - }) - .then(result => setNews(result.Items || [])) - .catch(() => setNews([])); - // Programs row - fetchItems(apiClient, { - IncludeItemTypes: 'LiveTvProgram', - IsMovie: false, - IsSeries: false, - IsSports: false, - IsKids: false, - IsNews: false - }) - .then(result => setPrograms(result.Items || [])) - .catch(() => setPrograms([])); - // Channels row - fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) - .then(result => setChannels(result.Items || [])) - .catch(() => setChannels([])); + if (!debouncedQuery || collectionType !== CollectionType.Livetv) { + return; } - }, [collectionType, parentId, query, serverId]); + + const apiClient = ServerConnections.getApiClient(serverId); + + // Movies row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: true + }) + .then(result => setMovies(result.Items || [])) + .catch(() => setMovies([])); + // Episodes row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: true, + IsSports: false, + IsKids: false, + IsNews: false + }) + .then(result => setEpisodes(result.Items || [])) + .catch(() => setEpisodes([])); + // Sports row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsSports: true + }) + .then(result => setSports(result.Items || [])) + .catch(() => setSports([])); + // Kids row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsKids: true + }) + .then(result => setKids(result.Items || [])) + .catch(() => setKids([])); + // News row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsNews: true + }) + .then(result => setNews(result.Items || [])) + .catch(() => setNews([])); + // Programs row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: false, + IsSports: false, + IsKids: false, + IsNews: false + }) + .then(result => setPrograms(result.Items || [])) + .catch(() => setPrograms([])); + // Channels row + fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) + .then(result => setChannels(result.Items || [])) + .catch(() => setChannels([])); + }, [collectionType, debouncedQuery, getDefaultParameters, parentId, serverId]); return (
= ({ serv 'searchResults', 'padded-bottom-page', 'padded-top', - { 'hide': !query || collectionType !== CollectionType.Livetv } + { 'hide': !debouncedQuery || collectionType !== CollectionType.Livetv } )} > ({ - __html: `` -}); +import 'material-design-icons-iconfont'; -const normalizeInput = (value = '') => value.trim(); +import '../../styles/flexstyles.scss'; +import './searchfields.scss'; type SearchFieldsProps = { + query: string, onSearch?: (query: string) => void }; -// eslint-disable-next-line @typescript-eslint/no-empty-function -const SearchFields: FunctionComponent = ({ onSearch = () => {} }: SearchFieldsProps) => { - const element = useRef(null); - - const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch'); - - const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), [onSearch]); - - useEffect(() => { - getSearchInput()?.addEventListener('input', e => { - debouncedOnSearch(normalizeInput((e.target as HTMLInputElement).value)); - }); - getSearchInput()?.focus(); - - return () => { - debouncedOnSearch.cancel(); - }; - }, [debouncedOnSearch]); - +const SearchFields: FC = ({ + onSearch = () => { /* no-op */ }, + query +}: SearchFieldsProps) => { const onAlphaPicked = useCallback((e: Event) => { const value = (e as CustomEvent).detail.value; - const searchInput = getSearchInput(); - - if (!searchInput) { - console.error('Unexpected null reference'); - return; - } if (value === 'backspace') { - const currentValue = searchInput.value; - searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : ''; + onSearch(query.length ? query.substring(0, query.length - 1) : ''); } else { - searchInput.value += value; + onSearch(query + value); } + }, [ onSearch, query ]); - searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); - }, []); + const onChange = useCallback((e: ChangeEvent) => { + onSearch(e.target.value); + }, [ onSearch ]); return ( -
+
{layoutManager.tv && !browser.tv && diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index e267a4f5db..f7ab5909b3 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,8 +1,9 @@ import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import type { ApiClient } from 'jellyfin-apiclient'; import classNames from 'classnames'; -import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useState } from 'react'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import { useDebounceValue } from 'usehooks-ts'; import globalize from '../../scripts/globalize'; import ServerConnections from '../ServerConnections'; @@ -30,7 +31,7 @@ const isTVShows = (collectionType: string) => collectionType === CollectionType. /* * React component to display search result rows for global search and non-live tv library search */ -const SearchResults: FunctionComponent = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => { +const SearchResults: FC = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => { const [ movies, setMovies ] = useState([]); const [ shows, setShows ] = useState([]); const [ episodes, setEpisodes ] = useState([]); @@ -47,11 +48,12 @@ const SearchResults: FunctionComponent = ({ serverId = windo const [ books, setBooks ] = useState([]); const [ people, setPeople ] = useState([]); const [ collections, setCollections ] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [ isLoading, setIsLoading ] = useState(false); + const [ debouncedQuery ] = useDebounceValue(query, 500); const getDefaultParameters = useCallback(() => ({ ParentId: parentId, - searchTerm: query, + searchTerm: debouncedQuery, Limit: 100, Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount', Recursive: true, @@ -62,7 +64,7 @@ const SearchResults: FunctionComponent = ({ serverId = windo IncludeGenres: false, IncludeStudios: false, IncludeArtists: false - }), [parentId, query]); + }), [ parentId, debouncedQuery ]); const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => ( apiClient?.getArtists( @@ -97,6 +99,10 @@ const SearchResults: FunctionComponent = ({ serverId = windo ).then(ensureNonNullItems) ), [getDefaultParameters]); + useEffect(() => { + if (query) setIsLoading(true); + }, [ query ]); + useEffect(() => { // Reset state setMovies([]); @@ -116,13 +122,11 @@ const SearchResults: FunctionComponent = ({ serverId = windo setPeople([]); setCollections([]); - if (!query) { + if (!debouncedQuery) { setIsLoading(false); return; } - setIsLoading(true); - const apiClient = ServerConnections.getApiClient(serverId); const fetchPromises = []; @@ -230,7 +234,7 @@ const SearchResults: FunctionComponent = ({ serverId = windo console.error('An error occurred while fetching data:', error); setIsLoading(false); // Set loading to false even if an error occurs }); - }, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]); + }, [collectionType, fetchArtists, fetchItems, fetchPeople, debouncedQuery, serverId]); const allEmpty = [movies, shows, episodes, videos, programs, channels, playlists, artists, albums, songs, photoAlbums, photos, audioBooks, books, people, collections].every(arr => arr.length === 0); @@ -240,7 +244,7 @@ const SearchResults: FunctionComponent = ({ serverId = windo 'searchResults', 'padded-bottom-page', 'padded-top', - { 'hide': !query || collectionType === CollectionType.Livetv } + { 'hide': !debouncedQuery || collectionType === CollectionType.Livetv } )} > {isLoading ? ( @@ -335,8 +339,10 @@ const SearchResults: FunctionComponent = ({ serverId = windo cardOptions={{ coverImage: true }} /> - {allEmpty && query && !isLoading && ( -
{globalize.translate('SearchResultsEmpty', query)}
+ {allEmpty && debouncedQuery && !isLoading && ( +
+ {globalize.translate('SearchResultsEmpty', debouncedQuery)} +
)} )} diff --git a/src/components/search/searchfields.scss b/src/components/search/searchfields.scss index b93638bdf3..08d8515c86 100644 --- a/src/components/search/searchfields.scss +++ b/src/components/search/searchfields.scss @@ -9,14 +9,3 @@ font-size: 2em; align-self: flex-end; } - -.sorry-text { - font-size: 2em; - text-align: center; - font-family: inherit; - width: 100%; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} diff --git a/src/components/shortcuts.js b/src/components/shortcuts.js index 03418f3e21..cadb8cc20e 100644 --- a/src/components/shortcuts.js +++ b/src/components/shortcuts.js @@ -282,11 +282,15 @@ function executeAction(card, target, action) { function addToPlaylist(item) { import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => { - new PlaylistEditor().show({ + const playlistEditor = new PlaylistEditor(); + playlistEditor.show({ items: [item.Id], serverId: item.ServerId - + }).catch(() => { + // Dialog closed }); + }).catch(err => { + console.error('[addToPlaylist] failed to load playlist editor', err); }); } diff --git a/src/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index aed6bb206f..4dd260877c 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -8,16 +8,22 @@ -
-

-

+
+
${LabelServerName}
+
+
${LabelServerVersion}
+
+
${LabelWebVersion}
+
+
${LabelBuildVersion}
+
-