Merge branch 'master' into fix_long_getItems_request_URL

This commit is contained in:
dann-merlin 2023-04-13 11:05:09 +00:00 committed by GitHub
commit 6d6d03a9c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
220 changed files with 21471 additions and 16761 deletions

View File

@ -64,7 +64,9 @@ module.exports = {
'no-var': ['error'],
'no-void': ['error', { 'allowAsStatement': true }],
'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }],
'object-curly-spacing': ['error', 'always'],
'one-var': ['error', 'never'],
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'padded-blocks': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
@ -267,7 +269,6 @@ module.exports = {
'no-useless-constructor': ['off'],
'@typescript-eslint/no-useless-constructor': ['error'],
'max-params': ['error', 7],
'sonarjs/cognitive-complexity': ['warn']
}
}

View File

@ -17,4 +17,5 @@ jobs:
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -19,13 +19,13 @@ jobs:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Initialize CodeQL
uses: github/codeql-action/init@16964e90ba004cdf0cd845b866b5df21038b7723 # v2.2.6
uses: github/codeql-action/init@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@16964e90ba004cdf0cd845b866b5df21038b7723 # v2.2.6
uses: github/codeql-action/autobuild@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16964e90ba004cdf0cd845b866b5df21038b7723 # v2.2.6
uses: github/codeql-action/analyze@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9

View File

@ -12,13 +12,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0

View File

@ -13,7 +13,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
@ -37,7 +37,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
@ -58,7 +58,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
@ -82,7 +82,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0

View File

@ -1,18 +1,24 @@
name: Issue Stale Check
name: Stale Check
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
issues:
name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
@ -25,3 +31,21 @@ jobs:
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.org/contact).
prs-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.

View File

@ -13,7 +13,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0

View File

@ -60,6 +60,7 @@
- [edvwib](https://github.com/edvwib)
- [Rob Farraher](https://github.com/farraherbg)
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
- [Anantharaju S](https://github.com/Anantharajus)
- [Merlin Sievers](https://github.com/dann-merlin)
# Emby Contributors

View File

@ -1,4 +1,4 @@
FROM centos:8
FROM quay.io/centos/centos:stream8
# Docker build arguments
ARG SOURCE_DIR=/jellyfin

1178
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@babel/core": "7.21.0",
"@babel/eslint-parser": "7.19.1",
"@babel/core": "7.21.3",
"@babel/eslint-parser": "7.21.3",
"@babel/eslint-plugin": "7.19.1",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-private-methods": "7.18.6",
@ -16,13 +16,13 @@
"@babel/preset-typescript": "7.21.0",
"@types/escape-html": "1.0.2",
"@types/loadable__component": "5.13.4",
"@types/lodash-es": "4.17.6",
"@types/lodash-es": "4.17.7",
"@types/react": "17.0.53",
"@types/react-dom": "17.0.19",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@typescript-eslint/eslint-plugin": "5.56.0",
"@typescript-eslint/parser": "5.56.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.13",
"autoprefixer": "10.4.14",
"babel-loader": "9.1.2",
"babel-plugin-dynamic-import-polyfill": "1.0.0",
"clean-webpack-plugin": "4.0.0",
@ -32,7 +32,7 @@
"css-loader": "6.7.3",
"cssnano": "5.1.15",
"es-check": "7.1.0",
"eslint": "8.35.0",
"eslint": "8.36.0",
"eslint-plugin-compat": "4.1.2",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.27.5",
@ -40,29 +40,29 @@
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-sonarjs": "0.18.0",
"expose-loader": "4.0.0",
"eslint-plugin-sonarjs": "0.19.0",
"expose-loader": "4.1.0",
"html-loader": "4.2.0",
"html-webpack-plugin": "5.5.0",
"mini-css-extract-plugin": "2.7.2",
"mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.21",
"postcss-loader": "7.0.2",
"postcss-loader": "7.1.0",
"postcss-preset-env": "8.0.1",
"postcss-scss": "4.0.6",
"sass": "1.58.3",
"sass-loader": "13.2.0",
"sass": "1.60.0",
"sass-loader": "13.2.1",
"source-map-loader": "4.0.1",
"style-loader": "3.3.1",
"stylelint": "15.2.0",
"style-loader": "3.3.2",
"stylelint": "15.3.0",
"stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "6.0.3",
"stylelint-scss": "4.4.0",
"stylelint-scss": "4.5.0",
"ts-loader": "9.4.2",
"typescript": "4.9.5",
"webpack": "5.75.0",
"typescript": "5.0.2",
"webpack": "5.76.3",
"webpack-cli": "5.0.1",
"webpack-dev-server": "4.11.1",
"webpack-dev-server": "4.13.1",
"webpack-merge": "5.8.0",
"workbox-webpack-plugin": "6.5.4",
"worker-loader": "3.0.8"
@ -80,29 +80,29 @@
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.3.2",
"core-js": "3.29.0",
"core-js": "3.29.1",
"date-fns": "2.29.3",
"dompurify": "3.0.1",
"epubjs": "0.4.2",
"epubjs": "0.3.93",
"escape-html": "1.0.3",
"fast-text-encoding": "1.0.6",
"flv.js": "1.6.2",
"headroom.js": "0.12.0",
"history": "5.3.0",
"hls.js": "1.3.4",
"hls.js": "1.2.4",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.10.0",
"jquery": "3.6.3",
"jquery": "3.6.4",
"jstree": "3.3.15",
"libarchive.js": "1.3.0",
"lodash-es": "4.17.21",
"marked": "4.2.12",
"marked": "4.3.0",
"material-design-icons-iconfont": "6.7.0",
"native-promise-only": "0.8.1",
"pdfjs-dist": "2.16.105",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "6.8.2",
"react-router-dom": "6.9.0",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2",
"sortablejs": "1.15.0",

View File

@ -7,8 +7,8 @@ const config = () => ({
plugins: [
// Explicitly specify browserslist to override ones from node_modules
// For example, Swiper has it in its package.json
postcssPresetEnv({browsers: packageConfig.browserslist}),
autoprefixer({overrideBrowserslist: packageConfig.browserslist}),
postcssPresetEnv({ browsers: packageConfig.browserslist }),
autoprefixer({ overrideBrowserslist: packageConfig.browserslist }),
cssnano()
]
});

View File

@ -3,13 +3,14 @@ import React from 'react';
import { HistoryRouter } from './components/HistoryRouter';
import { ApiProvider } from './hooks/useApi';
import AppRoutes from './routes/index';
import { AppRoutes, ExperimentalAppRoutes } from './routes';
const App = ({ history }: { history: History }) => {
const layoutMode = localStorage.getItem('layout');
return (
<ApiProvider>
<HistoryRouter history={history}>
<AppRoutes />
{layoutMode === 'experimental' ? <ExperimentalAppRoutes /> : <AppRoutes /> }
</HistoryRouter>
</ApiProvider>
);

View File

@ -37,7 +37,7 @@ import template from './accessSchedule.template.html';
context.querySelector('#selectEnd').innerHTML = html;
}
function loadSchedule(context, {DayOfWeek, StartHour, EndHour}) {
function loadSchedule(context, { DayOfWeek, StartHour, EndHour }) {
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
context.querySelector('#selectStart').value = StartHour || 0;
context.querySelector('#selectEnd').value = EndHour || 0;

View File

@ -309,8 +309,8 @@ function askForExit() {
exitPromise = actionsheet.show({
title: globalize.translate('MessageConfirmAppExit'),
items: [
{id: 'yes', name: globalize.translate('Yes')},
{id: 'no', name: globalize.translate('No')}
{ id: 'yes', name: globalize.translate('Yes') },
{ id: 'no', name: globalize.translate('No') }
]
}).then(function (value) {
if (value === 'yes') {
@ -366,20 +366,20 @@ export const appHost = {
};
},
deviceName: function () {
return window.NativeShell?.AppHost?.deviceName
? window.NativeShell.AppHost.deviceName() : getDeviceName();
return window.NativeShell?.AppHost?.deviceName ?
window.NativeShell.AppHost.deviceName() : getDeviceName();
},
deviceId: function () {
return window.NativeShell?.AppHost?.deviceId
? window.NativeShell.AppHost.deviceId() : getDeviceId();
return window.NativeShell?.AppHost?.deviceId ?
window.NativeShell.AppHost.deviceId() : getDeviceId();
},
appName: function () {
return window.NativeShell?.AppHost?.appName
? window.NativeShell.AppHost.appName() : appName;
return window.NativeShell?.AppHost?.appName ?
window.NativeShell.AppHost.appName() : appName;
},
appVersion: function () {
return window.NativeShell?.AppHost?.appVersion
? window.NativeShell.AppHost.appVersion() : Package.version;
return window.NativeShell?.AppHost?.appVersion ?
window.NativeShell.AppHost.appVersion() : Package.version;
},
getPushTokenInfo: function () {
return {};

View File

@ -896,13 +896,13 @@ import { appRouter } from '../appRouter';
}
if (options.showYear || options.showSeriesYear) {
const productionYear = item.ProductionYear && datetime.toLocaleString(item.ProductionYear, {useGrouping: false});
const productionYear = item.ProductionYear && datetime.toLocaleString(item.ProductionYear, { useGrouping: false });
if (item.Type === 'Series') {
if (item.Status === 'Continuing') {
lines.push(globalize.translate('SeriesYearToPresent', productionYear || ''));
} else {
if (item.EndDate && item.ProductionYear) {
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), {useGrouping: false});
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
} else {
lines.push(productionYear || '');

View File

@ -28,7 +28,7 @@ import ServerConnections from '../ServerConnections';
}
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
const videoStream = mediaStreams.filter(({Type}) => {
const videoStream = mediaStreams.filter(({ Type }) => {
return Type === 'Video';
})[0] || {};
@ -68,7 +68,7 @@ import ServerConnections from '../ServerConnections';
return html;
}
function getImgUrl({Id}, {ImageTag}, index, maxWidth, apiClient) {
function getImgUrl({ Id }, { ImageTag }, index, maxWidth, apiClient) {
if (ImageTag) {
return apiClient.getScaledImageUrl(Id, {
@ -82,7 +82,7 @@ import ServerConnections from '../ServerConnections';
return null;
}
function buildChapterCard(item, apiClient, chapter, index, {width, coverImage}, className, shape) {
function buildChapterCard(item, apiClient, chapter, index, { width, coverImage }, className, shape) {
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';

View File

@ -22,7 +22,7 @@ const Filter: FC<FilterProps> = ({
const element = useRef<HTMLDivElement>(null);
const showFilterMenu = useCallback(() => {
import('../filtermenu/filtermenu').then(({default: FilterMenu}) => {
import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => {
const filterMenu = new FilterMenu();
filterMenu.show({
settings: viewQuerySettings,

View File

@ -6,7 +6,7 @@ const NewCollection: FC = () => {
const element = useRef<HTMLDivElement>(null);
const showCollectionEditor = useCallback(() => {
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const serverId = window.ApiClient.serverId();
const collectionEditor = new CollectionEditor();
collectionEditor.show({

View File

@ -16,7 +16,7 @@ const SelectView: FC<SelectViewProps> = ({
const element = useRef<HTMLDivElement>(null);
const showViewSettingsMenu = useCallback(() => {
import('../viewSettings/viewSettings').then(({default: ViewSettings}) => {
import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => {
const viewsettings = new ViewSettings();
viewsettings.show({
settings: viewQuerySettings,

View File

@ -19,7 +19,7 @@ const Sort: FC<SortProps> = ({
const element = useRef<HTMLDivElement>(null);
const showSortMenu = useCallback(() => {
import('../sortmenu/sortmenu').then(({default: SortMenu}) => {
import('../sortmenu/sortmenu').then(({ default: SortMenu }) => {
const sortMenu = new SortMenu();
sortMenu.show({
settings: viewQuerySettings,

View File

@ -14,7 +14,7 @@ type IProps = {
children?: React.ReactNode
}
const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
const AccessContainer: FunctionComponent<IProps> = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
return (
<div className={containerClassName}>
<h2>{globalize.translate(headerTitle)}</h2>

View File

@ -22,7 +22,7 @@ function getDisplayTime(hours = 0) {
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({index, DayOfWeek, StartHour, EndHour}: AccessScheduleListProps) => {
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({ index, DayOfWeek, StartHour, EndHour }: AccessScheduleListProps) => {
return (
<div
className='liSchedule listItem'

View File

@ -5,7 +5,7 @@ type IProps = {
tag?: string;
}
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
return (
<div className='paperList'>
<div className='listItem'>

View File

@ -36,7 +36,7 @@ const createLinkElement = (activeTab: string) => ({
</a>`
});
const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
const SectionTabs: FunctionComponent<IProps> = ({ activeTab }: IProps) => {
return (
<div
data-role='controlgroup'

View File

@ -74,7 +74,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
</div>
<div className='cardFooter visualCardBox-cardFooter'>
<div
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
style={{ textAlign: 'right', float: 'right', paddingTop: '5px' }}
>
<IconButtonElement
is='paper-icon-button-light'

View File

@ -14,7 +14,7 @@ type IProps = {
userId: string;
}
const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const element = useRef<HTMLDivElement>(null);
const loadUser = useCallback(() => {
@ -76,7 +76,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
import('../../autoFocuser').then(({default: autoFocuser}) => {
import('../../autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
@ -214,7 +214,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<div ref={element}>
<form
className='updatePasswordForm passwordSection hide'
style={{margin: '0 auto 2em'}}
style={{ margin: '0 auto 2em' }}
>
<div className='detailSection'>
<div id='fldCurrentPassword' className='inputContainer hide'>
@ -260,7 +260,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<br />
<form
className='localAccessForm localAccessSection'
style={{margin: '0 auto'}}
style={{ margin: '0 auto' }}
>
<div className='detailSection'>
<div className='detailSectionHeader'>

View File

@ -250,7 +250,6 @@ import '../../styles/scrollstyles.scss';
}
function isOpened(dlg) {
//return dlg.opened;
return !dlg.classList.contains('hide');
}

View File

@ -297,10 +297,8 @@ class FilterMenu {
}
if (submitted) {
//if (!options.onChange) {
saveValues(dlg, options.settings, options.settingsKey, options.setfilters);
return resolve();
//}
}
return resolve();
});

View File

@ -29,7 +29,7 @@ import ServerConnections from '../ServerConnections';
import template from './tvguide.template.html';
function showViewSettings(instance) {
import('./guide-settings').then(({default: guideSettingsDialog}) => {
import('./guide-settings').then(({ default: guideSettingsDialog }) => {
guideSettingsDialog.show(instance.categoryOptions).then(function () {
instance.refresh();
});

View File

@ -26,8 +26,8 @@ import Events from '../utils/events.ts';
function canPlayNativeHls() {
const media = document.createElement('video');
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '') ||
media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '')
|| media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
}
export function enableHlsJsPlayer(runTimeTicks, mediaType) {
@ -51,18 +51,10 @@ import Events from '../utils/events.ts';
return true;
}
if (browser.edge && mediaType === 'Video') {
//return true;
}
// simple playback should use the native support
if (runTimeTicks) {
//if (!browser.edge) {
return false;
//}
}
//return false;
}
return true;
@ -201,8 +193,8 @@ import Events from '../utils/events.ts';
.catch((e) => {
const errorName = (e.name || '').toLowerCase();
// safari uses aborterror
if (errorName === 'notallowederror' ||
errorName === 'aborterror') {
if (errorName === 'notallowederror'
|| errorName === 'aborterror') {
// swallow this error because the user can still click the play button on the video element
return Promise.resolve();
}

View File

@ -282,7 +282,7 @@ import template from './imageeditor.template.html';
const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10);
const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10);
import('../actionSheet/actionSheet').then(({default: actionSheet}) => {
import('../actionSheet/actionSheet').then(({ default: actionSheet }) => {
const commands = [];
commands.push({
@ -353,7 +353,7 @@ import template from './imageeditor.template.html';
addListeners(context, 'btnOpenUploadMenu', 'click', function () {
const imageType = this.getAttribute('data-imagetype');
import('../imageUploader/imageUploader').then(({default: imageUploader}) => {
import('../imageUploader/imageUploader').then(({ default: imageUploader }) => {
imageUploader.show({
theme: options.theme,

View File

@ -326,7 +326,7 @@ import toast from './toast/toast';
// eslint-disable-next-line sonarjs/max-switch-cases
switch (id) {
case 'addtocollection':
import('./collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
import('./collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: [itemId],
@ -335,7 +335,7 @@ import toast from './toast/toast';
});
break;
case 'addtoplaylist':
import('./playlisteditor/playlisteditor').then(({default: playlistEditor}) => {
import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor({
items: [itemId],
serverId: serverId
@ -408,7 +408,7 @@ import toast from './toast/toast';
break;
}
case 'editsubtitles':
import('./subtitleeditor/subtitleeditor').then(({default: subtitleEditor}) => {
import('./subtitleeditor/subtitleeditor').then(({ default: subtitleEditor }) => {
subtitleEditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
});
break;
@ -464,7 +464,7 @@ import toast from './toast/toast';
playbackManager.clearQueue();
break;
case 'record':
import('./recordingcreator/recordingcreator').then(({default: recordingCreator}) => {
import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
recordingCreator.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
});
break;
@ -535,7 +535,7 @@ import toast from './toast/toast';
}
function deleteTimer(apiClient, item, resolve, command) {
import('./recordingcreator/recordinghelper').then(({default: recordingHelper}) => {
import('./recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
const timerId = item.TimerId || item.Id;
recordingHelper.cancelTimerWithConfirmation(timerId, item.ServerId).then(function () {
getResolveFunction(resolve, command, true)();
@ -544,7 +544,7 @@ import toast from './toast/toast';
}
function deleteSeriesTimer(apiClient, item, resolve, command) {
import('./recordingcreator/recordinghelper').then(({default: recordingHelper}) => {
import('./recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
recordingHelper.cancelSeriesTimerWithConfirmation(item.Id, item.ServerId).then(function () {
getResolveFunction(resolve, command, true)();
});
@ -585,15 +585,15 @@ import toast from './toast/toast';
const serverId = apiClient.serverInfo().Id;
if (item.Type === 'Timer') {
import('./recordingcreator/recordingeditor').then(({default: recordingEditor}) => {
import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, serverId).then(resolve, reject);
});
} else if (item.Type === 'SeriesTimer') {
import('./recordingcreator/seriesrecordingeditor').then(({default: recordingEditor}) => {
import('./recordingcreator/seriesrecordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, serverId).then(resolve, reject);
});
} else {
import('./metadataEditor/metadataEditor').then(({default: metadataEditor}) => {
import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
metadataEditor.show(item.Id, serverId).then(resolve, reject);
});
}
@ -614,7 +614,7 @@ import toast from './toast/toast';
}
function refresh(apiClient, item) {
import('./refreshdialog/refreshdialog').then(({default: refreshDialog}) => {
import('./refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
new refreshDialog({
itemIds: [item.Id],
serverId: apiClient.serverInfo().Id,

View File

@ -167,7 +167,7 @@ import datetime from '../../scripts/datetime';
lines.push(escapeHtml(identifyResult.Name));
if (identifyResult.ProductionYear) {
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, {useGrouping: false}));
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, { useGrouping: false }));
}
let resultHtml = lines.join('<br/>');

View File

@ -316,7 +316,7 @@ import template from './libraryoptionseditor.template.html';
}
function showImageOptionsForType(type) {
import('../imageOptionsEditor/imageOptionsEditor').then(({default: ImageOptionsEditor}) => {
import('../imageOptionsEditor/imageOptionsEditor').then(({ default: ImageOptionsEditor }) => {
let typeOptions = getTypeOptions(currentLibraryOptions, type);
if (!typeOptions) {
typeOptions = {

View File

@ -65,7 +65,7 @@ import '../elements/emby-button/emby-button';
}
};
import('../scripts/touchHelper').then(({default: TouchHelper}) => {
import('../scripts/touchHelper').then(({ default: TouchHelper }) => {
const touchHelper = new TouchHelper(view.parentNode.parentNode);
Events.on(touchHelper, 'swipeleft', onSwipeLeft);

View File

@ -25,7 +25,7 @@ import toast from '../toast/toast';
import alert from '../alert';
import template from './mediaLibraryCreator.template.html';
function onAddLibrary() {
function onAddLibrary(e) {
if (isCreating) {
return false;
}
@ -62,7 +62,7 @@ import template from './mediaLibraryCreator.template.html';
isCreating = false;
loading.hide();
});
return false;
e.preventDefault();
}
function getCollectionTypeOptionsHtml(collectionTypeOptions) {
@ -96,14 +96,14 @@ import template from './mediaLibraryCreator.template.html';
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
});
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
page.querySelector('.btnSubmit').addEventListener('click', onAddLibrary);
page.querySelector('.addLibraryForm').addEventListener('submit', onAddLibrary);
page.querySelector('.folderList').addEventListener('click', onRemoveClick);
}
function onAddButtonClick() {
const page = dom.parentWithClass(this, 'dlg-librarycreator');
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,

View File

@ -1,37 +1,39 @@
<div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
</div>
<div class="formDialogContent scrollY" style="padding-top:2em;">
<div class="dialogContentInner dialog-content-centered">
<div id="fldCollectionType" class="selectContainer">
<select is="emby-select" id="selectCollectionType" data-mini="true" required="required" label="${LabelContentType}"></select>
<div class="collectionTypeFieldDescription fieldDescription">
</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="text" id="txtValue" required="required" label="${LabelDisplayName}" />
</div>
<div class="folders">
<div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1>
<button is="emby-button" type="button" class="fab btnAddFolder submit" title="${Add}">
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div class="paperList folderList hide" style="margin-bottom:2em;"></div>
</div>
<div class="libraryOptions"></div>
<form class="addLibraryForm" style="max-width:100%;">
<div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
</div>
</div>
<div class="formDialogFooter">
<button is="emby-button" type="button" class="raised btnSubmit button-submit block formDialogFooterItem">
<span>${ButtonOk}</span>
</button>
</div>
<div class="formDialogContent scrollY" style="padding-top:2em;">
<div class="dialogContentInner dialog-content-centered">
<div id="fldCollectionType" class="selectContainer">
<select is="emby-select" id="selectCollectionType" data-mini="true" required="required" label="${LabelContentType}"></select>
<div class="collectionTypeFieldDescription fieldDescription">
</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="text" id="txtValue" required="required" label="${LabelDisplayName}" />
</div>
<div class="folders">
<div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1>
<button is="emby-button" type="button" class="fab btnAddFolder submit" title="${Add}">
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div class="paperList folderList hide" style="margin-bottom:2em;"></div>
</div>
<div class="libraryOptions"></div>
</div>
</div>
<div class="formDialogFooter">
<button is="emby-button" type="submit" class="raised btnSubmit button-submit block formDialogFooterItem">
<span>${ButtonOk}</span>
</button>
</div>
</form>

View File

@ -164,7 +164,7 @@ import template from './mediaLibraryEditor.template.html';
}
function showDirectoryBrowser(context, originalPath, networkPath) {
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,

View File

@ -177,13 +177,13 @@ import * as userSettings from '../../scripts/settings/userSettings';
if (options.year !== false && item.ProductionYear && item.Type === 'Series') {
if (item.Status === 'Continuing') {
miscInfo.push(globalize.translate('SeriesYearToPresent', datetime.toLocaleString(item.ProductionYear, {useGrouping: false})));
miscInfo.push(globalize.translate('SeriesYearToPresent', datetime.toLocaleString(item.ProductionYear, { useGrouping: false })));
} else if (item.ProductionYear) {
text = datetime.toLocaleString(item.ProductionYear, {useGrouping: false});
text = datetime.toLocaleString(item.ProductionYear, { useGrouping: false });
if (item.EndDate) {
try {
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), {useGrouping: false});
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
if (endYear !== item.ProductionYear) {
text += `-${endYear}`;
@ -253,7 +253,7 @@ import * as userSettings from '../../scripts/settings/userSettings';
miscInfo.push(item.ProductionYear);
} else if (item.PremiereDate) {
try {
text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), {useGrouping: false});
text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false });
miscInfo.push(text);
} catch (e) {
console.error('error parsing date:', item.PremiereDate);

View File

@ -211,7 +211,7 @@ import template from './metadataEditor.template.html';
}
function addElementToList(source, sortCallback) {
import('../prompt/prompt').then(({default: prompt}) => {
import('../prompt/prompt').then(({ default: prompt }) => {
prompt({
label: 'Value:'
}).then(function (text) {
@ -229,7 +229,7 @@ import template from './metadataEditor.template.html';
}
function editPerson(context, person, index) {
import('./personEditor').then(({default: personEditor}) => {
import('./personEditor').then(({ default: personEditor }) => {
personEditor.show(person).then(function (updatedPerson) {
const isNew = index === -1;
@ -253,7 +253,7 @@ import template from './metadataEditor.template.html';
}
function showMoreMenu(context, button, user) {
import('../itemContextMenu').then(({default: itemContextMenu}) => {
import('../itemContextMenu').then(({ default: itemContextMenu }) => {
const item = currentItem;
itemContextMenu.show({
@ -588,12 +588,12 @@ import template from './metadataEditor.template.html';
hideElement('#collapsibleSpecialEpisodeInfo', context);
}
if (item.Type === 'Person' ||
item.Type === 'Genre' ||
item.Type === 'Studio' ||
item.Type === 'MusicGenre' ||
item.Type === 'TvChannel' ||
item.Type === 'Book') {
if (item.Type === 'Person'
|| item.Type === 'Genre'
|| item.Type === 'Studio'
|| item.Type === 'MusicGenre'
|| item.Type === 'TvChannel'
|| item.Type === 'Book') {
hideElement('#peopleCollapsible', context);
} else {
showElement('#peopleCollapsible', context);

View File

@ -205,13 +205,6 @@ import datetime from '../../scripts/datetime';
if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) {
// Disabled because there is no callback for this item
/*
menuItems.push({
name: globalize.translate('Download'),
id: 'download',
icon: 'file_download'
});
*/
}
if (user.Policy.IsAdministrator) {
@ -267,7 +260,7 @@ import datetime from '../../scripts/datetime';
}
break;
case 'addtocollection':
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: items,
@ -308,7 +301,7 @@ import datetime from '../../scripts/datetime';
dispatchNeedsRefresh();
break;
case 'refresh':
import('../refreshdialog/refreshdialog').then(({default: refreshDialog}) => {
import('../refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
new refreshDialog({
itemIds: items,
serverId: serverId

View File

@ -69,7 +69,7 @@ import shell from '../../scripts/shell';
const list = [];
imageSizes.forEach((size) => {
const url = getImageUrl(item, {height: size});
const url = getImageUrl(item, { height: size });
if (url !== null) {
list.push(url);
}

View File

@ -1039,7 +1039,6 @@ class PlaybackManager {
}
}
//var mediaType = item.MediaType;
return getPlayer(item, getDefaultPlayOptions()) != null;
};

View File

@ -4,7 +4,8 @@ import Events from '../../utils/events.ts';
import layoutManager from '../layoutManager';
import { playbackManager } from '../playback/playbackmanager';
import playMethodHelper from '../playback/playmethodhelper';
import SyncPlay from '../../plugins/syncPlay/core';
import { pluginManager } from '../pluginManager';
import { PluginType } from '../../types/plugin.ts';
import './playerstats.scss';
import ServerConnections from '../ServerConnections';
@ -325,6 +326,12 @@ import ServerConnections from '../ServerConnections';
}
function getSyncPlayStats() {
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
if (!SyncPlay?.Manager.isSyncPlayEnabled()) {
return [];
}
const syncStats = [];
const stats = SyncPlay.Manager.getStats();
@ -422,10 +429,10 @@ import ServerConnections from '../ServerConnections';
name: globalize.translate('LabelOriginalMediaInfo')
});
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId);
if (SyncPlay.Manager.isSyncPlayEnabled() && apiClient.isMinServerVersion('10.6.0')) {
const syncPlayStats = getSyncPlayStats();
if (syncPlayStats.length > 0) {
categories.push({
stats: getSyncPlayStats(),
stats: syncPlayStats,
name: globalize.translate('LabelSyncPlayInfo')
});
}

View File

@ -4,10 +4,12 @@ import dialogHelper from '../dialogHelper/dialogHelper';
import loading from '../loading/loading';
import layoutManager from '../layoutManager';
import { playbackManager } from '../playback/playbackmanager';
import SyncPlay from '../../plugins/syncPlay/core';
import { pluginManager } from '../pluginManager';
import * as userSettings from '../../scripts/settings/userSettings';
import { appRouter } from '../appRouter';
import globalize from '../../scripts/globalize';
import { PluginType } from '../../types/plugin.ts';
import '../../elements/emby-button/emby-button';
import '../../elements/emby-input/emby-input';
import '../../elements/emby-button/paper-icon-button-light';
@ -117,10 +119,12 @@ import ServerConnections from '../ServerConnections';
};
const apiClient = ServerConnections.getApiClient(currentServerId);
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
let html = '';
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay.Manager.isSyncPlayEnabled()) {
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) {
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
}

View File

@ -119,9 +119,14 @@ class PluginManager {
}
ofType(type) {
return this.pluginsList.filter((o) => {
return o.type === type;
});
return this.pluginsList.filter(plugin => plugin.type === type);
}
firstOfType(type) {
// Get all plugins of the specified type
return this.ofType(type)
// Return the plugin with the "highest" (lowest numeric value) priority
.sort((p1, p2) => (p1.priority || 0) - (p2.priority || 0))[0];
}
#mapRoute(plugin, route) {

View File

@ -141,7 +141,7 @@ function onManageRecordingClick() {
}
const self = this;
import('./recordingeditor').then(({default: recordingEditor}) => {
import('./recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(self.TimerId, options.serverId, {
enableCancel: false
}).then(function () {
@ -159,7 +159,7 @@ function onManageSeriesRecordingClick() {
const self = this;
import('./seriesrecordingeditor').then(({default: seriesRecordingEditor}) => {
import('./seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
seriesRecordingEditor.show(self.SeriesTimerId, options.serverId, {
enableCancel: false

View File

@ -389,13 +389,13 @@ import layoutManager from './layoutManager';
if (xScroller !== yScroller) {
if (xScroller) {
scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior});
scrollToHelper(xScroller, { left: scrollX, behavior: scrollBehavior });
}
if (yScroller) {
scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior});
scrollToHelper(yScroller, { top: scrollY, behavior: scrollBehavior });
}
} else if (xScroller) {
scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior});
scrollToHelper(xScroller, { left: scrollX, top: scrollY, behavior: scrollBehavior });
}
}
@ -597,7 +597,7 @@ import layoutManager from './layoutManager';
setTimeout(function() {
scrollToElement(e.target, useSmoothScroll());
}, 0);
}, {capture: true});
}, { capture: true });
}
/* eslint-enable indent */

View File

@ -85,8 +85,8 @@ const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {
dangerouslySetInnerHTML={createInputElement()}
/>
</div>
{layoutManager.tv && !browser.tv &&
<AlphaPicker onAlphaPicked={onAlphaPicked} />
{layoutManager.tv && !browser.tv
&& <AlphaPicker onAlphaPicked={onAlphaPicked} />
}
</div>
);

View File

@ -71,7 +71,7 @@ import toast from './toast/toast';
}
function showProgramDialog(item) {
import('./recordingcreator/recordingcreator').then(({default:recordingCreator}) => {
import('./recordingcreator/recordingcreator').then(({ default:recordingCreator }) => {
recordingCreator.show(item.Id, item.ServerId);
});
}
@ -272,7 +272,7 @@ import toast from './toast/toast';
}
function addToPlaylist(item) {
import('./playlisteditor/playlisteditor').then(({default: playlistEditor}) => {
import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor().show({
items: [item.Id],
serverId: item.ServerId
@ -297,16 +297,16 @@ import toast from './toast/toast';
if (item.Type === 'Timer') {
if (item.ProgramId) {
import('./recordingcreator/recordingcreator').then(({default: recordingCreator}) => {
import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
});
} else {
import('./recordingcreator/recordingeditor').then(({default: recordingEditor}) => {
import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
});
}
} else {
import('./metadataEditor/metadataEditor').then(({default: metadataEditor}) => {
import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
});
}

View File

@ -342,7 +342,7 @@ function showDownloadOptions(button, context, subtitleId) {
}
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then(({default: scrollHelper}) => {
import('../../scripts/scrollHelper').then(({ default: scrollHelper }) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
@ -353,7 +353,7 @@ function onOpenUploadMenu(e) {
const selectLanguage = dialog.querySelector('#selectLanguage');
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
import('../subtitleuploader/subtitleuploader').then(({default: subtitleUploader}) => {
import('../subtitleuploader/subtitleuploader').then(({ default: subtitleUploader }) => {
subtitleUploader.show({
languages: {
list: selectLanguage.innerHTML,

View File

@ -94,9 +94,9 @@ function init(instance) {
subtitleSyncSlider.getBubbleHtml = function (value) {
const newOffset = getOffsetFromPercentage(value);
return '<h1 class="sliderBubbleText">' +
(newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's' +
'</h1>';
return '<h1 class="sliderBubbleText">'
+ (newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's'
+ '</h1>';
};
subtitleSyncCloseButton.addEventListener('click', function () {

View File

@ -0,0 +1,114 @@
import { clearBackdrop } from '../backdrop/backdrop';
import * as mainTabsManager from '../maintabsmanager';
import layoutManager from '../layoutManager';
import '../../elements/emby-tabs/emby-tabs';
import LibraryMenu from '../../scripts/libraryMenu';
function onViewDestroy() {
const tabControllers = this.tabControllers;
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
this.tabControllers = null;
}
this.view = null;
this.params = null;
this.currentTabController = null;
this.initialTabIndex = null;
}
class TabbedView {
constructor(view, params) {
this.tabControllers = [];
this.view = view;
this.params = params;
const self = this;
let currentTabIndex = parseInt(params.tab || this.getDefaultTabIndex(params.parentId), 10);
this.initialTabIndex = currentTabIndex;
function validateTabLoad(index) {
return self.validateTabLoad ? self.validateTabLoad(index) : Promise.resolve();
}
function loadTab(index, previousIndex) {
validateTabLoad(index).then(function () {
self.getTabController(index).then(function (controller) {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
currentTabIndex = index;
self.currentTabController = controller;
});
});
}
function getTabContainers() {
return view.querySelectorAll('.tabContent');
}
function onTabChange(e) {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : self.tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}
view.addEventListener('viewbeforehide', this.onPause.bind(this));
view.addEventListener('viewbeforeshow', function () {
mainTabsManager.setTabs(view, currentTabIndex, self.getTabs, getTabContainers, null, onTabChange, false);
});
view.addEventListener('viewshow', function (e) {
self.onResume(e.detail);
});
view.addEventListener('viewdestroy', onViewDestroy.bind(this));
}
onResume() {
this.setTitle();
clearBackdrop();
const currentTabController = this.currentTabController;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(this.initialTabIndex);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
}
onPause() {
const currentTabController = this.currentTabController;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
}
setTitle() {
LibraryMenu.setTitle('');
}
}
export default TabbedView;

View File

@ -48,7 +48,7 @@ function refreshTunerDevices(page, providerInfo, devices) {
function onSelectPathClick(e) {
const page = $(e.target).parents('.xmltvForm')[0];
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
includeFiles: true,

View File

@ -97,7 +97,7 @@ function dispatchViewEvent(view, eventInfo, eventName, isCancellable) {
return eventResult;
}
function getViewEventDetail(view, {state, url, options = {}}, isRestored) {
function getViewEventDetail(view, { state, url, options = {} }, isRestored) {
const index = url.indexOf('?');
// eslint-disable-next-line compat/compat
const searchParams = new URLSearchParams(url.substring(index + 1));

View File

@ -52,7 +52,7 @@ import { pageIdOn } from '../../utils/dashboard';
}
function showNewKeyPrompt(page) {
import('../../components/prompt/prompt').then(({default: prompt}) => {
import('../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
title: globalize.translate('HeaderNewApiKey'),
label: globalize.translate('LabelAppName'),

View File

@ -65,7 +65,7 @@ import confirm from '../../components/confirm/confirm';
}
function showSendMessageForm(btn, session) {
import('../../components/prompt/prompt').then(({default: prompt}) => {
import('../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
title: globalize.translate('HeaderSendMessage'),
label: globalize.translate('LabelMessageText'),
@ -82,7 +82,7 @@ import confirm from '../../components/confirm/confirm';
}
function showOptionsMenu(btn, session) {
import('../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
const menuItems = [];
if (session.ServerId && session.DeviceId !== ServerConnections.deviceId()) {

View File

@ -68,7 +68,7 @@ import confirm from '../../../components/confirm/confirm';
});
}
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
positionTo: btn,

View File

@ -111,7 +111,7 @@
<span>${EnableIntelLowPowerHevcHwEncoder}</span>
</label>
<div class="fieldDescription">
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://01.org/linuxgraphics/downloads/firmware" target="_blank">${IntelLowPowerEncHelp}</a>
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/administration/hardware-acceleration/intel#configure-and-verify-lp-mode-on-linux" target="_blank">${IntelLowPowerEncHelp}</a>
</div>
</div>
</div>
@ -253,6 +253,13 @@
</label>
<div class="fieldDescription checkboxFieldDescription">${EnableFallbackFontHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkEnableAudioVbr" />
<span>${LabelEnableAudioVbr}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelEnableAudioVbrHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtDownMixAudioBoost" pattern="[0-9]*" required="required" min=".5" max="3" step=".1" label="${LabelDownMixAudioScale}" />
<div class="fieldDescription">${LabelDownMixAudioScaleHelp}</div>

View File

@ -22,6 +22,7 @@ import alert from '../../components/alert';
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
$('#selectThreadCount', page).val(config.EncodingThreadCount);
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
$('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost);
$('#selectStereoDownmixAlgorithm').val(config.DownMixStereoAlgorithm || 'None');
page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || '';
@ -78,6 +79,7 @@ import alert from '../../components/alert';
const onDecoderConfirmed = function () {
loading.show();
ApiClient.getNamedConfiguration('encoding').then(function (config) {
config.EnableAudioVbr = form.querySelector('#chkEnableAudioVbr').checked;
config.DownMixAudioBoost = $('#txtDownMixAudioBoost', form).val();
config.DownMixStereoAlgorithm = $('#selectStereoDownmixAlgorithm', form).val() || 'None';
config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value;
@ -237,7 +239,7 @@ import alert from '../../components/alert';
setDecodingCodecsVisible(page, this.value);
});
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
includeFiles: true,
@ -252,7 +254,7 @@ import alert from '../../components/alert';
});
});
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
callback: function (path) {
@ -269,7 +271,7 @@ import alert from '../../components/alert';
});
});
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
includeDirectories: true,

View File

@ -80,6 +80,13 @@
</div>
</div>
<div class="verticalSection">
<h2>${HeaderPerformance}</h2>
<div class="inputContainer">
<input is="emby-input" id="txtParallelImageEncodingLimit" label="${LabelParallelImageEncodingLimit}" type="number" pattern="[0-9]*" min="0" step="1" />
<div class="fieldDescription">${LabelParallelImageEncodingLimitHelp}</div>
</div>
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block">

View File

@ -21,6 +21,7 @@ import alert from '../../components/alert';
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
return '<option value="' + language.Value + '">' + language.Name + '</option>';
})).val(config.UICulture);
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
loading.hide();
}
@ -36,6 +37,7 @@ import alert from '../../components/alert';
config.MetadataPath = $('#txtMetadataPath', form).val();
config.MetadataNetworkPath = $('#txtMetadataNetworkPath', form).val();
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
ApiClient.updateServerConfiguration(config).then(function() {
ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) {
@ -58,7 +60,7 @@ import alert from '../../components/alert';
const brandingConfigKey = 'branding';
export default function (view) {
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
callback: function (path) {
@ -75,7 +77,7 @@ import alert from '../../components/alert';
});
});
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
path: $('#txtMetadataPath', view).val(),

View File

@ -15,7 +15,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
/* eslint-disable indent */
function addVirtualFolder(page) {
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({default: medialibrarycreator}) => {
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: medialibrarycreator }) => {
new medialibrarycreator({
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
return !f.hidden;
@ -30,7 +30,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
}
function editVirtualFolder(page, virtualFolder) {
import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({default: medialibraryeditor}) => {
import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: medialibraryeditor }) => {
new medialibraryeditor({
refresh: shouldRefreshLibraryAfterChanges(page),
library: virtualFolder
@ -64,7 +64,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
}
function refreshVirtualFolder(page, virtualFolder) {
import('../../components/refreshdialog/refreshdialog').then(({default: refreshDialog}) => {
import('../../components/refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
new refreshDialog({
itemIds: [virtualFolder.ItemId],
serverId: ApiClient.serverId(),
@ -74,7 +74,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
}
function renameVirtualFolder(page, virtualFolder) {
import('../../components/prompt/prompt').then(({default: prompt}) => {
import('../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
label: globalize.translate('LabelNewName'),
description: globalize.translate('MessageRenameMediaFolder'),

View File

@ -181,7 +181,7 @@ import alert from '../../components/alert';
}
});
view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
includeFiles: true,

View File

@ -0,0 +1,198 @@
<div id="editUserPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" id="userProfileNavigation" data-mini="true">
<a href="#" is="emby-linkbutton" data-role="button" class="ui-btn-active">${Profile}</a>
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
</div>
<p class="lnkEditUserPreferencesContainer">
<a class="lnkEditUserPreferences button-link" href="#" is="emby-linkbutton">${ButtonEditOtherUserPreferences}</a>
</p>
<form class="editUserProfileForm">
<div class="disabledUserBanner" style="display: none;">
<div class="btn btnDarkAccent btnStatic">
<div>
${HeaderThisUserIsCurrentlyDisabled}
</div>
<div style="margin-top: 5px;">
${MessageReenableUser}
</div>
</div>
</div>
<div id="fldUserName" class="inputContainer">
<input is="emby-input" id="txtUserName" required type="text" label="${LabelName}" />
</div>
<div class="selectContainer fldSelectLoginProvider hide">
<select class="selectLoginProvider" is="emby-select" label="${LabelAuthProvider}"></select>
<div class="fieldDescription">${AuthProviderHelp}</div>
</div>
<div class="selectContainer fldSelectPasswordResetProvider hide">
<select class="selectPasswordResetProvider" is="emby-select" label="${LabelPasswordResetProvider}"></select>
<div class="fieldDescription">${PasswordResetProviderHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide">
<label>
<input type="checkbox" is="emby-checkbox" id="chkRemoteAccess" />
<span>${AllowRemoteAccess}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${AllowRemoteAccessHelp}</div>
</div>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkIsAdmin" />
<span>${OptionAllowUserToManageServer}</span>
</label>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableCollectionManagement" />
<span>${AllowCollectionManagement}</span>
</label>
<div id="featureAccessFields" class="verticalSection">
<h2 class="paperListLabel">${HeaderFeatureAccess}</h2>
<div class="checkboxList paperList" style="padding:.5em 1em;">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableLiveTvAccess" />
<span>${OptionAllowBrowsingLiveTv}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkManageLiveTv" />
<span>${OptionAllowManageLiveTv}</span>
</label>
</div>
</div>
<div class="verticalSection">
<h2 class="paperListLabel">${HeaderPlayback}</h2>
<div class="checkboxList paperList" style="padding:.5em 1em;">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableMediaPlayback" />
<span>${OptionAllowMediaPlayback}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableAudioPlaybackTranscoding" />
<span>${OptionAllowAudioPlaybackTranscoding}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackTranscoding" />
<span>${OptionAllowVideoPlaybackTranscoding}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackRemuxing" />
<span>${OptionAllowVideoPlaybackRemuxing}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkForceRemoteSourceTranscoding" />
<span>${OptionForceRemoteSourceTranscoding}</span>
</label>
</div>
<div class="fieldDescription">${OptionAllowMediaPlaybackTranscodingHelp}</div>
</div>
<br />
<div class="verticalSection">
<div class="inputContainer">
<input is="emby-input" type="number" id="txtRemoteClientBitrateLimit" inputmode="decimal" pattern="[0-9]*(\.[0-9]+)?" min="0" step=".25" label="${LabelRemoteClientBitrateLimit}" />
<div class="fieldDescription">${LabelRemoteClientBitrateLimitHelp}</div>
<div class="fieldDescription">${LabelUserRemoteClientBitrateLimitHelp}</div>
</div>
</div>
<div class="verticalSection">
<div class="selectContainer fldSelectSyncPlayAccess">
<select class="selectSyncPlayAccess" is="emby-select" id="selectSyncPlayAccess" label="${LabelSyncPlayAccess}">
<option value="CreateAndJoinGroups">${LabelSyncPlayAccessCreateAndJoinGroups}</option>
<option value="JoinGroups">${LabelSyncPlayAccessJoinGroups}</option>
<option value="None">${LabelSyncPlayAccessNone}</option>
</select>
<div class="fieldDescription">${SyncPlayAccessHelp}</div>
</div>
</div>
<div class="verticalSection">
<h2 class="checkboxListLabel" style="margin-bottom:1em;">${HeaderAllowMediaDeletionFrom}</h2>
<div class="checkboxList paperList checkboxList-paperList">
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableDeleteAllFolders" />
<span>${AllLibraries}</span>
</label>
<div class="deleteAccess">
</div>
</div>
</div>
<div class="verticalSection">
<h2 class="checkboxListLabel">${HeaderRemoteControl}</h2>
<div class="checkboxList paperList" style="padding:.5em 1em;">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableRemoteControlOtherUsers" />
<span>${OptionAllowRemoteControlOthers}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkRemoteControlSharedDevices" />
<span>${OptionAllowRemoteSharedDevices}</span>
</label>
</div>
<div class="fieldDescription">${OptionAllowRemoteSharedDevicesHelp}</div>
</div>
<h2 class="checkboxListLabel">${Other}</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableDownloading" />
<span>${OptionAllowContentDownload}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${OptionAllowContentDownloadHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsEnabled">
<label>
<input type="checkbox" is="emby-checkbox" id="chkDisabled" />
<span>${OptionDisableUser}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${OptionDisableUserHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsHidden">
<label>
<input type="checkbox" is="emby-checkbox" id="chkIsHidden" />
<span>${OptionHideUser}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${OptionHideUserFromLoginHelp}</div>
</div>
<br/>
<div class=verticalSection>
<div class="inputContainer" id="fldLoginAttemptsBeforeLockout">
<input is="emby-input" type="number" id="txtLoginAttemptsBeforeLockout" min="-1" step="1" label="${LabelUserLoginAttemptsBeforeLockout}"/>
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockout}</div>
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockoutHelp}</div>
</div>
</div>
<br />
<div class=verticalSection>
<div class="inputContainer" id="fldMaxActiveSessions">
<input is="emby-input" type="number" id="txtMaxActiveSessions" min="0" step="1" label="${LabelUserMaxActiveSessions}"/>
<div class="fieldDescription">${OptionMaxActiveSessions}</div>
<div class="fieldDescription">${OptionMaxActiveSessionsHelp}</div>
</div>
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
<span>${ButtonCancel}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,196 @@
import 'jquery';
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
import { getParameterByName } from '../../../utils/url.ts';
function loadDeleteFolders(page, user, mediaFolders) {
ApiClient.getJSON(ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
let isChecked;
let checkedAttribute;
let html = '';
for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
for (const folder of channelsResult.Items) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
$('.deleteAccess', page).html(html).trigger('create');
$('#chkEnableDeleteAllFolders', page).prop('checked', user.Policy.EnableContentDeletion);
});
}
function loadAuthProviders(page, user, providers) {
if (providers.length > 1) {
page.querySelector('.fldSelectLoginProvider').classList.remove('hide');
} else {
page.querySelector('.fldSelectLoginProvider').classList.add('hide');
}
const currentProviderId = user.Policy.AuthenticationProviderId;
page.querySelector('.selectLoginProvider').innerHTML = providers.map(function (provider) {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
});
}
function loadPasswordResetProviders(page, user, providers) {
if (providers.length > 1) {
page.querySelector('.fldSelectPasswordResetProvider').classList.remove('hide');
} else {
page.querySelector('.fldSelectPasswordResetProvider').classList.add('hide');
}
const currentProviderId = user.Policy.PasswordResetProviderId;
page.querySelector('.selectPasswordResetProvider').innerHTML = providers.map(function (provider) {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
});
}
function loadUser(page, user) {
ApiClient.getJSON(ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(page, user, providers);
});
ApiClient.getJSON(ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
loadPasswordResetProviders(page, user, providers);
});
ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
})).then(function (folders) {
loadDeleteFolders(page, user, folders.Items);
});
if (user.Policy.IsDisabled) {
$('.disabledUserBanner', page).show();
} else {
$('.disabledUserBanner', page).hide();
}
$('#txtUserName', page).prop('disabled', '').removeAttr('disabled');
$('#fldConnectInfo', page).show();
$('.lnkEditUserPreferences', page).attr('href', 'mypreferencesmenu.html?userId=' + user.Id);
libraryMenu.setTitle(user.Name);
page.querySelector('.username').innerHTML = user.Name;
$('#txtUserName', page).val(user.Name);
$('#chkIsAdmin', page).prop('checked', user.Policy.IsAdministrator);
$('#chkDisabled', page).prop('checked', user.Policy.IsDisabled);
$('#chkIsHidden', page).prop('checked', user.Policy.IsHidden);
$('#chkEnableCollectionManagement', page).prop('checked', user.Policy.chkEnableCollectionManagement);
$('#chkRemoteControlSharedDevices', page).prop('checked', user.Policy.EnableSharedDeviceControl);
$('#chkEnableRemoteControlOtherUsers', page).prop('checked', user.Policy.EnableRemoteControlOfOtherUsers);
$('#chkEnableDownloading', page).prop('checked', user.Policy.EnableContentDownloading);
$('#chkManageLiveTv', page).prop('checked', user.Policy.EnableLiveTvManagement);
$('#chkEnableLiveTvAccess', page).prop('checked', user.Policy.EnableLiveTvAccess);
$('#chkEnableMediaPlayback', page).prop('checked', user.Policy.EnableMediaPlayback);
$('#chkEnableAudioPlaybackTranscoding', page).prop('checked', user.Policy.EnableAudioPlaybackTranscoding);
$('#chkEnableVideoPlaybackTranscoding', page).prop('checked', user.Policy.EnableVideoPlaybackTranscoding);
$('#chkEnableVideoPlaybackRemuxing', page).prop('checked', user.Policy.EnablePlaybackRemuxing);
$('#chkForceRemoteSourceTranscoding', page).prop('checked', user.Policy.ForceRemoteSourceTranscoding);
$('#chkRemoteAccess', page).prop('checked', user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess);
$('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || '');
$('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0');
$('#txtMaxActiveSessions', page).val(user.Policy.MaxActiveSessions || '0');
if (ApiClient.isMinServerVersion('10.6.0')) {
$('#selectSyncPlayAccess').val(user.Policy.SyncPlayAccess);
}
loading.hide();
}
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
function saveUser(user, page) {
user.Name = $('#txtUserName', page).val();
user.Policy.IsAdministrator = $('#chkIsAdmin', page).is(':checked');
user.Policy.IsHidden = $('#chkIsHidden', page).is(':checked');
user.Policy.IsDisabled = $('#chkDisabled', page).is(':checked');
user.Policy.EnableRemoteControlOfOtherUsers = $('#chkEnableRemoteControlOtherUsers', page).is(':checked');
user.Policy.EnableLiveTvManagement = $('#chkManageLiveTv', page).is(':checked');
user.Policy.EnableLiveTvAccess = $('#chkEnableLiveTvAccess', page).is(':checked');
user.Policy.EnableSharedDeviceControl = $('#chkRemoteControlSharedDevices', page).is(':checked');
user.Policy.EnableMediaPlayback = $('#chkEnableMediaPlayback', page).is(':checked');
user.Policy.EnableAudioPlaybackTranscoding = $('#chkEnableAudioPlaybackTranscoding', page).is(':checked');
user.Policy.EnableVideoPlaybackTranscoding = $('#chkEnableVideoPlaybackTranscoding', page).is(':checked');
user.Policy.EnablePlaybackRemuxing = $('#chkEnableVideoPlaybackRemuxing', page).is(':checked');
user.Policy.EnableCollectionManagement = $('#chkEnableCollectionManagement', page).is(':checked');
user.Policy.ForceRemoteSourceTranscoding = $('#chkForceRemoteSourceTranscoding', page).is(':checked');
user.Policy.EnableContentDownloading = $('#chkEnableDownloading', page).is(':checked');
user.Policy.EnableRemoteAccess = $('#chkRemoteAccess', page).is(':checked');
user.Policy.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat($('#txtRemoteClientBitrateLimit', page).val() || '0'), 10);
user.Policy.LoginAttemptsBeforeLockout = parseInt($('#txtLoginAttemptsBeforeLockout', page).val() || '0', 10);
user.Policy.MaxActiveSessions = parseInt($('#txtMaxActiveSessions', page).val() || '0', 10);
user.Policy.AuthenticationProviderId = page.querySelector('.selectLoginProvider').value;
user.Policy.PasswordResetProviderId = page.querySelector('.selectPasswordResetProvider').value;
user.Policy.EnableContentDeletion = $('#chkEnableDeleteAllFolders', page).is(':checked');
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : $('.chkFolder', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
if (ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = page.querySelector('#selectSyncPlayAccess').value;
}
ApiClient.updateUser(user).then(function () {
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
});
}
function onSubmit() {
const page = $(this).parents('.page')[0];
loading.show();
getUser().then(function (result) {
saveUser(result, page);
});
return false;
}
function getUser() {
const userId = getParameterByName('userId');
return ApiClient.getUser(userId);
}
function loadData(page) {
loading.show();
getUser().then(function (user) {
loadUser(page, user);
});
}
$(document).on('pageinit', '#editUserPage', function () {
$('.editUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
const page = this;
$('#chkEnableDeleteAllFolders', this).on('change', function () {
if (this.checked) {
$('.deleteAccess', page).hide();
} else {
$('.deleteAccess', page).show();
}
});
ApiClient.getServerConfiguration().then(function (config) {
if (config.EnableRemoteAccess) {
page.querySelector('.fldRemoteAccess').classList.remove('hide');
} else {
page.querySelector('.fldRemoteAccess').classList.add('hide');
}
});
}).on('pagebeforeshow', '#editUserPage', function () {
loadData(this);
});

View File

@ -0,0 +1,68 @@
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);" class="ui-btn-active">${TabAccess}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
</div>
<form class="userLibraryAccessForm">
<div class="folderAccessContainer">
<h2>${HeaderLibraryAccess}</h2>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
<span>${OptionEnableAccessToAllLibraries}</span>
</label>
<div class="folderAccessListContainer">
<div class="folderAccess">
</div>
<div class="fieldDescription">${LibraryAccessHelp}</div>
</div>
</div>
<div class="channelAccessContainer" style="display:none;">
<h2>${HeaderChannelAccess}</h2>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
<span>${OptionEnableAccessToAllChannels}</span>
</label>
<div class="channelAccessListContainer">
<div class="channelAccess">
</div>
<div class="fieldDescription">${ChannelAccessHelp}</div>
</div>
</div>
<br />
<div class="deviceAccessContainer hide">
<h2>${HeaderDeviceAccess}</h2>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableAllDevices" />
<span>${OptionEnableAccessFromAllDevices}</span>
</label>
<div class="deviceAccessListContainer">
<div class="deviceAccess">
</div>
<div class="fieldDescription">${DeviceAccessHelp}</div>
</div>
<br />
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,184 @@
import 'jquery';
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
import { getParameterByName } from '../../../utils/url.ts';
function triggerChange(select) {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
}
function loadMediaFolders(page, user, mediaFolders) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = mediaFolders.length; i < length; i++) {
const folder = mediaFolders[i];
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
html += '</div>';
page.querySelector('.folderAccess').innerHTML = html;
const chkEnableAllFolders = page.querySelector('#chkEnableAllFolders');
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders);
}
function loadChannels(page, user, channels) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = channels.length; i < length; i++) {
const folder = channels[i];
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.channelAccess', page).show().html(html);
if (channels.length) {
$('.channelAccessContainer', page).show();
} else {
$('.channelAccessContainer', page).hide();
}
const chkEnableAllChannels = page.querySelector('#chkEnableAllChannels');
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
triggerChange(chkEnableAllChannels);
}
function loadDevices(page, user, devices) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderDevices') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = devices.length; i < length; i++) {
const device = devices[i];
const checkedAttribute = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1 ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkDevice" data-id="' + device.Id + '" ' + checkedAttribute + '><span>' + device.Name + ' - ' + device.AppName + '</span></label>';
}
html += '</div>';
$('.deviceAccess', page).show().html(html);
const chkEnableAllDevices = page.querySelector('#chkEnableAllDevices');
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices);
if (user.Policy.IsAdministrator) {
page.querySelector('.deviceAccessContainer').classList.add('hide');
} else {
page.querySelector('.deviceAccessContainer').classList.remove('hide');
}
}
function loadUser(page, user, loggedInUser, mediaFolders, channels, devices) {
page.querySelector('.username').innerHTML = user.Name;
libraryMenu.setTitle(user.Name);
loadChannels(page, user, channels);
loadMediaFolders(page, user, mediaFolders);
loadDevices(page, user, devices);
loading.hide();
}
function onSaveComplete() {
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
function saveUser(user, page) {
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : $('.chkFolder', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : $('.chkChannel', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllDevices = $('#chkEnableAllDevices', page).is(':checked');
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : $('.chkDevice', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.BlockedChannels = null;
user.Policy.BlockedMediaFolders = null;
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
}
function onSubmit() {
const page = $(this).parents('.page');
loading.show();
const userId = getParameterByName('userId');
ApiClient.getUser(userId).then(function (result) {
saveUser(result, page);
});
return false;
}
$(document).on('pageinit', '#userLibraryAccessPage', function () {
const page = this;
$('#chkEnableAllDevices', page).on('change', function () {
if (this.checked) {
$('.deviceAccessListContainer', page).hide();
} else {
$('.deviceAccessListContainer', page).show();
}
});
$('#chkEnableAllChannels', page).on('change', function () {
if (this.checked) {
$('.channelAccessListContainer', page).hide();
} else {
$('.channelAccessListContainer', page).show();
}
});
page.querySelector('#chkEnableAllFolders').addEventListener('change', function () {
if (this.checked) {
page.querySelector('.folderAccessListContainer').classList.add('hide');
} else {
page.querySelector('.folderAccessListContainer').classList.remove('hide');
}
});
$('.userLibraryAccessForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#userLibraryAccessPage', function () {
const page = this;
loading.show();
let promise1;
const userId = getParameterByName('userId');
if (userId) {
promise1 = ApiClient.getUser(userId);
} else {
const deferred = $.Deferred();
deferred.resolveWith(null, [{
Configuration: {}
}]);
promise1 = deferred.promise();
}
const promise2 = Dashboard.getCurrentUser();
const promise4 = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promise5 = ApiClient.getJSON(ApiClient.getUrl('Channels'));
const promise6 = ApiClient.getJSON(ApiClient.getUrl('Devices'));
Promise.all([promise1, promise2, promise4, promise5, promise6]).then(function (responses) {
loadUser(page, responses[0], responses[1], responses[2].Items, responses[3].Items, responses[4].Items);
});
});

View File

@ -0,0 +1,62 @@
<div id="newUserPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<form class="newUserProfileForm">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${ButtonAddUser}</h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
<div class="inputContainer">
<input is="emby-input" id="txtUsername" required type="text" label="${LabelName}" />
</div>
<div class="inputContainer">
<input is="emby-input" id="txtPassword" type="password" label="${LabelPassword}" />
</div>
</div>
<div class="folderAccessContainer verticalSection">
<h2 class="sectionTitle">${HeaderLibraryAccess}</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
<span>${OptionEnableAccessToAllLibraries}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LibraryAccessHelp}</div>
</div>
<div class="folderAccessListContainer">
<div class="folderAccess">
</div>
</div>
</div>
<div class="channelAccessContainer verticalSection verticalSection-extrabottompadding" style="display:none;">
<h2 class="sectionTitle">${HeaderChannelAccess}</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
<span>${OptionEnableAccessToAllChannels}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${ChannelAccessHelp}</div>
</div>
<div class="channelAccessListContainer">
<div class="channelAccess">
</div>
</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
<span>${ButtonCancel}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,128 @@
import 'jquery';
import loading from '../../../components/loading/loading';
import globalize from '../../../scripts/globalize';
import '../../../elements/emby-checkbox/emby-checkbox';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
function loadMediaFolders(page, mediaFolders) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
for (let i = 0; i < mediaFolders.length; i++) {
const folder = mediaFolders[i];
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.folderAccess', page).html(html).trigger('create');
$('#chkEnableAllFolders', page).prop('checked', false);
}
function loadChannels(page, channels) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
for (let i = 0; i < channels.length; i++) {
const folder = channels[i];
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.channelAccess', page).show().html(html).trigger('create');
if (channels.length) {
$('.channelAccessContainer', page).show();
} else {
$('.channelAccessContainer', page).hide();
}
$('#chkEnableAllChannels', page).prop('checked', false);
}
function loadUser(page) {
$('#txtUsername', page).val('');
$('#txtPassword', page).val('');
loading.show();
const promiseFolders = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promiseChannels = ApiClient.getJSON(ApiClient.getUrl('Channels'));
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
loadMediaFolders(page, responses[0].Items);
loadChannels(page, responses[1].Items);
loading.hide();
});
}
function saveUser(page) {
const _user = {
Name: $('#txtUsername', page).val(),
Password: $('#txtPassword', page).val()
};
ApiClient.createUser(_user).then(function (user) {
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = $('.chkFolder', page).get().filter(function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = $('.chkChannel', page).get().filter(function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
Dashboard.navigate('useredit.html?userId=' + user.Id);
});
}, function () {
toast(globalize.translate('ErrorDefault'));
loading.hide();
});
}
function onSubmit() {
const page = $(this).parents('.page')[0];
loading.show();
saveUser(page);
return false;
}
function loadData(page) {
loadUser(page);
}
$(document).on('pageinit', '#newUserPage', function () {
const page = this;
$('#chkEnableAllChannels', page).on('change', function () {
if (this.checked) {
$('.channelAccessListContainer', page).hide();
} else {
$('.channelAccessListContainer', page).show();
}
});
$('#chkEnableAllFolders', page).on('change', function () {
if (this.checked) {
$('.folderAccessListContainer', page).hide();
} else {
$('.folderAccessListContainer', page).show();
}
});
$('.newUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#newUserPage', function () {
loadData(this);
});

View File

@ -0,0 +1,60 @@
<div id="userParentalControlPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);" class="ui-btn-active">${TabParentalControl}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
</div>
<form class="userParentalControlForm">
<div class="selectContainer">
<select is="emby-select" id="selectMaxParentalRating" label="${LabelMaxParentalRating}"></select>
<div class="fieldDescription">${MaxParentalRatingHelp}</div>
</div>
<div>
<div class="blockUnratedItems"></div>
</div>
<br />
<div class="verticalSection" style="margin-bottom:2em;">
<div class="detailSectionHeader sectionTitleContainer">
<h2 class="sectionTitle">${LabelBlockContentWithTags}</h2>
<button is="emby-button" type="button" class="fab btnAddBlockedTag submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
</button>
</div>
<div class="blockedTags" style="margin-top:.5em;"></div>
</div>
<div class="accessScheduleSection verticalSection" style="margin-bottom:2em;">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">${HeaderAccessSchedule}</h2>
<button is="emby-button" type="button" class="fab btnAddSchedule submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
</button>
</div>
<p>${HeaderAccessScheduleHelp}</p>
<div class="accessScheduleList paperList"></div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,278 @@
import 'jquery';
import datetime from '../../../scripts/datetime';
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import '../../../components/listview/listview.scss';
import '../../../elements/emby-button/paper-icon-button-light';
import toast from '../../../components/toast/toast';
import { getParameterByName } from '../../../utils/url.ts';
function populateRatings(allParentalRatings, page) {
let html = '';
html += "<option value=''></option>";
let rating;
const ratings = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
for (let i = 0, length = ratings.length; i < length; i++) {
rating = ratings[i];
html += "<option value='" + rating.Value + "'>" + rating.Name + '</option>';
}
$('#selectMaxParentalRating', page).html(html);
}
function loadUnratedItems(page, user) {
const items = [{
name: globalize.translate('Books'),
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: 'Series'
}];
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderBlockItemsWithNoRating') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
const checkedAttribute = user.Policy.BlockUnratedItems.indexOf(item.value) != -1 ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkUnratedItem" data-itemtype="' + item.value + '" type="checkbox"' + checkedAttribute + '><span>' + item.name + '</span></label>';
}
html += '</div>';
$('.blockUnratedItems', page).html(html).trigger('create');
}
function loadUser(page, user, allParentalRatings) {
page.querySelector('.username').innerHTML = user.Name;
libraryMenu.setTitle(user.Name);
loadUnratedItems(page, user);
loadBlockedTags(page, user.Policy.BlockedTags);
populateRatings(allParentalRatings, page);
let ratingValue = '';
if (user.Policy.MaxParentalRating) {
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
const rating = allParentalRatings[i];
if (user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = rating.Value;
}
}
}
$('#selectMaxParentalRating', page).val(ratingValue);
if (user.Policy.IsAdministrator) {
$('.accessScheduleSection', page).hide();
} else {
$('.accessScheduleSection', page).show();
}
renderAccessSchedule(page, user.Policy.AccessSchedules || []);
loading.hide();
}
function loadBlockedTags(page, tags) {
let html = tags.map(function (h) {
let li = '<div class="listItem">';
li += '<div class="listItemBody">';
li += '<h3 class="listItemBodyText">';
li += h;
li += '</h3>';
li += '</div>';
li += '<button type="button" is="paper-icon-button-light" class="blockedTag btnDeleteTag listItemButton" data-tag="' + h + '"><span class="material-icons delete"></span></button>';
li += '</div>';
return li;
}).join('');
if (html) {
html = '<div class="paperList">' + html + '</div>';
}
const blockedTags = page.querySelector('.blockedTags');
blockedTags.innerHTML = html;
for (const btnDeleteTag of blockedTags.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = this.getAttribute('data-tag');
const newTags = tags.filter(function (t) {
return t != tag;
});
loadBlockedTags(page, newTags);
});
}
}
function deleteAccessSchedule(page, schedules, index) {
schedules.splice(index, 1);
renderAccessSchedule(page, schedules);
}
function renderAccessSchedule(page, schedules) {
let html = '';
let index = 0;
html += schedules.map(function (a) {
let itemHtml = '';
itemHtml += '<div class="liSchedule listItem" data-day="' + a.DayOfWeek + '" data-start="' + a.StartHour + '" data-end="' + a.EndHour + '">';
itemHtml += '<div class="listItemBody two-line">';
itemHtml += '<h3 class="listItemBodyText">';
itemHtml += globalize.translate('Option' + a.DayOfWeek);
itemHtml += '</h3>';
itemHtml += '<div class="listItemBodyText secondary">' + getDisplayTime(a.StartHour) + ' - ' + getDisplayTime(a.EndHour) + '</div>';
itemHtml += '</div>';
itemHtml += '<button type="button" is="paper-icon-button-light" class="btnDelete listItemButton" data-index="' + index + '"><span class="material-icons delete"></span></button>';
itemHtml += '</div>';
index++;
return itemHtml;
}).join('');
const accessScheduleList = page.querySelector('.accessScheduleList');
accessScheduleList.innerHTML = html;
$('.btnDelete', accessScheduleList).on('click', function () {
deleteAccessSchedule(page, schedules, parseInt(this.getAttribute('data-index'), 10));
});
}
function onSaveComplete() {
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
function saveUser(user, page) {
user.Policy.MaxParentalRating = $('#selectMaxParentalRating', page).val() || null;
user.Policy.BlockUnratedItems = $('.chkUnratedItem', page).get().filter(function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-itemtype');
});
user.Policy.AccessSchedules = getSchedulesFromPage(page);
user.Policy.BlockedTags = getBlockedTagsFromPage(page);
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
}
function getDisplayTime(hours) {
let minutes = 0;
const pct = hours % 1;
if (pct) {
minutes = parseInt(60 * pct, 10);
}
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
function showSchedulePopup(page, schedule, index) {
schedule = schedule || {};
import('../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
accessschedule.show({
schedule: schedule
}).then(function (updatedSchedule) {
const schedules = getSchedulesFromPage(page);
if (index == -1) {
index = schedules.length;
}
schedules[index] = updatedSchedule;
renderAccessSchedule(page, schedules);
});
});
}
function getSchedulesFromPage(page) {
return $('.liSchedule', page).map(function () {
return {
DayOfWeek: this.getAttribute('data-day'),
StartHour: this.getAttribute('data-start'),
EndHour: this.getAttribute('data-end')
};
}).get();
}
function getBlockedTagsFromPage(page) {
return $('.blockedTag', page).map(function () {
return this.getAttribute('data-tag');
}).get();
}
function showBlockedTagPopup(page) {
import('../../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getBlockedTagsFromPage(page);
if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(page, tags);
}
});
});
}
window.UserParentalControlPage = {
onSubmit: function () {
const page = $(this).parents('.page');
loading.show();
const userId = getParameterByName('userId');
ApiClient.getUser(userId).then(function (result) {
saveUser(result, page);
});
return false;
}
};
$(document).on('pageinit', '#userParentalControlPage', function () {
const page = this;
$('.btnAddSchedule', page).on('click', function () {
showSchedulePopup(page, {}, -1);
});
$('.btnAddBlockedTag', page).on('click', function () {
showBlockedTagPopup(page);
});
$('.userParentalControlForm').off('submit', UserParentalControlPage.onSubmit).on('submit', UserParentalControlPage.onSubmit);
}).on('pageshow', '#userParentalControlPage', function () {
const page = this;
loading.show();
const userId = getParameterByName('userId');
const promise1 = ApiClient.getUser(userId);
const promise2 = ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
loadUser(page, responses[0], responses[1]);
});
});

View File

@ -0,0 +1,72 @@
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);" class="ui-btn-active">${HeaderPassword}</a>
</div>
<div class="readOnlyContent">
<form class="updatePasswordForm passwordSection hide" style="margin: 0 auto 2em;">
<div class="detailSection">
<div id="fldCurrentPassword" class="inputContainer hide">
<input is="emby-input" type="password" id="txtCurrentPassword" label="${LabelCurrentPassword}" autocomplete="off" />
</div>
<div class="inputContainer">
<input is="emby-input" type="password" id="txtNewPassword" label="${LabelNewPassword}" autocomplete="off" />
</div>
<div class="inputContainer">
<input is="emby-input" type="password" id="txtNewPasswordConfirm" label="${LabelNewPasswordConfirm}" autocomplete="off" />
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button>
<button is="emby-button" type="button" id="btnResetPassword" class="raised button-cancel block hide">
<span>${ResetPassword}</span>
</button>
</div>
</div>
</form>
<br />
<form class="localAccessForm localAccessSection" style="margin: 0 auto;">
<div class="detailSection">
<div class="detailSectionHeader">
${HeaderEasyPinCode}
</div>
<br />
<div>${EasyPasswordHelp}</div>
<br />
<div class="inputContainer">
<input is="emby-input" type="number" id="txtEasyPassword" label="${LabelEasyPinCode}" autocomplete="off" pattern="[0-9]*" step="1" maxlength="5" />
</div>
<br />
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" class="chkEnableLocalEasyPassword" />
<span>${LabelInNetworkSignInWithEasyPassword}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelInNetworkSignInWithEasyPasswordHelp}</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" id="btnResetEasyPassword" class="raised button-cancel block hide">
<span>${ButtonResetEasyPassword}</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,179 @@
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import '../../../elements/emby-button/emby-button';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
import confirm from '../../../components/confirm/confirm';
function loadUser(page, params) {
const userid = params.userId;
ApiClient.getUser(userid).then(function (user) {
Dashboard.getCurrentUser().then(function (loggedInUser) {
libraryMenu.setTitle(user.Name);
page.querySelector('.username').innerText = user.Name;
let showPasswordSection = true;
let showLocalAccessSection = false;
if (user.ConnectLinkType == 'Guest') {
page.querySelector('.localAccessSection').classList.add('hide');
showPasswordSection = false;
} else if (user.HasConfiguredPassword) {
page.querySelector('#btnResetPassword').classList.remove('hide');
page.querySelector('#fldCurrentPassword').classList.remove('hide');
showLocalAccessSection = true;
} else {
page.querySelector('#btnResetPassword').classList.add('hide');
page.querySelector('#fldCurrentPassword').classList.add('hide');
}
if (showPasswordSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
page.querySelector('.passwordSection').classList.remove('hide');
} else {
page.querySelector('.passwordSection').classList.add('hide');
}
if (showLocalAccessSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
page.querySelector('.localAccessSection').classList.remove('hide');
} else {
page.querySelector('.localAccessSection').classList.add('hide');
}
const txtEasyPassword = page.querySelector('#txtEasyPassword');
txtEasyPassword.value = '';
if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******';
page.querySelector('#btnResetEasyPassword').classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
page.querySelector('#btnResetEasyPassword').classList.add('hide');
}
page.querySelector('.chkEnableLocalEasyPassword').checked = user.Configuration.EnableLocalPassword;
import('../../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
});
page.querySelector('#txtCurrentPassword').value = '';
page.querySelector('#txtNewPassword').value = '';
page.querySelector('#txtNewPasswordConfirm').value = '';
}
export default function (view, params) {
function saveEasyPassword() {
const userId = params.userId;
const easyPassword = view.querySelector('#txtEasyPassword').value;
if (easyPassword) {
ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
onEasyPasswordSaved(userId);
});
} else {
onEasyPasswordSaved(userId);
}
}
function onEasyPasswordSaved(userId) {
ApiClient.getUser(userId).then(function (user) {
user.Configuration.EnableLocalPassword = view.querySelector('.chkEnableLocalEasyPassword').checked;
ApiClient.updateUserConfiguration(user.Id, user.Configuration).then(function () {
loading.hide();
toast(globalize.translate('SettingsSaved'));
loadUser(view, params);
});
});
}
function savePassword() {
const userId = params.userId;
let currentPassword = view.querySelector('#txtCurrentPassword').value;
const newPassword = view.querySelector('#txtNewPassword').value;
if (view.querySelector('#fldCurrentPassword').classList.contains('hide')) {
// Firefox does not respect autocomplete=off, so clear it if the field is supposed to be hidden (and blank)
// This should only happen when user.HasConfiguredPassword is false, but this information is not passed on
currentPassword = '';
}
ApiClient.updateUserPassword(userId, currentPassword, newPassword).then(function () {
loading.hide();
toast(globalize.translate('PasswordSaved'));
loadUser(view, params);
}, function () {
loading.hide();
Dashboard.alert({
title: globalize.translate('HeaderLoginFailure'),
message: globalize.translate('MessageInvalidUser')
});
});
}
function onSubmit(e) {
const form = this;
if (form.querySelector('#txtNewPassword').value != form.querySelector('#txtNewPasswordConfirm').value) {
toast(globalize.translate('PasswordMatchError'));
} else {
loading.show();
savePassword();
}
e.preventDefault();
return false;
}
function onLocalAccessSubmit(e) {
loading.show();
saveEasyPassword();
e.preventDefault();
return false;
}
function resetPassword() {
const msg = globalize.translate('PasswordResetConfirmation');
confirm(msg, globalize.translate('ResetPassword')).then(function () {
const userId = params.userId;
loading.show();
ApiClient.resetUserPassword(userId).then(function () {
loading.hide();
Dashboard.alert({
message: globalize.translate('PasswordResetComplete'),
title: globalize.translate('ResetPassword')
});
loadUser(view, params);
});
});
}
function resetEasyPassword() {
const msg = globalize.translate('PinCodeResetConfirmation');
confirm(msg, globalize.translate('HeaderPinCodeReset')).then(function () {
const userId = params.userId;
loading.show();
ApiClient.resetEasyPassword(userId).then(function () {
loading.hide();
Dashboard.alert({
message: globalize.translate('PinCodeResetComplete'),
title: globalize.translate('HeaderPinCodeReset')
});
loadUser(view, params);
});
});
}
view.querySelector('.updatePasswordForm').addEventListener('submit', onSubmit);
view.querySelector('.localAccessForm').addEventListener('submit', onLocalAccessSubmit);
view.querySelector('#btnResetEasyPassword').addEventListener('click', resetEasyPassword);
view.querySelector('#btnResetPassword').addEventListener('click', resetPassword);
view.addEventListener('viewshow', function () {
loadUser(view, params);
});
}

View File

@ -0,0 +1,16 @@
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
<div>
<div class="content-primary">
<div class="verticalSection verticalSection-extrabottompadding">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards">${HeaderUsers}</h2>
<button is="emby-button" type="button" class="fab btnAddUser submit sectionTitleButton" style="margin-left:1em;" title="${ButtonAddUser}">
<span class="material-icons add"></span>
</button>
<a is="emby-linkbutton" rel="noopener noreferrer" style="margin-left:2em!important;" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/adding-managing-users">${Help}</a>
</div>
<div class="localUsers itemsContainer vertical-wrap"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,184 @@
import loading from '../../../components/loading/loading';
import dom from '../../../scripts/dom';
import globalize from '../../../scripts/globalize';
import { formatDistanceToNow } from 'date-fns';
import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale.ts';
import '../../../elements/emby-button/paper-icon-button-light';
import '../../../components/cardbuilder/card.scss';
import '../../../elements/emby-button/emby-button';
import '../../../components/indicators/indicators.scss';
import '../../../styles/flexstyles.scss';
import Dashboard, { pageIdOn } from '../../../utils/dashboard';
import confirm from '../../../components/confirm/confirm';
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
function deleteUser(page, id) {
const msg = globalize.translate('DeleteUserConfirmation');
confirm({
title: globalize.translate('DeleteUser'),
text: msg,
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
loading.show();
ApiClient.deleteUser(id).then(function () {
loadData(page);
});
});
}
function showUserMenu(elem) {
const card = dom.parentWithClass(elem, 'card');
const page = dom.parentWithClass(card, 'page');
const userId = card.getAttribute('data-userid');
const menuItems = [];
menuItems.push({
name: globalize.translate('ButtonOpen'),
id: 'open',
icon: 'mode_edit'
});
menuItems.push({
name: globalize.translate('ButtonLibraryAccess'),
id: 'access',
icon: 'lock'
});
menuItems.push({
name: globalize.translate('ButtonParentalControl'),
id: 'parentalcontrol',
icon: 'person'
});
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
});
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
positionTo: card,
callback: function (id) {
switch (id) {
case 'open':
Dashboard.navigate('useredit.html?userId=' + userId);
break;
case 'access':
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
break;
case 'parentalcontrol':
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
break;
case 'delete':
deleteUser(page, userId);
}
}
});
});
}
function getUserHtml(user) {
let html = '';
let cssClass = 'card squareCard scalableCard squareCard-scalable';
if (user.Policy.IsDisabled) {
cssClass += ' grayscale';
}
html += "<div data-userid='" + user.Id + "' class='" + cssClass + "'>";
html += '<div class="cardBox visualCardBox">';
html += '<div class="cardScalable visualCardBox-cardScalable">';
html += '<div class="cardPadder cardPadder-square"></div>';
html += `<a is="emby-linkbutton" class="cardContent ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()}" href="#!/useredit.html?userId=${user.Id}">`;
let imgUrl;
if (user.PrimaryImageTag) {
imgUrl = ApiClient.getUserImageUrl(user.Id, {
width: 300,
tag: user.PrimaryImageTag,
type: 'Primary'
});
}
let imageClass = 'cardImage';
if (user.Policy.IsDisabled) {
imageClass += ' disabledUser';
}
if (imgUrl) {
html += '<div class="' + imageClass + '" style="background-image:url(\'' + imgUrl + "');\">";
} else {
html += `<div class="${imageClass} ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()} flex align-items-center justify-content-center">`;
html += '<span class="material-icons cardImageIcon person"></span>';
}
html += '</div>';
html += '</a>';
html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<div class="cardText flex align-items-center">';
html += '<div class="flex-grow" style="overflow:hidden;text-overflow:ellipsis;">';
html += user.Name;
html += '</div>';
html += '<button type="button" is="paper-icon-button-light" class="btnUserMenu flex-shrink-zero"><span class="material-icons more_vert"></span></button>';
html += '</div>';
html += '<div class="cardText cardText-secondary">';
const lastSeen = getLastSeenText(user.LastActivityDate);
html += lastSeen != '' ? lastSeen : '&nbsp;';
html += '</div>';
html += '</div>';
html += '</div>';
return html + '</div>';
}
// FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix
// how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences
function getLastSeenText(lastActivityDate) {
const localeWithSuffix = getLocaleWithSuffix();
if (lastActivityDate) {
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
}
return '';
}
function getUserSectionHtml(users) {
return users.map(function (u__q) {
return getUserHtml(u__q);
}).join('');
}
function renderUsers(page, users) {
page.querySelector('.localUsers').innerHTML = getUserSectionHtml(users);
}
function loadData(page) {
loading.show();
ApiClient.getUsers().then(function (users) {
renderUsers(page, users);
loading.hide();
});
}
pageIdOn('pageinit', 'userProfilesPage', function () {
const page = this;
page.querySelector('.btnAddUser').addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
page.querySelector('.localUsers').addEventListener('click', function (e__e) {
const btnUserMenu = dom.parentWithClass(e__e.target, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
});
pageIdOn('pagebeforeshow', 'userProfilesPage', function () {
loadData(this);
});

View File

@ -0,0 +1,9 @@
<div id="indexPage" style="outline: none;" data-role="page" data-dom-cache="true" class="page homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie,series,book">
<div class="tabContent pageTabContent" id="homeTab" data-index="0">
<div class="sections"></div>
</div>
<div class="tabContent pageTabContent" id="favoritesTab" data-index="1">
<div class="sections"></div>
</div>
</div>

65
src/controllers/home.js Normal file
View File

@ -0,0 +1,65 @@
import TabbedView from '../components/tabbedview/tabbedview';
import globalize from '../scripts/globalize';
import '../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button';
import '../elements/emby-scroller/emby-scroller';
import LibraryMenu from '../scripts/libraryMenu';
class HomeView extends TabbedView {
setTitle() {
LibraryMenu.setTitle(null);
}
onPause() {
super.onPause(this);
document.querySelector('.skinHeader').classList.remove('noHomeButtonHeader');
}
onResume(options) {
super.onResume(this, options);
document.querySelector('.skinHeader').classList.add('noHomeButtonHeader');
}
getDefaultTabIndex() {
return 0;
}
getTabs() {
return [{
name: globalize.translate('Home')
}, {
name: globalize.translate('Favorites')
}];
}
getTabController(index) {
if (index == null) {
throw new Error('index cannot be null');
}
let depends = '';
switch (index) {
case 0:
depends = 'hometab';
break;
case 1:
depends = 'favorites';
}
const instance = this;
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = instance.tabControllers[index];
if (!controller) {
controller = new controllerFactory(instance.view.querySelector(".tabContent[data-index='" + index + "']"), instance.params);
instance.tabControllers[index] = controller;
}
return controller;
});
}
}
export default HomeView;

View File

@ -349,7 +349,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function showViewSettingsMenu() {
const instance = this;
import('../components/viewSettings/viewSettings').then(({default: ViewSettings}) => {
import('../components/viewSettings/viewSettings').then(({ default: ViewSettings }) => {
new ViewSettings().show({
settingsKey: instance.getSettingsKey(),
settings: instance.getViewSettings(),
@ -364,7 +364,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function showFilterMenu() {
const instance = this;
import('../components/filtermenu/filtermenu').then(({default: FilterMenu}) => {
import('../components/filtermenu/filtermenu').then(({ default: FilterMenu }) => {
new FilterMenu().show({
settingsKey: instance.getSettingsKey(),
settings: instance.getFilters(),
@ -383,7 +383,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function showSortMenu() {
const instance = this;
import('../components/sortmenu/sortmenu').then(({default: SortMenu}) => {
import('../components/sortmenu/sortmenu').then(({ default: SortMenu }) => {
new SortMenu().show({
settingsKey: instance.getSettingsKey(),
settings: instance.getSortValues(),
@ -401,7 +401,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function onNewItemClick() {
const instance = this;
import('../components/playlisteditor/playlisteditor').then(({default: playlistEditor}) => {
import('../components/playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor({
items: [],
serverId: instance.params.serverId
@ -772,7 +772,7 @@ class ItemsView {
}
function autoFocus() {
import('../components/autoFocuser').then(({default: autoFocuser}) => {
import('../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(view);
});
}

View File

@ -100,7 +100,7 @@ export default function (view, params, tabContent) {
}
function showFilterMenu(context) {
import('../../components/filterdialog/filterdialog').then(({default: FilterDialog}) => {
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
const filterDialog = new FilterDialog({
query: getQuery(),
mode: 'livetvchannels',
@ -124,7 +124,7 @@ export default function (view, params, tabContent) {
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(context);
});
});

View File

@ -61,7 +61,7 @@ function loadRecommendedPrograms(page) {
});
loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
@ -273,7 +273,7 @@ export default function (view, params) {
break;
}
import(`../livetv/${depends}`).then(({default: controllerFactory}) => {
import(`../livetv/${depends}`).then(({ default: controllerFactory }) => {
let tabContent;
if (index === 0) {

View File

@ -9,7 +9,7 @@ function onListingsSubmitted() {
}
function init(page, type, providerId) {
import(`../components/tvproviders/${type}`).then(({default: factory}) => {
import(`../components/tvproviders/${type}`).then(({ default: factory }) => {
const instance = new factory(page, providerId, {});
Events.on(instance, 'submitted', onListingsSubmitted);
instance.init();
@ -17,7 +17,7 @@ function init(page, type, providerId) {
}
function loadTemplate(page, type, providerId) {
import(`../components/tvproviders/${type}.template.html`).then(({default: html}) => {
import(`../components/tvproviders/${type}.template.html`).then(({ default: html }) => {
page.querySelector('.providerTemplate').innerHTML = globalize.translateHtml(html);
init(page, type, providerId);
});

View File

@ -64,7 +64,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
const page = this;
$('.liveTvSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
$('#btnSelectRecordingPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
callback: function (path) {
@ -79,7 +79,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
});
});
$('#btnSelectMovieRecordingPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
callback: function (path) {
@ -94,7 +94,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
});
});
$('#btnSelectSeriesRecordingPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
callback: function (path) {
@ -109,7 +109,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
});
});
$('#btnSelectPostProcessorPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
includeFiles: true,

View File

@ -147,7 +147,7 @@ function showProviderOptions(page, providerId, button) {
id: 'map'
});
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: items,
positionTo: button
@ -165,7 +165,7 @@ function showProviderOptions(page, providerId, button) {
}
function mapChannels(page, providerId) {
import('../components/channelMapper/channelMapper').then(({default: channelMapper}) => {
import('../components/channelMapper/channelMapper').then(({ default: channelMapper }) => {
new channelMapper({
serverId: ApiClient.serverInfo().Id,
providerId: providerId
@ -237,7 +237,7 @@ function addProvider(button) {
id: 'xmltv'
});
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
positionTo: button,
@ -263,7 +263,7 @@ function showDeviceMenu(button, tunerDeviceId) {
id: 'edit'
});
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: items,
positionTo: button

View File

@ -106,7 +106,7 @@ function submitForm(page) {
}
function getDetectedDevice() {
return import('../components/tunerPicker').then(({default: tunerPicker}) => {
return import('../components/tunerPicker').then(({ default: tunerPicker }) => {
return new tunerPicker().show({
serverId: ApiClient.serverId()
});
@ -222,7 +222,7 @@ export default function (view, params) {
});
});
view.querySelector('.btnSelectPath').addEventListener('click', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
includeFiles: true,

View File

@ -0,0 +1,261 @@
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import imageLoader from '../../components/images/imageLoader';
import listView from '../../components/listview/listview';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'BoxSet',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,SortName',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
StartIndex: 0
},
view: libraryBrowser.getSavedView(key) || 'Poster'
};
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
}
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return params.topParentId + '-' + 'moviecollections';
}
const onViewStyleChange = () => {
const viewStyle = this.getCurrentViewStyle();
const itemsContainer = tabContent.querySelector('.itemsContainer');
if (viewStyle == 'List') {
itemsContainer.classList.add('vertical-list');
itemsContainer.classList.remove('vertical-wrap');
} else {
itemsContainer.classList.remove('vertical-list');
itemsContainer.classList.add('vertical-wrap');
}
itemsContainer.innerHTML = '';
};
const reloadItems = (page) => {
loading.show();
isLoading = true;
const query = getQuery();
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
reloadItems(tabContent);
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
reloadItems(tabContent);
}
window.scrollTo(0, 0);
let html;
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
const viewStyle = this.getCurrentViewStyle();
if (viewStyle == 'Thumb') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
overlayPlayButton: true,
centerText: true,
showTitle: true
});
} else if (viewStyle == 'ThumbCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
lazy: true,
cardLayout: true,
showTitle: true
});
} else if (viewStyle == 'Banner') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'banner',
preferBanner: true,
context: 'movies',
lazy: true
});
} else if (viewStyle == 'List') {
html = listView.getListViewHtml({
items: result.Items,
context: 'movies',
sortBy: query.SortBy
});
} else if (viewStyle == 'PosterCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'auto',
context: 'movies',
showTitle: true,
centerText: false,
cardLayout: true
});
} else {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'auto',
context: 'movies',
centerText: true,
overlayPlayButton: true,
showTitle: true
});
}
let elems = tabContent.querySelectorAll('.paging');
for (const elem of elems) {
elem.innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (const elem of elems) {
elem.addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (const elem of elems) {
elem.addEventListener('click', onPreviousPageClick);
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoCollectionsAvailable') + '</p>';
html += '</div>';
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
};
const data = {};
let isLoading = false;
this.getCurrentViewStyle = function () {
return getPageData().view;
};
const initPage = (tabElement) => {
tabElement.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('Name'),
id: 'SortName'
}, {
name: globalize.translate('OptionImdbRating'),
id: 'CommunityRating,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionParentalRating'),
id: 'OfficialRating,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,SortName'
}],
callback: function () {
getQuery().StartIndex = 0;
reloadItems(tabElement);
},
query: getQuery(),
button: e.target
});
});
const btnSelectView = tabElement.querySelector('.btnSelectView');
btnSelectView.addEventListener('click', (e) => {
libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
});
btnSelectView.addEventListener('layoutchange', function (e) {
const viewStyle = e.detail.viewStyle;
getPageData().view = viewStyle;
libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle);
getQuery().StartIndex = 0;
onViewStyleChange();
reloadItems(tabElement);
});
tabElement.querySelector('.btnNewCollection').addEventListener('click', () => {
import('../../components/collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const serverId = ApiClient.serverInfo().Id;
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: [],
serverId: serverId
});
});
});
};
initPage(tabContent);
onViewStyleChange();
this.renderTab = function () {
reloadItems(tabContent);
};
}

View File

@ -0,0 +1,221 @@
import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver';
import globalize from '../../scripts/globalize';
import { appRouter } from '../../components/appRouter';
import '../../elements/emby-button/emby-button';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
EnableTotalRecordCount: false
},
view: 'Poster'
};
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return params.topParentId + '-' + 'moviegenres';
}
function getPromise() {
loading.show();
const query = getQuery();
return ApiClient.getGenres(ApiClient.getCurrentUserId(), query);
}
function enableScrollX() {
return !layoutManager.desktop;
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
const fillItemsContainer = (entry) => {
const elem = entry.target;
const id = elem.getAttribute('data-id');
const viewStyle = this.getCurrentViewStyle();
let limit = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 5 : 9;
if (enableScrollX()) {
limit = 10;
}
const enableImageTypes = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 'Primary,Backdrop,Thumb' : 'Primary';
const query = {
SortBy: 'Random',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: enableImageTypes,
Limit: limit,
GenreIds: id,
EnableTotalRecordCount: false,
ParentId: params.topParentId
};
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then(function (result) {
if (viewStyle == 'Thumb') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getThumbShape(),
preferThumb: true,
showTitle: true,
scalable: true,
centerText: true,
overlayMoreButton: true,
allowBottomPadding: false
});
} else if (viewStyle == 'ThumbCard') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getThumbShape(),
preferThumb: true,
showTitle: true,
scalable: true,
centerText: false,
cardLayout: true,
showYear: true
});
} else if (viewStyle == 'PosterCard') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getPortraitShape(),
showTitle: true,
scalable: true,
centerText: false,
cardLayout: true,
showYear: true
});
} else if (viewStyle == 'Poster') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getPortraitShape(),
scalable: true,
overlayMoreButton: true,
allowBottomPadding: true,
showTitle: true,
centerText: true,
showYear: true
});
}
if (result.Items.length >= query.Limit) {
tabContent.querySelector('.btnMoreFromGenre' + id + ' .material-icons').classList.remove('hide');
}
});
};
function reloadItems(context, promise) {
const query = getQuery();
promise.then(function (result) {
const elem = context.querySelector('#items');
let html = '';
const items = result.Items;
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
html += '<div class="verticalSection">';
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item, {
context: 'movies',
parentId: params.topParentId
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre' + item.Id + '">';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += escapeHtml(item.Name);
html += '</h2>';
html += '<span class="material-icons hide chevron_right" aria-hidden="true"></span>';
html += '</a>';
html += '</div>';
if (enableScrollX()) {
let scrollXClass = 'scrollX hiddenScrollX';
if (layoutManager.tv) {
scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale';
}
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' lazy padded-left padded-right" data-id="' + item.Id + '">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap lazy padded-left padded-right" data-id="' + item.Id + '">';
}
html += '</div>';
html += '</div>';
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoGenresAvailable') + '</p>';
html += '</div>';
}
elem.innerHTML = html;
lazyLoader.lazyChildren(elem, fillItemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
});
}
const fullyReload = () => {
this.preRender();
this.renderTab();
};
const data = {};
this.getViewStyles = function () {
return 'Poster,PosterCard,Thumb,ThumbCard'.split(',');
};
this.getCurrentViewStyle = function () {
return getPageData().view;
};
this.setCurrentViewStyle = function (viewStyle) {
getPageData().view = viewStyle;
libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle);
fullyReload();
};
this.enableViewSelection = true;
let promise;
this.preRender = function () {
promise = getPromise();
};
this.renderTab = function () {
reloadItems(tabContent, promise);
};
}

View File

@ -0,0 +1,92 @@
<div id="moviesPage" data-role="page" data-dom-cache="true" class="page libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie">
<div class="pageTabContent" id="moviesTab" data-index="0">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnShuffle autoSize hide" title="${Shuffle}"><span class="material-icons shuffle" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
</div>
<div class="alphaPicker alphaPicker-fixed alphaPicker-vertical">
</div>
<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="suggestionsTab" data-index="1">
<div id="resumableSection" class="verticalSection hide">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards padded-left">${HeaderContinueWatching}</h2>
</div>
<div is="emby-itemscontainer" id="resumableItems" class="itemsContainer padded-left padded-right">
</div>
</div>
<div class="verticalSection">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards padded-left">${HeaderLatestMovies}</h2>
</div>
<div is="emby-itemscontainer" id="recentlyAddedItems" class="itemsContainer padded-left padded-right">
</div>
</div>
<div class="recommendations">
</div>
<div class="noItemsMessage hide padded-left padded-right">
<br />
<p>${MessageNoMovieSuggestionsAvailable}</p>
</div>
</div>
<div class="pageTabContent" id="trailersTab" data-index="2">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
</div>
<div class="alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical">
</div>
<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="favoritesTab" data-index="3">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
</div>
<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="collectionsTab" data-index="4">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button type="button" is="paper-icon-button-light" class="btnNewCollection autoSize" title="${NewCollection}"><span class="material-icons add" aria-hidden="true"></span></button>
</div>
<div is="emby-itemscontainer" class="itemsContainer vertical-wrap centered padded-left padded-right" style="text-align:center;">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="genresTab" data-index="5">
<div id="items"></div>
</div>
</div>

View File

@ -0,0 +1,324 @@
import loading from '../../components/loading/loading';
import * as userSettings from '../../scripts/settings/userSettings';
import libraryBrowser from '../../scripts/libraryBrowser';
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
import listView from '../../components/listview/listview';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import globalize from '../../scripts/globalize';
import Events from '../../utils/events.ts';
import { playbackManager } from '../../components/playback/playbackmanager';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
export default function (view, params, tabContent, options) {
const onViewStyleChange = () => {
if (this.getCurrentViewStyle() == 'List') {
itemsContainer.classList.add('vertical-list');
itemsContainer.classList.remove('vertical-wrap');
} else {
itemsContainer.classList.remove('vertical-list');
itemsContainer.classList.add('vertical-wrap');
}
itemsContainer.innerHTML = '';
};
function fetchData() {
isLoading = true;
loading.show();
return ApiClient.getItems(ApiClient.getCurrentUserId(), query);
}
function shuffle() {
ApiClient.getItem(
ApiClient.getCurrentUserId(),
params.topParentId
).then((item) => {
playbackManager.shuffle(item);
});
}
const afterRefresh = (result) => {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
itemsContainer.refreshItems();
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
itemsContainer.refreshItems();
}
window.scrollTo(0, 0);
this.alphaPicker?.updateControls(query);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
for (const elem of tabContent.querySelectorAll('.paging')) {
elem.innerHTML = pagingHtml;
}
for (const elem of tabContent.querySelectorAll('.btnNextPage')) {
elem.addEventListener('click', onNextPageClick);
}
for (const elem of tabContent.querySelectorAll('.btnPreviousPage')) {
elem.addEventListener('click', onPreviousPageClick);
}
tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1);
isLoading = false;
loading.hide();
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(tabContent);
});
};
const getItemsHtml = (items) => {
let html;
const viewStyle = this.getCurrentViewStyle();
if (viewStyle == 'Thumb') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
lazy: true,
overlayPlayButton: true,
showTitle: true,
showYear: true,
centerText: true
});
} else if (viewStyle == 'ThumbCard') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
lazy: true,
cardLayout: true,
showTitle: true,
showYear: true,
centerText: true
});
} else if (viewStyle == 'Banner') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'banner',
preferBanner: true,
context: 'movies',
lazy: true
});
} else if (viewStyle == 'List') {
html = listView.getListViewHtml({
items: items,
context: 'movies',
sortBy: query.SortBy
});
} else if (viewStyle == 'PosterCard') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'portrait',
context: 'movies',
showTitle: true,
showYear: true,
centerText: true,
lazy: true,
cardLayout: true
});
} else {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'portrait',
context: 'movies',
overlayPlayButton: true,
showTitle: true,
showYear: true,
centerText: true
});
}
return html;
};
const initPage = (tabElement) => {
itemsContainer.fetchData = fetchData;
itemsContainer.getItemsHtml = getItemsHtml;
itemsContainer.afterRefresh = afterRefresh;
const alphaPickerElement = tabElement.querySelector('.alphaPicker');
if (alphaPickerElement) {
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
const newValue = e.detail.value;
if (newValue === '#') {
query.NameLessThan = 'A';
delete query.NameStartsWith;
} else {
query.NameStartsWith = newValue;
delete query.NameLessThan;
}
query.StartIndex = 0;
itemsContainer.refreshItems();
});
this.alphaPicker = new AlphaPicker({
element: alphaPickerElement,
valueChangeEvent: 'click'
});
tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right');
alphaPickerElement.classList.add('alphaPicker-fixed-right');
itemsContainer.classList.add('padded-right-withalphapicker');
}
const btnFilter = tabElement.querySelector('.btnFilter');
if (btnFilter) {
btnFilter.addEventListener('click', () => {
this.showFilterMenu();
});
}
const btnSort = tabElement.querySelector('.btnSort');
if (btnSort) {
btnSort.addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('Name'),
id: 'SortName,ProductionYear'
}, {
name: globalize.translate('OptionRandom'),
id: 'Random'
}, {
name: globalize.translate('OptionImdbRating'),
id: 'CommunityRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionCriticRating'),
id: 'CriticRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName,ProductionYear'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName,ProductionYear'
}, {
name: globalize.translate('OptionParentalRating'),
id: 'OfficialRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName,ProductionYear'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,SortName,ProductionYear'
}, {
name: globalize.translate('Runtime'),
id: 'Runtime,SortName,ProductionYear'
}],
callback: function () {
query.StartIndex = 0;
userSettings.saveQuerySettings(savedQueryKey, query);
itemsContainer.refreshItems();
},
query: query,
button: e.target
});
});
}
const btnSelectView = tabElement.querySelector('.btnSelectView');
btnSelectView.addEventListener('click', (e) => {
libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'Banner,List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
});
btnSelectView.addEventListener('layoutchange', function (e) {
const viewStyle = e.detail.viewStyle;
userSettings.set(savedViewKey, viewStyle);
query.StartIndex = 0;
onViewStyleChange();
itemsContainer.refreshItems();
});
tabElement.querySelector('.btnShuffle').addEventListener('click', shuffle);
};
let itemsContainer = tabContent.querySelector('.itemsContainer');
const savedQueryKey = params.topParentId + '-' + options.mode;
const savedViewKey = savedQueryKey + '-view';
let query = {
SortBy: 'SortName,ProductionYear',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
StartIndex: 0,
ParentId: params.topParentId
};
if (userSettings.libraryPageSize() > 0) {
query['Limit'] = userSettings.libraryPageSize();
}
let isLoading = false;
if (options.mode === 'favorites') {
query.IsFavorite = true;
}
query = userSettings.loadQuerySettings(savedQueryKey, query);
this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: query,
mode: 'movies',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', () => {
query.StartIndex = 0;
itemsContainer.refreshItems();
});
filterDialog.show();
});
};
this.getCurrentViewStyle = function () {
return userSettings.get(savedViewKey) || 'Poster';
};
this.initTab = function () {
initPage(tabContent);
onViewStyleChange();
};
this.renderTab = () => {
itemsContainer.refreshItems();
this.alphaPicker?.updateControls(query);
};
this.destroy = function () {
itemsContainer = null;
};
}

View File

@ -0,0 +1,425 @@
import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import inputManager from '../../scripts/inputManager';
import * as userSettings from '../../scripts/settings/userSettings';
import libraryMenu from '../../scripts/libraryMenu';
import * as mainTabsManager from '../../components/maintabsmanager';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import dom from '../../scripts/dom';
import imageLoader from '../../components/images/imageLoader';
import { playbackManager } from '../../components/playback/playbackmanager';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
import Events from '../../utils/events.ts';
import '../../elements/emby-scroller/emby-scroller';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-tabs/emby-tabs';
import '../../elements/emby-button/emby-button';
function enableScrollX() {
return !layoutManager.desktop;
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function loadLatest(page, userId, parentId) {
const options = {
IncludeItemTypes: 'Movie',
Limit: 18,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
ApiClient.getJSON(ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
const allowBottomPadding = !enableScrollX();
const container = page.querySelector('#recentlyAddedItems');
cardBuilder.buildCards(items, {
itemsContainer: container,
shape: getPortraitShape(),
scalable: true,
overlayPlayButton: true,
allowBottomPadding: allowBottomPadding,
showTitle: true,
showYear: true,
centerText: true
});
// FIXME: Wait for all sections to load
autoFocus(page);
});
}
function loadResume(page, userId, parentId) {
const screenWidth = dom.getWindowSize().innerWidth;
const options = {
SortBy: 'DatePlayed',
SortOrder: 'Descending',
IncludeItemTypes: 'Movie',
Filters: 'IsResumable',
Limit: screenWidth >= 1600 ? 5 : 3,
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
CollapseBoxSetItems: false,
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
ApiClient.getItems(userId, options).then(function (result) {
if (result.Items.length) {
page.querySelector('#resumableSection').classList.remove('hide');
} else {
page.querySelector('#resumableSection').classList.add('hide');
}
const allowBottomPadding = !enableScrollX();
const container = page.querySelector('#resumableItems');
cardBuilder.buildCards(result.Items, {
itemsContainer: container,
preferThumb: true,
shape: getThumbShape(),
scalable: true,
overlayPlayButton: true,
allowBottomPadding: allowBottomPadding,
cardLayout: false,
showTitle: true,
showYear: true,
centerText: true
});
// FIXME: Wait for all sections to load
autoFocus(page);
});
}
function getRecommendationHtml(recommendation) {
let html = '';
let title = '';
switch (recommendation.RecommendationType) {
case 'SimilarToRecentlyPlayed':
title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName);
break;
case 'SimilarToLikedItem':
title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName);
break;
case 'HasDirectorFromRecentlyPlayed':
case 'HasLikedDirector':
title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName);
break;
case 'HasActorFromRecentlyPlayed':
case 'HasLikedActor':
title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName);
break;
}
html += '<div class="verticalSection">';
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + escapeHtml(title) + '</h2>';
const allowBottomPadding = true;
if (enableScrollX()) {
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-mousewheel="false" data-centerfocus="true">';
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
}
html += cardBuilder.getCardsHtml(recommendation.Items, {
shape: getPortraitShape(),
scalable: true,
overlayPlayButton: true,
allowBottomPadding: allowBottomPadding,
showTitle: true,
showYear: true,
centerText: true
});
if (enableScrollX()) {
html += '</div>';
}
html += '</div>';
html += '</div>';
return html;
}
function loadSuggestions(page, userId) {
const screenWidth = dom.getWindowSize().innerWidth;
let itemLimit = 5;
if (screenWidth >= 1600) {
itemLimit = 8;
} else if (screenWidth >= 1200) {
itemLimit = 6;
}
const url = ApiClient.getUrl('Movies/Recommendations', {
userId: userId,
categoryLimit: 6,
ItemLimit: itemLimit,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
});
ApiClient.getJSON(url).then(function (recommendations) {
if (!recommendations.length) {
page.querySelector('.noItemsMessage').classList.remove('hide');
page.querySelector('.recommendations').innerHTML = '';
return;
}
const html = recommendations.map(getRecommendationHtml).join('');
page.querySelector('.noItemsMessage').classList.add('hide');
const recs = page.querySelector('.recommendations');
recs.innerHTML = html;
imageLoader.lazyChildren(recs);
// FIXME: Wait for all sections to load
autoFocus(page);
});
}
function autoFocus(page) {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
}
function setScrollClasses(elem, scrollX) {
if (scrollX) {
elem.classList.add('hiddenScrollX');
if (layoutManager.tv) {
elem.classList.add('smoothScrollX');
elem.classList.add('padded-top-focusscale');
elem.classList.add('padded-bottom-focusscale');
}
elem.classList.add('scrollX');
elem.classList.remove('vertical-wrap');
} else {
elem.classList.remove('hiddenScrollX');
elem.classList.remove('smoothScrollX');
elem.classList.remove('scrollX');
elem.classList.add('vertical-wrap');
}
}
function initSuggestedTab(page, tabContent) {
const containers = tabContent.querySelectorAll('.itemsContainer');
for (const container of containers) {
setScrollClasses(container, enableScrollX());
}
}
function loadSuggestionsTab(view, params, tabContent) {
const parentId = params.topParentId;
const userId = ApiClient.getCurrentUserId();
loadResume(tabContent, userId, parentId);
loadLatest(tabContent, userId, parentId);
loadSuggestions(tabContent, userId);
}
function getTabs() {
return [{
name: globalize.translate('Movies')
}, {
name: globalize.translate('Suggestions')
}, {
name: globalize.translate('Trailers')
}, {
name: globalize.translate('Favorites')
}, {
name: globalize.translate('Collections')
}, {
name: globalize.translate('Genres')
}];
}
function getDefaultTabIndex(folderId) {
switch (userSettings.get('landing-' + folderId)) {
case 'suggestions':
return 1;
case 'favorites':
return 3;
case 'collections':
return 4;
case 'genres':
return 5;
default:
return 0;
}
}
export default function (view, params) {
function onBeforeTabChange(e) {
preLoadTab(view, parseInt(e.detail.selectedTabIndex, 10));
}
function onTabChange(e) {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
loadTab(view, newIndex);
}
function getTabContainers() {
return view.querySelectorAll('.pageTabContent');
}
function initTabs() {
mainTabsManager.setTabs(view, currentTabIndex, getTabs, getTabContainers, onBeforeTabChange, onTabChange);
}
const getTabController = (page, index, callback) => {
let depends = '';
switch (index) {
case 0:
depends = 'movies';
break;
case 1:
depends = 'moviesrecommended.js';
break;
case 2:
depends = 'movietrailers';
break;
case 3:
depends = 'movies';
break;
case 4:
depends = 'moviecollections';
break;
case 5:
depends = 'moviegenres';
break;
}
import(`../movies/${depends}`).then(({ default: controllerFactory }) => {
let tabContent;
if (index === suggestionsTabIndex) {
tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']");
this.tabContent = tabContent;
}
let controller = tabControllers[index];
if (!controller) {
tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']");
if (index === suggestionsTabIndex) {
controller = this;
} else if (index == 0 || index == 3) {
controller = new controllerFactory(view, params, tabContent, {
mode: index ? 'favorites' : 'movies'
});
} else {
controller = new controllerFactory(view, params, tabContent);
}
tabControllers[index] = controller;
if (controller.initTab) {
controller.initTab();
}
}
callback(controller);
});
};
function preLoadTab(page, index) {
getTabController(page, index, function (controller) {
if (renderedTabs.indexOf(index) == -1 && controller.preRender) {
controller.preRender();
}
});
}
function loadTab(page, index) {
currentTabIndex = index;
getTabController(page, index, ((controller) => {
if (renderedTabs.indexOf(index) == -1) {
renderedTabs.push(index);
controller.renderTab();
}
}));
}
function onPlaybackStop(e, state) {
if (state.NowPlayingItem && state.NowPlayingItem.MediaType == 'Video') {
renderedTabs = [];
mainTabsManager.getTabsElement().triggerTabChange();
}
}
function onInputCommand(e) {
if (e.detail.command === 'search') {
e.preventDefault();
Dashboard.navigate('search.html?collectionType=movies&parentId=' + params.topParentId);
}
}
let currentTabIndex = parseInt(params.tab || getDefaultTabIndex(params.topParentId), 10);
const suggestionsTabIndex = 1;
this.initTab = function () {
const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']");
initSuggestedTab(view, tabContent);
};
this.renderTab = function () {
const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']");
loadSuggestionsTab(view, params, tabContent);
};
const tabControllers = [];
let renderedTabs = [];
view.addEventListener('viewshow', function () {
initTabs();
if (!view.getAttribute('data-title')) {
const parentId = params.topParentId;
if (parentId) {
ApiClient.getItem(ApiClient.getCurrentUserId(), parentId).then(function (item) {
view.setAttribute('data-title', item.Name);
libraryMenu.setTitle(item.Name);
});
} else {
view.setAttribute('data-title', globalize.translate('Movies'));
libraryMenu.setTitle(globalize.translate('Movies'));
}
}
Events.on(playbackManager, 'playbackstop', onPlaybackStop);
inputManager.on(window, onInputCommand);
});
view.addEventListener('viewbeforehide', function () {
inputManager.off(window, onInputCommand);
});
for (const tabController of tabControllers) {
if (tabController.destroy) {
tabController.destroy();
}
}
}

View File

@ -0,0 +1,272 @@
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import imageLoader from '../../components/images/imageLoader';
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
import listView from '../../components/listview/listview';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import Events from '../../utils/events.ts';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Trailer',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
StartIndex: 0
},
view: libraryBrowser.getSavedView(key) || 'Poster'
};
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
}
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return params.topParentId + '-' + 'trailers';
}
const reloadItems = () => {
loading.show();
isLoading = true;
const query = getQuery();
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
reloadItems();
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
reloadItems();
}
window.scrollTo(0, 0);
this.alphaPicker?.updateControls(query);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
let html;
const viewStyle = this.getCurrentViewStyle();
if (viewStyle == 'Thumb') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
overlayPlayButton: true
});
} else if (viewStyle == 'ThumbCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
cardLayout: true,
showTitle: true,
showYear: true,
centerText: true
});
} else if (viewStyle == 'Banner') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'banner',
preferBanner: true,
context: 'movies'
});
} else if (viewStyle == 'List') {
html = listView.getListViewHtml({
items: result.Items,
context: 'movies',
sortBy: query.SortBy
});
} else if (viewStyle == 'PosterCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'portrait',
context: 'movies',
showTitle: true,
showYear: true,
cardLayout: true,
centerText: true
});
} else {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'portrait',
context: 'movies',
centerText: true,
overlayPlayButton: true,
showTitle: true,
showYear: true
});
}
let elems = tabContent.querySelectorAll('.paging');
for (const elem of elems) {
elem.innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (const elem of elems) {
elem.addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (const elem of elems) {
elem.addEventListener('click', onPreviousPageClick);
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoTrailersFound') + '</p>';
html += '</div>';
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
isLoading = false;
});
};
const data = {};
let isLoading = false;
this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: getQuery(),
mode: 'movies',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', function () {
getQuery().StartIndex = 0;
reloadItems();
});
filterDialog.show();
});
};
this.getCurrentViewStyle = function () {
return getPageData().view;
};
const initPage = (tabElement) => {
const alphaPickerElement = tabElement.querySelector('.alphaPicker');
const itemsContainer = tabElement.querySelector('.itemsContainer');
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
const newValue = e.detail.value;
const query = getQuery();
if (newValue === '#') {
query.NameLessThan = 'A';
delete query.NameStartsWith;
} else {
query.NameStartsWith = newValue;
delete query.NameLessThan;
}
query.StartIndex = 0;
reloadItems();
});
this.alphaPicker = new AlphaPicker({
element: alphaPickerElement,
valueChangeEvent: 'click'
});
tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right');
alphaPickerElement.classList.add('alphaPicker-fixed-right');
itemsContainer.classList.add('padded-right-withalphapicker');
tabElement.querySelector('.btnFilter').addEventListener('click', () => {
this.showFilterMenu();
});
tabElement.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('Name'),
id: 'SortName'
}, {
name: globalize.translate('OptionImdbRating'),
id: 'CommunityRating,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName'
}, {
name: globalize.translate('OptionParentalRating'),
id: 'OfficialRating,SortName'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,SortName'
}],
callback: function () {
getQuery().StartIndex = 0;
reloadItems();
},
query: getQuery(),
button: e.target
});
});
};
initPage(tabContent);
this.renderTab = () => {
reloadItems();
this.alphaPicker?.updateControls(getQuery());
};
}

View File

@ -180,7 +180,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(tabContent);
});
});
@ -191,7 +191,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
let isLoading = false;
this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: getQuery(),
mode: 'albums',

View File

@ -162,7 +162,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(tabContent);
});
});
@ -172,7 +172,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
let isLoading = false;
this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: getQuery(tabContent),
mode: this.mode,

View File

@ -92,7 +92,7 @@ import loading from '../../components/loading/loading';
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(context);
});
});

View File

@ -63,7 +63,7 @@ import loading from '../../components/loading/loading';
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(context);
});
});

View File

@ -75,7 +75,7 @@ import Dashboard from '../../utils/dashboard';
imageLoader.lazyChildren(elem);
loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
@ -171,7 +171,7 @@ import Dashboard from '../../utils/dashboard';
loadRecentlyPlayed(tabContent, parentId);
loadFrequentlyPlayed(tabContent, parentId);
import('../../components/favoriteitems').then(({default: favoriteItems}) => {
import('../../components/favoriteitems').then(({ default: favoriteItems }) => {
favoriteItems.render(tabContent, ApiClient.getCurrentUserId(), parentId, ['favoriteArtists', 'favoriteAlbums', 'favoriteSongs']);
});
}
@ -290,7 +290,7 @@ import Dashboard from '../../utils/dashboard';
break;
}
import(`../music/${depends}`).then(({default: controllerFactory}) => {
import(`../music/${depends}`).then(({ default: controllerFactory }) => {
let tabContent;
if (index == 1) {

View File

@ -124,7 +124,7 @@ export default function (view, params, tabContent) {
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
@ -135,7 +135,7 @@ export default function (view, params, tabContent) {
let isLoading = false;
self.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: getQuery(tabContent),
mode: 'songs',

View File

@ -1,6 +1,5 @@
import escapeHtml from 'escape-html';
import { playbackManager } from '../../../components/playback/playbackmanager';
import SyncPlay from '../../../plugins/syncPlay/core';
import browser from '../../../scripts/browser';
import dom from '../../../scripts/dom';
import inputManager from '../../../scripts/inputManager';
@ -25,6 +24,8 @@ import SubtitleSync from '../../../components/subtitlesync/subtitlesync';
import { appRouter } from '../../../components/appRouter';
import LibraryMenu from '../../../scripts/libraryMenu';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop';
import { pluginManager } from '../../../components/pluginManager';
import { PluginType } from '../../../types/plugin.ts';
/* eslint-disable indent */
const TICKS_PER_MINUTE = 600000000;
@ -64,7 +65,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(function (user) {
if (user.Policy.EnableLiveTvManagement) {
import('../../../components/recordingcreator/recordingbutton').then(({default: RecordingButton}) => {
import('../../../components/recordingcreator/recordingbutton').then(({ default: RecordingButton }) => {
if (recordingButtonManager) {
recordingButtonManager.refreshItem(item);
return;
@ -216,7 +217,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
let title = itemName;
if (item.PremiereDate) {
try {
const year = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), {useGrouping: false});
const year = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false });
title += ` (${year})`;
} catch (e) {
console.error(e);
@ -622,7 +623,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
}
function showComingUpNext(player) {
import('../../../components/upnextdialog/upnextdialog').then(({default: UpNextDialog}) => {
import('../../../components/upnextdialog/upnextdialog').then(({ default: UpNextDialog }) => {
if (!(currentVisibleMenu || currentUpNextDialog)) {
currentVisibleMenu = 'upnext';
comingUpNextDisplayed = true;
@ -896,8 +897,8 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
const state = playbackManager.getPlayerState(player);
// show subtitle offset feature only if player and media support it
const showSubOffset = playbackManager.supportSubtitleOffset(player) &&
playbackManager.canHandleOffsetOnCurrentSubtitle(player);
const showSubOffset = playbackManager.supportSubtitleOffset(player)
&& playbackManager.canHandleOffsetOnCurrentSubtitle(player);
playerSettingsMenu.show({
mediaType: 'Video',
@ -929,7 +930,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
}
function toggleStats() {
import('../../../components/playerstats/playerstats').then(({default: PlayerStats}) => {
import('../../../components/playerstats/playerstats').then(({ default: PlayerStats }) => {
const player = currentPlayer;
if (player) {
@ -969,7 +970,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
});
const positionTo = this;
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
title: globalize.translate('Audio'),
@ -1086,7 +1087,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
const positionTo = this;
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
title: globalize.translate('Subtitles'),
items: menuItems,
@ -1774,38 +1775,39 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
}, iconVisibilityTime);
};
Events.on(SyncPlay.Manager, 'enabled', (event, enabled) => {
if (enabled) {
// SyncPlay enabled
} else {
const syncPlayIcon = view.querySelector('#syncPlayIcon');
syncPlayIcon.style.visibility = 'hidden';
}
});
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
if (SyncPlay) {
Events.on(SyncPlay.Manager, 'enabled', (_event, enabled) => {
if (!enabled) {
const syncPlayIcon = view.querySelector('#syncPlayIcon');
syncPlayIcon.style.visibility = 'hidden';
}
});
Events.on(SyncPlay.Manager, 'notify-osd', (event, action) => {
showIcon(action);
});
Events.on(SyncPlay.Manager, 'notify-osd', (_event, action) => {
showIcon(action);
});
Events.on(SyncPlay.Manager, 'group-state-update', (event, state, reason) => {
if (state === 'Playing' && reason === 'Unpause') {
showIcon('schedule-play');
} else if (state === 'Playing' && reason === 'Ready') {
showIcon('schedule-play');
} else if (state === 'Paused' && reason === 'Pause') {
showIcon('pause');
} else if (state === 'Paused' && reason === 'Ready') {
showIcon('clear');
} else if (state === 'Waiting' && reason === 'Seek') {
showIcon('seek');
} else if (state === 'Waiting' && reason === 'Buffer') {
showIcon('buffering');
} else if (state === 'Waiting' && reason === 'Pause') {
showIcon('wait-pause');
} else if (state === 'Waiting' && reason === 'Unpause') {
showIcon('wait-unpause');
}
});
Events.on(SyncPlay.Manager, 'group-state-update', (_event, state, reason) => {
if (state === 'Playing' && reason === 'Unpause') {
showIcon('schedule-play');
} else if (state === 'Playing' && reason === 'Ready') {
showIcon('schedule-play');
} else if (state === 'Paused' && reason === 'Pause') {
showIcon('pause');
} else if (state === 'Paused' && reason === 'Ready') {
showIcon('clear');
} else if (state === 'Waiting' && reason === 'Seek') {
showIcon('seek');
} else if (state === 'Waiting' && reason === 'Buffer') {
showIcon('buffering');
} else if (state === 'Waiting' && reason === 'Pause') {
showIcon('wait-pause');
} else if (state === 'Waiting' && reason === 'Unpause') {
showIcon('wait-unpause');
}
});
}
}
/* eslint-enable indent */

View File

@ -54,7 +54,7 @@ import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionSta
view.querySelector('.addServerForm').addEventListener('submit', onServerSubmit);
view.querySelector('.btnCancel').addEventListener('click', goBack);
import('../../../components/autoFocuser').then(({default: autoFocuser}) => {
import('../../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(view);
});
@ -65,7 +65,7 @@ import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionSta
}
function goBack() {
import('../../../components/appRouter').then(({appRouter}) => {
import('../../../components/appRouter').then(({ appRouter }) => {
appRouter.back();
});
}

Some files were not shown because too many files have changed in this diff Show More