mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into trickplay-new
This commit is contained in:
commit
7f7c1be5e1
44 changed files with 548 additions and 277 deletions
|
@ -261,7 +261,11 @@ module.exports = {
|
||||||
'ServerNotifications': 'writable',
|
'ServerNotifications': 'writable',
|
||||||
'TaskButton': 'writable',
|
'TaskButton': 'writable',
|
||||||
'UserParentalControlPage': 'writable',
|
'UserParentalControlPage': 'writable',
|
||||||
'Windows': 'readonly'
|
'Windows': 'readonly',
|
||||||
|
// Build time definitions
|
||||||
|
__JF_BUILD_VERSION__: 'readonly',
|
||||||
|
__USE_SYSTEM_FONTS__: 'readonly',
|
||||||
|
__WEBPACK_SERVE__: 'readonly'
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
|
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
|
||||||
|
|
29
package-lock.json
generated
29
package-lock.json
generated
|
@ -58,6 +58,7 @@
|
||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "11.0.5",
|
"swiper": "11.0.5",
|
||||||
|
"usehooks-ts": "2.14.0",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
},
|
},
|
||||||
|
@ -12671,8 +12672,7 @@
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
|
@ -21900,6 +21900,20 @@
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -31866,8 +31880,7 @@
|
||||||
"lodash.debounce": {
|
"lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.memoize": {
|
"lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
|
@ -38668,6 +38681,14 @@
|
||||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
"requires": {}
|
"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": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "11.0.5",
|
"swiper": "11.0.5",
|
||||||
|
"usehooks-ts": "2.14.0",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,15 +3,13 @@ import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useApi } from 'hooks/useApi';
|
|
||||||
import { useSystemInfo } from 'hooks/useSystemInfo';
|
import { useSystemInfo } from 'hooks/useSystemInfo';
|
||||||
import ListItemLink from 'components/ListItemLink';
|
import ListItemLink from 'components/ListItemLink';
|
||||||
|
|
||||||
import appIcon from 'assets/img/icon-transparent.png';
|
import appIcon from 'assets/img/icon-transparent.png';
|
||||||
|
|
||||||
const DrawerHeaderLink = () => {
|
const DrawerHeaderLink = () => {
|
||||||
const { api } = useApi();
|
const { data: systemInfo } = useSystemInfo();
|
||||||
const { data: systemInfo } = useSystemInfo(api);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemLink to='/'>
|
<ListItemLink to='/'>
|
||||||
|
|
|
@ -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 { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import Page from '../../../components/Page';
|
import Page from 'components/Page';
|
||||||
import SearchFields from '../../../components/search/SearchFields';
|
import SearchFields from 'components/search/SearchFields';
|
||||||
import SearchResults from '../../../components/search/SearchResults';
|
import SearchResults from 'components/search/SearchResults';
|
||||||
import SearchSuggestions from '../../../components/search/SearchSuggestions';
|
import SearchSuggestions from 'components/search/SearchSuggestions';
|
||||||
import LiveTVSearchResults from '../../../components/search/LiveTVSearchResults';
|
import LiveTVSearchResults from 'components/search/LiveTVSearchResults';
|
||||||
import globalize from '../../../scripts/globalize';
|
import { usePrevious } from 'hooks/usePrevious';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
const Search: FunctionComponent = () => {
|
const COLLECTION_TYPE_PARAM = 'collectionType';
|
||||||
const [ query, setQuery ] = useState<string>();
|
const PARENT_ID_PARAM = 'parentId';
|
||||||
const [ searchParams ] = useSearchParams();
|
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 (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -18,22 +48,22 @@ const Search: FunctionComponent = () => {
|
||||||
title={globalize.translate('Search')}
|
title={globalize.translate('Search')}
|
||||||
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
||||||
>
|
>
|
||||||
<SearchFields onSearch={setQuery} />
|
<SearchFields query={query} onSearch={setQuery} />
|
||||||
{!query
|
{!query
|
||||||
&& <SearchSuggestions
|
&& <SearchSuggestions
|
||||||
parentId={searchParams.get('parentId')}
|
parentId={searchParams.get(PARENT_ID_PARAM)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<SearchResults
|
<SearchResults
|
||||||
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
|
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
|
||||||
parentId={searchParams.get('parentId')}
|
parentId={searchParams.get(PARENT_ID_PARAM)}
|
||||||
collectionType={searchParams.get('collectionType')}
|
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
|
||||||
query={query}
|
query={query}
|
||||||
/>
|
/>
|
||||||
<LiveTVSearchResults
|
<LiveTVSearchResults
|
||||||
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
|
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
|
||||||
parentId={searchParams.get('parentId')}
|
parentId={searchParams.get(PARENT_ID_PARAM)}
|
||||||
collectionType={searchParams.get('collectionType')}
|
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
|
||||||
query={query}
|
query={query}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -339,7 +339,8 @@ function executeCommand(item, id, options) {
|
||||||
break;
|
break;
|
||||||
case 'addtoplaylist':
|
case 'addtoplaylist':
|
||||||
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
new PlaylistEditor({
|
const playlistEditor = new PlaylistEditor();
|
||||||
|
playlistEditor.show({
|
||||||
items: [itemId],
|
items: [itemId],
|
||||||
serverId: serverId
|
serverId: serverId
|
||||||
}).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
}).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
|
|
|
@ -6,7 +6,6 @@ import dom from '../../scripts/dom';
|
||||||
import './multiSelect.scss';
|
import './multiSelect.scss';
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
import alert from '../alert';
|
import alert from '../alert';
|
||||||
import PlaylistEditor from '../playlisteditor/playlisteditor';
|
|
||||||
import confirm from '../confirm/confirm';
|
import confirm from '../confirm/confirm';
|
||||||
import itemHelper from '../itemHelper';
|
import itemHelper from '../itemHelper';
|
||||||
import datetime from '../../scripts/datetime';
|
import datetime from '../../scripts/datetime';
|
||||||
|
@ -269,9 +268,16 @@ function showMenuForSelectedItems(e) {
|
||||||
dispatchNeedsRefresh();
|
dispatchNeedsRefresh();
|
||||||
break;
|
break;
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
new PlaylistEditor({
|
import('../playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
items: items,
|
const playlistEditor = new PlaylistEditor();
|
||||||
serverId: serverId
|
playlistEditor.show({
|
||||||
|
items: items,
|
||||||
|
serverId: serverId
|
||||||
|
}).catch(() => {
|
||||||
|
// Dialog closed
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[AddToPlaylist] failed to load playlist editor', err);
|
||||||
});
|
});
|
||||||
hideSelections();
|
hideSelections();
|
||||||
dispatchNeedsRefresh();
|
dispatchNeedsRefresh();
|
||||||
|
|
|
@ -222,7 +222,7 @@ function centerFocus(elem, horiz, on) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlaylistEditor {
|
export class PlaylistEditor {
|
||||||
constructor(options) {
|
show(options) {
|
||||||
const items = options.items || {};
|
const items = options.items || {};
|
||||||
currentServerId = options.serverId;
|
currentServerId = options.serverId;
|
||||||
|
|
||||||
|
|
|
@ -704,15 +704,20 @@ export default function () {
|
||||||
import('../playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
import('../playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
getSaveablePlaylistItems().then(function (items) {
|
getSaveablePlaylistItems().then(function (items) {
|
||||||
const serverId = items.length ? items[0].ServerId : ApiClient.serverId();
|
const serverId = items.length ? items[0].ServerId : ApiClient.serverId();
|
||||||
new PlaylistEditor({
|
const playlistEditor = new PlaylistEditor();
|
||||||
|
playlistEditor.show({
|
||||||
items: items.map(function (i) {
|
items: items.map(function (i) {
|
||||||
return i.Id;
|
return i.Id;
|
||||||
}),
|
}),
|
||||||
serverId: serverId,
|
serverId: serverId,
|
||||||
enableAddToPlayQueue: false,
|
enableAddToPlayQueue: false,
|
||||||
defaultValue: 'new'
|
defaultValue: 'new'
|
||||||
|
}).catch(() => {
|
||||||
|
// Dialog closed
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[savePlaylist] failed to load playlist editor', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import type { ApiClient } from 'jellyfin-apiclient';
|
import type { ApiClient } from 'jellyfin-apiclient';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import classNames from 'classnames';
|
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 globalize from '../../scripts/globalize';
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
|
@ -30,7 +31,7 @@ type LiveTVSearchResultsProps = {
|
||||||
/*
|
/*
|
||||||
* React component to display search result rows for live tv library search
|
* React component to display search result rows for live tv library search
|
||||||
*/
|
*/
|
||||||
const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
|
const LiveTVSearchResults: FC<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
|
||||||
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
||||||
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
||||||
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
|
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
|
||||||
|
@ -38,23 +39,24 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
||||||
const [ news, setNews ] = useState<BaseItemDto[]>([]);
|
const [ news, setNews ] = useState<BaseItemDto[]>([]);
|
||||||
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
|
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
|
||||||
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
|
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
|
||||||
|
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(() => {
|
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(
|
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
|
||||||
apiClient?.getCurrentUserId(),
|
apiClient?.getCurrentUserId(),
|
||||||
{
|
{
|
||||||
|
@ -73,65 +75,67 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
||||||
setPrograms([]);
|
setPrograms([]);
|
||||||
setChannels([]);
|
setChannels([]);
|
||||||
|
|
||||||
if (query && collectionType === CollectionType.Livetv) {
|
if (!debouncedQuery || collectionType !== CollectionType.Livetv) {
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
return;
|
||||||
|
|
||||||
// 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, 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -139,7 +143,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
||||||
'searchResults',
|
'searchResults',
|
||||||
'padded-bottom-page',
|
'padded-bottom-page',
|
||||||
'padded-top',
|
'padded-top',
|
||||||
{ 'hide': !query || collectionType !== CollectionType.Livetv }
|
{ 'hide': !debouncedQuery || collectionType !== CollectionType.Livetv }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SearchResultsRow
|
<SearchResultsRow
|
||||||
|
|
|
@ -1,89 +1,61 @@
|
||||||
import debounce from 'lodash-es/debounce';
|
import React, { type ChangeEvent, type FC, useCallback } from 'react';
|
||||||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
||||||
|
import Input from 'elements/emby-input/Input';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
|
||||||
import 'material-design-icons-iconfont';
|
|
||||||
|
|
||||||
import '../../elements/emby-input/emby-input';
|
|
||||||
import '../../styles/flexstyles.scss';
|
|
||||||
import './searchfields.scss';
|
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from '../layoutManager';
|
||||||
import browser from '../../scripts/browser';
|
import browser from '../../scripts/browser';
|
||||||
|
|
||||||
// There seems to be some compatibility issues here between
|
import 'material-design-icons-iconfont';
|
||||||
// React and our legacy web components, so we need to inject
|
|
||||||
// them as an html string for now =/
|
|
||||||
const createInputElement = () => ({
|
|
||||||
__html: `<input
|
|
||||||
is="emby-input"
|
|
||||||
class="searchfields-txtSearch"
|
|
||||||
type="text"
|
|
||||||
data-keyboard="true"
|
|
||||||
placeholder="${globalize.translate('Search')}"
|
|
||||||
autocomplete="off"
|
|
||||||
maxlength="40"
|
|
||||||
autofocus
|
|
||||||
/>`
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalizeInput = (value = '') => value.trim();
|
import '../../styles/flexstyles.scss';
|
||||||
|
import './searchfields.scss';
|
||||||
|
|
||||||
type SearchFieldsProps = {
|
type SearchFieldsProps = {
|
||||||
|
query: string,
|
||||||
onSearch?: (query: string) => void
|
onSearch?: (query: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
const SearchFields: FC<SearchFieldsProps> = ({
|
||||||
const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {} }: SearchFieldsProps) => {
|
onSearch = () => { /* no-op */ },
|
||||||
const element = useRef<HTMLDivElement>(null);
|
query
|
||||||
|
}: SearchFieldsProps) => {
|
||||||
const getSearchInput = () => element?.current?.querySelector<HTMLInputElement>('.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 onAlphaPicked = useCallback((e: Event) => {
|
const onAlphaPicked = useCallback((e: Event) => {
|
||||||
const value = (e as CustomEvent).detail.value;
|
const value = (e as CustomEvent).detail.value;
|
||||||
const searchInput = getSearchInput();
|
|
||||||
|
|
||||||
if (!searchInput) {
|
|
||||||
console.error('Unexpected null reference');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 'backspace') {
|
if (value === 'backspace') {
|
||||||
const currentValue = searchInput.value;
|
onSearch(query.length ? query.substring(0, query.length - 1) : '');
|
||||||
searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : '';
|
|
||||||
} else {
|
} else {
|
||||||
searchInput.value += value;
|
onSearch(query + value);
|
||||||
}
|
}
|
||||||
|
}, [ onSearch, query ]);
|
||||||
|
|
||||||
searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
}, []);
|
onSearch(e.target.value);
|
||||||
|
}, [ onSearch ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='padded-left padded-right searchFields'>
|
||||||
className='padded-left padded-right searchFields'
|
|
||||||
ref={element}
|
|
||||||
>
|
|
||||||
<div className='searchFieldsInner flex align-items-center justify-content-center'>
|
<div className='searchFieldsInner flex align-items-center justify-content-center'>
|
||||||
<span className='searchfields-icon material-icons search' aria-hidden='true' />
|
<span className='searchfields-icon material-icons search' aria-hidden='true' />
|
||||||
<div
|
<div
|
||||||
className='inputContainer flex-grow'
|
className='inputContainer flex-grow'
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
dangerouslySetInnerHTML={createInputElement()}
|
>
|
||||||
/>
|
<Input
|
||||||
|
id='searchTextInput'
|
||||||
|
className='searchfields-txtSearch'
|
||||||
|
type='text'
|
||||||
|
data-keyboard='true'
|
||||||
|
placeholder={globalize.translate('Search')}
|
||||||
|
autoComplete='off'
|
||||||
|
maxLength={40}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
|
autoFocus
|
||||||
|
value={query}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{layoutManager.tv && !browser.tv
|
{layoutManager.tv && !browser.tv
|
||||||
&& <AlphaPicker onAlphaPicked={onAlphaPicked} />
|
&& <AlphaPicker onAlphaPicked={onAlphaPicked} />
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import type { ApiClient } from 'jellyfin-apiclient';
|
import type { ApiClient } from 'jellyfin-apiclient';
|
||||||
import classNames from 'classnames';
|
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 { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import { useDebounceValue } from 'usehooks-ts';
|
||||||
|
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
import ServerConnections from '../ServerConnections';
|
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
|
* React component to display search result rows for global search and non-live tv library search
|
||||||
*/
|
*/
|
||||||
const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
|
const SearchResults: FC<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
|
||||||
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
||||||
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
|
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
|
||||||
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
||||||
|
@ -47,11 +48,12 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
||||||
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
|
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
|
||||||
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
||||||
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
|
const [ debouncedQuery ] = useDebounceValue(query, 500);
|
||||||
|
|
||||||
const getDefaultParameters = useCallback(() => ({
|
const getDefaultParameters = useCallback(() => ({
|
||||||
ParentId: parentId,
|
ParentId: parentId,
|
||||||
searchTerm: query,
|
searchTerm: debouncedQuery,
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
|
@ -62,7 +64,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
||||||
IncludeGenres: false,
|
IncludeGenres: false,
|
||||||
IncludeStudios: false,
|
IncludeStudios: false,
|
||||||
IncludeArtists: false
|
IncludeArtists: false
|
||||||
}), [parentId, query]);
|
}), [ parentId, debouncedQuery ]);
|
||||||
|
|
||||||
const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
|
const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
|
||||||
apiClient?.getArtists(
|
apiClient?.getArtists(
|
||||||
|
@ -97,6 +99,10 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
||||||
).then(ensureNonNullItems)
|
).then(ensureNonNullItems)
|
||||||
), [getDefaultParameters]);
|
), [getDefaultParameters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query) setIsLoading(true);
|
||||||
|
}, [ query ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset state
|
// Reset state
|
||||||
setMovies([]);
|
setMovies([]);
|
||||||
|
@ -116,13 +122,11 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
||||||
setPeople([]);
|
setPeople([]);
|
||||||
setCollections([]);
|
setCollections([]);
|
||||||
|
|
||||||
if (!query) {
|
if (!debouncedQuery) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
const apiClient = ServerConnections.getApiClient(serverId);
|
||||||
const fetchPromises = [];
|
const fetchPromises = [];
|
||||||
|
|
||||||
|
@ -230,7 +234,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
||||||
console.error('An error occurred while fetching data:', error);
|
console.error('An error occurred while fetching data:', error);
|
||||||
setIsLoading(false); // Set loading to false even if an error occurs
|
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);
|
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<SearchResultsProps> = ({ serverId = windo
|
||||||
'searchResults',
|
'searchResults',
|
||||||
'padded-bottom-page',
|
'padded-bottom-page',
|
||||||
'padded-top',
|
'padded-top',
|
||||||
{ 'hide': !query || collectionType === CollectionType.Livetv }
|
{ 'hide': !debouncedQuery || collectionType === CollectionType.Livetv }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
@ -335,8 +339,10 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
||||||
cardOptions={{ coverImage: true }}
|
cardOptions={{ coverImage: true }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{allEmpty && query && !isLoading && (
|
{allEmpty && debouncedQuery && !isLoading && (
|
||||||
<div className='sorry-text'>{globalize.translate('SearchResultsEmpty', query)}</div>
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
{globalize.translate('SearchResultsEmpty', debouncedQuery)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,14 +9,3 @@
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
align-self: flex-end;
|
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%);
|
|
||||||
}
|
|
||||||
|
|
|
@ -282,11 +282,15 @@ function executeAction(card, target, action) {
|
||||||
|
|
||||||
function addToPlaylist(item) {
|
function addToPlaylist(item) {
|
||||||
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
new PlaylistEditor().show({
|
const playlistEditor = new PlaylistEditor();
|
||||||
|
playlistEditor.show({
|
||||||
items: [item.Id],
|
items: [item.Id],
|
||||||
serverId: item.ServerId
|
serverId: item.ServerId
|
||||||
|
}).catch(() => {
|
||||||
|
// Dialog closed
|
||||||
});
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[addToPlaylist] failed to load playlist editor', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,22 @@
|
||||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="paperList" style="padding: 1em;">
|
<div class="serverInfo paperList">
|
||||||
<p id="serverName"></p>
|
<div>${LabelServerName}</div>
|
||||||
<p id="versionNumber"></p>
|
<div id="serverName"></div>
|
||||||
|
<div>${LabelServerVersion}</div>
|
||||||
|
<div id="versionNumber"></div>
|
||||||
|
<div>${LabelWebVersion}</div>
|
||||||
|
<div id="webVersion"></div>
|
||||||
|
<div>${LabelBuildVersion}</div>
|
||||||
|
<div id="buildVersion"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboardActionsContainer">
|
<div class="dashboardActionsContainer">
|
||||||
<button is="emby-button" type="button" class="raised btnRefresh">
|
<button is="emby-button" type="button" class="raised btnRefresh">
|
||||||
<span>${ButtonScanAllLibraries}</span>
|
<span>${ButtonScanAllLibraries}</span>
|
||||||
</button>
|
</button>
|
||||||
<button is="emby-button" type="button" id="btnRestartServer" class="raised hide" onclick="DashboardPage.restart(this);">
|
<button is="emby-button" type="button" id="btnRestartServer" class="raised" onclick="DashboardPage.restart(this);">
|
||||||
<span>${Restart}</span>
|
<span>${Restart}</span>
|
||||||
</button>
|
</button>
|
||||||
<button is="emby-button" type="button" id="btnShutdown" class="raised" onclick="DashboardPage.shutdown(this);">
|
<button is="emby-button" type="button" id="btnShutdown" class="raised" onclick="DashboardPage.shutdown(this);">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
import datetime from '../../scripts/datetime';
|
import datetime from '../../scripts/datetime';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from '../../utils/events.ts';
|
||||||
import itemHelper from '../../components/itemHelper';
|
import itemHelper from '../../components/itemHelper';
|
||||||
|
@ -14,10 +15,6 @@ import imageLoader from '../../components/images/imageLoader';
|
||||||
import ActivityLog from '../../components/activitylog';
|
import ActivityLog from '../../components/activitylog';
|
||||||
import imageHelper from '../../utils/image';
|
import imageHelper from '../../utils/image';
|
||||||
import indicators from '../../components/indicators/indicators';
|
import indicators from '../../components/indicators/indicators';
|
||||||
import '../../components/listview/listview.scss';
|
|
||||||
import '../../elements/emby-button/emby-button';
|
|
||||||
import '../../styles/flexstyles.scss';
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
import taskButton from '../../scripts/taskbutton';
|
import taskButton from '../../scripts/taskbutton';
|
||||||
import Dashboard from '../../utils/dashboard';
|
import Dashboard from '../../utils/dashboard';
|
||||||
import ServerConnections from '../../components/ServerConnections';
|
import ServerConnections from '../../components/ServerConnections';
|
||||||
|
@ -25,6 +22,19 @@ import alert from '../../components/alert';
|
||||||
import confirm from '../../components/confirm/confirm';
|
import confirm from '../../components/confirm/confirm';
|
||||||
import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils';
|
import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils';
|
||||||
|
|
||||||
|
import { getSystemInfoQuery } from 'hooks/useSystemInfo';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
|
import { version as WEB_VERSION } from '../../../package.json';
|
||||||
|
|
||||||
|
import '../../elements/emby-button/emby-button';
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
import '../../components/listview/listview.scss';
|
||||||
|
import '../../styles/flexstyles.scss';
|
||||||
|
import './dashboard.scss';
|
||||||
|
|
||||||
function showPlaybackInfo(btn, session) {
|
function showPlaybackInfo(btn, session) {
|
||||||
let title;
|
let title;
|
||||||
const text = [];
|
const text = [];
|
||||||
|
@ -199,22 +209,21 @@ function refreshActiveRecordings(view, apiClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadSystemInfo(view, apiClient) {
|
function reloadSystemInfo(view, apiClient) {
|
||||||
apiClient.getSystemInfo().then(function (systemInfo) {
|
view.querySelector('#buildVersion').innerText = __JF_BUILD_VERSION__;
|
||||||
view.querySelector('#serverName').innerText = globalize.translate('DashboardServerName', systemInfo.ServerName);
|
view.querySelector('#webVersion').innerText = WEB_VERSION;
|
||||||
view.querySelector('#versionNumber').innerText = globalize.translate('DashboardVersionNumber', systemInfo.Version);
|
|
||||||
|
|
||||||
if (systemInfo.CanSelfRestart) {
|
queryClient
|
||||||
view.querySelector('#btnRestartServer').classList.remove('hide');
|
.fetchQuery(getSystemInfoQuery(toApi(apiClient)))
|
||||||
} else {
|
.then(systemInfo => {
|
||||||
view.querySelector('#btnRestartServer').classList.add('hide');
|
view.querySelector('#serverName').innerText = systemInfo.ServerName;
|
||||||
}
|
view.querySelector('#versionNumber').innerText = systemInfo.Version;
|
||||||
|
|
||||||
view.querySelector('#cachePath').innerText = systemInfo.CachePath;
|
view.querySelector('#cachePath').innerText = systemInfo.CachePath;
|
||||||
view.querySelector('#logPath').innerText = systemInfo.LogPath;
|
view.querySelector('#logPath').innerText = systemInfo.LogPath;
|
||||||
view.querySelector('#transcodePath').innerText = systemInfo.TranscodingTempPath;
|
view.querySelector('#transcodePath').innerText = systemInfo.TranscodingTempPath;
|
||||||
view.querySelector('#metadataPath').innerText = systemInfo.InternalMetadataPath;
|
view.querySelector('#metadataPath').innerText = systemInfo.InternalMetadataPath;
|
||||||
view.querySelector('#webPath').innerText = systemInfo.WebPath;
|
view.querySelector('#webPath').innerText = systemInfo.WebPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInfo(view, sessions) {
|
function renderInfo(view, sessions) {
|
||||||
|
|
17
src/controllers/dashboard/dashboard.scss
Normal file
17
src/controllers/dashboard/dashboard.scss
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.serverInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
> *:nth-child(odd) {
|
||||||
|
flex: 1 0 20%;
|
||||||
|
min-width: 7.5em;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:nth-child(even) {
|
||||||
|
flex: 1 0 70%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1257,12 +1257,16 @@ function renderTags(page, item) {
|
||||||
tags = [];
|
tags = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0, length = tags.length; i < length; i++) {
|
tags.forEach(tag => {
|
||||||
tagElements.push(tags[i]);
|
tagElements.push(
|
||||||
}
|
`<a href="#/search.html?query=${encodeURIComponent(tag)}" class="button-link emby-button" is="emby-linkbutton">`
|
||||||
|
+ escapeHtml(tag)
|
||||||
|
+ '</a>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (tagElements.length) {
|
if (tagElements.length) {
|
||||||
itemTags.innerText = globalize.translate('TagsValue', tagElements.join(', '));
|
itemTags.innerHTML = globalize.translate('TagsValue', tagElements.join(', '));
|
||||||
itemTags.classList.remove('hide');
|
itemTags.classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
itemTags.innerHTML = '';
|
itemTags.innerHTML = '';
|
||||||
|
|
|
@ -401,10 +401,15 @@ function onNewItemClick() {
|
||||||
const instance = this;
|
const instance = this;
|
||||||
|
|
||||||
import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
new PlaylistEditor({
|
const playlistEditor = new PlaylistEditor();
|
||||||
|
playlistEditor.show({
|
||||||
items: [],
|
items: [],
|
||||||
serverId: instance.params.serverId
|
serverId: instance.params.serverId
|
||||||
|
}).catch(() => {
|
||||||
|
// Dialog closed
|
||||||
});
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[onNewItemClick] failed to load playlist editor', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
59
src/elements/emby-input/Input.tsx
Normal file
59
src/elements/emby-input/Input.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { type DetailedHTMLProps, type InputHTMLAttributes, type FC, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import './emby-input.scss';
|
||||||
|
|
||||||
|
interface InputProps extends DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
|
||||||
|
id: string,
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input: FC<InputProps> = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [ isFocused, setIsFocused ] = useState(false);
|
||||||
|
|
||||||
|
const onBlurInternal = useCallback(e => {
|
||||||
|
setIsFocused(false);
|
||||||
|
onBlur?.(e);
|
||||||
|
}, [ onBlur ]);
|
||||||
|
|
||||||
|
const onFocusInternal = useCallback(e => {
|
||||||
|
setIsFocused(true);
|
||||||
|
onFocus?.(e);
|
||||||
|
}, [ onFocus ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className={classNames(
|
||||||
|
'inputLabel',
|
||||||
|
{
|
||||||
|
inputLabelUnfocused: !isFocused,
|
||||||
|
inputLabelFocused: isFocused
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className={classNames(
|
||||||
|
'emby-input',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onBlur={onBlurInternal}
|
||||||
|
onFocus={onFocusInternal}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
4
src/global.d.ts
vendored
4
src/global.d.ts
vendored
|
@ -14,4 +14,8 @@ export declare global {
|
||||||
interface DocumentEventMap {
|
interface DocumentEventMap {
|
||||||
'viewshow': CustomEvent;
|
'viewshow': CustomEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const __JF_BUILD_VERSION__: string;
|
||||||
|
const __USE_SYSTEM_FONTS__: string;
|
||||||
|
const __WEBPACK_SERVE__: string;
|
||||||
}
|
}
|
||||||
|
|
17
src/hooks/usePrevious.ts
Normal file
17
src/hooks/usePrevious.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns the previous value of a stateful value.
|
||||||
|
* @param value A stateful value created by a `useState` hook.
|
||||||
|
* @param initialValue The default value.
|
||||||
|
* @returns The previous value.
|
||||||
|
*/
|
||||||
|
export function usePrevious<T>(value: T, initialValue?: T): T | undefined {
|
||||||
|
const ref = useRef<T | undefined>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [ value ]);
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
|
@ -3,21 +3,34 @@ import type { Api } from '@jellyfin/sdk';
|
||||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
import { useApi } from './useApi';
|
||||||
|
import { queryOptions } from 'utils/query/queryOptions';
|
||||||
|
|
||||||
const fetchSystemInfo = async (
|
const fetchSystemInfo = async (
|
||||||
api: Api | undefined,
|
api?: Api,
|
||||||
options: AxiosRequestConfig
|
options?: AxiosRequestConfig
|
||||||
) => {
|
) => {
|
||||||
if (!api) throw new Error('No API instance available');
|
if (!api) {
|
||||||
|
console.warn('[fetchSystemInfo] No API instance available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await getSystemApi(api)
|
const response = await getSystemApi(api)
|
||||||
.getSystemInfo(options);
|
.getSystemInfo(options);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSystemInfo = (api: Api | undefined) => {
|
export const getSystemInfoQuery = (
|
||||||
return useQuery({
|
api?: Api
|
||||||
queryKey: [ 'SystemInfo' ],
|
) => queryOptions({
|
||||||
queryFn: ({ signal }) => fetchSystemInfo(api, { signal }),
|
queryKey: [ 'SystemInfo' ],
|
||||||
enabled: !!api
|
queryFn: ({ signal }) => fetchSystemInfo(api, { signal }),
|
||||||
});
|
// Allow for query reuse in legacy javascript.
|
||||||
|
staleTime: 1000, // 1 second
|
||||||
|
enabled: !!api
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useSystemInfo = () => {
|
||||||
|
const { api } = useApi();
|
||||||
|
return useQuery(getSystemInfoQuery(api));
|
||||||
};
|
};
|
||||||
|
|
|
@ -94,7 +94,7 @@ function onGlobalizeInit() {
|
||||||
if (browser.tv && !browser.android) {
|
if (browser.tv && !browser.android) {
|
||||||
console.debug('using system fonts with explicit sizes');
|
console.debug('using system fonts with explicit sizes');
|
||||||
import('./styles/fonts.sized.scss');
|
import('./styles/fonts.sized.scss');
|
||||||
} else if (__USE_SYSTEM_FONTS__) { // eslint-disable-line no-undef
|
} else if (__USE_SYSTEM_FONTS__) {
|
||||||
console.debug('using system fonts');
|
console.debug('using system fonts');
|
||||||
import('./styles/fonts.scss');
|
import('./styles/fonts.scss');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -187,10 +187,15 @@ export default function (view) {
|
||||||
view.querySelector('.btnNewPlaylist').addEventListener('click', function () {
|
view.querySelector('.btnNewPlaylist').addEventListener('click', function () {
|
||||||
import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
const serverId = ApiClient.serverInfo().Id;
|
const serverId = ApiClient.serverInfo().Id;
|
||||||
new PlaylistEditor({
|
const playlistEditor = new PlaylistEditor();
|
||||||
|
playlistEditor.show({
|
||||||
items: [],
|
items: [],
|
||||||
serverId: serverId
|
serverId: serverId
|
||||||
|
}).catch(() => {
|
||||||
|
// Dialog closed
|
||||||
});
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[btnNewPlaylist] failed to load playlist editor', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
onViewStyleChange();
|
onViewStyleChange();
|
||||||
|
|
|
@ -35,7 +35,7 @@ export function getIncludeCorsCredentials() {
|
||||||
|
|
||||||
export function getMultiServer() {
|
export function getMultiServer() {
|
||||||
// Enable multi-server support when served by webpack
|
// Enable multi-server support when served by webpack
|
||||||
if (__WEBPACK_SERVE__) { // eslint-disable-line no-undef
|
if (__WEBPACK_SERVE__) {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1782,5 +1782,8 @@
|
||||||
"LabelTrackGain": "Na úrovni skladby",
|
"LabelTrackGain": "Na úrovni skladby",
|
||||||
"SelectAudioNormalizationHelp": "Normalizace na úrovni skladby upraví hlasitost všech skladeb tak, aby byla všude stejná. Normalizace na úrovni alba upraví hlasitost všech skladeb tak, aby byla hlasitost stejná v rámci jednotlivých alb.",
|
"SelectAudioNormalizationHelp": "Normalizace na úrovni skladby upraví hlasitost všech skladeb tak, aby byla všude stejná. Normalizace na úrovni alba upraví hlasitost všech skladeb tak, aby byla hlasitost stejná v rámci jednotlivých alb.",
|
||||||
"SearchResultsEmpty": "Pro “{0}” nebylo nic nalezeno",
|
"SearchResultsEmpty": "Pro “{0}” nebylo nic nalezeno",
|
||||||
"HeaderAllRecordings": "Všechny nahrávky"
|
"HeaderAllRecordings": "Všechny nahrávky",
|
||||||
|
"LabelBuildVersion": "Verze sestavení",
|
||||||
|
"LabelServerVersion": "Verze serveru",
|
||||||
|
"LabelWebVersion": "Verze webu"
|
||||||
}
|
}
|
||||||
|
|
|
@ -428,7 +428,7 @@
|
||||||
"LabelCollection": "Sammlung",
|
"LabelCollection": "Sammlung",
|
||||||
"LabelCommunityRating": "Community Bewertung",
|
"LabelCommunityRating": "Community Bewertung",
|
||||||
"LabelContentType": "Typ des Inhalts",
|
"LabelContentType": "Typ des Inhalts",
|
||||||
"LabelCountry": "Land",
|
"LabelCountry": "Land/Region",
|
||||||
"LabelCriticRating": "Kritikerbewertung",
|
"LabelCriticRating": "Kritikerbewertung",
|
||||||
"LabelCurrentPassword": "Aktuelles Passwort",
|
"LabelCurrentPassword": "Aktuelles Passwort",
|
||||||
"LabelCustomCertificatePath": "Benutzerdefinierter Pfad zum SSL-Zertifikat",
|
"LabelCustomCertificatePath": "Benutzerdefinierter Pfad zum SSL-Zertifikat",
|
||||||
|
@ -1782,5 +1782,8 @@
|
||||||
"SelectAudioNormalizationHelp": "Track Gain - passt die Lautstärke der einzelnen Tracks an, so dass sie mit der gleichen Lautstärke wiedergegeben werden. Albumverstärkung - passt die Lautstärke aller Titel eines Albums an, wobei der Dynamikbereich des Albums erhalten bleibt.",
|
"SelectAudioNormalizationHelp": "Track Gain - passt die Lautstärke der einzelnen Tracks an, so dass sie mit der gleichen Lautstärke wiedergegeben werden. Albumverstärkung - passt die Lautstärke aller Titel eines Albums an, wobei der Dynamikbereich des Albums erhalten bleibt.",
|
||||||
"LabelAlbumGain": "Albumlautstärke",
|
"LabelAlbumGain": "Albumlautstärke",
|
||||||
"LabelSelectAudioNormalization": "Audio Normalisierung",
|
"LabelSelectAudioNormalization": "Audio Normalisierung",
|
||||||
"HeaderAllRecordings": "Alle Aufnahmen"
|
"HeaderAllRecordings": "Alle Aufnahmen",
|
||||||
|
"LabelBuildVersion": "Build-Version",
|
||||||
|
"LabelServerVersion": "Server-Version",
|
||||||
|
"LabelWebVersion": "Web-Version"
|
||||||
}
|
}
|
||||||
|
|
|
@ -984,7 +984,7 @@
|
||||||
"LabelCustomCertificatePathHelp": "Path to a PKCS #12 file containing a certificate and private key to enable TLS support on a custom domain.",
|
"LabelCustomCertificatePathHelp": "Path to a PKCS #12 file containing a certificate and private key to enable TLS support on a custom domain.",
|
||||||
"LabelCurrentPassword": "Current password",
|
"LabelCurrentPassword": "Current password",
|
||||||
"LabelCriticRating": "Critics rating",
|
"LabelCriticRating": "Critics rating",
|
||||||
"LabelCountry": "Country",
|
"LabelCountry": "Country/Region",
|
||||||
"LabelContentType": "Content type",
|
"LabelContentType": "Content type",
|
||||||
"LabelCommunityRating": "Community rating",
|
"LabelCommunityRating": "Community rating",
|
||||||
"LabelCertificatePassword": "Certificate password",
|
"LabelCertificatePassword": "Certificate password",
|
||||||
|
|
|
@ -190,8 +190,6 @@
|
||||||
"Cursive": "Cursive",
|
"Cursive": "Cursive",
|
||||||
"CustomDlnaProfilesHelp": "Create a custom profile to target a new device or override a system profile.",
|
"CustomDlnaProfilesHelp": "Create a custom profile to target a new device or override a system profile.",
|
||||||
"DailyAt": "Daily at {0}",
|
"DailyAt": "Daily at {0}",
|
||||||
"DashboardServerName": "Server: {0}",
|
|
||||||
"DashboardVersionNumber": "Version: {0}",
|
|
||||||
"Data": "Data",
|
"Data": "Data",
|
||||||
"DateAdded": "Date added",
|
"DateAdded": "Date added",
|
||||||
"DatePlayed": "Date played",
|
"DatePlayed": "Date played",
|
||||||
|
@ -626,6 +624,7 @@
|
||||||
"LabelBlastMessageInterval": "Alive message interval",
|
"LabelBlastMessageInterval": "Alive message interval",
|
||||||
"LabelBlastMessageIntervalHelp": "Determine the duration in seconds between blast alive messages.",
|
"LabelBlastMessageIntervalHelp": "Determine the duration in seconds between blast alive messages.",
|
||||||
"LabelBlockContentWithTags": "Block items with tags",
|
"LabelBlockContentWithTags": "Block items with tags",
|
||||||
|
"LabelBuildVersion": "Build version",
|
||||||
"LabelBurnSubtitles": "Burn subtitles",
|
"LabelBurnSubtitles": "Burn subtitles",
|
||||||
"LabelCache": "Cache",
|
"LabelCache": "Cache",
|
||||||
"LabelCachePath": "Cache path",
|
"LabelCachePath": "Cache path",
|
||||||
|
@ -939,6 +938,7 @@
|
||||||
"LabelServerHostHelp": "192.168.1.100:8096 or https://myserver.com",
|
"LabelServerHostHelp": "192.168.1.100:8096 or https://myserver.com",
|
||||||
"LabelServerName": "Server name",
|
"LabelServerName": "Server name",
|
||||||
"LabelServerNameHelp": "This name will be used to identify the server and will default to the server's hostname.",
|
"LabelServerNameHelp": "This name will be used to identify the server and will default to the server's hostname.",
|
||||||
|
"LabelServerVersion": "Server version",
|
||||||
"LabelSimultaneousConnectionLimit": "Simultaneous stream limit",
|
"LabelSimultaneousConnectionLimit": "Simultaneous stream limit",
|
||||||
"LabelSize": "Size",
|
"LabelSize": "Size",
|
||||||
"LabelSkipBackLength": "Skip back length",
|
"LabelSkipBackLength": "Skip back length",
|
||||||
|
@ -1079,6 +1079,7 @@
|
||||||
"LabelVppTonemappingContrast": "VPP Tone mapping contrast gain",
|
"LabelVppTonemappingContrast": "VPP Tone mapping contrast gain",
|
||||||
"LabelVppTonemappingContrastHelp": "Apply contrast gain in VPP tone mapping. Both recommended and default values are 1.",
|
"LabelVppTonemappingContrastHelp": "Apply contrast gain in VPP tone mapping. Both recommended and default values are 1.",
|
||||||
"LabelWeb": "Web",
|
"LabelWeb": "Web",
|
||||||
|
"LabelWebVersion": "Web version",
|
||||||
"LabelWidthResolutions": "Width Resolutions",
|
"LabelWidthResolutions": "Width Resolutions",
|
||||||
"LabelWidthResolutionsHelp": "Comma separated list of the width (px) that trickplay images will be generated at. All images should generate proportionally to the source, so a width of 320 on a 16:9 video ends up around 320x180.",
|
"LabelWidthResolutionsHelp": "Comma separated list of the width (px) that trickplay images will be generated at. All images should generate proportionally to the source, so a width of 320 on a 16:9 video ends up around 320x180.",
|
||||||
"LabelXDlnaCap": "Device Capability ID",
|
"LabelXDlnaCap": "Device Capability ID",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"LabelCollection": "Collection",
|
"LabelCollection": "Collection",
|
||||||
"LabelCommunityRating": "Note de la communauté",
|
"LabelCommunityRating": "Note de la communauté",
|
||||||
"LabelContentType": "Type de contenu",
|
"LabelContentType": "Type de contenu",
|
||||||
"LabelCountry": "Pays",
|
"LabelCountry": "Pays/Région",
|
||||||
"LabelCriticRating": "Note des critiques",
|
"LabelCriticRating": "Note des critiques",
|
||||||
"LabelCurrentPassword": "Mot de passe actuel",
|
"LabelCurrentPassword": "Mot de passe actuel",
|
||||||
"LabelCustomCertificatePath": "Chemin vers le certificat SSL personnalisé",
|
"LabelCustomCertificatePath": "Chemin vers le certificat SSL personnalisé",
|
||||||
|
@ -1782,5 +1782,8 @@
|
||||||
"LabelAlbumGain": "Gain de l'album",
|
"LabelAlbumGain": "Gain de l'album",
|
||||||
"LabelSelectAudioNormalization": "Normalisation de l'audio",
|
"LabelSelectAudioNormalization": "Normalisation de l'audio",
|
||||||
"LabelTrackGain": "Gain de piste",
|
"LabelTrackGain": "Gain de piste",
|
||||||
"HeaderAllRecordings": "Tous les enregistrements"
|
"HeaderAllRecordings": "Tous les enregistrements",
|
||||||
|
"LabelBuildVersion": "Numéro de build",
|
||||||
|
"LabelServerVersion": "Version du serveur",
|
||||||
|
"LabelWebVersion": "Version web"
|
||||||
}
|
}
|
||||||
|
|
26
src/strings/ga.json
Normal file
26
src/strings/ga.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"Actor": "Aisteoir",
|
||||||
|
"Add": "Cuir le",
|
||||||
|
"AddedOnValue": "Curtha {0}",
|
||||||
|
"AddToFavorites": "Cur le ceanáin",
|
||||||
|
"AgeValue": "({0} bliana d'aois)",
|
||||||
|
"AirDate": "Dáta Croalta",
|
||||||
|
"Aired": "Craolta",
|
||||||
|
"AlbumArtist": "Ealaíontóir Albam",
|
||||||
|
"Albums": "Albaim",
|
||||||
|
"Alerts": "Foláirimh",
|
||||||
|
"AllChannels": "Gach cainéal",
|
||||||
|
"AllLanguages": "Gach teanga",
|
||||||
|
"AllEpisodes": "Gach eagrán",
|
||||||
|
"AllLibraries": "Gach leabharlann",
|
||||||
|
"AllowCollectionManagement": "Lig don úsáideoir seo bailiúcháin a bhainistiú",
|
||||||
|
"Absolute": "Absalóideach",
|
||||||
|
"AccessRestrictedTryAgainLater": "Tá rochtain srianta faoi láthair. Bain triail eile as ar ball.",
|
||||||
|
"All": "Uilig",
|
||||||
|
"Album": "Albam",
|
||||||
|
"AddToCollection": "Cur le bailiúcháin",
|
||||||
|
"AddToPlaylist": "Cur le seinnliosta",
|
||||||
|
"AddToPlayQueue": "Cur le scuaine seinnteoir",
|
||||||
|
"AllComplexFormats": "Gach formáid chasta (ASS, SSA, VobSub, PGS, SUB, IDX, ...)",
|
||||||
|
"AllowedRemoteAddressesHelp": "Liosta scartha camóg de seoltaí IP nó iontráil IP/netmask do líonraí a ligfear nascadh go cianda. Má fhágtar folamh é, ceadófar gach ciansheoladh."
|
||||||
|
}
|
|
@ -294,7 +294,7 @@
|
||||||
"LabelCollection": "Kolekcija",
|
"LabelCollection": "Kolekcija",
|
||||||
"LabelCommunityRating": "Ocjene zajednice",
|
"LabelCommunityRating": "Ocjene zajednice",
|
||||||
"LabelContentType": "Tip sadržaja",
|
"LabelContentType": "Tip sadržaja",
|
||||||
"LabelCountry": "Zemlja",
|
"LabelCountry": "Zemlja/Regija",
|
||||||
"LabelCriticRating": "Ocjena kritičara",
|
"LabelCriticRating": "Ocjena kritičara",
|
||||||
"LabelCurrentPassword": "Trenutna lozinka",
|
"LabelCurrentPassword": "Trenutna lozinka",
|
||||||
"LabelCustomCss": "Prilagođeni CSS kod",
|
"LabelCustomCss": "Prilagođeni CSS kod",
|
||||||
|
@ -1512,5 +1512,8 @@
|
||||||
"ShowYear": "Prikaži godinu",
|
"ShowYear": "Prikaži godinu",
|
||||||
"LabelLocalCustomCss": "Prilagođeni CSS kod za stil koji se odnosi samo na ovog klijenta. Možda ćete htjeti onemogućiti prilagođeni CSS kod poslužitelja.",
|
"LabelLocalCustomCss": "Prilagođeni CSS kod za stil koji se odnosi samo na ovog klijenta. Možda ćete htjeti onemogućiti prilagođeni CSS kod poslužitelja.",
|
||||||
"LabelPlayerDimensions": "Dimenzije izvođača",
|
"LabelPlayerDimensions": "Dimenzije izvođača",
|
||||||
"LabelSegmentKeepSecondsHelp": "Vrijeme u sekundama za koje se segmenti trebaju čuvati prije nego što se prebrišu. Mora biti veći od \"Throttle after\". Radi samo ako je omogućeno brisanje segmenta."
|
"LabelSegmentKeepSecondsHelp": "Vrijeme u sekundama za koje se segmenti trebaju čuvati prije nego što se prebrišu. Mora biti veći od \"Throttle after\". Radi samo ako je omogućeno brisanje segmenta.",
|
||||||
|
"LabelKnownProxies": "Poznati proxy-i",
|
||||||
|
"ButtonBackspace": "Backspace",
|
||||||
|
"LabelHomeScreenSectionValue": "{0}. odjeljak početne"
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@
|
||||||
"LabelCollection": "Gyűjtemény",
|
"LabelCollection": "Gyűjtemény",
|
||||||
"LabelCommunityRating": "Közösségi értékelés",
|
"LabelCommunityRating": "Közösségi értékelés",
|
||||||
"LabelContentType": "Tartalom típusa",
|
"LabelContentType": "Tartalom típusa",
|
||||||
"LabelCountry": "Ország",
|
"LabelCountry": "Ország/Régió",
|
||||||
"LabelCriticRating": "Kritikusok értékelése",
|
"LabelCriticRating": "Kritikusok értékelése",
|
||||||
"LabelCurrentPassword": "Jelenlegi jelszó",
|
"LabelCurrentPassword": "Jelenlegi jelszó",
|
||||||
"LabelCustomDeviceDisplayNameHelp": "Adj meg egy egyedi nevet, vagy hagyd üresen a készülék által elküldött név használatához.",
|
"LabelCustomDeviceDisplayNameHelp": "Adj meg egy egyedi nevet, vagy hagyd üresen a készülék által elküldött név használatához.",
|
||||||
|
@ -1781,5 +1781,6 @@
|
||||||
"LabelTrackGain": "Címnyereség",
|
"LabelTrackGain": "Címnyereség",
|
||||||
"ForeignPartsOnly": "Kényszerített/külföldi alkatrészek",
|
"ForeignPartsOnly": "Kényszerített/külföldi alkatrészek",
|
||||||
"HearingImpairedShort": "HI/SDH",
|
"HearingImpairedShort": "HI/SDH",
|
||||||
"HeaderGuestCast": "Vendégsztárok"
|
"HeaderGuestCast": "Vendégsztárok",
|
||||||
|
"HeaderAllRecordings": "Összes Felvétel"
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
"AskAdminToCreateLibrary": "Minta pentadbir untuk membuat perpustakaan.",
|
"AskAdminToCreateLibrary": "Minta pentadbir untuk membuat perpustakaan.",
|
||||||
"Artist": "Artis",
|
"Artist": "Artis",
|
||||||
"ApiKeysCaption": "Senarai kunci API yang diaktifkan sekarang",
|
"ApiKeysCaption": "Senarai kunci API yang diaktifkan sekarang",
|
||||||
"AllowTonemappingHelp": "Pemetaan-tone dapat mengubah dinamik video daripada HDR ke SDR sambil mengekalkan perincian dan warna gambar, yang merupakan maklumat yang sangat penting untuk mewakili pemandangan asal. Pada masa ini ia hanya berfungsi dengan video 10bit HDR10, HLG dan Dolby Visoion. Ini memerlukan pemadanan bersesuian dengan OpenCL atau CUDA runtime.",
|
"AllowTonemappingHelp": "Pemetaan-tone dapat mengubah dinamik video daripada HDR ke SDR sambil mengekalkan perincian dan warna gambar, yang merupakan maklumat yang sangat penting untuk mewakili pemandangan asal. Pada masa ini ia hanya berfungsi dengan video 10bit HDR10, HLG, dan Dolby Vision. Ini memerlukan pemadanan bersesuian dengan OpenCL atau CUDA runtime.",
|
||||||
"Songs": "Lagu-lagu",
|
"Songs": "Lagu-lagu",
|
||||||
"Playlists": "Senarai ulangmain",
|
"Playlists": "Senarai ulangmain",
|
||||||
"Photos": "Gambar-gambar",
|
"Photos": "Gambar-gambar",
|
||||||
|
@ -210,7 +210,7 @@
|
||||||
"DisplayInMyMedia": "Pamerkan di skrin utama",
|
"DisplayInMyMedia": "Pamerkan di skrin utama",
|
||||||
"DirectStreaming": "Mainkan secara terus",
|
"DirectStreaming": "Mainkan secara terus",
|
||||||
"DirectStreamHelp1": "Strim video ini serasi dengan peranti ini, tetapi ia mempunya format audio (DTS, Dolby TrueHD, dll.) ataupun jumlah saluran audio yang tidak sepadan. Strim video ini akan dipakej tanpa dimampatkan secara masa sebenar sebelum dihantar ke peranti. Hanya strim audio sahaja yang akan ditranskod.",
|
"DirectStreamHelp1": "Strim video ini serasi dengan peranti ini, tetapi ia mempunya format audio (DTS, Dolby TrueHD, dll.) ataupun jumlah saluran audio yang tidak sepadan. Strim video ini akan dipakej tanpa dimampatkan secara masa sebenar sebelum dihantar ke peranti. Hanya strim audio sahaja yang akan ditranskod.",
|
||||||
"DirectPlayHelp": "Fail sumber ini serasi sepenuhnya dengan klien ini, dan sesi ini menerima fail tanpa pengubahsuaian.",
|
"DirectPlayHelp": "Fail sumber ini serasi sepenuhnya dengan klien ini dan sesi ini menerima fail tanpa pengubahsuaian.",
|
||||||
"DirectPlaying": "Mainkan secara terus",
|
"DirectPlaying": "Mainkan secara terus",
|
||||||
"Directors": "Pengarah",
|
"Directors": "Pengarah",
|
||||||
"Digital": "Digital",
|
"Digital": "Digital",
|
||||||
|
@ -258,5 +258,42 @@
|
||||||
"Cursive": "Sambung",
|
"Cursive": "Sambung",
|
||||||
"DefaultSubtitlesHelp": "Sari kata dimuatkan berdasarkan bendera lalai dan paksa dalam metadata yang disematkan. Keutamaan bahasa diambil kira apabila terdapat beberapa pilihan.",
|
"DefaultSubtitlesHelp": "Sari kata dimuatkan berdasarkan bendera lalai dan paksa dalam metadata yang disematkan. Keutamaan bahasa diambil kira apabila terdapat beberapa pilihan.",
|
||||||
"LabelThrottleDelaySecondsHelp": "Masa dalam saat selepas mana transkoder akan dihadkan. Mesti cukup besar untuk klien mengekalkan penimbal yang sihat. Hanya berfungsi jika penghadan diaktifkan.",
|
"LabelThrottleDelaySecondsHelp": "Masa dalam saat selepas mana transkoder akan dihadkan. Mesti cukup besar untuk klien mengekalkan penimbal yang sihat. Hanya berfungsi jika penghadan diaktifkan.",
|
||||||
"LabelSegmentKeepSeconds": "Masa untuk mengekalkan segmen"
|
"LabelSegmentKeepSeconds": "Masa untuk mengekalkan segmen",
|
||||||
|
"PreviousTrack": "Langkau ke sebelumnya",
|
||||||
|
"QuickConnectAuthorizeFail": "Kod Sambung Pantas tidak dikenali",
|
||||||
|
"AllowEmbeddedSubtitlesAllowImageOption": "Benarkan Imej",
|
||||||
|
"QuickConnect": "Sambung Pantas",
|
||||||
|
"Production": "Penerbitan",
|
||||||
|
"Primary": "Utama",
|
||||||
|
"Print": "Cetak",
|
||||||
|
"Producer": "Penerbit",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Quality": "Kualiti",
|
||||||
|
"ProductionLocations": "Lokasi penerbitan",
|
||||||
|
"QuickConnectNotActive": "Sambung Pantas tidak aktif pada pelayan ini",
|
||||||
|
"QuickConnectNotAvailable": "Minta pentadbir pelayan anda untuk mendayakan Sambung Pantas",
|
||||||
|
"Rate": "Kadar",
|
||||||
|
"PreviousChapter": "Bab sebelumnya",
|
||||||
|
"QuickConnectAuthorizeCode": "Masukkan kod {0} untuk log masuk",
|
||||||
|
"QuickConnectDeactivated": "Sambungan Pantas telah dinyahaktifkan sebelum permintaan log masuk dapat diluluskan",
|
||||||
|
"Previous": "Sebelumnya",
|
||||||
|
"LabelSegmentKeepSecondsHelp": "Masa dalam saat yang segmen harus disimpan sebelum ia ditulis ganti. Mesti melebihi \"Penghad laju setelah\". Hanya berfungsi jika pemadaman segmen didayakan.",
|
||||||
|
"AllowEmbeddedSubtitlesAllowAllOption": "Benarkan Semua",
|
||||||
|
"AllowEmbeddedSubtitlesAllowTextOption": "Benarkan Teks",
|
||||||
|
"QuickConnectDescription": "Untuk log masuk dengan Sambung Pantas, pilihlah butang 'Sambung Pantas' pada peranti yang anda menggunakan untuk log masuk dan memasukkan kod yang muncul di bawah.",
|
||||||
|
"QuickConnectInvalidCode": "Kod Sambung Pantas tidak sah",
|
||||||
|
"EnableNextVideoInfoOverlay": "Tunjukkan informasi video seterusnya semasa main balik",
|
||||||
|
"EnablePhotosHelp": "Imej akan dikesan dan dipaparkan bersama fail media lain.",
|
||||||
|
"Help": "Bantuan",
|
||||||
|
"DownloadAll": "Muat Turun Semua",
|
||||||
|
"EnablePhotos": "Paparkan gambar",
|
||||||
|
"EnableQuickConnect": "Dayakan Sambung Pantas pada pelayan ini",
|
||||||
|
"DisplayMissingEpisodesWithinSeasonsHelp": "Ini juga mesti didayakan untuk perpustakaan TV dalam konfigurasi pelayan.",
|
||||||
|
"EnableExternalVideoPlayers": "Pemain video luaran",
|
||||||
|
"Desktop": "Desktop",
|
||||||
|
"EnableNextVideoInfoOverlayHelp": "Pada penghujung video, paparkan maklumat tentang video seterusnya yang muncul dalam senarai main semasa.",
|
||||||
|
"EnableCinemaMode": "Mod pawagam",
|
||||||
|
"DisableCustomCss": "Lumpuhkan kod CSS kustom yang disediakan oleh pelayan",
|
||||||
|
"EnablePlugin": "Dayakan",
|
||||||
|
"DisablePlugin": "Lumpuhkan"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1781,5 +1781,8 @@
|
||||||
"LabelSelectAudioNormalization": "Geluidsnormalisatie",
|
"LabelSelectAudioNormalization": "Geluidsnormalisatie",
|
||||||
"LabelTrackGain": "Nummerversterking",
|
"LabelTrackGain": "Nummerversterking",
|
||||||
"SearchResultsEmpty": "Sorry! Geen resultaten gevonden voor \"{0}\"",
|
"SearchResultsEmpty": "Sorry! Geen resultaten gevonden voor \"{0}\"",
|
||||||
"HeaderAllRecordings": "Alle opnamen"
|
"HeaderAllRecordings": "Alle opnamen",
|
||||||
|
"LabelBuildVersion": "Buildversie",
|
||||||
|
"LabelServerVersion": "Serverversie",
|
||||||
|
"LabelWebVersion": "Webversie"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1782,5 +1782,8 @@
|
||||||
"LabelTrackGain": "Wzmocnienie utworu",
|
"LabelTrackGain": "Wzmocnienie utworu",
|
||||||
"SelectAudioNormalizationHelp": "Wzmocnienie utworu – reguluje głośność każdego utworu tak, aby odtwarzał się z tą samą głośnością. Wzmocnienie albumu – reguluje głośność tylko wszystkich utworów w albumie, zachowując zakres dynamiki albumu.",
|
"SelectAudioNormalizationHelp": "Wzmocnienie utworu – reguluje głośność każdego utworu tak, aby odtwarzał się z tą samą głośnością. Wzmocnienie albumu – reguluje głośność tylko wszystkich utworów w albumie, zachowując zakres dynamiki albumu.",
|
||||||
"SearchResultsEmpty": "Wybacz! Nie znaleziono wyników dla „{0}”",
|
"SearchResultsEmpty": "Wybacz! Nie znaleziono wyników dla „{0}”",
|
||||||
"HeaderAllRecordings": "Wszystkie nagrania"
|
"HeaderAllRecordings": "Wszystkie nagrania",
|
||||||
|
"LabelBuildVersion": "Wersja kompilacji",
|
||||||
|
"LabelServerVersion": "Wersja serwera",
|
||||||
|
"LabelWebVersion": "Wersja sieciowa"
|
||||||
}
|
}
|
||||||
|
|
|
@ -581,5 +581,7 @@
|
||||||
"HeaderAutoDiscovery": "Zbulim ne rrjet",
|
"HeaderAutoDiscovery": "Zbulim ne rrjet",
|
||||||
"HeaderStopRecording": "Ndaloni regjistrimin",
|
"HeaderStopRecording": "Ndaloni regjistrimin",
|
||||||
"HeaderSubtitleProfilesHelp": "Profilet e titrave përshkruajnë formatin e titrave që suportohen nga paisja.",
|
"HeaderSubtitleProfilesHelp": "Profilet e titrave përshkruajnë formatin e titrave që suportohen nga paisja.",
|
||||||
"HeaderSyncPlaySelectGroup": "Bashkohuni një grupi"
|
"HeaderSyncPlaySelectGroup": "Bashkohuni një grupi",
|
||||||
|
"DirectStreamHelp2": "Energjia e konsumuar nga transmetimi i drejtpërdrejtë zakonisht varet nga profili i audios. Vetëm transmetimi i videove është pa humbje.",
|
||||||
|
"EnableDetailsBannerHelp": "Shfaqni një imazh baneri në krye të faqes së artikullit."
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
"LabelCachePath": "Önbellek yolu",
|
"LabelCachePath": "Önbellek yolu",
|
||||||
"LabelCachePathHelp": "Sunucunun resim vb. önbellek dosyalarını tutması için bir yer belirt. Öntanımlı yeri kullanmak için boş bırak.",
|
"LabelCachePathHelp": "Sunucunun resim vb. önbellek dosyalarını tutması için bir yer belirt. Öntanımlı yeri kullanmak için boş bırak.",
|
||||||
"LabelContentType": "İçerik türü",
|
"LabelContentType": "İçerik türü",
|
||||||
"LabelCountry": "Ülke",
|
"LabelCountry": "Ülke/Bölge",
|
||||||
"LabelCurrentPassword": "Kullanımdaki parola",
|
"LabelCurrentPassword": "Kullanımdaki parola",
|
||||||
"LabelDay": "Haftanın günü",
|
"LabelDay": "Haftanın günü",
|
||||||
"LabelEnableDlnaServer": "DLNA sunucusunu etkinleştir",
|
"LabelEnableDlnaServer": "DLNA sunucusunu etkinleştir",
|
||||||
|
@ -248,7 +248,7 @@
|
||||||
"ChannelNameOnly": "Yalnızca {0} kanalı",
|
"ChannelNameOnly": "Yalnızca {0} kanalı",
|
||||||
"ConfirmDeleteItems": "Bu ögeleri silmek, onları hem dosya sisteminden hem de medya kütüphanenizden siler. Devam etmek istediğinize emin misiniz?",
|
"ConfirmDeleteItems": "Bu ögeleri silmek, onları hem dosya sisteminden hem de medya kütüphanenizden siler. Devam etmek istediğinize emin misiniz?",
|
||||||
"ConfirmDeletion": "Silme İşlemini Onayla",
|
"ConfirmDeletion": "Silme İşlemini Onayla",
|
||||||
"ConfirmEndPlayerSession": "Jellyfin'i {0} tarihinde kapatmak ister misiniz?",
|
"ConfirmEndPlayerSession": "{0} üzerindeki Jellyfin'i kapatmak ister misiniz?",
|
||||||
"Connect": "Bağlan",
|
"Connect": "Bağlan",
|
||||||
"Disconnect": "Bağlantıyı Kes",
|
"Disconnect": "Bağlantıyı Kes",
|
||||||
"Display": "Görünüm",
|
"Display": "Görünüm",
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"HeaderUsers": "Користувачі",
|
"HeaderUsers": "Користувачі",
|
||||||
"LabelBirthDate": "Дата народження",
|
"LabelBirthDate": "Дата народження",
|
||||||
"LabelBirthYear": "Рік народження",
|
"LabelBirthYear": "Рік народження",
|
||||||
"LabelCountry": "Країна",
|
"LabelCountry": "Країна/Регіон",
|
||||||
"LabelCurrentPassword": "Поточний пароль",
|
"LabelCurrentPassword": "Поточний пароль",
|
||||||
"LabelDeathDate": "Дата смерті",
|
"LabelDeathDate": "Дата смерті",
|
||||||
"LabelLanguage": "Мова",
|
"LabelLanguage": "Мова",
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
"LabelNewPassword": "Mật khẩu mới",
|
"LabelNewPassword": "Mật khẩu mới",
|
||||||
"LabelNewPasswordConfirm": "Xác nhận mật khẩu mới",
|
"LabelNewPasswordConfirm": "Xác nhận mật khẩu mới",
|
||||||
"LabelSaveLocalMetadata": "Lưu các ảnh bìa minh họa và dữ liệu mô tả vào trong các thư mục phương tiện",
|
"LabelSaveLocalMetadata": "Lưu các ảnh bìa minh họa và dữ liệu mô tả vào trong các thư mục phương tiện",
|
||||||
"LabelSaveLocalMetadataHelp": "Lưu ảnh bìa minh họa vào các thư mục phương tiện giúp đưa chúng vào một nơi dễ chỉnh sửa hơn.",
|
"LabelSaveLocalMetadataHelp": "Lưu ảnh bìa minh họa vào thư mục phương tiện để đưa chúng vào nơi dễ chỉnh sửa hơn.",
|
||||||
"LabelTime": "Thời gian",
|
"LabelTime": "Thời gian",
|
||||||
"LabelYoureDone": "Bạn đã hoàn thành!",
|
"LabelYoureDone": "Bạn đã hoàn thành!",
|
||||||
"MaxParentalRatingHelp": "Nội dung với đánh giá cao hơn sẽ được ẩn đi từ người dùng này.",
|
"MaxParentalRatingHelp": "Nội dung với đánh giá cao hơn sẽ được ẩn đi từ người dùng này.",
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
"ButtonGotIt": "Hiểu rồi",
|
"ButtonGotIt": "Hiểu rồi",
|
||||||
"ButtonFullscreen": "Toàn màn hình",
|
"ButtonFullscreen": "Toàn màn hình",
|
||||||
"ButtonForgotPassword": "Quên mật khẩu",
|
"ButtonForgotPassword": "Quên mật khẩu",
|
||||||
"ButtonEditOtherUserPreferences": "Chỉnh sửa hồ sơ, ảnh và sở thích cá nhân của người dùng này.",
|
"ButtonEditOtherUserPreferences": "Sửa hồ sơ, ảnh và sở thích cá nhân của người dùng này.",
|
||||||
"ButtonChangeServer": "Đổi máy chủ",
|
"ButtonChangeServer": "Đổi máy chủ",
|
||||||
"ButtonBack": "Quay lại",
|
"ButtonBack": "Quay lại",
|
||||||
"ButtonAudioTracks": "Bản Ghi Âm Thanh",
|
"ButtonAudioTracks": "Bản Ghi Âm Thanh",
|
||||||
|
@ -206,7 +206,7 @@
|
||||||
"BoxSet": "",
|
"BoxSet": "",
|
||||||
"Box": "Hộp",
|
"Box": "Hộp",
|
||||||
"Banner": "Ảnh bìa",
|
"Banner": "Ảnh bìa",
|
||||||
"Art": "",
|
"Art": "Minh họa",
|
||||||
"Artist": "Ca Sĩ",
|
"Artist": "Ca Sĩ",
|
||||||
"AllowFfmpegThrottlingHelp": "Tạm dừng quá trình chuyển mã hoặc chuyển đổi định dạng để tiết kiệm tài nguyên máy chủ khi việc này đã đủ để phát so với vị trí hiện tại. Nó hữu ích khi xem mà không cần tua. Tắt tính năng này nếu bạn gặp sự cố phát lại.",
|
"AllowFfmpegThrottlingHelp": "Tạm dừng quá trình chuyển mã hoặc chuyển đổi định dạng để tiết kiệm tài nguyên máy chủ khi việc này đã đủ để phát so với vị trí hiện tại. Nó hữu ích khi xem mà không cần tua. Tắt tính năng này nếu bạn gặp sự cố phát lại.",
|
||||||
"AllowFfmpegThrottling": "Điều tiết sự chuyển mã",
|
"AllowFfmpegThrottling": "Điều tiết sự chuyển mã",
|
||||||
|
@ -235,7 +235,7 @@
|
||||||
"DeleteDeviceConfirmation": "Bạn có chắc chắn muốn xoá thiết bị này? Nó sẽ xuất hiện lại khi người dùng đăng nhập bằng thiết bị đó.",
|
"DeleteDeviceConfirmation": "Bạn có chắc chắn muốn xoá thiết bị này? Nó sẽ xuất hiện lại khi người dùng đăng nhập bằng thiết bị đó.",
|
||||||
"DeinterlaceMethodHelp": "Chọn cách khử xen kẽ để dùng khi phần mềm chuyển mã xen kẽ nội dung. Khi hỗ trợ tăng tốc phần cứng khử xen kẽ được bật, trình khử xen kẽ phần cứng sẽ dùng cài đặt này.",
|
"DeinterlaceMethodHelp": "Chọn cách khử xen kẽ để dùng khi phần mềm chuyển mã xen kẽ nội dung. Khi hỗ trợ tăng tốc phần cứng khử xen kẽ được bật, trình khử xen kẽ phần cứng sẽ dùng cài đặt này.",
|
||||||
"DefaultSubtitlesHelp": "Phụ đề được tải dựa trên mặc định và bắt buộc gắn cờ trong dữ liệu mô tả. Ngôn ngữ ưa thích được xem xét khi có nhiều lựa chọn.",
|
"DefaultSubtitlesHelp": "Phụ đề được tải dựa trên mặc định và bắt buộc gắn cờ trong dữ liệu mô tả. Ngôn ngữ ưa thích được xem xét khi có nhiều lựa chọn.",
|
||||||
"DefaultMetadataLangaugeDescription": "Đây là thiết lập mặc định chung, bạn có thể tuỳ chỉnh thiết lập riêng cho từng thư viện.",
|
"DefaultMetadataLangaugeDescription": "Đây là mặc định của bạn và có thể tùy chỉnh trên cơ sở từng thư viện.",
|
||||||
"DisplayModeHelp": "Chọn kiểu bố trí giao diện mà bạn muốn.",
|
"DisplayModeHelp": "Chọn kiểu bố trí giao diện mà bạn muốn.",
|
||||||
"Download": "Tải về",
|
"Download": "Tải về",
|
||||||
"Down": "Xuống",
|
"Down": "Xuống",
|
||||||
|
@ -243,9 +243,9 @@
|
||||||
"EnableCinemaMode": "Chế độ rạp phim",
|
"EnableCinemaMode": "Chế độ rạp phim",
|
||||||
"EnableBackdropsHelp": "Hiển thị phông nền trong nền của một số trang trong khi duyệt thư viện.",
|
"EnableBackdropsHelp": "Hiển thị phông nền trong nền của một số trang trong khi duyệt thư viện.",
|
||||||
"EditSubtitles": "Chỉnh sửa phụ đề",
|
"EditSubtitles": "Chỉnh sửa phụ đề",
|
||||||
"EditMetadata": "Chỉnh sửa thông tin",
|
"EditMetadata": "Sửa dữ liệu mô tả",
|
||||||
"EditImages": "Chỉnh sửa hình ảnh",
|
"EditImages": "Sửa ảnh",
|
||||||
"Edit": "Chỉnh sửa",
|
"Edit": "Sửa",
|
||||||
"EasyPasswordHelp": "Mã PIN Tiện Lợi dùng cho việc truy cập ngoại tuyến trên thiết bị hỗ trợ và cũng dùng để đăng nhập trong mạng dễ dàng.",
|
"EasyPasswordHelp": "Mã PIN Tiện Lợi dùng cho việc truy cập ngoại tuyến trên thiết bị hỗ trợ và cũng dùng để đăng nhập trong mạng dễ dàng.",
|
||||||
"DropShadow": "Bóng đổ",
|
"DropShadow": "Bóng đổ",
|
||||||
"DrmChannelsNotImported": "Những kênh được bảo vệ bản quyền sẽ không được nhập vào.",
|
"DrmChannelsNotImported": "Những kênh được bảo vệ bản quyền sẽ không được nhập vào.",
|
||||||
|
@ -405,7 +405,7 @@
|
||||||
"HeaderError": "Lỗi",
|
"HeaderError": "Lỗi",
|
||||||
"HeaderEnabledFieldsHelp": "Bỏ chọn một mục để khóa nó và ngăn dữ liệu của nó bị thay đổi.",
|
"HeaderEnabledFieldsHelp": "Bỏ chọn một mục để khóa nó và ngăn dữ liệu của nó bị thay đổi.",
|
||||||
"HeaderEnabledFields": "Những Mục Được Kích Hoạt",
|
"HeaderEnabledFields": "Những Mục Được Kích Hoạt",
|
||||||
"HeaderEditImages": "Chỉnh Sửa Hình Ảnh",
|
"HeaderEditImages": "Sửa Ảnh",
|
||||||
"HeaderEasyPinCode": "Mã PIN Tiện Lợi",
|
"HeaderEasyPinCode": "Mã PIN Tiện Lợi",
|
||||||
"HeaderDownloadSync": "Tải Xuống Và Đồng Bộ",
|
"HeaderDownloadSync": "Tải Xuống Và Đồng Bộ",
|
||||||
"HeaderDirectPlayProfileHelp": "Thêm cấu hình phát lại trực tiếp để chỉ ra những định dạng mà thiết bị có thể xử lý nguyên bản.",
|
"HeaderDirectPlayProfileHelp": "Thêm cấu hình phát lại trực tiếp để chỉ ra những định dạng mà thiết bị có thể xử lý nguyên bản.",
|
||||||
|
@ -439,7 +439,7 @@
|
||||||
"HeaderScenes": "Phân Cảnh",
|
"HeaderScenes": "Phân Cảnh",
|
||||||
"HeaderRunningTasks": "Những Tác Vụ Hoạt Động",
|
"HeaderRunningTasks": "Những Tác Vụ Hoạt Động",
|
||||||
"HeaderRevisionHistory": "Lịch Sử Chỉnh Sửa",
|
"HeaderRevisionHistory": "Lịch Sử Chỉnh Sửa",
|
||||||
"HeaderResponseProfileHelp": "Hồ sơ phản hồi là phương thức tuỳ chỉnh thông tin gửi về thiết bị phát khi phát một số nội dung nhất định.",
|
"HeaderResponseProfileHelp": "Cấu hình phản hồi là phương thức tuỳ chỉnh thông tin gửi về thiết bị phát khi phát một số loại phương tiện.",
|
||||||
"HeaderResponseProfile": "Hồ Sơ Phản Hồi",
|
"HeaderResponseProfile": "Hồ Sơ Phản Hồi",
|
||||||
"HeaderRemoveMediaLocation": "Xoá Đường Dẫn Nội Dung",
|
"HeaderRemoveMediaLocation": "Xoá Đường Dẫn Nội Dung",
|
||||||
"HeaderRemoveMediaFolder": "Xoá Thư Mục Phương Tiện",
|
"HeaderRemoveMediaFolder": "Xoá Thư Mục Phương Tiện",
|
||||||
|
@ -605,7 +605,7 @@
|
||||||
"HeaderDVR": "DVR",
|
"HeaderDVR": "DVR",
|
||||||
"LabelExtractChaptersDuringLibraryScanHelp": "Tạo hình ảnh phân cảnh khi video được nhập trong quá trình quét thư viện. Nếu không chúng sẽ được trích xuất thông qua những tác vụ định kì, giúp cho quá trình quét thư viện diễn ra nhanh hơn.",
|
"LabelExtractChaptersDuringLibraryScanHelp": "Tạo hình ảnh phân cảnh khi video được nhập trong quá trình quét thư viện. Nếu không chúng sẽ được trích xuất thông qua những tác vụ định kì, giúp cho quá trình quét thư viện diễn ra nhanh hơn.",
|
||||||
"LabelExtractChaptersDuringLibraryScan": "Trích xuất hình ảnh phân đoạn khi quét thư viện",
|
"LabelExtractChaptersDuringLibraryScan": "Trích xuất hình ảnh phân đoạn khi quét thư viện",
|
||||||
"LabelBaseUrlHelp": "Thêm một thư mục con tùy chỉnh vào đường dẫn máy chủ. Ví dụ: <code>http://example.com/<b><baseurl></b></code>",
|
"LabelBaseUrlHelp": "Thêm một thư mục con tùy chỉnh vào URL máy chủ. Ví dụ: <code>http://example.com/<b><baseurl></b></code>",
|
||||||
"LabelLoginDisclaimerHelp": "Một thông báo sẽ được hiển thị ở cuối trang đăng nhập.",
|
"LabelLoginDisclaimerHelp": "Một thông báo sẽ được hiển thị ở cuối trang đăng nhập.",
|
||||||
"LabelLoginDisclaimer": "Hiển thị khi đăng nhập",
|
"LabelLoginDisclaimer": "Hiển thị khi đăng nhập",
|
||||||
"LabelLockItemToPreventChanges": "Khoá mục này để ngăn những thay đổi trong tương lai",
|
"LabelLockItemToPreventChanges": "Khoá mục này để ngăn những thay đổi trong tương lai",
|
||||||
|
@ -954,7 +954,7 @@
|
||||||
"LiveBroadcasts": "Chương trình phát sóng trực tiếp",
|
"LiveBroadcasts": "Chương trình phát sóng trực tiếp",
|
||||||
"Live": "Trực tiếp",
|
"Live": "Trực tiếp",
|
||||||
"List": "Danh sách",
|
"List": "Danh sách",
|
||||||
"LibraryAccessHelp": "Chọn thư viện để chia sẻ với người dùng này. Quản trị viên có thể chỉnh sửa các thư mục bằng trình quản lý dữ liệu mô tả.",
|
"LibraryAccessHelp": "Chọn thư viện chia sẻ với người dùng này. Quản trị viên có thể sửa các thư mục bằng trình quản lý dữ liệu mô tả.",
|
||||||
"LeaveBlankToNotSetAPassword": "Bạn có thể để trống trường này để không đặt mật khẩu.",
|
"LeaveBlankToNotSetAPassword": "Bạn có thể để trống trường này để không đặt mật khẩu.",
|
||||||
"LearnHowYouCanContribute": "Tìm hiểu cách bạn có thể đóng góp.",
|
"LearnHowYouCanContribute": "Tìm hiểu cách bạn có thể đóng góp.",
|
||||||
"LatestFromLibrary": "{0} Đã Thêm Vào Gần Đây",
|
"LatestFromLibrary": "{0} Đã Thêm Vào Gần Đây",
|
||||||
|
@ -999,7 +999,7 @@
|
||||||
"LabelTranscodingProgress": "Tiến trình chuyển mã",
|
"LabelTranscodingProgress": "Tiến trình chuyển mã",
|
||||||
"LabelTranscodingFramerate": "Tốc độ khung hình chuyển mã",
|
"LabelTranscodingFramerate": "Tốc độ khung hình chuyển mã",
|
||||||
"LabelTranscodes": "Chuyển mã",
|
"LabelTranscodes": "Chuyển mã",
|
||||||
"LabelTranscodingTempPathHelp": "Chỉ định một đường dẫn tùy chỉnh cho các tệp chuyển mã được cung cấp cho máy khách. Để trống để sử dụng mặc định của máy chủ.",
|
"LabelTranscodingTempPathHelp": "Tùy chỉnh một đường dẫn cho các tệp chuyển mã để cung cấp cho máy khách. Để trống để dùng mặc định.",
|
||||||
"LabelTranscodePath": "Đường dẫn chuyển mã",
|
"LabelTranscodePath": "Đường dẫn chuyển mã",
|
||||||
"LabelTrackNumber": "Số bản ghi",
|
"LabelTrackNumber": "Số bản ghi",
|
||||||
"LabelTitle": "Tiêu đề",
|
"LabelTitle": "Tiêu đề",
|
||||||
|
@ -1271,7 +1271,7 @@
|
||||||
"Transcoding": "Chuyển mã",
|
"Transcoding": "Chuyển mã",
|
||||||
"Trailers": "Đoạn giới thiệu",
|
"Trailers": "Đoạn giới thiệu",
|
||||||
"TabAccess": "Truy cập",
|
"TabAccess": "Truy cập",
|
||||||
"SystemDlnaProfilesHelp": "Cấu hình hệ thống ở chế độ chỉ đọc. Thay đổi cấu hình hệ thống sẽ được lưu vào cấu hình tùy chỉnh mới.",
|
"SystemDlnaProfilesHelp": "Cấu hình hệ thống là chỉ đọc. Thay đổi cấu hình hệ thống sẽ được lưu vào cấu hình tùy chỉnh mới.",
|
||||||
"Sports": "Thể thao",
|
"Sports": "Thể thao",
|
||||||
"SpecialFeatures": "Các tính năng đặc biệt",
|
"SpecialFeatures": "Các tính năng đặc biệt",
|
||||||
"SortName": "Sắp xếp tên",
|
"SortName": "Sắp xếp tên",
|
||||||
|
@ -1322,7 +1322,7 @@
|
||||||
"UnsupportedPlayback": "Jellyfin không thể giải mã nội dung bảo vệ DRM nhưng sẽ cố gắng thử với tất cả nội dung, bao gồm cả các tiêu đề được bảo vệ. Một số tệp có thể xuất hiện toàn màu đen do mã hóa hoặc tính năng không được hỗ trợ, chẳng hạn như tiêu đề tương tác.",
|
"UnsupportedPlayback": "Jellyfin không thể giải mã nội dung bảo vệ DRM nhưng sẽ cố gắng thử với tất cả nội dung, bao gồm cả các tiêu đề được bảo vệ. Một số tệp có thể xuất hiện toàn màu đen do mã hóa hoặc tính năng không được hỗ trợ, chẳng hạn như tiêu đề tương tác.",
|
||||||
"EnableBlurHashHelp": "Hình ảnh đang được tải sẽ được hiển thị với một trình giữ chỗ duy nhất.",
|
"EnableBlurHashHelp": "Hình ảnh đang được tải sẽ được hiển thị với một trình giữ chỗ duy nhất.",
|
||||||
"ButtonPlayer": "Trình Phát",
|
"ButtonPlayer": "Trình Phát",
|
||||||
"LabelOpenclDeviceHelp": "Đây là thiết bị OpenCL dùng để chỉnh tông màu. Phía bên trái của dấu chấm là số nền tảng và phía bên phải là số thiết bị trên nền tảng. Mặc định là 0.0. Bắt buộc tệp ứng dụng FFmpeg phải có tính năng tăng tốc phần cứng OpenCL.",
|
"LabelOpenclDeviceHelp": "Đây là thiết bị OpenCL dùng để chỉnh tông màu. Phía bên trái của dấu chấm là số nền tảng và phía bên phải là số thiết bị trên nền tảng. Mặc định là 0.0. Cần có tệp ứng dụng FFmpeg chứa phương pháp tăng tốc phần cứng OpenCL.",
|
||||||
"LabelMaxMuxingQueueSizeHelp": "Số gói tối đa có thể được lưu vào bộ đệm trong khi chờ tất cả các luồng khởi tạo. Hãy thử tăng lên nếu bạn vẫn gặp lỗi \"Quá nhiều gói được lưu vào bộ đệm cho luồng đầu ra\" trong nhật ký FFmpeg. Giá trị đề xuất là 2048.",
|
"LabelMaxMuxingQueueSizeHelp": "Số gói tối đa có thể được lưu vào bộ đệm trong khi chờ tất cả các luồng khởi tạo. Hãy thử tăng lên nếu bạn vẫn gặp lỗi \"Quá nhiều gói được lưu vào bộ đệm cho luồng đầu ra\" trong nhật ký FFmpeg. Giá trị đề xuất là 2048.",
|
||||||
"ClearQueue": "Xóa hàng đợi",
|
"ClearQueue": "Xóa hàng đợi",
|
||||||
"LabelTonemappingParamHelp": "Điều chỉnh thuật toán ánh xạ tông màu. Các giá trị được đề xuất và mặc định là NaN. Nói chung là để trống.",
|
"LabelTonemappingParamHelp": "Điều chỉnh thuật toán ánh xạ tông màu. Các giá trị được đề xuất và mặc định là NaN. Nói chung là để trống.",
|
||||||
|
@ -1427,7 +1427,7 @@
|
||||||
"HeaderDeleteDevices": "Xóa Tất Cả Thiết Bị",
|
"HeaderDeleteDevices": "Xóa Tất Cả Thiết Bị",
|
||||||
"DeleteDevicesConfirmation": "Bạn có chắc muốn xóa hết các thiết bị không? Tất cả các phiên khác sẽ được đăng xuất. Thiết bị sẽ xuất hiện lại vào lần tiếp theo khi người dùng đăng nhập.",
|
"DeleteDevicesConfirmation": "Bạn có chắc muốn xóa hết các thiết bị không? Tất cả các phiên khác sẽ được đăng xuất. Thiết bị sẽ xuất hiện lại vào lần tiếp theo khi người dùng đăng nhập.",
|
||||||
"DeleteAll": "Xóa Hết",
|
"DeleteAll": "Xóa Hết",
|
||||||
"EnableFallbackFontHelp": "Bật phông chữ thay thế tùy chỉnh. Điều này có thể tránh sự cố hiển thị phụ đề không chính xác.",
|
"EnableFallbackFontHelp": "Bật phông chữ thay thế tùy chỉnh. Nó có thể tránh lỗi kết xuất phụ đề không chính xác.",
|
||||||
"EnableFallbackFont": "Bật phông chữ dự phòng",
|
"EnableFallbackFont": "Bật phông chữ dự phòng",
|
||||||
"LabelFallbackFontPathHelp": "Những phông chữ này được một số máy khách sử dụng để hiển thị phụ đề. Vui lòng tham khảo tài liệu để biết thêm thông tin.",
|
"LabelFallbackFontPathHelp": "Những phông chữ này được một số máy khách sử dụng để hiển thị phụ đề. Vui lòng tham khảo tài liệu để biết thêm thông tin.",
|
||||||
"LabelFallbackFontPath": "Đường dẫn thư mục phông chữ dự phòng",
|
"LabelFallbackFontPath": "Đường dẫn thư mục phông chữ dự phòng",
|
||||||
|
@ -1665,7 +1665,7 @@
|
||||||
"VideoRangeTypeNotSupported": "Không hỗ trợ loại dải động của video",
|
"VideoRangeTypeNotSupported": "Không hỗ trợ loại dải động của video",
|
||||||
"Interview": "Phỏng vấn",
|
"Interview": "Phỏng vấn",
|
||||||
"Sample": "Mẫu",
|
"Sample": "Mẫu",
|
||||||
"Trailer": "",
|
"Trailer": "Đoạn giới thiệu",
|
||||||
"TypeOptionPluralVideo": "Videos",
|
"TypeOptionPluralVideo": "Videos",
|
||||||
"ButtonSpace": "Cách",
|
"ButtonSpace": "Cách",
|
||||||
"ButtonBackspace": "Xóa",
|
"ButtonBackspace": "Xóa",
|
||||||
|
@ -1722,7 +1722,7 @@
|
||||||
"EnableAudioNormalization": "Chuẩn hóa âm thanh",
|
"EnableAudioNormalization": "Chuẩn hóa âm thanh",
|
||||||
"GetThePlugin": "Nhận phần bổ sung",
|
"GetThePlugin": "Nhận phần bổ sung",
|
||||||
"Notifications": "Thông báo",
|
"Notifications": "Thông báo",
|
||||||
"NotificationsMovedMessage": "Chức năng thông báo đã chuyển sang plugin Webhook.",
|
"NotificationsMovedMessage": "Chức năng thông báo đã được chuyển sang plugin Webhook.",
|
||||||
"LabelEnableLUFSScan": "Bật tính năng quét LUFS",
|
"LabelEnableLUFSScan": "Bật tính năng quét LUFS",
|
||||||
"LabelEnableLUFSScanHelp": "Khách hàng có thể bình thường hóa việc phát lại âm thanh để có được âm lượng như nhau trên các bản nhạc. Điều này sẽ khiến việc quét thư viện lâu hơn và tốn nhiều tài nguyên hơn.",
|
"LabelEnableLUFSScanHelp": "Khách hàng có thể bình thường hóa việc phát lại âm thanh để có được âm lượng như nhau trên các bản nhạc. Điều này sẽ khiến việc quét thư viện lâu hơn và tốn nhiều tài nguyên hơn.",
|
||||||
"PasswordRequiredForAdmin": "Cần có mật khẩu cho tài khoản quản trị viên.",
|
"PasswordRequiredForAdmin": "Cần có mật khẩu cho tài khoản quản trị viên.",
|
||||||
|
@ -1761,13 +1761,14 @@
|
||||||
"LabelSelectAudioNormalization": "Chuẩn Hóa Âm Thanh",
|
"LabelSelectAudioNormalization": "Chuẩn Hóa Âm Thanh",
|
||||||
"SearchResultsEmpty": "Rất tiếc! Không có kết quả tìm thấy cho \"{0}\"",
|
"SearchResultsEmpty": "Rất tiếc! Không có kết quả tìm thấy cho \"{0}\"",
|
||||||
"HeaderAllRecordings": "Tất Cả Bản Ghi",
|
"HeaderAllRecordings": "Tất Cả Bản Ghi",
|
||||||
"LabelTrackGain": "Chỉnh Nhạc",
|
"LabelTrackGain": "Điều Chỉnh Nhạc",
|
||||||
"GridView": "Xem Lưới",
|
"GridView": "Xem Lưới",
|
||||||
"ListView": "Xem Danh Sách",
|
"ListView": "Xem Danh Sách",
|
||||||
"SelectAudioNormalizationHelp": "Chỉnh nhạc - điều chỉnh âm lượng của mỗi bản nhạc để chúng phát lại với cùng một độ to. Chỉnh Album - điều chỉnh âm lượng của tất cả các bản nhạc trong một album, giữ nguyên dải động của album.",
|
"SelectAudioNormalizationHelp": "Điều chỉnh nhạc - điều chỉnh âm lượng của mỗi bản nhạc để chúng phát lại với cùng một độ to. Điều chỉnh album - điều chỉnh âm lượng tất cả bản nhạc trong album, giữ nguyên dải động của album.",
|
||||||
"LabelAlbumGain": "Chỉnh Album",
|
"LabelAlbumGain": "Điều Chỉnh Album",
|
||||||
"LabelSegmentKeepSecondsHelp": "Thời gian tính bằng giây mà các phân đoạn cần được giữ lại trước khi chúng bị ghi đè. Phải lớn hơn \"Throle after\". Chỉ hoạt động nếu tính năng xóa phân đoạn được bật.",
|
"LabelSegmentKeepSecondsHelp": "Thời gian tính bằng giây mà các phân đoạn cần được giữ lại trước khi chúng bị ghi đè. Phải lớn hơn \"Throle after\". Chỉ hoạt động nếu tính năng xóa phân đoạn được bật.",
|
||||||
"MenuClose": "Đóng Menu",
|
"MenuClose": "Đóng Menu",
|
||||||
"MenuOpen": "Mở Menu",
|
"MenuOpen": "Mở Menu",
|
||||||
"UserMenu": "Menu Người Dùng"
|
"UserMenu": "Menu Người Dùng",
|
||||||
|
"LogLevel.Trace": "Dấu vết"
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,7 +272,7 @@
|
||||||
"HeaderImageSettings": "图片设置",
|
"HeaderImageSettings": "图片设置",
|
||||||
"HeaderInstall": "安装",
|
"HeaderInstall": "安装",
|
||||||
"HeaderInstantMix": "速成合辑",
|
"HeaderInstantMix": "速成合辑",
|
||||||
"HeaderKodiMetadataHelp": "要启用或禁用 NFO 元数据,请编辑库并找到“元数据保护程序”部分。",
|
"HeaderKodiMetadataHelp": "要启用或禁用 NFO 元数据,请编辑库并找到“元数据储存方式”部分。",
|
||||||
"HeaderLatestEpisodes": "新增剧集",
|
"HeaderLatestEpisodes": "新增剧集",
|
||||||
"HeaderLatestMedia": "新增媒体",
|
"HeaderLatestMedia": "新增媒体",
|
||||||
"HeaderLatestMovies": "新增电影",
|
"HeaderLatestMovies": "新增电影",
|
||||||
|
|
|
@ -264,7 +264,7 @@
|
||||||
"ButtonMore": "更多",
|
"ButtonMore": "更多",
|
||||||
"ButtonNetwork": "網路",
|
"ButtonNetwork": "網路",
|
||||||
"ButtonNextTrack": "下一首",
|
"ButtonNextTrack": "下一首",
|
||||||
"ButtonOpen": "開",
|
"ButtonOpen": "開啟",
|
||||||
"ButtonParentalControl": "家長控制",
|
"ButtonParentalControl": "家長控制",
|
||||||
"ButtonPause": "暫停",
|
"ButtonPause": "暫停",
|
||||||
"ButtonPreviousTrack": "上一首",
|
"ButtonPreviousTrack": "上一首",
|
||||||
|
|
|
@ -47,6 +47,10 @@ const config = {
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
|
__JF_BUILD_VERSION__: JSON.stringify(
|
||||||
|
process.env.WEBPACK_SERVE ?
|
||||||
|
'Dev Server' :
|
||||||
|
process.env.JELLYFIN_VERSION || 'Release'),
|
||||||
__USE_SYSTEM_FONTS__: JSON.stringify(!!process.env.USE_SYSTEM_FONTS),
|
__USE_SYSTEM_FONTS__: JSON.stringify(!!process.env.USE_SYSTEM_FONTS),
|
||||||
__WEBPACK_SERVE__: JSON.stringify(!!process.env.WEBPACK_SERVE)
|
__WEBPACK_SERVE__: JSON.stringify(!!process.env.WEBPACK_SERVE)
|
||||||
}),
|
}),
|
||||||
|
@ -204,6 +208,7 @@ const config = {
|
||||||
path.resolve(__dirname, 'node_modules/screenfull'),
|
path.resolve(__dirname, 'node_modules/screenfull'),
|
||||||
path.resolve(__dirname, 'node_modules/ssr-window'),
|
path.resolve(__dirname, 'node_modules/ssr-window'),
|
||||||
path.resolve(__dirname, 'node_modules/swiper'),
|
path.resolve(__dirname, 'node_modules/swiper'),
|
||||||
|
path.resolve(__dirname, 'node_modules/usehooks-ts'),
|
||||||
path.resolve(__dirname, 'src')
|
path.resolve(__dirname, 'src')
|
||||||
],
|
],
|
||||||
use: [{
|
use: [{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue