Compare commits
19 Commits
ad3223cb77
...
c66db4e18d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c66db4e18d | ||
|
|
ea2abad3e1 | ||
|
|
6d8c8c0566 | ||
|
|
a2855c785e | ||
|
|
bf31a733a7 | ||
|
|
bf70fb80aa | ||
|
|
2acc6f360a | ||
|
|
a36eb7b546 | ||
|
|
fb6250d108 | ||
|
|
a82ae33aa3 | ||
|
|
32d916b420 | ||
|
|
014af0ebe9 | ||
|
|
9b80917cd1 | ||
|
|
238c5bbf58 | ||
|
|
264cdafaff | ||
|
|
1459a11320 | ||
|
|
e28d70d34c | ||
|
|
9a207e9ba9 | ||
|
|
5db40d03ac |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.11.5",
|
||||
"version": "10.11.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.11.5",
|
||||
"version": "10.11.6",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.11.5",
|
||||
"version": "10.11.6",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
|
||||
@@ -98,164 +98,166 @@ export const Component = () => {
|
||||
className='type-interior mainAnimatedPage'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
{isError ? (
|
||||
<Alert
|
||||
severity='error'
|
||||
sx={{ marginBottom: 2 }}
|
||||
<Stack spacing={2}>
|
||||
<Stack
|
||||
direction='row'
|
||||
sx={{
|
||||
flexWrap: {
|
||||
xs: 'wrap',
|
||||
sm: 'nowrap'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{globalize.translate('PluginsLoadError')}
|
||||
</Alert>
|
||||
) : (
|
||||
<Stack spacing={2}>
|
||||
<Stack
|
||||
direction='row'
|
||||
<Typography
|
||||
variant='h1'
|
||||
component='span'
|
||||
sx={{
|
||||
flexWrap: {
|
||||
xs: 'wrap',
|
||||
sm: 'nowrap'
|
||||
flexGrow: 1,
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('TabPlugins')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to='/dashboard/plugins/repositories'
|
||||
variant='outlined'
|
||||
sx={{
|
||||
marginLeft: 2
|
||||
}}
|
||||
>
|
||||
{globalize.translate('ManageRepositories')}
|
||||
</Button>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
marginTop: {
|
||||
xs: 2,
|
||||
sm: 0
|
||||
},
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: 2
|
||||
},
|
||||
width: {
|
||||
xs: '100%',
|
||||
sm: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant='h1'
|
||||
component='span'
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('TabPlugins')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to='/dashboard/plugins/repositories'
|
||||
variant='outlined'
|
||||
sx={{
|
||||
marginLeft: 2
|
||||
}}
|
||||
>
|
||||
{globalize.translate('ManageRepositories')}
|
||||
</Button>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
marginTop: {
|
||||
xs: 2,
|
||||
sm: 0
|
||||
},
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: 2
|
||||
},
|
||||
width: {
|
||||
xs: '100%',
|
||||
sm: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchInput
|
||||
label={globalize.translate('Search')}
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={1}
|
||||
sx={{
|
||||
marginLeft: '-1rem',
|
||||
marginRight: '-1rem',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
paddingBottom: {
|
||||
xs: 1,
|
||||
md: 0.5
|
||||
},
|
||||
overflowX: 'auto'
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
color={status === PluginStatusOption.All ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setStatus(PluginStatusOption.All)}
|
||||
label={globalize.translate('All')}
|
||||
/>
|
||||
|
||||
<Chip
|
||||
color={status === PluginStatusOption.Available ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setStatus(PluginStatusOption.Available)}
|
||||
label={globalize.translate('LabelAvailable')}
|
||||
/>
|
||||
|
||||
<Chip
|
||||
color={status === PluginStatusOption.Installed ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setStatus(PluginStatusOption.Installed)}
|
||||
label={globalize.translate('LabelInstalled')}
|
||||
/>
|
||||
|
||||
<Divider orientation='vertical' flexItem />
|
||||
|
||||
<Chip
|
||||
color={!category ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setCategory('')}
|
||||
label={globalize.translate('All')}
|
||||
/>
|
||||
|
||||
{Object.values(PluginCategory).map(c => (
|
||||
<Chip
|
||||
key={c}
|
||||
color={category === c.toLowerCase() ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setCategory(c.toLowerCase())}
|
||||
label={globalize.translate(CATEGORY_LABELS[c as PluginCategory])}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
{filteredPlugins.length > 0 ? (
|
||||
// NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
<Grid container spacing={2}>
|
||||
{filteredPlugins.map(plugin => (
|
||||
// NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
<Grid
|
||||
key={plugin.id}
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
md={4}
|
||||
lg={3}
|
||||
xl={2}
|
||||
>
|
||||
<PluginCard
|
||||
plugin={plugin}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<NoPluginResults
|
||||
isFiltered={!!category || status !== PluginStatusOption.All}
|
||||
onViewAll={onViewAll}
|
||||
query={searchQuery}
|
||||
/>
|
||||
)}
|
||||
<SearchInput
|
||||
label={globalize.translate('Search')}
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{isError ? (
|
||||
<Alert
|
||||
severity='error'
|
||||
sx={{ marginBottom: 2 }}
|
||||
>
|
||||
{globalize.translate('PluginsLoadError')}
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Box>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={1}
|
||||
sx={{
|
||||
marginLeft: '-1rem',
|
||||
marginRight: '-1rem',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
paddingBottom: {
|
||||
xs: 1,
|
||||
md: 0.5
|
||||
},
|
||||
overflowX: 'auto'
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
color={status === PluginStatusOption.All ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setStatus(PluginStatusOption.All)}
|
||||
label={globalize.translate('All')}
|
||||
/>
|
||||
|
||||
<Chip
|
||||
color={status === PluginStatusOption.Available ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setStatus(PluginStatusOption.Available)}
|
||||
label={globalize.translate('LabelAvailable')}
|
||||
/>
|
||||
|
||||
<Chip
|
||||
color={status === PluginStatusOption.Installed ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setStatus(PluginStatusOption.Installed)}
|
||||
label={globalize.translate('LabelInstalled')}
|
||||
/>
|
||||
|
||||
<Divider orientation='vertical' flexItem />
|
||||
|
||||
<Chip
|
||||
color={!category ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setCategory('')}
|
||||
label={globalize.translate('All')}
|
||||
/>
|
||||
|
||||
{Object.values(PluginCategory).map(c => (
|
||||
<Chip
|
||||
key={c}
|
||||
color={category === c.toLowerCase() ? 'primary' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setCategory(c.toLowerCase())}
|
||||
label={globalize.translate(CATEGORY_LABELS[c as PluginCategory])}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
{filteredPlugins.length > 0 ? (
|
||||
// NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
<Grid container spacing={2}>
|
||||
{filteredPlugins.map(plugin => (
|
||||
// NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
<Grid
|
||||
key={plugin.id}
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
md={4}
|
||||
lg={3}
|
||||
xl={2}
|
||||
>
|
||||
<PluginCard
|
||||
plugin={plugin}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<NoPluginResults
|
||||
isFiltered={!!category || status !== PluginStatusOption.All}
|
||||
onViewAll={onViewAll}
|
||||
query={searchQuery}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import Stack from '@mui/material/Stack';
|
||||
import React, { type FC } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { appRouter, PUBLIC_PATHS } from 'components/router/appRouter';
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import ServerButton from 'components/toolbar/ServerButton';
|
||||
|
||||
@@ -16,14 +17,6 @@ interface AppToolbarProps {
|
||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const PUBLIC_PATHS = [
|
||||
'/addserver',
|
||||
'/selectserver',
|
||||
'/login',
|
||||
'/forgotpassword',
|
||||
'/forgotpasswordpin'
|
||||
];
|
||||
|
||||
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
isDrawerAvailable,
|
||||
isDrawerOpen,
|
||||
@@ -34,6 +27,10 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
// The video osd does not show the standard toolbar
|
||||
if (location.pathname === '/video') return null;
|
||||
|
||||
// Only show the back button in apps when appropriate
|
||||
const isBackButtonAvailable = window.NativeShell && appRouter.canGoBack(location.pathname);
|
||||
|
||||
// Check if the current path is a public path to hide user content
|
||||
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
@@ -48,6 +45,7 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
isDrawerAvailable={isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onDrawerButtonClick}
|
||||
isBackButtonAvailable={isBackButtonAvailable}
|
||||
isUserMenuAvailable={!isPublicPath}
|
||||
>
|
||||
{!isDrawerAvailable && (
|
||||
|
||||
@@ -221,9 +221,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||
const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some(
|
||||
(filter) => !!filter
|
||||
);
|
||||
const hasSortName = libraryViewSettings.SortBy.includes(
|
||||
ItemSortBy.SortName
|
||||
);
|
||||
const hasSortName = libraryViewSettings.SortBy !== ItemSortBy.Random;
|
||||
|
||||
const itemsContainerClass = classNames(
|
||||
'centered padded-left padded-right padded-right-withalphapicker',
|
||||
|
||||
@@ -22,6 +22,14 @@ type SortOption = {
|
||||
|
||||
type SortOptionsMapping = Record<string, SortOption[]>;
|
||||
|
||||
const collectionMovieOptions: SortOption[] = [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating },
|
||||
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }
|
||||
];
|
||||
|
||||
const movieOrFavoriteOptions = [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||
@@ -43,6 +51,7 @@ const photosOrPhotoAlbumsOptions = [
|
||||
|
||||
const sortOptionsMapping: SortOptionsMapping = {
|
||||
[LibraryTab.Movies]: movieOrFavoriteOptions,
|
||||
[LibraryTab.Collections]: collectionMovieOptions,
|
||||
[LibraryTab.Favorites]: movieOrFavoriteOptions,
|
||||
[LibraryTab.Series]: [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
|
||||
1
src/assets/img/devices/firetv.svg
Normal file
1
src/assets/img/devices/firetv.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Amazon Fire TV</title><path d="M20.196 15.12c.265.337-.294 1.73-.542 2.353-.077.19.085.266.257.123 1.106-.926 1.39-2.867 1.166-3.149-.226-.277-2.16-.516-3.341.314-.183.127-.151.304.05.279.665-.08 2.147-.257 2.41.08m-.858.981c-2.064 1.523-5.056 2.333-7.632 2.333-3.611 0-6.862-1.334-9.322-3.555-.194-.176-.02-.414.21-.28 2.655 1.545 5.939 2.477 9.328 2.477 2.287 0 4.803-.476 7.115-1.458.348-.147.642.231.3.483m2.034-3.155a.388.388 0 0 1-.201-.04c-.041-.026-.087-.1-.133-.225l-1.734-4.355a1.79 1.79 0 0 0-.046-.117.266.266 0 0 1-.023-.108c0-.084.049-.128.146-.128h.58c.098 0 .165.014.205.04.04.026.082.102.127.226l1.344 3.823 1.343-3.823c.046-.124.089-.2.128-.226a.402.402 0 0 1 .205-.04h.54c.1 0 .148.044.148.128a.3.3 0 0 1-.025.108c-.016.04-.032.078-.044.117l-1.727 4.355c-.045.124-.09.199-.132.225a.388.388 0 0 1-.201.04zm-3.644.068c-.929 0-1.392-.463-1.392-1.392V8.739h-.706c-.13 0-.197-.066-.197-.196v-.246a.22.22 0 0 1 .045-.147c.03-.031.086-.055.171-.067l.717-.09.127-1.215c.013-.13.082-.196.207-.196h.41c.13 0 .196.066.196.196v1.196h1.276c.13 0 .195.065.195.197v.372c0 .13-.064.196-.195.196h-1.276v2.834c0 .243.055.411.162.51.108.098.293.147.555.147.124 0 .277-.016.46-.049.099-.02.164-.03.197-.03.052 0 .088.014.108.044.02.03.029.077.029.142v.266a.366.366 0 0 1-.04.19c-.026.043-.078.078-.157.103a3.018 3.018 0 0 1-.892.118m-4.665-2.976c.006-.052.011-.137.011-.255 0-.399-.094-.698-.28-.901-.186-.204-.46-.306-.818-.306-.412 0-.732.123-.962.369-.228.245-.36.61-.392 1.093zm-.942 3.07c-.803 0-1.411-.222-1.824-.667-.412-.444-.616-1.102-.616-1.972 0-.83.204-1.475.616-1.937.413-.46.988-.691 1.728-.691.62 0 1.098.176 1.432.524.332.351.5.846.5 1.487 0 .21-.017.422-.05.638-.014.077-.034.13-.064.156-.029.027-.077.04-.142.04h-3.08c.013.563.154.977.418 1.245.265.268.674.403 1.23.403.196 0 .385-.014.564-.04a5.04 5.04 0 0 0 .682-.166l.117-.035a.284.284 0 0 1 .09-.016c.085 0 .125.06.125.177v.276c0 .085-.012.144-.037.18a.441.441 0 0 1-.167.114 3.38 3.38 0 0 1-.701.205 4.236 4.236 0 0 1-.82.079m-5.424-.147c-.13 0-.195-.066-.195-.197v-4.58c0-.13.064-.195.195-.195h.432c.064 0 .116.012.153.039.036.025.06.076.072.146l.07.55c.176-.19.343-.34.499-.452a1.725 1.725 0 0 1 1.02-.323c.079 0 .158.003.235.01.112.014.168.072.168.176v.53c0 .117-.058.177-.178.177-.058 0-.114-.004-.17-.01a1.638 1.638 0 0 0-.18-.01c-.524 0-.973.157-1.346.47v3.472c0 .131-.066.197-.195.197zm-2.249 0c-.13 0-.196-.066-.196-.197v-4.58c0-.13.066-.195.196-.195h.579c.13 0 .195.064.195.195v4.58c0 .131-.065.197-.195.197zm.295-5.856c-.19 0-.339-.054-.447-.16a.581.581 0 0 1-.161-.428c0-.176.054-.318.16-.426.11-.109.257-.163.448-.163.189 0 .337.054.446.163.107.108.16.25.16.426a.581.581 0 0 1-.16.427.608.608 0 0 1-.446.161m-3.625 5.856c-.132 0-.197-.066-.197-.197v-4.01H.195c-.13 0-.195-.066-.195-.197v-.245c0-.065.014-.114.043-.147.03-.033.088-.055.173-.07l.705-.087v-.804c0-1.091.523-1.638 1.57-1.638.248 0 .51.036.784.109.072.019.122.047.152.088.029.038.044.107.044.205v.255c0 .124-.048.186-.148.186-.058 0-.14-.01-.248-.029-.11-.02-.23-.03-.369-.03-.3 0-.51.057-.633.172-.121.115-.181.303-.181.564v.903h1.324c.131 0 .197.064.197.195v.373c0 .13-.066.197-.197.197H1.892v4.01c0 .131-.065.197-.196.197Z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -13,6 +13,7 @@ const BrowserName = {
|
||||
tizen: 'Samsung Smart TV',
|
||||
web0s: 'LG Smart TV',
|
||||
titanos: 'Titan OS',
|
||||
vega: 'Vega OS',
|
||||
operaTv: 'Opera TV',
|
||||
xboxOne: 'Xbox One',
|
||||
ps4: 'Sony PS4',
|
||||
|
||||
@@ -577,9 +577,10 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
|
||||
if (flags.isOuterFooter && item.AlbumArtists?.length) {
|
||||
const artistText = item.AlbumArtists
|
||||
.map(artist => {
|
||||
artist.ServerId = serverId;
|
||||
artist.Type = BaseItemKind.MusicArtist;
|
||||
artist.IsFolder = true;
|
||||
return getTextActionButton(artist, null, serverId);
|
||||
return getTextActionButton(artist);
|
||||
})
|
||||
.join(' / ');
|
||||
lines.push(artistText);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { history } from 'RootAppRouter';
|
||||
const START_PAGE_PATHS = ['/home', '/login', '/selectserver'];
|
||||
|
||||
/** Pages that do not require a user to be logged in to view. */
|
||||
const PUBLIC_PATHS = [
|
||||
export const PUBLIC_PATHS = [
|
||||
'/addserver',
|
||||
'/selectserver',
|
||||
'/login',
|
||||
@@ -121,9 +121,7 @@ class AppRouter {
|
||||
return this.baseRoute;
|
||||
}
|
||||
|
||||
canGoBack() {
|
||||
const path = history.location.pathname;
|
||||
|
||||
canGoBack(path = history.location.pathname) {
|
||||
if (
|
||||
!document.querySelector('.dialogContainer')
|
||||
&& START_PAGE_PATHS.includes(path)
|
||||
@@ -261,15 +259,15 @@ class AppRouter {
|
||||
}
|
||||
|
||||
if (item === 'recordedtv') {
|
||||
return '#/livetv?tab=3&serverId=' + options.serverId;
|
||||
return '#/livetv?tab=3&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (item === 'nextup') {
|
||||
return '#/list?type=nextup&serverId=' + options.serverId;
|
||||
return '#/list?type=nextup&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (item === 'list') {
|
||||
let urlForList = '#/list?serverId=' + options.serverId + '&type=' + options.itemTypes;
|
||||
let urlForList = '#/list?serverId=' + serverId + '&type=' + options.itemTypes;
|
||||
|
||||
if (options.isFavorite) {
|
||||
urlForList += '&IsFavorite=true';
|
||||
@@ -304,49 +302,49 @@ class AppRouter {
|
||||
|
||||
if (item === 'livetv') {
|
||||
if (options.section === 'programs') {
|
||||
return '#/livetv?tab=0&serverId=' + options.serverId;
|
||||
return '#/livetv?tab=0&serverId=' + serverId;
|
||||
}
|
||||
if (options.section === 'guide') {
|
||||
return '#/livetv?tab=1&serverId=' + options.serverId;
|
||||
return '#/livetv?tab=1&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'movies') {
|
||||
return '#/list?type=Programs&IsMovie=true&serverId=' + options.serverId;
|
||||
return '#/list?type=Programs&IsMovie=true&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'shows') {
|
||||
return '#/list?type=Programs&IsSeries=true&IsMovie=false&IsNews=false&serverId=' + options.serverId;
|
||||
return '#/list?type=Programs&IsSeries=true&IsMovie=false&IsNews=false&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'sports') {
|
||||
return '#/list?type=Programs&IsSports=true&serverId=' + options.serverId;
|
||||
return '#/list?type=Programs&IsSports=true&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'kids') {
|
||||
return '#/list?type=Programs&IsKids=true&serverId=' + options.serverId;
|
||||
return '#/list?type=Programs&IsKids=true&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'news') {
|
||||
return '#/list?type=Programs&IsNews=true&serverId=' + options.serverId;
|
||||
return '#/list?type=Programs&IsNews=true&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'onnow') {
|
||||
return '#/list?type=Programs&IsAiring=true&serverId=' + options.serverId;
|
||||
return '#/list?type=Programs&IsAiring=true&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'channels') {
|
||||
return '#/livetv?tab=2&serverId=' + options.serverId;
|
||||
return '#/livetv?tab=2&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'dvrschedule') {
|
||||
return '#/livetv?tab=4&serverId=' + options.serverId;
|
||||
return '#/livetv?tab=4&serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'seriesrecording') {
|
||||
return '#/livetv?tab=5&serverId=' + options.serverId;
|
||||
return '#/livetv?tab=5&serverId=' + serverId;
|
||||
}
|
||||
|
||||
return '#/livetv?serverId=' + options.serverId;
|
||||
return '#/livetv?serverId=' + serverId;
|
||||
}
|
||||
|
||||
if (itemType == 'SeriesTimer') {
|
||||
|
||||
1
src/scripts/browser.d.ts
vendored
1
src/scripts/browser.d.ts
vendored
@@ -21,6 +21,7 @@ declare namespace browser {
|
||||
export let animate: boolean;
|
||||
export let hisense: boolean;
|
||||
export let tizen: boolean;
|
||||
export let vega: boolean;
|
||||
export let vidaa: boolean;
|
||||
export let web0s: boolean;
|
||||
export let titanos: boolean;
|
||||
|
||||
@@ -235,10 +235,10 @@ const uaMatch = function (ua) {
|
||||
}
|
||||
|
||||
return {
|
||||
browser: browser,
|
||||
version: version,
|
||||
browser,
|
||||
version,
|
||||
platform: platformMatch[0] || '',
|
||||
versionMajor: versionMajor
|
||||
versionMajor
|
||||
};
|
||||
};
|
||||
|
||||
@@ -283,10 +283,11 @@ export const detectBrowser = (userAgent = navigator.userAgent) => {
|
||||
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
|
||||
browser.hisense = normalizedUA.includes('hisense');
|
||||
browser.tizen = normalizedUA.includes('tizen') || window.tizen != null;
|
||||
browser.vega = normalizedUA.includes('kepler');
|
||||
browser.vidaa = normalizedUA.includes('vidaa');
|
||||
browser.web0s = isWeb0s(normalizedUA);
|
||||
|
||||
browser.tv = browser.ps4 || browser.xboxOne || isTv(normalizedUA);
|
||||
browser.tv = browser.ps4 || browser.vega || browser.xboxOne || isTv(normalizedUA);
|
||||
browser.operaTv = browser.tv && normalizedUA.includes('opr/');
|
||||
|
||||
browser.edgeUwp = (browser.edge || browser.edgeChromium) && (normalizedUA.includes('msapphost') || normalizedUA.includes('webview'));
|
||||
@@ -305,9 +306,15 @@ export const detectBrowser = (userAgent = navigator.userAgent) => {
|
||||
delete browser.chrome;
|
||||
delete browser.safari;
|
||||
} else if (browser.titanos) {
|
||||
// UserAgent string contains 'Opr' and 'Safari', but we only want 'titanos' to be true
|
||||
// UserAgent string contains 'Opr' and 'Safari', but we only want 'titanos' to be true
|
||||
delete browser.operaTv;
|
||||
delete browser.safari;
|
||||
} else if (browser.vega) {
|
||||
// UserAgent string contains 'Chrome' and 'Safari', but we only want 'vega' to be true
|
||||
delete browser.chrome;
|
||||
delete browser.safari;
|
||||
// UserAgent string contains 'Mobile Chrome', but it is a TV
|
||||
delete browser.mobile;
|
||||
} else {
|
||||
browser.orsay = normalizedUA.includes('smarthub');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import { detectBrowser } from './browser';
|
||||
|
||||
describe('Browser', () => {
|
||||
it('should identify TitanOS devices', async () => {
|
||||
it('should identify TitanOS devices', () => {
|
||||
// Ref: https://docs.titanos.tv/user-agents-specifications
|
||||
// Philips example
|
||||
let browser = detectBrowser('Mozilla/5.0 (Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.4147.62 Safari/537.36 OPR/46.0.2207.0 OMI/4.24, TV_NT72690_2025_4K /<SW version> (Philips, <CTN>, wired) CE-HTML/1.0 NETTV/4.6.0.8 SignOn/2.0 SmartTvA/5.0.0 TitanOS/3.0 en Ginga');
|
||||
@@ -20,7 +20,17 @@ describe('Browser', () => {
|
||||
expect(browser.tv).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Xbox devices', async () => {
|
||||
it('should identify Vega devices', () => {
|
||||
// Ref: https://developer.amazon.com/docs/vega/0.21/webview-development-best-practices-tv.html#avoid-relying-on-the-useragent
|
||||
const browser = detectBrowser('Mozilla/5.0 (Linux; Kepler 1.1; AFTCA002 user/1234; wv) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Chrome/130.0.6723.192 Safari/537.36');
|
||||
expect(browser.vega).toBe(true);
|
||||
expect(browser.chrome).toBeFalsy();
|
||||
expect(browser.safari).toBeFalsy();
|
||||
expect(browser.mobile).toBeFalsy();
|
||||
expect(browser.tv).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Xbox devices', () => {
|
||||
const browser = detectBrowser('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 WebView2 Xbox');
|
||||
expect(browser.xboxOne).toBe(true);
|
||||
expect(browser.tv).toBe(true);
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
|
||||
html {
|
||||
@include fullpage;
|
||||
|
||||
/* Set the default font for cases we don't load the brand font */
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ function getWebDeviceIcon(browser: string | null | undefined) {
|
||||
return BASE_DEVICE_IMAGE_URL + 'msie.svg';
|
||||
case 'Titan OS':
|
||||
return BASE_DEVICE_IMAGE_URL + 'titanos.svg';
|
||||
case 'Vega OS':
|
||||
return BASE_DEVICE_IMAGE_URL + 'firetv.svg';
|
||||
default:
|
||||
return BASE_DEVICE_IMAGE_URL + 'html5.svg';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user