Merge branch 'master' into chapter-markers

This commit is contained in:
Viperinius 2022-09-17 12:04:18 +02:00 committed by GitHub
commit 33fe2c51d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 23903 additions and 4619 deletions

View file

@ -42,6 +42,7 @@ module.exports = {
'indent': ['error', 4, { 'SwitchCase': 1 }],
'jsx-quotes': ['error', 'prefer-single'],
'keyword-spacing': ['error'],
'no-throw-literal': ['error'],
'max-statements-per-line': ['error'],
'no-duplicate-imports': ['error'],
'no-empty-function': ['error'],
@ -49,6 +50,7 @@ module.exports = {
'no-multi-spaces': ['error'],
'no-multiple-empty-lines': ['error', { 'max': 1 }],
'no-restricted-globals': ['error'].concat(restrictedGlobals),
'no-return-await': ['error'],
'no-trailing-spaces': ['error'],
'@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
'no-void': ['error', { 'allowAsStatement': true }],

51
.github/renovate.json vendored
View file

@ -1,51 +1,4 @@
{
"packageRules": [
{
"matchManagers": ["npm"],
"addLabels": ["javascript"]
},
{
"description": "Adds label to dev dependency updates",
"matchDepTypes": ["devDependencies"],
"addLabels": ["dev-deps"]
},
{
"description": "Collects and groups dev dependency updates",
"matchDepTypes": ["devDependencies"],
"groupName": "development dependencies",
"groupSlug": "dev-deps"
},
{
"description": "Collects and groups npm dependency updates",
"matchDepTypes": ["dependencies"],
"groupName": "dependencies",
"groupSlug": "deps"
},
{
"description": "Collects and groups GitHub Action dependency updates",
"matchDepTypes": ["action"],
"addLabels": ["github_actions"],
"groupName": "CI dependencies",
"groupSlug": "ci-deps"
},
{
"description": "Disables HLS.js major updates",
"matchPackageNames": ["hls.js"],
"matchUpdateTypes": "major",
"enabled": false
}
],
"vulnerabilityAlerts": {
"enabled": false
},
"dependencyDashboard": false,
"ignoreDeps": ["npm", "node"],
"lockFileMaintenance": {
"enabled": false
},
"enabledManagers": ["npm", "github-actions"],
"labels": ["dependencies"],
"prHourlyLimit": 2,
"rebaseWhen": "conflicted",
"rangeStrategy": "pin"
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>jellyfin/.github//renovate-presets/nodejs", ":semanticCommitsDisabled"]
}

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
- uses: eps1lon/actions-label-merge-conflict@b8bf8341285ec9a4567d4318ba474fee998a6919 # tag=v2.0.1
with:
dirtyLabel: 'merge conflict'
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View file

@ -19,13 +19,13 @@ jobs:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@6a38b7d4a1af70deb1b561eb77db2b5e5a6a1e69 # tag=v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@6a38b7d4a1af70deb1b561eb77db2b5e5a6a1e69 # tag=v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@6a38b7d4a1af70deb1b561eb77db2b5e5a6a1e69 # tag=v2

View file

@ -12,17 +12,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@v2.0.0
uses: peter-evans/create-or-update-comment@c9fcb64660bc90ec1cc535646af190c992007c32 # tag=v2.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@v3.0.2
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.7
uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

View file

@ -13,10 +13,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup node environment
uses: actions/setup-node@v3.3.0
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3.4.1
with:
node-version: 12
check-latest: true
@ -26,7 +26,7 @@ jobs:
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node_modules
uses: actions/cache@v3.0.4
uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 # tag=v3.0.8
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
@ -48,10 +48,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup node environment
uses: actions/setup-node@v3.3.0
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3.4.1
with:
node-version: 12
check-latest: true
@ -61,7 +61,7 @@ jobs:
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node_modules
uses: actions/cache@v3.0.4
uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 # tag=v3.0.8
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
@ -70,7 +70,7 @@ jobs:
${{ runner.os }}-npm-
- name: Set up stylelint matcher
uses: xt0rted/stylelint-problem-matcher@v1
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
- name: Install Node.js dependencies
run: npm ci --no-audit
@ -86,10 +86,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup node environment
uses: actions/setup-node@v3.3.0
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3.4.1
with:
node-version: 12
check-latest: true
@ -99,7 +99,7 @@ jobs:
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node_modules
uses: actions/cache@v3.0.4
uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 # tag=v3.0.8
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
@ -108,7 +108,7 @@ jobs:
${{ runner.os }}-npm-
- name: Set up stylelint matcher
uses: xt0rted/stylelint-problem-matcher@v1
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
- name: Install Node.js dependencies
run: npm ci --no-audit

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@v5.0.0
- uses: actions/stale@9c1b1c6e115ca2af09755448e0dbba24e5061cc8 # tag=v5.1.1
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120

View file

@ -53,6 +53,7 @@
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [taku0](https://github.com/taku0)
- [Viperinius](https://github.com/Viperinius)
- [is343](https://github.com/is343)
# Emby Contributors
@ -117,3 +118,4 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [jomp16](https://github.com/jomp16)
- [Leon de Klerk](https://github.com/leondeklerk)

23304
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,48 +14,47 @@
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@thornbill/jellyfin-sdk": "0.4.1",
"@types/escape-html": "1.0.1",
"@types/escape-html": "1.0.2",
"@types/lodash-es": "4.17.6",
"@types/react": "17.0.40",
"@types/react-dom": "17.0.13",
"@typescript-eslint/eslint-plugin": "5.15.0",
"@typescript-eslint/parser": "5.15.0",
"@types/react": "17.0.49",
"@types/react-dom": "17.0.17",
"@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.36.2",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.4",
"autoprefixer": "10.4.8",
"babel-loader": "8.2.3",
"babel-plugin-dynamic-import-polyfill": "1.0.0",
"clean-webpack-plugin": "4.0.0",
"confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "10.2.4",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.7.1",
"cssnano": "5.1.4",
"eslint": "8.11.0",
"cssnano": "5.1.13",
"eslint": "8.23.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-promise": "6.0.0",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"expose-loader": "3.1.0",
"html-loader": "3.1.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-promise": "6.0.1",
"eslint-plugin-react": "7.31.7",
"eslint-plugin-react-hooks": "4.6.0",
"expose-loader": "4.0.0",
"html-loader": "4.1.0",
"html-webpack-plugin": "5.5.0",
"postcss": "8.4.12",
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.4.2",
"postcss-scss": "4.0.3",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"postcss": "8.4.16",
"postcss-loader": "7.0.1",
"postcss-preset-env": "7.8.0",
"postcss-scss": "4.0.4",
"sass": "1.54.8",
"sass-loader": "13.0.2",
"source-map-loader": "3.0.1",
"style-loader": "3.3.1",
"stylelint": "14.6.0",
"stylelint": "14.11.0",
"stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "5.0.0",
"stylelint-scss": "4.2.0",
"stylelint-scss": "4.3.0",
"ts-loader": "9.2.8",
"typescript": "4.6.2",
"typescript": "4.8.2",
"webpack": "5.70.0",
"webpack-cli": "4.9.2",
"webpack-dev-server": "4.7.4",
@ -64,42 +63,43 @@
"worker-loader": "3.0.8"
},
"dependencies": {
"@fontsource/noto-sans": "4.5.1",
"@fontsource/noto-sans-hk": "4.5.2",
"@fontsource/noto-sans-jp": "4.5.2",
"@fontsource/noto-sans-kr": "4.5.2",
"@fontsource/noto-sans-sc": "4.5.2",
"@fontsource/noto-sans-tc": "4.5.2",
"@fontsource/noto-sans": "4.5.11",
"@fontsource/noto-sans-hk": "4.5.11",
"@fontsource/noto-sans-jp": "4.5.11",
"@fontsource/noto-sans-kr": "4.5.11",
"@fontsource/noto-sans-sc": "4.5.11",
"@fontsource/noto-sans-tc": "4.5.11",
"@jellyfin/libass-wasm": "4.1.1",
"blurhash": "1.1.4",
"@jellyfin/sdk": "0.7.0",
"blurhash": "1.1.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.3.1",
"core-js": "3.20.2",
"date-fns": "2.28.0",
"dompurify": "2.3.4",
"core-js": "3.25.0",
"date-fns": "2.29.2",
"dompurify": "2.4.0",
"epubjs": "0.3.93",
"escape-html": "1.0.3",
"fast-text-encoding": "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": "0.14.17",
"intersection-observer": "0.12.0",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.10.0",
"jquery": "3.6.0",
"jquery": "3.6.1",
"jstree": "3.3.12",
"libarchive.js": "1.3.0",
"lodash-es": "4.17.21",
"marked": "4.0.10",
"material-design-icons-iconfont": "6.1.1",
"marked": "4.1.0",
"material-design-icons-iconfont": "6.7.0",
"native-promise-only": "0.8.1",
"pdfjs-dist": "2.12.313",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "6.3.0",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.0",
"sortablejs": "1.14.0",
"screenfull": "6.0.2",
"sortablejs": "1.15.0",
"swiper": "6.8.4",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.2",

2
src/apiclient.d.ts vendored
View file

@ -1,7 +1,7 @@
// TODO: Move to jellyfin-apiclient
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'jellyfin-apiclient' {
import {
import type {
AllThemeMediaResult,
AuthenticationResult,
BaseItemDto,

View file

@ -175,6 +175,9 @@
flex-direction: column;
contain: layout style paint;
transition: background ease-in-out 0.5s;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
}
.layout-tv .skinHeader {
@ -1146,10 +1149,12 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
.padded-left {
padding-left: 3.3%;
padding-left: max(env(safe-area-inset-left), 3.3%);
}
.padded-right {
padding-right: 3.3%;
padding-right: max(env(safe-area-inset-right), 3.3%);
}
.padded-top {
@ -1173,6 +1178,7 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
@media all and (min-height: 31.25em) {
.padded-right-withalphapicker {
padding-right: 7.5%;
padding-right: max(env(safe-area-inset-left), 7.5%);
}
}

View file

@ -84,6 +84,7 @@ div[data-role="page"] {
.pageWithAbsoluteTabs .pageTabContent {
/* provides room for the music controls */
padding-bottom: 5em !important;
padding-bottom: calc(env(safe-area-inset-bottom) + 5em) !important;
}
.readOnlyContent {

View file

@ -12,8 +12,11 @@
right: 0;
position: fixed;
background: linear-gradient(0deg, rgba(16, 16, 16, 0.75) 0%, rgba(16, 16, 16, 0) 100%);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: 7.5em;
padding-bottom: 1.75em;
padding-bottom: max(env(safe-area-inset-bottom), 1.75em);
display: flex;
flex-direction: row;
justify-content: center;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import alert from './alert';
import { appRouter } from './appRouter';
@ -33,7 +33,6 @@ type ConnectionRequiredProps = {
* If a condition fails, this component will navigate to the appropriate page.
*/
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
children,
isAdminRequired = false,
isUserRequired = true
}) => {
@ -147,12 +146,14 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
setIsLoading(false);
};
loading.show();
validateConnection();
}, [ isAdminRequired, isUserRequired, navigate ]);
// Show/hide the loading indicator
useEffect(() => {
if (!isLoading) {
if (isLoading) {
loading.show();
} else {
loading.hide();
}
}, [ isLoading ]);
@ -162,7 +163,9 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
}
return (
<>{children}</>
<div className='skinBody'>
<Outlet />
</div>
);
};

View file

@ -6,8 +6,10 @@ type PageProps = {
id: string, // id is required for libraryMenu
title?: string,
isBackButtonEnabled?: boolean,
isMenuButtonEnabled?: boolean,
isNowPlayingBarEnabled?: boolean,
isThemeMediaSupported?: boolean
isThemeMediaSupported?: boolean,
backDropType?: string
};
/**
@ -20,8 +22,10 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
className = '',
title,
isBackButtonEnabled = true,
isMenuButtonEnabled = false,
isNowPlayingBarEnabled = true,
isThemeMediaSupported = false
isThemeMediaSupported = false,
backDropType
}) => {
const element = useRef<HTMLDivElement>(null);
@ -59,7 +63,9 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
data-role='page'
className={`page ${className}`}
data-title={title}
data-backbutton={`${isBackButtonEnabled}`}
data-backbutton={isBackButtonEnabled}
data-menubutton={isMenuButtonEnabled}
data-backdroptype={backDropType}
>
{children}
</div>

View file

@ -301,7 +301,7 @@ export function show(options) {
resolve(selectedId);
} else {
reject();
reject('ActionSheet closed without resolving');
}
}
});

View file

@ -12,6 +12,7 @@
.alphaPicker-fixed {
position: fixed;
bottom: 5.5em;
bottom: max(env(safe-area-inset-bottom), 5.5em);
}
.alphaPickerRow {
@ -45,6 +46,7 @@
@media all and (max-height: 50em) {
.alphaPicker-fixed {
bottom: 5em;
bottom: max(env(safe-area-inset-bottom), 5em);
}
.alphaPickerButton-vertical {
@ -104,15 +106,18 @@
.alphaPicker-fixed.alphaPicker-tv {
bottom: 1%;
bottom: max(env(safe-area-inset-bottom), 1%);
}
.alphaPicker-fixed-right {
right: 0.4em;
right: max(env(safe-area-inset-right), 0.4em);
}
@media all and (min-width: 62.5em) {
.alphaPicker-fixed-right {
right: 1em;
right: max(env(safe-area-inset-right), 1em);
}
}

View file

@ -6,6 +6,13 @@
bottom: 0;
transition: transform 180ms linear;
contain: layout style;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
.appfooter:empty {
padding: 0;
}
.appfooter.headroom--unpinned {

View file

@ -9,7 +9,6 @@ import loading from './loading/loading';
import viewManager from './viewManager/viewManager';
import ServerConnections from './ServerConnections';
import alert from './alert';
import reactControllerFactory from './reactControllerFactory';
export const history = createHashHistory();
@ -264,9 +263,7 @@ class AppRouter {
this.#sendRouteToViewManager(ctx, next, route, controllerFactory);
};
if (route.pageComponent) {
onInitComplete(reactControllerFactory);
} else if (route.controller) {
if (route.controller) {
import('../controllers/' + route.controller).then(onInitComplete);
} else {
onInitComplete();
@ -293,7 +290,6 @@ class AppRouter {
fullscreen: route.fullscreen,
controllerFactory: controllerFactory,
options: {
pageComponent: route.pageComponent,
supportsThemeMedia: route.supportsThemeMedia || false,
enableMediaControl: route.enableMediaControl !== false
},

View file

@ -821,7 +821,7 @@ import { appRouter } from '../appRouter';
if (isUsingLiveTvNaming(item)) {
lines.push(escapeHtml(item.Name));
if (!item.EpisodeTitle) {
if (!item.EpisodeTitle && !item.IndexNumber) {
titleAdded = true;
}
} else {
@ -1349,7 +1349,7 @@ import { appRouter } from '../appRouter';
cardImageContainerClose = '</div>';
} else {
const cardImageContainerAriaLabelAttribute = ` aria-label="${item.Name}"`;
const cardImageContainerAriaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
const url = appRouter.getRouteUrl(item);
// Don't use the IMG tag with safari because it puts a white border around it
@ -1433,7 +1433,7 @@ import { appRouter } from '../appRouter';
if (tagName === 'button') {
className += ' itemAction';
actionAttribute = ' data-action="' + action + '"';
ariaLabelAttribute = ` aria-label="${item.Name}"`;
ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
} else {
actionAttribute = '';
}

View file

@ -1,6 +1,6 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
import CheckBoxElement from './CheckBoxElement';
import CheckBoxElement from '../../../elements/CheckBoxElement';
type IProps = {
containerClassName?: string;
@ -18,7 +18,11 @@ const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerT
return (
<div className={containerClassName}>
<h2>{globalize.translate(headerTitle)}</h2>
<CheckBoxElement labelClassName='checkboxContainer' type='checkbox' className={checkBoxClassName} title={checkBoxTitle} />
<CheckBoxElement
labelClassName='checkboxContainer'
className={checkBoxClassName}
title={checkBoxTitle}
/>
<div className={listContainerClassName}>
<div className={accessClassName}>
<h3 className='checkboxListLabel'>

View file

@ -1,21 +1,11 @@
import React, { FunctionComponent } from 'react';
import datetime from '../../../scripts/datetime';
import globalize from '../../../scripts/globalize';
import IconButtonElement from '../../../elements/IconButtonElement';
const createButtonElement = (index: number) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='btnDelete listItemButton'
data-index='${index}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
type IProps = {
type AccessScheduleListProps = {
index: number;
Id: number;
Id?: number;
DayOfWeek?: string;
StartHour?: number ;
EndHour?: number;
@ -32,7 +22,7 @@ function getDisplayTime(hours = 0) {
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({index, DayOfWeek, StartHour, EndHour}: AccessScheduleListProps) => {
return (
<div
className='liSchedule listItem'
@ -48,8 +38,12 @@ const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartH
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
</div>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(index)}
<IconButtonElement
is='paper-icon-button-light'
className='btnDelete listItemButton'
title='Delete'
icon='delete'
dataIndex={index}
/>
</div>
);

View file

@ -1,15 +1,5 @@
import React, { FunctionComponent } from 'react';
const createButtonElement = (tag?: string) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='blockedTag btnDeleteTag listItemButton'
data-tag='${tag}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
import IconButtonElement from '../../../elements/IconButtonElement';
type IProps = {
tag?: string;
@ -24,11 +14,14 @@ const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
{tag}
</h3>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(tag)}
<IconButtonElement
is='paper-icon-button-light'
className='blockedTag btnDeleteTag listItemButton'
title='Delete'
icon='delete'
dataTag={tag}
/>
</div>
</div>
);
};

View file

@ -1,32 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
__html: `<button
is="emby-button"
type="${type}"
class="${className}"
>
<span>${title}</span>
</button>`
});
type IProps = {
type?: string;
className?: string;
title?: string
}
const ButtonElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default ButtonElement;

View file

@ -1,36 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, type, className, title }: { labelClassName?: string, type?: string, className?: string, title?: string }) => ({
__html: `<label class="${labelClassName}">
<input
is="emby-checkbox"
type="${type}"
class="${className}"
/>
<span>${title}</span>
</label>`
});
type IProps = {
labelClassName?: string;
type?: string;
className?: string;
title?: string
}
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, type, className, title }: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
labelClassName: labelClassName ? labelClassName : '',
type: type,
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default CheckBoxElement;

View file

@ -1,41 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
type IProps = {
className?: string;
Name?: string;
Id?: string;
ItemType?: string;
AppName?: string;
checkedAttribute?: string;
}
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
__html: `<label>
<input
type="checkbox"
is="emby-checkbox"
class="${className}"
${dataAttributes} ${checkedAttribute}
/>
<span>${escapeHtml(Name || '')} ${AppName}</span>
</label>`
});
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
className: className,
Name: Name,
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
AppName: AppName ? `- ${AppName}` : '',
checkedAttribute: checkedAttribute
})}
/>
);
};
export default CheckBoxListItem;

View file

@ -1,34 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
type IProps = {
title: string;
className?: string;
icon: string,
}
const createButtonElement = ({ className, title, icon }: { className?: string, title: string, icon: string }) => ({
__html: `<button
is="emby-button"
type="button"
class="${className}"
style="margin-left:1em;"
title="${title}"
>
<span class="material-icons ${icon}" aria-hidden="true"></span>
</button>`
});
const SectionTitleButtonElement: FunctionComponent<IProps> = ({ className, title, icon }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
className: className,
title: globalize.translate(title),
icon: icon
})}
/>
);
};
export default SectionTitleButtonElement;

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import SectionTitleButtonElement from './SectionTitleButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement';
type IProps = {
title: string;
isBtnVisible?: boolean;
titleLink?: string;
}
const SectionTitleContainer: FunctionComponent<IProps> = ({title, isBtnVisible = false, titleLink}: IProps) => {
return (
<div className='verticalSection'>
<div className='sectionTitleContainer flex align-items-center'>
<h2 className='sectionTitle'>
{title}
</h2>
{isBtnVisible && <SectionTitleButtonElement
className='fab btnAddUser submit sectionTitleButton'
title='ButtonAddUser'
icon='add'
/>}
<SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url={titleLink}
/>
</div>
</div>
);
};
export default SectionTitleContainer;

View file

@ -1,44 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string[] }) => ({
__html: `<select
class="${className}"
is="emby-select"
label="${label}"
>
${option}
</select>`
});
type ProvidersArr = {
Name?: string;
Id?: string;
}
type IProps = {
className?: string;
label?: string;
currentProviderId: string;
providers: ProvidersArr[]
}
const SelectElement: FunctionComponent<IProps> = ({ className, label, currentProviderId, providers }: IProps) => {
const renderOption = providers.map((provider) => {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + escapeHtml(provider.Name) + '</option>';
});
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption
})}
/>
);
};
export default SelectElement;

View file

@ -1,47 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string }) => ({
__html: `<select
class="${className}"
is="emby-select"
label="${label}"
>
<option value=''></option>
${option}
</select>`
});
type RatingsArr = {
Name: string;
Value: number;
}
type IProps = {
className?: string;
label?: string;
parentalRatings: RatingsArr[];
}
const SelectMaxParentalRating: FunctionComponent<IProps> = ({ className, label, parentalRatings }: IProps) => {
const renderOption = () => {
let content = '';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHtml(rating.Name)}</option>`;
}
return content;
};
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption()
})}
/>
);
};
export default SelectMaxParentalRating;

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, id, label }: { className?: string, id?: string, label: string }) => ({
__html: `<select
class="${className}"
is="emby-select"
id="${id}"
label="${label}"
>
<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>
<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>
<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>
</select>`
});
type IProps = {
className?: string;
id?: string;
label?: string
}
const SelectSyncPlayAccessElement: FunctionComponent<IProps> = ({ className, id, label }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
id: id,
label: globalize.translate(label)
})}
/>
);
};
export default SelectSyncPlayAccessElement;

View file

@ -1,9 +1,11 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { localeWithSuffix } from '../../../scripts/dfnshelper';
import globalize from '../../../scripts/globalize';
import cardBuilder from '../../cardbuilder/cardBuilder';
import IconButtonElement from '../../../elements/IconButtonElement';
import escapeHTML from 'escape-html';
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a
@ -15,16 +17,6 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
</a>`
});
const createButtonElement = () => ({
__html: `<button
is="paper-icon-button-light"
type="button"
class="btnUserMenu flex-shrink-zero"
>
<span class="material-icons more_vert" aria-hidden="true"></span>
</button>`
});
type IProps = {
user?: UserDto;
}
@ -81,16 +73,20 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
/>
</div>
<div className='cardFooter visualCardBox-cardFooter'>
<div className='cardText flex align-items-center'>
<div className='flex-grow' style={{overflow: 'hidden', textOverflow: 'ellipsis'}}>
{user.Name}
</div>
<div
dangerouslySetInnerHTML={createButtonElement()}
<div
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
>
<IconButtonElement
is='paper-icon-button-light'
className='btnUserMenu flex-shrink-zero'
icon='more_vert'
/>
</div>
<div className='cardText'>
<span>{escapeHTML(user.Name)}</span>
</div>
<div className='cardText cardText-secondary'>
{lastSeen != '' ? lastSeen : ''}
<span>{lastSeen != '' ? lastSeen : ''}</span>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
import Dashboard from '../../../utils/dashboard';
import globalize from '../../../scripts/globalize';
@ -6,9 +6,9 @@ import LibraryMenu from '../../../scripts/libraryMenu';
import confirm from '../../confirm/confirm';
import loading from '../../loading/loading';
import toast from '../../toast/toast';
import ButtonElement from './ButtonElement';
import CheckBoxElement from './CheckBoxElement';
import InputElement from './InputElement';
import ButtonElement from '../../../elements/ButtonElement';
import CheckBoxElement from '../../../elements/CheckBoxElement';
import InputElement from '../../../elements/InputElement';
type IProps = {
userId: string;
@ -40,11 +40,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
let showLocalAccessSection = false;
if (user.HasConfiguredPassword) {
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true;
} else {
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
}
@ -65,11 +65,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******';
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
}
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
@ -206,8 +206,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
(page.querySelector('.updatePasswordForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.localAccessForm') as HTMLFormElement).addEventListener('submit', onLocalAccessSubmit);
(page.querySelector('.btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
(page.querySelector('.btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
(page.querySelector('#btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
(page.querySelector('#btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
}, [loadUser, userId]);
return (
@ -250,7 +250,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
/>
<ButtonElement
type='button'
className='raised btnResetPassword button-cancel block hide'
id='btnResetPassword'
className='raised button-cancel block hide'
title='ResetPassword'
/>
</div>
@ -281,7 +282,6 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<br />
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
type='checkbox'
className='chkEnableLocalEasyPassword'
title='LabelInNetworkSignInWithEasyPassword'
/>
@ -297,7 +297,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
/>
<ButtonElement
type='button'
className='raised btnResetEasyPassword button-cancel block hide'
id='btnResetEasyPassword'
className='raised button-cancel block hide'
title='ButtonResetEasyPassword'
/>
</div>

View file

@ -75,8 +75,7 @@
*/
function paramsToString(params) {
return Object.entries(params)
// eslint-disable-next-line no-unused-vars
.filter(([_, v]) => v !== null && v !== undefined && v !== '')
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}

View file

@ -123,7 +123,7 @@ export function canEdit(user, item) {
}
export function isLocalItem(item) {
if (item && item.Id && item.Id.indexOf('local') === 0) {
if (item && item.Id && typeof item.Id === 'string' && item.Id.indexOf('local') === 0) {
return true;
}

View file

@ -14,7 +14,13 @@
top: 0;
left: 0;
right: 0;
padding: 1em 0.5em;
padding-left: 0.5em;
padding-left: max(env(safe-area-inset-left), 0.5em);
padding-right: 0.5em;
padding-right: max(env(safe-area-inset-right), 0.5em);
padding-top: 1em;
padding-top: max(env(safe-area-inset-top), 1em);
padding-bottom: 1em;
display: flex;
align-items: center;
z-index: 99999;

View file

@ -238,6 +238,7 @@ function showWithUser(options, player, user) {
return actionsheet.show({
items: menuItems,
resolveOnClick: true,
positionTo: options.positionTo
}).then(function (id) {
return handleSelectedOption(id, options, player);

View file

@ -34,7 +34,7 @@ class PluginManager {
// translations won't be loaded for skins until needed
return plugin;
} else {
return await this.#loadStrings(plugin);
return this.#loadStrings(plugin);
}
}

View file

@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
export default (view, params, { detail }) => {
if (detail.options?.pageComponent) {
// Fetch and render the page component to the view
import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`)
.then(({ default: component }) => {
ReactDOM.render(React.createElement(component, params), view);
});
// Unmount component when view is destroyed
view.addEventListener('viewdestroy', () => {
ReactDOM.unmountComponentAtNode(view);
});
}
};

View file

@ -139,7 +139,7 @@ function updateNowPlayingInfo(context, state, serverId) {
const displayName = item ? getNowPlayingNameHtml(item).replace('<br/>', ' - ') : '';
if (item) {
const nowPlayingServerId = (item.ServerId || serverId);
if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {
if (item.Type == 'AudioBook' || item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {
let artistsSeries = '';
let albumName = '';
if (item.Artists != null) {

View file

@ -195,7 +195,10 @@
height: 4.2em;
right: 0;
padding-left: 7.3%;
padding-left: max(env(safe-area-inset-left), 7.3%);
padding-right: 7.3%;
padding-right: max(env(safe-area-inset-right), 7.3%);
padding-bottom: env(safe-area-inset-bottom);
}
.layout-desktop .playlistSectionButton,

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';
@ -33,6 +33,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
@ -99,6 +100,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
setAudioBooks([]);
setBooks([]);
setPeople([]);
setCollections([]);
if (query) {
const apiClient = ServerConnections.getApiClient(serverId);
@ -166,6 +168,9 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items || []));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items || []));
}
}
}, [collectionType, parentId, query, serverId]);
@ -257,6 +262,10 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
title={globalize.translate('Books')}
items={books}
/>
<SearchResultsRow
title={globalize.translate('Collections')}
items={collections}
/>
<SearchResultsRow
title={globalize.translate('People')}
items={people}

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useEffect, useRef } from 'react';
import cardBuilder from '../cardbuilder/cardBuilder';

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import escapeHtml from 'escape-html';
import React, { FunctionComponent, useEffect, useState } from 'react';

View file

@ -2,7 +2,7 @@
<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">${Subtitles}</h3>
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://docs.jellyfin.org/general/server/media/subtitles.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://jellyfin.org/docs/general/server/media/external-files.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
</div>
<div class="formDialogContent smoothScrollY">
<div class="dialogContentInner dialog-content-centered">

View file

@ -1,114 +0,0 @@
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));
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);
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

@ -4,7 +4,12 @@
bottom: 0;
pointer-events: none;
z-index: 9999999;
padding: 1em;
padding-left: 1em;
padding-left: max(env(safe-area-inset-left), 1em);
padding-right: 1em;
padding-top: 1em;
padding-bottom: 1em;
padding-bottom: max(env(safe-area-inset-bottom), 1em);
display: flex;
flex-direction: column;
}

View file

@ -21,9 +21,9 @@ viewContainer.setOnBeforeChange(function (newView, isRestored, options) {
newView.initComplete = true;
if (typeof options.controllerFactory === 'function') {
new options.controllerFactory(newView, eventDetail.detail.params, eventDetail);
new options.controllerFactory(newView, eventDetail.detail.params);
} else if (options.controllerFactory && typeof options.controllerFactory.default === 'function') {
new options.controllerFactory.default(newView, eventDetail.detail.params, eventDetail);
new options.controllerFactory.default(newView, eventDetail.detail.params);
}
if (!options.controllerFactory || dispatchPageEvents) {

View file

@ -15,7 +15,6 @@ import alert from '../../components/alert';
page.querySelector('#txtServerName').value = systemInfo.ServerName;
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
page.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
$('#txtMetadataPath', page).val(systemInfo.InternalMetadataPath || '');
$('#txtMetadataNetworkPath', page).val(systemInfo.MetadataNetworkPath || '');
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
@ -108,6 +107,7 @@ import alert from '../../components/alert';
ApiClient.getNamedConfiguration(brandingConfigKey).then(function (config) {
view.querySelector('#txtLoginDisclaimer').value = config.LoginDisclaimer || '';
view.querySelector('#txtCustomCss').value = config.CustomCss || '';
view.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
});
});
}

View file

@ -9,7 +9,8 @@ import alert from '../../components/alert';
/* eslint-disable indent */
function onSubmit() {
function onSubmit(event) {
event.preventDefault();
loading.show();
const form = this;
ApiClient.getServerConfiguration().then(function (config) {

View file

@ -1,3 +0,0 @@
<div id="editUserPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="newUserPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userParentalControlPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
</div>

View file

@ -1,3 +0,0 @@
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
</div>

View file

@ -1,9 +0,0 @@
<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>

View file

@ -1,69 +0,0 @@
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 {
constructor(view, params) {
super(view, params);
}
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

@ -1985,7 +1985,9 @@ export default function (view, params) {
download([{
url: downloadHref,
itemId: currentItem.Id,
serverId: currentItem.serverId
serverId: currentItem.ServerId,
title: currentItem.Name,
filename: currentItem.Path.replace(/^.*[\\/]/, '')
}]);
}

View file

@ -62,6 +62,14 @@
<div class="fieldDescription checkboxFieldDescription">${EnableStreamLoopingHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldIgnoreDts hide">
<label>
<input type="checkbox" is="emby-checkbox" class="chkIgnoreDts" checked />
<span>${IgnoreDts}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${IgnoreDtsHelp}</div>
</div>
<p class="drmMessage hide">${DrmChannelsNotImported}</p>
<br />
<input type="hidden" class="fldDeviceId" />

View file

@ -61,6 +61,7 @@ function fillTunerHostInfo(view, info) {
view.querySelector('.chkFavorite').checked = info.ImportFavoritesOnly;
view.querySelector('.chkTranscode').checked = info.AllowHWTranscoding;
view.querySelector('.chkStreamLoop').checked = info.EnableStreamLooping;
view.querySelector('.chkIgnoreDts').checked = info.IgnoreDts;
view.querySelector('.txtTunerCount').value = info.TunerCount || '0';
}
@ -75,7 +76,8 @@ function submitForm(page) {
TunerCount: page.querySelector('.txtTunerCount').value || 0,
ImportFavoritesOnly: page.querySelector('.chkFavorite').checked,
AllowHWTranscoding: page.querySelector('.chkTranscode').checked,
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked,
IgnoreDts: page.querySelector('.chkIgnoreDts').checked
};
if (isM3uVariant(info.Type)) {
@ -120,6 +122,7 @@ function onTypeChange() {
const supportsTunerIpAddress = value === 'hdhomerun';
const supportsTunerFileOrUrl = value === 'm3u';
const supportsStreamLooping = value === 'm3u';
const supportsIgnoreDts = value === 'm3u';
const supportsTunerCount = value === 'm3u';
const supportsUserAgent = value === 'm3u';
const suppportsSubmit = value !== 'other';
@ -168,6 +171,12 @@ function onTypeChange() {
view.querySelector('.fldStreamLoop').classList.add('hide');
}
if (supportsIgnoreDts) {
view.querySelector('.fldIgnoreDts').classList.remove('hide');
} else {
view.querySelector('.fldIgnoreDts').classList.add('hide');
}
if (supportsTunerCount) {
view.querySelector('.fldTunerCount').classList.remove('hide');
view.querySelector('.txtTunerCount').setAttribute('required', 'required');

View file

@ -92,6 +92,7 @@
<div class="pageTabContent" id="songsTab" data-index="5">
<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" title="${Shuffle}"><span class="material-icons shuffle" 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>

View file

@ -8,197 +8,207 @@ import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import Dashboard from '../../utils/dashboard';
import {playbackManager} from '../../components/playback/playbackmanager';
/* eslint-disable indent */
export default function (view, params, tabContent) {
function getPageData(context) {
const key = getSavedQueryKey(context);
let pageData = data[key];
export default function (view, params, tabContent) {
function getPageData(context) {
const key = getSavedQueryKey(context);
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'Album,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Audio',
Recursive: true,
Fields: 'AudioInfo,ParentId',
StartIndex: 0,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary'
}
};
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'Album,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Audio',
Recursive: true,
Fields: 'AudioInfo,ParentId',
StartIndex: 0,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary'
}
};
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
}
return pageData;
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
function getQuery(context) {
return getPageData(context).query;
}
function getSavedQueryKey(context) {
if (!context.savedQueryKey) {
context.savedQueryKey = libraryBrowser.getSavedQueryKey('songs');
}
return context.savedQueryKey;
}
function reloadItems(page) {
loading.show();
isLoading = true;
const query = getQuery(page);
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (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);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
const html = listView.getListViewHtml({
items: result.Items,
action: 'playallfromhere',
smallIcon: true,
artist: true,
addToListButton: true
});
let elems = tabContent.querySelectorAll('.paging');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onPreviousPageClick);
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
autoFocuser.autoFocus(page);
});
});
}
const self = this;
const data = {};
let isLoading = false;
self.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
const filterDialog = new filterDialogFactory({
query: getQuery(tabContent),
mode: 'songs',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
});
filterDialog.show();
});
};
self.getCurrentViewStyle = function () {
return getPageData(tabContent).view;
};
function initPage(tabContent) {
tabContent.querySelector('.btnFilter').addEventListener('click', function () {
self.showFilterMenu();
});
tabContent.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('OptionTrackName'),
id: 'Name'
}, {
name: globalize.translate('Album'),
id: 'Album,SortName'
}, {
name: globalize.translate('AlbumArtist'),
id: 'AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Artist'),
id: 'Artist,Album,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Runtime'),
id: 'Runtime,AlbumArtist,Album,SortName'
}],
callback: function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
},
query: getQuery(tabContent),
button: e.target
});
});
}
initPage(tabContent);
self.renderTab = function () {
reloadItems(tabContent);
};
return pageData;
}
/* eslint-enable indent */
function getQuery(context) {
return getPageData(context).query;
}
function getSavedQueryKey(context) {
if (!context.savedQueryKey) {
context.savedQueryKey = libraryBrowser.getSavedQueryKey('songs');
}
return context.savedQueryKey;
}
function reloadItems(page) {
loading.show();
isLoading = true;
const query = getQuery(page);
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (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);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
const html = listView.getListViewHtml({
items: result.Items,
action: 'playallfromhere',
smallIcon: true,
artist: true,
addToListButton: true
});
let elems = tabContent.querySelectorAll('.paging');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onPreviousPageClick);
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1);
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
autoFocuser.autoFocus(page);
});
});
}
const self = this;
const data = {};
let isLoading = false;
self.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
const filterDialog = new filterDialogFactory({
query: getQuery(tabContent),
mode: 'songs',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
});
filterDialog.show();
});
};
function shuffle() {
ApiClient.getItem(ApiClient.getCurrentUserId(), params.topParentId).then(function (item) {
playbackManager.shuffle(item);
});
}
self.getCurrentViewStyle = function () {
return getPageData(tabContent).view;
};
function initPage(tabContent) {
tabContent.querySelector('.btnFilter').addEventListener('click', function () {
self.showFilterMenu();
});
tabContent.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('OptionTrackName'),
id: 'Name'
}, {
name: globalize.translate('Album'),
id: 'Album,SortName'
}, {
name: globalize.translate('AlbumArtist'),
id: 'AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Artist'),
id: 'Artist,Album,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Runtime'),
id: 'Runtime,AlbumArtist,Album,SortName'
}, {
name: globalize.translate('OptionRandom'),
id: 'Random,SortName'
}],
callback: function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
},
query: getQuery(tabContent),
button: e.target
});
});
tabContent.querySelector('.btnShuffle').addEventListener('click', shuffle);
}
initPage(tabContent);
self.renderTab = function () {
reloadItems(tabContent);
};
}

View file

@ -3,7 +3,7 @@
<div class="readOnlyContent" style="margin: 0 auto;">
<div class="verticalSection verticalSection-extrabottompadding">
<h2 class="sectionTitle headerUsername" style="padding-left:.25em;"></h2>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkMyProfile listItem-border">
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkUserProfile listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent person" aria-hidden="true"></span>
<div class="listItemBody">

View file

@ -26,7 +26,7 @@ export default function (view, params) {
const userId = params.userId || Dashboard.getCurrentUserId();
const page = this;
page.querySelector('.lnkMyProfile').setAttribute('href', '#/myprofile.html?userId=' + userId);
page.querySelector('.lnkUserProfile').setAttribute('href', '#/userprofile.html?userId=' + userId);
page.querySelector('.lnkDisplayPreferences').setAttribute('href', '#/mypreferencesdisplay.html?userId=' + userId);
page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId);
page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId);

View file

@ -1,3 +0,0 @@
<div id="userProfilePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
</div>

View file

@ -0,0 +1,41 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createButtonElement = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => ({
__html: `<button
is="emby-button"
type="${type}"
${id}
class="${className}"
>
${leftIcon}
<span>${title}</span>
${rightIcon}
</button>`
});
type IProps = {
type?: string;
id?: string;
className?: string;
title?: string;
leftIcon?: string;
rightIcon?: string;
}
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
id: id ? `id="${id}"` : '',
className: className,
title: globalize.translate(title),
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
})}
/>
);
};
export default ButtonElement;

View file

@ -0,0 +1,57 @@
import escapeHTML from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, className, id, dataFilter, dataItemType, dataId, checkedAttribute, renderContent }: { labelClassName?: string, type?: string, className?: string, id?: string, dataFilter?: string, dataItemType?: string, dataId?: string, checkedAttribute?: string, renderContent?: string }) => ({
__html: `<label ${labelClassName}>
<input
is="emby-checkbox"
type="checkbox"
class="${className}"
${id}
${dataFilter}
${dataItemType}
${dataId}
${checkedAttribute}
/>
${renderContent}
</label>`
});
type IProps = {
labelClassName?: string;
className?: string;
elementId?: string;
dataFilter?: string;
itemType?: string;
itemId?: string;
itemAppName?: string;
itemCheckedAttribute?: string;
itemName?: string
title?: string
}
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, className, elementId, dataFilter, itemType, itemId, itemAppName, itemCheckedAttribute, itemName, title }: IProps) => {
const appName = itemAppName ? `- ${itemAppName}` : '';
const renderContent = itemName ?
`<span>${escapeHTML(itemName || '')} ${appName}</span>` :
`<span>${globalize.translate(title)}</span>`;
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
labelClassName: labelClassName ? `class='${labelClassName}'` : '',
className: className,
id: elementId ? `id='${elementId}'` : '',
dataFilter: dataFilter ? `data-filter='${dataFilter}'` : '',
dataItemType: itemType ? `data-itemtype='${itemType}'` : '',
dataId: itemId ? `data-id='${itemId}'` : '',
checkedAttribute: itemCheckedAttribute ? itemCheckedAttribute : '',
renderContent: renderContent
})}
/>
);
};
export default CheckBoxElement;

View file

@ -0,0 +1,47 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
type IProps = {
is?: string;
id?: string;
title?: string;
className?: string;
icon?: string,
dataIndex?: string | number;
dataTag?: string | number;
dataProfileid?: string | number;
}
const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => ({
__html: `<button
is="${is}"
type="button"
${id}
class="${className}"
${title}
${dataIndex}
${dataTag}
${dataProfileid}
>
<span class="material-icons ${icon}" aria-hidden="true"></span>
</button>`
});
const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createIconButtonElement({
is: is,
id: id ? `id="${id}"` : '',
className: className,
title: title ? `title="${globalize.translate(title)}"` : '',
icon: icon,
dataIndex: dataIndex ? `data-index="${dataIndex}"` : '',
dataTag: dataTag ? `data-tag="${dataTag}"` : '',
dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : ''
})}
/>
);
};
export default IconButtonElement;

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
import globalize from '../scripts/globalize';
const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({
__html: `<input

View file

@ -0,0 +1,41 @@
import React, { FunctionComponent } from 'react';
import IconButtonElement from './IconButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement';
type IProps = {
SectionClassName?: string;
title?: string;
isBtnVisible?: boolean;
btnId?: string;
btnClassName?: string;
btnTitle?: string;
btnIcon?: string;
isLinkVisible?: boolean;
url?: string;
}
const SectionTitleContainer: FunctionComponent<IProps> = ({SectionClassName, title, isBtnVisible = false, btnId, btnClassName, btnTitle, btnIcon, isLinkVisible = true, url}: IProps) => {
return (
<div className={`${SectionClassName} sectionTitleContainer flex align-items-center`}>
<h2 className='sectionTitle'>
{title}
</h2>
{isBtnVisible && <IconButtonElement
is='emby-button'
id={btnId}
className={btnClassName}
title={btnTitle}
icon={btnIcon}
/>}
{isLinkVisible && <SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url={url}
/>}
</div>
);
};
export default SectionTitleContainer;

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
import globalize from '../scripts/globalize';
const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
__html: `<a

View file

@ -0,0 +1,38 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createSelectElement = ({ name, id, required, label, option }: { name?: string, id?: string, required?: string, label?: string, option?: React.ReactNode }) => ({
__html: `<select
is="emby-select"
${name}
id="${id}"
${required}
label="${label}"
>
${option}
</select>`
});
type IProps = {
name?: string;
id?: string;
required?: string;
label?: string;
children?: React.ReactNode
}
const SelectElement: FunctionComponent<IProps> = ({ name, id, required, label, children }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createSelectElement({
name: name ? `name='${name}'` : '',
id: id,
required: required ? `required='${required}'` : '',
label: globalize.translate(label),
option: children
})}
/>
);
};
export default SelectElement;

View file

@ -45,6 +45,9 @@ const EmbyScrollButtonsPrototype = Object.create(HTMLDivElement.prototype);
if (scrollWidth <= scrollSize + 20) {
scrollButtons.scrollButtonsLeft.classList.add('hide');
scrollButtons.scrollButtonsRight.classList.add('hide');
} else {
scrollButtons.scrollButtonsLeft.classList.remove('hide');
scrollButtons.scrollButtonsRight.classList.remove('hide');
}
if (scrollPos > 0) {

View file

@ -4,7 +4,9 @@
.emby-scroller {
margin-left: 3.3%;
margin-left: max(env(safe-area-inset-left), 3.3%);
margin-right: 3.3%;
margin-right: max(env(safe-area-inset-right), 3.3%);
}
/* align first card in scroller to heading */
@ -21,7 +23,9 @@
.layout-tv .emby-scroller,
.layout-mobile .emby-scroller {
padding-left: 3.3%;
padding-left: max(env(safe-area-inset-left), 3.3%);
padding-right: 3.3%;
padding-right: max(env(safe-area-inset-right), 3.3%);
margin-left: 0;
margin-right: 0;
}

View file

@ -75,13 +75,27 @@
background-color: transparent !important;
}
.mouseIdle,
.mouseIdle button,
.mouseIdle select,
.mouseIdle input,
.mouseIdle textarea,
.mouseIdle a,
.mouseIdle label {
.layout-tv .mouseIdle,
.layout-tv .mouseIdle button,
.layout-tv .mouseIdle select,
.layout-tv .mouseIdle input,
.layout-tv .mouseIdle textarea,
.layout-tv .mouseIdle a,
.layout-tv .mouseIdle label,
.transparentDocument .mouseIdle,
.transparentDocument .mouseIdle button,
.transparentDocument .mouseIdle select,
.transparentDocument .mouseIdle input,
.transparentDocument .mouseIdle textarea,
.transparentDocument .mouseIdle a,
.transparentDocument .mouseIdle label,
.screensaver-noScroll.mouseIdle,
.screensaver-noScroll.mouseIdle button,
.screensaver-noScroll.mouseIdle select,
.screensaver-noScroll.mouseIdle input,
.screensaver-noScroll.mouseIdle textarea,
.screensaver-noScroll.mouseIdle a,
.screensaver-noScroll.mouseIdle label {
cursor: none !important;
}
@ -102,6 +116,7 @@
bottom: 0;
z-index: 1;
width: 0.8em;
padding-left: env(safe-area-inset-left);
}
@-webkit-keyframes fadein {

View file

@ -4,6 +4,8 @@
top: 0;
bottom: 0;
contain: strict;
box-sizing: border-box;
padding-left: env(safe-area-inset-left);
}
.touch-menu-la {

View file

@ -42,6 +42,12 @@
#dialogToc {
background-color: white;
height: fit-content;
width: fit-content;
max-height: 80%;
max-width: 60%;
padding-right: 50px;
padding-bottom: 15px;
.bookplayerButtonIcon {
color: black;
@ -49,5 +55,19 @@
.toc li {
margin-bottom: 5px;
list-style-type: none;
font-size: 120%;
a:link {
color: #000;
text-decoration: none;
}
a:active,
a:hover {
color: #00a4dc;
text-decoration: none;
}
}
}

View file

@ -12,5 +12,6 @@
.swiper-slide-img {
max-height: 100%;
max-width: 100%;
}
}

View file

@ -1150,8 +1150,7 @@ function tryRemoveElement(elem) {
return true;
}
// This is unfortunate, but we're unable to remove the textTrack that gets added via addTextTrack
if (browser.firefox || browser.web0s) {
if (browser.web0s) {
return true;
}

View file

@ -7,6 +7,10 @@
display: flex;
align-items: center;
background: #000 !important;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.videoPlayerContainer-onTop {
@ -58,6 +62,9 @@ video[controls]::-webkit-media-controls {
right: 0;
color: #fff;
font-size: 170%;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
.videoSubtitlesInner {

View file

@ -7,7 +7,6 @@ import { appRouter } from '../../components/appRouter';
import './style.scss';
import '../../elements/emby-button/paper-icon-button-light';
import { Events } from 'jellyfin-apiclient';
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
export class PdfPlayer {
constructor() {
@ -200,14 +199,14 @@ export class PdfPlayer {
const serverId = item.ServerId;
const apiClient = ServerConnections.getApiClient(serverId);
return new Promise((resolve) => {
return import('pdfjs-dist').then(({ GlobalWorkerOptions, getDocument }) => {
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
this.bindEvents();
GlobalWorkerOptions.workerSrc = appRouter.baseUrl() + '/libraries/pdf.worker.js';
const downloadTask = getDocument(downloadHref);
downloadTask.promise.then(book => {
return downloadTask.promise.then(book => {
if (this.cancellationToken) return;
this.book = book;
this.loaded = true;
@ -219,8 +218,6 @@ export class PdfPlayer {
} else {
this.loadPage(1);
}
return resolve();
});
});
}

View file

@ -7,6 +7,10 @@
right: 0;
display: flex;
align-items: center;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.youtubePlayerContainer.onTop {

177
src/routes/home.tsx Normal file
View file

@ -0,0 +1,177 @@
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
import globalize from '../scripts/globalize';
import LibraryMenu from '../scripts/libraryMenu';
import { clearBackdrop } from '../components/backdrop/backdrop';
import layoutManager from '../components/layoutManager';
import * as mainTabsManager from '../components/maintabsmanager';
import '../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button';
import '../elements/emby-scroller/emby-scroller';
import Page from '../components/Page';
type IProps = {
tab?: string;
}
type OnResumeOptions = {
autoFocus?: boolean;
refresh?: boolean
}
type ControllerProps = {
onResume: (
options: OnResumeOptions
) => void;
refreshed: boolean;
onPause: () => void;
destroy: () => void;
}
const Home: FunctionComponent<IProps> = (props: IProps) => {
const getDefaultTabIndex = () => {
return 0;
};
const tabController = useRef<ControllerProps | null>();
const currentTabIndex = useRef(parseInt(props.tab || getDefaultTabIndex().toString()));
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
const initialTabIndex = useRef<number | null>(currentTabIndex.current);
const element = useRef<HTMLDivElement>(null);
const setTitle = () => {
LibraryMenu.setTitle(null);
};
const getTabs = () => {
return [{
name: globalize.translate('Home')
}, {
name: globalize.translate('Favorites')
}];
};
const getTabContainers = () => {
return element.current?.querySelectorAll('.tabContent');
};
const getTabController = useCallback((index: number) => {
if (index == null) {
throw new Error('index cannot be null');
}
let depends = '';
switch (index) {
case 0:
depends = 'hometab';
break;
case 1:
depends = 'favorites';
}
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = tabControllers[index];
if (!controller) {
const tabContent = element.current?.querySelector(".tabContent[data-index='" + index + "']");
controller = new controllerFactory(tabContent, props);
tabControllers[index] = controller;
}
return controller;
});
}, [props, tabControllers]);
const onViewDestroy = useCallback(() => {
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
}
tabController.current = null;
initialTabIndex.current = null;
}, [tabControllers]);
const loadTab = useCallback((index: number, previousIndex: number | null) => {
getTabController(index).then((controller) => {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
currentTabIndex.current = index;
tabController.current = controller;
});
}, [getTabController]);
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; previousIndex: number | null }; }) => {
const newIndex = parseInt(e.detail.selectedTabIndex);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}, [loadTab, tabControllers]);
const onResume = useCallback(() => {
setTitle();
clearBackdrop();
const currentTabController = tabController.current;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(initialTabIndex.current);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.add('noHomeButtonHeader');
}, []);
const onPause = useCallback(() => {
const currentTabController = tabController.current;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.remove('noHomeButtonHeader');
}, []);
useEffect(() => {
mainTabsManager.setTabs(element.current, currentTabIndex.current, getTabs, getTabContainers, null, onTabChange, false);
onResume();
return () => {
onPause();
onViewDestroy();
};
}, [onPause, onResume, onTabChange, onViewDestroy]);
return (
<div ref={element}>
<Page
id='indexPage'
className='mainAnimatedPage homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs'
isBackButtonEnabled={false}
backDropType='movie,series,book'
>
<div className='tabContent pageTabContent' id='homeTab' data-index='0'>
<div className='sections'></div>
</div>
<div className='tabContent pageTabContent' id='favoritesTab' data-index='1'>
<div className='sections'></div>
</div>
</Page>
</div>
);
};
export default Home;

View file

@ -2,19 +2,36 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';
import ConnectionRequired from '../components/ConnectionRequired';
import SearchPage from './search';
import UserNew from './user/usernew';
import Search from './search';
import UserEdit from './user/useredit';
import UserLibraryAccess from './user/userlibraryaccess';
import UserParentalControl from './user/userparentalcontrol';
import UserPassword from './user/userpassword';
import UserProfile from './user/userprofile';
import UserProfiles from './user/userprofiles';
import Home from './home';
const AppRoutes = () => (
<Routes>
<Route path='/'>
<Route
path='search.html'
element={
<ConnectionRequired>
<SearchPage />
</ConnectionRequired>
}
/>
{/* User routes */}
<Route path='/' element={<ConnectionRequired />}>
<Route path='search.html' element={<Search />} />
<Route path='userprofile.html' element={<UserProfile />} />
<Route path='home.html' element={<Home />} />
</Route>
{/* Admin routes */}
<Route path='/' element={<ConnectionRequired isAdminRequired={true} />}>
<Route path='usernew.html' element={<UserNew />} />
<Route path='userprofiles.html' element={<UserProfiles />} />
<Route path='useredit.html' element={<UserEdit />} />
<Route path='userlibraryaccess.html' element={<UserLibraryAccess />} />
<Route path='userparentalcontrol.html' element={<UserParentalControl />} />
<Route path='userpassword.html' element={<UserPassword />} />
</Route>
{/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} />
</Route>

View file

@ -8,7 +8,7 @@ import SearchSuggestions from '../components/search/SearchSuggestions';
import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
import globalize from '../scripts/globalize';
const SearchPage: FunctionComponent = () => {
const Search: FunctionComponent = () => {
const [ query, setQuery ] = useState<string>();
const [ searchParams ] = useSearchParams();
@ -41,4 +41,4 @@ const SearchPage: FunctionComponent = () => {
);
};
export default SearchPage;
export default Search;

View file

@ -1,32 +1,35 @@
import { SyncPlayUserAccessType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import ButtonElement from '../dashboard/users/ButtonElement';
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import InputElement from '../dashboard/users/InputElement';
import LinkEditUserPreferences from '../dashboard/users/LinkEditUserPreferences';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import SelectElement from '../dashboard/users/SelectElement';
import SelectSyncPlayAccessElement from '../dashboard/users/SelectSyncPlayAccessElement';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
import ButtonElement from '../../elements/ButtonElement';
import CheckBoxElement from '../../elements/CheckBoxElement';
import InputElement from '../../elements/InputElement';
import LinkEditUserPreferences from '../../components/dashboard/users/LinkEditUserPreferences';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import escapeHTML from 'escape-html';
import SelectElement from '../../elements/SelectElement';
import Page from '../../components/Page';
type ItemsArr = {
Name?: string;
Id?: string;
type ResetProvider = AuthProvider & {
checkedAttribute: string
}
const UserEditPage: FunctionComponent = () => {
type AuthProvider = {
Name?: string;
Id?: string;
}
const UserEdit: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ItemsArr[]>([]);
const [ authProviders, setAuthProviders ] = useState([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
@ -91,7 +94,7 @@ const UserEditPage: FunctionComponent = () => {
})).then(function (channelsResult) {
let isChecked;
let checkedAttribute;
const itemsArr: ItemsArr[] = [];
const itemsArr: ResetProvider[] = [];
for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
@ -172,7 +175,7 @@ const UserEditPage: FunctionComponent = () => {
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
if (window.ApiClient.isMinServerVersion('10.6.0')) {
(page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value = user.Policy.SyncPlayAccess;
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
}
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
@ -227,8 +230,8 @@ const UserEditPage: FunctionComponent = () => {
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0'));
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0');
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0');
user.Policy.AuthenticationProviderId = (page.querySelector('.selectLoginProvider') as HTMLInputElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('.selectPasswordResetProvider') as HTMLInputElement).value;
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked;
@ -236,7 +239,7 @@ const UserEditPage: FunctionComponent = () => {
return c.getAttribute('data-id');
});
if (window.ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value as SyncPlayUserAccessType;
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
}
window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
@ -263,25 +266,49 @@ const UserEditPage: FunctionComponent = () => {
}
});
window.ApiClient.getServerConfiguration().then(function (config) {
window.ApiClient.getNamedConfiguration('network').then(function (config) {
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
});
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadData]);
const optionLoginProvider = authProviders.map((provider) => {
const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionPasswordResetProvider = passwordResetProviders.map((provider) => {
const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionSyncPlayAccess = () => {
let content = '';
content += `<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>`;
content += `<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>`;
content += `<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>`;
return content;
};
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='editUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='useredit'/>
<div
className='lnkEditUserPreferencesContainer'
@ -313,29 +340,29 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement
className= 'selectLoginProvider'
label= 'LabelAuthProvider'
currentProviderId={authenticationProviderId}
providers={authProviders}
/>
id='selectLoginProvider'
label='LabelAuthProvider'
>
{optionLoginProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('AuthProviderHelp')}
</div>
</div>
<div className='selectContainer fldSelectPasswordResetProvider hide'>
<SelectElement
className= 'selectPasswordResetProvider'
label= 'LabelPasswordResetProvider'
currentProviderId={passwordResetProviderId}
providers={passwordResetProviders}
/>
id='selectPasswordResetProvider'
label='LabelPasswordResetProvider'
>
{optionPasswordResetProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('PasswordResetProviderHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
<CheckBoxElement
type='checkbox'
className='chkRemoteAccess'
title='AllowRemoteAccess'
/>
@ -345,7 +372,6 @@ const UserEditPage: FunctionComponent = () => {
</div>
<CheckBoxElement
labelClassName='checkboxContainer'
type='checkbox'
className='chkIsAdmin'
title='OptionAllowUserToManageServer'
/>
@ -355,12 +381,10 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement
type='checkbox'
className='chkEnableLiveTvAccess'
title='OptionAllowBrowsingLiveTv'
/>
<CheckBoxElement
type='checkbox'
className='chkManageLiveTv'
title='OptionAllowManageLiveTv'
/>
@ -372,27 +396,22 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement
type='checkbox'
className='chkEnableMediaPlayback'
title='OptionAllowMediaPlayback'
/>
<CheckBoxElement
type='checkbox'
className='chkEnableAudioPlaybackTranscoding'
title='OptionAllowAudioPlaybackTranscoding'
/>
<CheckBoxElement
type='checkbox'
className='chkEnableVideoPlaybackTranscoding'
title='OptionAllowVideoPlaybackTranscoding'
/>
<CheckBoxElement
type='checkbox'
className='chkEnableVideoPlaybackRemuxing'
title='OptionAllowVideoPlaybackRemuxing'
/>
<CheckBoxElement
type='checkbox'
className='chkForceRemoteSourceTranscoding'
title='OptionForceRemoteSourceTranscoding'
/>
@ -420,11 +439,12 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectSyncPlayAccess'>
<SelectSyncPlayAccessElement
className='selectSyncPlayAccess'
<SelectElement
id='selectSyncPlayAccess'
label='LabelSyncPlayAccess'
/>
>
{optionSyncPlayAccess()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('SyncPlayAccessHelp')}
</div>
@ -437,18 +457,17 @@ const UserEditPage: FunctionComponent = () => {
<div className='checkboxList paperList checkboxList-paperList'>
<CheckBoxElement
labelClassName='checkboxContainer'
type='checkbox'
className='chkEnableDeleteAllFolders'
title='AllLibraries'
/>
<div className='deleteAccess'>
{deleteFoldersAccess.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</div>
@ -460,12 +479,10 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement
type='checkbox'
className='chkEnableRemoteControlOtherUsers'
title='OptionAllowRemoteControlOthers'
/>
<CheckBoxElement
type='checkbox'
className='chkRemoteControlSharedDevices'
title='OptionAllowRemoteSharedDevices'
/>
@ -479,7 +496,6 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
type='checkbox'
className='chkEnableDownloading'
title='OptionAllowContentDownload'
/>
@ -489,7 +505,6 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
<CheckBoxElement
type='checkbox'
className='chkDisabled'
title='OptionDisableUser'
/>
@ -499,7 +514,6 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
<CheckBoxElement
type='checkbox'
className='chkIsHidden'
title='OptionHideUser'
/>
@ -550,14 +564,16 @@ const UserEditPage: FunctionComponent = () => {
/>
<ButtonElement
type='button'
className='raised button-cancel block btnCancel'
id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel'
/>
</div>
</form>
</div>
</div>
</Page>
);
};
export default UserEditPage;
export default UserEdit;

View file

@ -1,16 +1,17 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import loading from '../loading/loading';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import toast from '../toast/toast';
import SectionTabs from '../dashboard/users/SectionTabs';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import ButtonElement from '../dashboard/users/ButtonElement';
import toast from '../../components/toast/toast';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import ButtonElement from '../../elements/ButtonElement';
import { getParameterByName } from '../../utils/url';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import AccessContainer from '../dashboard/users/AccessContainer';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import AccessContainer from '../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement';
import Page from '../../components/Page';
type ItemsArr = {
Name?: string;
@ -19,7 +20,7 @@ type ItemsArr = {
checkedAttribute?: string
}
const UserLibraryAccessPage: FunctionComponent = () => {
const UserLibraryAccess: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
@ -226,12 +227,17 @@ const UserLibraryAccessPage: FunctionComponent = () => {
}, [loadData]);
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='userLibraryAccessPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userlibraryaccess'/>
<form className='userLibraryAccessForm'>
<AccessContainer
@ -245,12 +251,12 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
@ -266,12 +272,12 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkChannel'
Id={Item.Id}
Name={Item.Name}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
@ -287,13 +293,13 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='DeviceAccessHelp'
>
{devicesItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkDevice'
Id={Item.Id}
Name={Item.Name}
AppName={Item.AppName}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemAppName={Item.AppName}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
@ -307,8 +313,9 @@ const UserLibraryAccessPage: FunctionComponent = () => {
</div>
</form>
</div>
</div>
</Page>
);
};
export default UserLibraryAccessPage;
export default UserLibraryAccess;

View file

@ -2,13 +2,14 @@ import React, { FunctionComponent, useCallback, useEffect, useState, useRef } fr
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import toast from '../toast/toast';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import InputElement from '../dashboard/users/InputElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import ButtonElement from '../dashboard/users/ButtonElement';
import AccessContainer from '../dashboard/users/AccessContainer';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import InputElement from '../../elements/InputElement';
import ButtonElement from '../../elements/ButtonElement';
import AccessContainer from '../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement';
import Page from '../../components/Page';
type userInput = {
Name?: string;
@ -20,7 +21,7 @@ type ItemsArr = {
Id?: string;
}
const NewUserPage: FunctionComponent = () => {
const UserNew: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
@ -169,18 +170,24 @@ const NewUserPage: FunctionComponent = () => {
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadUser]);
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={globalize.translate('HeaderAddUser')}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='newUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderAddUser')}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<form className='newUserProfileForm'>
<div className='inputContainer'>
<InputElement
@ -208,12 +215,11 @@ const NewUserPage: FunctionComponent = () => {
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute=''
itemId={Item.Id}
itemName={Item.Name}
/>
))}
</AccessContainer>
@ -229,12 +235,11 @@ const NewUserPage: FunctionComponent = () => {
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkChannel'
Id={Item.Id}
Name={Item.Name}
checkedAttribute=''
itemId={Item.Id}
itemName={Item.Name}
/>
))}
</AccessContainer>
@ -246,14 +251,16 @@ const NewUserPage: FunctionComponent = () => {
/>
<ButtonElement
type='button'
className='raised button-cancel block btnCancel'
id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel'
/>
</div>
</form>
</div>
</div>
</Page>
);
};
export default NewUserPage;
export default UserNew;

View file

@ -1,25 +1,22 @@
import { AccessSchedule, DynamicDayOfWeek, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import AccessScheduleList from '../dashboard/users/AccessScheduleList';
import BlockedTagList from '../dashboard/users/BlockedTagList';
import ButtonElement from '../dashboard/users/ButtonElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
import AccessScheduleList from '../../components/dashboard/users/AccessScheduleList';
import BlockedTagList from '../../components/dashboard/users/BlockedTagList';
import ButtonElement from '../../elements/ButtonElement';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import CheckBoxElement from '../../elements/CheckBoxElement';
import escapeHTML from 'escape-html';
import SelectElement from '../../elements/SelectElement';
import Page from '../../components/Page';
type RatingsArr = {
Name: string;
Value: number;
}
type ItemsArr = {
type UnratedItem = {
name: string;
value: string;
checkedAttribute: string
@ -27,8 +24,8 @@ type ItemsArr = {
const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<RatingsArr[]>([]);
const [ unratedItems, setUnratedItems ] = useState<ItemsArr[]>([]);
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]);
@ -36,7 +33,7 @@ const UserParentalControl: FunctionComponent = () => {
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: RatingsArr[] = [];
const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
@ -90,7 +87,7 @@ const UserParentalControl: FunctionComponent = () => {
value: 'Series'
}];
const itemsArr: ItemsArr[] = [];
const itemsArr: UnratedItem[] = [];
for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
@ -181,7 +178,7 @@ const UserParentalControl: FunctionComponent = () => {
}
}
(page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value = ratingValue;
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
@ -226,7 +223,7 @@ const UserParentalControl: FunctionComponent = () => {
throw new Error('Unexpected null user.Policy');
}
user.Policy.MaxParentalRating = parseInt((page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value || '0', 10) || null;
user.Policy.MaxParentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value || '0', 10) || null;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
return i.checked;
}).map(function (i) {
@ -299,7 +296,7 @@ const UserParentalControl: FunctionComponent = () => {
return false;
};
(page.querySelector('.btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
@ -309,28 +306,43 @@ const UserParentalControl: FunctionComponent = () => {
}, -1);
});
(page.querySelector('.btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]);
const optionMaxParentalRating = () => {
let content = '';
content += '<option value=\'\'></option>';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHTML(rating.Name)}</option>`;
}
return content;
};
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectMaxParentalRating
className= 'selectMaxParentalRating'
label= 'LabelMaxParentalRating'
parentalRatings={parentalRatings}
/>
<SelectElement
id='selectMaxParentalRating'
label='LabelMaxParentalRating'
>
{optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
@ -342,12 +354,12 @@ const UserParentalControl: FunctionComponent = () => {
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxListItem
return <CheckBoxElement
key={Item.value}
className='chkUnratedItem'
ItemType={Item.value}
Name={Item.name}
checkedAttribute={Item.checkedAttribute}
itemType={Item.value}
itemName={Item.name}
itemCheckedAttribute={Item.checkedAttribute}
/>;
})}
</div>
@ -355,19 +367,16 @@ const UserParentalControl: FunctionComponent = () => {
</div>
<br />
<div className='verticalSection' style={{marginBottom: '2em'}}>
<div
className='detailSectionHeader sectionTitleContainer'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle'>
{globalize.translate('LabelBlockContentWithTags')}
</h2>
<SectionTitleButtonElement
className='fab btnAddBlockedTag submit'
title='Add'
icon='add'
/>
</div>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelBlockContentWithTags')}
isBtnVisible={true}
btnId='btnAddBlockedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<div className='blockedTags' style={{marginTop: '.5em'}}>
{blockedTags.map((tag, index) => {
return <BlockedTagList
@ -378,19 +387,15 @@ const UserParentalControl: FunctionComponent = () => {
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
<div
className='sectionTitleContainer'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle'>
{globalize.translate('HeaderAccessSchedule')}
</h2>
<SectionTitleButtonElement
className='fab btnAddSchedule submit'
title='Add'
icon='add'
/>
</div>
<SectionTitleContainer
title={globalize.translate('HeaderAccessSchedule')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
@ -414,7 +419,8 @@ const UserParentalControl: FunctionComponent = () => {
</div>
</form>
</div>
</div>
</Page>
);
};

View file

@ -1,19 +1,23 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import SectionTabs from '../dashboard/users/SectionTabs';
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
import { getParameterByName } from '../../utils/url';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import Page from '../../components/Page';
import loading from '../../components/loading/loading';
const UserPasswordPage: FunctionComponent = () => {
const UserPassword: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState('');
const loadUser = useCallback(() => {
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {
throw new Error('Unexpected null user.Name');
}
setUserName(user.Name);
loading.hide();
});
}, [userId]);
useEffect(() => {
@ -21,12 +25,17 @@ const UserPasswordPage: FunctionComponent = () => {
}, [loadUser]);
return (
<div>
<Page
id='userPasswordPage'
className='mainAnimatedPage type-interior userPasswordPage'
>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'>
<UserPasswordForm
@ -34,8 +43,9 @@ const UserPasswordPage: FunctionComponent = () => {
/>
</div>
</div>
</div>
</Page>
);
};
export default UserPasswordPage;
export default UserPassword;

View file

@ -1,21 +1,21 @@
import { ImageType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import { appHost } from '../apphost';
import confirm from '../confirm/confirm';
import ButtonElement from '../dashboard/users/ButtonElement';
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
import loading from '../loading/loading';
import toast from '../toast/toast';
import { appHost } from '../../components/apphost';
import confirm from '../../components/confirm/confirm';
import ButtonElement from '../../elements/ButtonElement';
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import Page from '../../components/Page';
type IProps = {
userId: string;
}
const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
const UserProfile: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState('');
const element = useRef<HTMLDivElement>(null);
@ -57,11 +57,11 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
}
if (user.PrimaryImageTag) {
(page.querySelector('.btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
} else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('.btnAddImage') as HTMLButtonElement).classList.remove('hide');
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
}
});
loading.hide();
@ -120,7 +120,7 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
reader.readAsDataURL(file);
};
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
confirm(
globalize.translate('DeleteImageConfirmation'),
globalize.translate('DeleteImage')
@ -133,7 +133,7 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
});
});
(page.querySelector('.btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement;
uploadImage.value = '';
uploadImage.click();
@ -145,13 +145,18 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
}, [reloadUser, userId]);
return (
<div ref={element}>
<div className='padded-left padded-right padded-bottom-page'>
<Page
id='userProfilePage'
title={globalize.translate('Profile')}
className='mainAnimatedPage libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage'
>
<div ref={element} className='padded-left padded-right padded-bottom-page'>
<div
className='readOnlyContent'
style={{margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center'}}
>
<div
className='imagePlaceHolder'
style={{position: 'relative', display: 'inline-block', maxWidth: 200 }}
>
<input
@ -172,12 +177,14 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
<br />
<ButtonElement
type='button'
className='raised btnAddImage hide'
id='btnAddImage'
className='raised button-submit hide'
title='ButtonAddImage'
/>
<ButtonElement
type='button'
className='raised btnDeleteImage hide'
id='btnDeleteImage'
className='raised hide'
title='DeleteImage'
/>
</div>
@ -186,8 +193,9 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
userId={userId}
/>
</div>
</div>
</Page>
);
};
export default UserProfilePage;
export default UserProfile;

View file

@ -1,17 +1,18 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import loading from '../../components/loading/loading';
import dom from '../../scripts/dom';
import confirm from '../../components/confirm/confirm';
import UserCardBox from '../dashboard/users/UserCardBox';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import UserCardBox from '../../components/dashboard/users/UserCardBox';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import '../../elements/emby-button/emby-button';
import '../../elements/emby-button/paper-icon-button-light';
import '../../components/cardbuilder/card.scss';
import '../../components/indicators/indicators.scss';
import '../../assets/css/flexstyles.scss';
import Page from '../../components/Page';
type MenuEntry = {
name?: string;
@ -19,7 +20,7 @@ type MenuEntry = {
icon?: string;
}
const UserProfilesPage: FunctionComponent = () => {
const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null);
@ -124,19 +125,28 @@ const UserProfilesPage: FunctionComponent = () => {
}
});
(page.querySelector('.btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
}, []);
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
titleLink='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
/>
<Page
id='userProfilesPage'
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
btnId='btnAddUser'
btnClassName='fab submit sectionTitleButton'
btnTitle='ButtonAddUser'
btnIcon='add'
url='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
/>
</div>
<div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => {
@ -144,8 +154,9 @@ const UserProfilesPage: FunctionComponent = () => {
})}
</div>
</div>
</div>
</Page>
);
};
export default UserProfilesPage;
export default UserProfiles;

View file

@ -390,6 +390,9 @@ import browser from './browser';
if (supportsMp3VideoAudio && (browser.chrome || browser.edgeChromium || (browser.firefox && browser.versionMajor >= 83))) {
supportsMp2VideoAudio = true;
}
if (browser.android) {
supportsMp2VideoAudio = false;
}
}
/* eslint-disable compat/compat */

View file

@ -19,7 +19,7 @@ import globalize from './globalize';
// "00", "00", ".000", "Z", undefined, undefined, undefined]
if (!d) {
throw "Couldn't parse ISO 8601 date string '" + s + "'";
throw new Error("Couldn't parse ISO 8601 date string '" + s + "'");
}
// parse strings, leading zeros into proper ints

View file

@ -355,14 +355,14 @@ import '../assets/css/flexstyles.scss';
}
}
function refreshDashboardInfoInDrawer(apiClient) {
function refreshDashboardInfoInDrawer(page, apiClient) {
currentDrawerType = 'admin';
loadNavDrawer();
if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) {
updateDashboardMenuSelectedItem();
updateDashboardMenuSelectedItem(page);
} else {
createDashboardMenu(apiClient);
createDashboardMenu(page, apiClient);
}
}
@ -370,9 +370,9 @@ import '../assets/css/flexstyles.scss';
return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1;
}
function updateDashboardMenuSelectedItem() {
function updateDashboardMenuSelectedItem(page) {
const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption');
const currentViewId = viewManager.currentView().id;
const currentViewId = page.id;
for (let i = 0, length = links.length; i < length; i++) {
let link = links[i];
@ -590,7 +590,7 @@ import '../assets/css/flexstyles.scss';
});
}
function createDashboardMenu(apiClient) {
function createDashboardMenu(page, apiClient) {
return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) {
let html = '';
html += '<a class="adminDrawerLogo clearLink" is="emby-linkbutton" href="#/home.html">';
@ -598,7 +598,7 @@ import '../assets/css/flexstyles.scss';
html += '</a>';
html += toolsMenuHtml;
navDrawerScrollContainer.innerHTML = html;
updateDashboardMenuSelectedItem();
updateDashboardMenuSelectedItem(page);
});
}
@ -1017,7 +1017,7 @@ import '../assets/css/flexstyles.scss';
mainDrawerButton.classList.remove('hide');
}
refreshDashboardInfoInDrawer(apiClient);
refreshDashboardInfoInDrawer(page, apiClient);
} else {
if (mainDrawerButton) {
if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) {

View file

@ -77,13 +77,6 @@ import { appRouter } from '../components/appRouter';
controller: 'user/menu/index'
});
defineRoute({
alias: '/myprofile.html',
path: 'user/profile/index.html',
autoFocus: false,
pageComponent: 'UserProfilePage'
});
defineRoute({
alias: '/mypreferencescontrols.html',
path: 'user/controls/index.html',
@ -300,14 +293,6 @@ import { appRouter } from '../components/appRouter';
controller: 'dashboard/plugins/repositories/index'
});
defineRoute({
alias: '/home.html',
path: 'home.html',
autoFocus: false,
controller: 'home',
type: 'home'
});
defineRoute({
alias: '/list.html',
path: 'list.html',
@ -429,53 +414,6 @@ import { appRouter } from '../components/appRouter';
controller: 'shows/tvrecommended'
});
defineRoute({
alias: '/useredit.html',
path: 'dashboard/users/useredit.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserEditPage'
});
defineRoute({
alias: '/userlibraryaccess.html',
path: 'dashboard/users/userlibraryaccess.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserLibraryAccessPage'
});
defineRoute({
alias: '/usernew.html',
path: 'dashboard/users/usernew.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'NewUserPage'
});
defineRoute({
alias: '/userparentalcontrol.html',
path: 'dashboard/users/userparentalcontrol.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserParentalControl'
});
defineRoute({
alias: '/userpassword.html',
path: 'dashboard/users/userpassword.html',
autoFocus: false,
pageComponent: 'UserPasswordPage'
});
defineRoute({
alias: '/userprofiles.html',
path: 'dashboard/users/userprofiles.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserProfilesPage'
});
defineRoute({
alias: '/wizardremoteaccess.html',
path: 'wizard/remote/index.html',

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