mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into eslint-no-sequences
This commit is contained in:
commit
a2fe92f192
80 changed files with 2166 additions and 1116 deletions
|
@ -1,20 +1,49 @@
|
|||
import { ConnectionManager, Credentials, ApiClient, Events } from 'jellyfin-apiclient';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import { setUserInfo } from '../scripts/settings/userSettings';
|
||||
import appSettings from '../scripts/settings/appSettings';
|
||||
|
||||
const normalizeImageOptions = options => {
|
||||
if (!options.quality && (options.maxWidth || options.width || options.maxHeight || options.height || options.fillWidth || options.fillHeight)) {
|
||||
options.quality = 90;
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxBandwidth = () => {
|
||||
/* eslint-disable compat/compat */
|
||||
if (navigator.connection) {
|
||||
let max = navigator.connection.downlinkMax;
|
||||
if (max && max > 0 && max < Number.POSITIVE_INFINITY) {
|
||||
max /= 8;
|
||||
max *= 1000000;
|
||||
max *= 0.7;
|
||||
return parseInt(max, 10);
|
||||
}
|
||||
}
|
||||
/* eslint-enable compat/compat */
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
class ServerConnections extends ConnectionManager {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.localApiClient = null;
|
||||
|
||||
Events.on(this, 'localusersignedout', function (eventName, logoutInfo) {
|
||||
Events.on(this, 'localusersignedout', (_e, logoutInfo) => {
|
||||
setUserInfo(null, null);
|
||||
|
||||
if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedOut === 'function') {
|
||||
window.NativeShell.onLocalUserSignedOut(logoutInfo);
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(this, 'apiclientcreated', (_e, apiClient) => {
|
||||
apiClient.getMaxBandwidth = getMaxBandwidth;
|
||||
apiClient.normalizeImageOptions = normalizeImageOptions;
|
||||
});
|
||||
}
|
||||
|
||||
initApiClient(server) {
|
||||
|
@ -38,6 +67,13 @@ class ServerConnections extends ConnectionManager {
|
|||
console.debug('loaded ApiClient singleton');
|
||||
}
|
||||
|
||||
connect(options) {
|
||||
return super.connect({
|
||||
enableAutoLogin: appSettings.enableAutoLogin(),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
setLocalApiClient(apiClient) {
|
||||
if (apiClient) {
|
||||
this.localApiClient = apiClient;
|
||||
|
|
|
@ -2,42 +2,38 @@ import { Events } from 'jellyfin-apiclient';
|
|||
import { Action, createHashHistory } from 'history';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import appSettings from '../scripts/settings/appSettings';
|
||||
import { clearBackdrop, setBackdropTransparency } from './backdrop/backdrop';
|
||||
import globalize from '../scripts/globalize';
|
||||
import itemHelper from './itemHelper';
|
||||
import loading from './loading/loading';
|
||||
import viewManager from './viewManager/viewManager';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import alert from './alert';
|
||||
import reactControllerFactory from './reactControllerFactory';
|
||||
|
||||
const history = createHashHistory();
|
||||
|
||||
/**
|
||||
* Page types of "no return" (when "Go back" should behave differently, probably quitting the application).
|
||||
*/
|
||||
const START_PAGE_TYPES = ['home', 'login', 'selectserver'];
|
||||
|
||||
class AppRouter {
|
||||
allRoutes = new Map();
|
||||
backdropContainer;
|
||||
backgroundContainer;
|
||||
currentRouteInfo;
|
||||
currentViewLoadRequest;
|
||||
firstConnectionResult;
|
||||
forcedLogoutMsg;
|
||||
isDummyBackToHome;
|
||||
msgTimeout;
|
||||
promiseShow;
|
||||
resolveOnNextShow;
|
||||
previousRoute = {};
|
||||
/**
|
||||
* Pages of "no return" (when "Go back" should behave differently, probably quitting the application).
|
||||
*/
|
||||
startPages = ['home', 'login', 'selectserver'];
|
||||
|
||||
constructor() {
|
||||
document.addEventListener('viewshow', () => this.onViewShow());
|
||||
|
||||
// TODO: Can this baseRoute logic be simplified?
|
||||
this.baseRoute = window.location.href.split('?')[0].replace(this.getRequestFile(), '');
|
||||
this.baseRoute = window.location.href.split('?')[0].replace(this.#getRequestFile(), '');
|
||||
// support hashbang
|
||||
this.baseRoute = this.baseRoute.split('#')[0];
|
||||
if (this.baseRoute.endsWith('/') && !this.baseRoute.endsWith('://')) {
|
||||
|
@ -52,33 +48,11 @@ class AppRouter {
|
|||
});
|
||||
}
|
||||
|
||||
showLocalLogin(serverId) {
|
||||
Dashboard.navigate('login.html?serverid=' + serverId);
|
||||
}
|
||||
|
||||
showVideoOsd() {
|
||||
return Dashboard.navigate('video');
|
||||
}
|
||||
|
||||
showSelectServer() {
|
||||
Dashboard.navigate('selectserver.html');
|
||||
}
|
||||
|
||||
showSettings() {
|
||||
Dashboard.navigate('mypreferencesmenu.html');
|
||||
}
|
||||
|
||||
showNowPlaying() {
|
||||
this.show('queue');
|
||||
}
|
||||
|
||||
beginConnectionWizard() {
|
||||
#beginConnectionWizard() {
|
||||
clearBackdrop();
|
||||
loading.show();
|
||||
ServerConnections.connect({
|
||||
enableAutoLogin: appSettings.enableAutoLogin()
|
||||
}).then((result) => {
|
||||
this.handleConnectionResult(result);
|
||||
ServerConnections.connect().then(result => {
|
||||
this.#handleConnectionResult(result);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -128,18 +102,6 @@ class AppRouter {
|
|||
return this.promiseShow;
|
||||
}
|
||||
|
||||
async showDirect(path) {
|
||||
if (this.promiseShow) await this.promiseShow;
|
||||
|
||||
this.promiseShow = new Promise((resolve) => {
|
||||
this.resolveOnNextShow = resolve;
|
||||
// Schedule a call to return the promise
|
||||
setTimeout(() => history.push(this.baseUrl() + path), 0);
|
||||
});
|
||||
|
||||
return this.promiseShow;
|
||||
}
|
||||
|
||||
#goToRoute({ location, action }) {
|
||||
// Strip the leading "!" if present
|
||||
const normalizedPath = location.pathname.replace(/^!/, '');
|
||||
|
@ -163,13 +125,18 @@ class AppRouter {
|
|||
|
||||
start() {
|
||||
loading.show();
|
||||
this.initApiClients();
|
||||
|
||||
Events.on(appHost, 'resume', this.onAppResume);
|
||||
ServerConnections.getApiClients().forEach(apiClient => {
|
||||
Events.off(apiClient, 'requestfail', this.onRequestFail);
|
||||
Events.on(apiClient, 'requestfail', this.onRequestFail);
|
||||
});
|
||||
|
||||
ServerConnections.connect({
|
||||
enableAutoLogin: appSettings.enableAutoLogin()
|
||||
}).then((result) => {
|
||||
Events.on(ServerConnections, 'apiclientcreated', (_e, apiClient) => {
|
||||
Events.off(apiClient, 'requestfail', this.onRequestFail);
|
||||
Events.on(apiClient, 'requestfail', this.onRequestFail);
|
||||
});
|
||||
|
||||
ServerConnections.connect().then(result => {
|
||||
this.firstConnectionResult = result;
|
||||
|
||||
// Handle the initial route
|
||||
|
@ -189,40 +156,18 @@ class AppRouter {
|
|||
}
|
||||
|
||||
canGoBack() {
|
||||
const curr = this.current();
|
||||
const curr = this.currentRouteInfo?.route;
|
||||
if (!curr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!document.querySelector('.dialogContainer') && this.startPages.indexOf(curr.type) !== -1) {
|
||||
if (!document.querySelector('.dialogContainer') && START_PAGE_TYPES.includes(curr.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.history.length > 1;
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.currentRouteInfo ? this.currentRouteInfo.route : null;
|
||||
}
|
||||
|
||||
invokeShortcut(id) {
|
||||
if (id.indexOf('library-') === 0) {
|
||||
id = id.replace('library-', '');
|
||||
id = id.split('_');
|
||||
|
||||
this.showItem(id[0], id[1]);
|
||||
} else if (id.indexOf('item-') === 0) {
|
||||
id = id.replace('item-', '');
|
||||
id = id.split('_');
|
||||
this.showItem(id[0], id[1]);
|
||||
} else {
|
||||
id = id.split('_');
|
||||
this.show(this.getRouteUrl(id[0], {
|
||||
serverId: id[1]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
showItem(item, serverId, options) {
|
||||
// TODO: Refactor this so it only gets items, not strings.
|
||||
if (typeof (item) === 'string') {
|
||||
|
@ -236,9 +181,7 @@ class AppRouter {
|
|||
}
|
||||
|
||||
const url = this.getRouteUrl(item, options);
|
||||
this.show(url, {
|
||||
item: item
|
||||
});
|
||||
this.show(url, { item });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,20 +195,14 @@ class AppRouter {
|
|||
setBackdropTransparency(level);
|
||||
}
|
||||
|
||||
handleConnectionResult(result) {
|
||||
#handleConnectionResult(result) {
|
||||
switch (result.State) {
|
||||
case 'SignedIn':
|
||||
loading.hide();
|
||||
this.goHome();
|
||||
break;
|
||||
case 'ServerSignIn':
|
||||
result.ApiClient.getPublicUsers().then((users) => {
|
||||
if (users.length) {
|
||||
this.showLocalLogin(result.Servers[0].Id);
|
||||
} else {
|
||||
this.showLocalLogin(result.Servers[0].Id, true);
|
||||
}
|
||||
});
|
||||
this.showLocalLogin(result.ApiClient.serverId());
|
||||
break;
|
||||
case 'ServerSelection':
|
||||
this.showSelectServer();
|
||||
|
@ -283,7 +220,7 @@ class AppRouter {
|
|||
}
|
||||
}
|
||||
|
||||
loadContentUrl(ctx, next, route, request) {
|
||||
#loadContentUrl(ctx, _next, route, request) {
|
||||
let url;
|
||||
if (route.contentPath && typeof (route.contentPath) === 'function') {
|
||||
url = route.contentPath(ctx.querystring);
|
||||
|
@ -305,19 +242,19 @@ class AppRouter {
|
|||
}
|
||||
|
||||
promise.then((html) => {
|
||||
this.loadContent(ctx, route, html, request);
|
||||
this.#loadContent(ctx, route, html, request);
|
||||
});
|
||||
}
|
||||
|
||||
handleRoute(ctx, next, route) {
|
||||
this.authenticate(ctx, route, () => {
|
||||
this.initRoute(ctx, next, route);
|
||||
#handleRoute(ctx, next, route) {
|
||||
this.#authenticate(ctx, route, () => {
|
||||
this.#initRoute(ctx, next, route);
|
||||
});
|
||||
}
|
||||
|
||||
initRoute(ctx, next, route) {
|
||||
#initRoute(ctx, next, route) {
|
||||
const onInitComplete = (controllerFactory) => {
|
||||
this.sendRouteToViewManager(ctx, next, route, controllerFactory);
|
||||
this.#sendRouteToViewManager(ctx, next, route, controllerFactory);
|
||||
};
|
||||
|
||||
if (route.pageComponent) {
|
||||
|
@ -329,20 +266,15 @@ class AppRouter {
|
|||
}
|
||||
}
|
||||
|
||||
cancelCurrentLoadRequest() {
|
||||
#cancelCurrentLoadRequest() {
|
||||
const currentRequest = this.currentViewLoadRequest;
|
||||
if (currentRequest) {
|
||||
currentRequest.cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
sendRouteToViewManager(ctx, next, route, controllerFactory) {
|
||||
if (this.isDummyBackToHome && route.type === 'home') {
|
||||
this.isDummyBackToHome = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelCurrentLoadRequest();
|
||||
#sendRouteToViewManager(ctx, next, route, controllerFactory) {
|
||||
this.#cancelCurrentLoadRequest();
|
||||
const isBackNav = ctx.isBack;
|
||||
|
||||
const currentRequest = {
|
||||
|
@ -364,7 +296,7 @@ class AppRouter {
|
|||
|
||||
const onNewViewNeeded = () => {
|
||||
if (typeof route.path === 'string') {
|
||||
this.loadContentUrl(ctx, next, route, currentRequest);
|
||||
this.#loadContentUrl(ctx, next, route, currentRequest);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
@ -413,7 +345,7 @@ class AppRouter {
|
|||
this.msgTimeout = setTimeout(this.onForcedLogoutMessageTimeout, 100);
|
||||
}
|
||||
|
||||
onRequestFail(e, data) {
|
||||
onRequestFail(_e, data) {
|
||||
const apiClient = this;
|
||||
|
||||
if (data.status === 403) {
|
||||
|
@ -429,62 +361,7 @@ class AppRouter {
|
|||
}
|
||||
}
|
||||
|
||||
normalizeImageOptions(options) {
|
||||
let setQuality;
|
||||
if (options.maxWidth || options.width || options.maxHeight || options.height || options.fillWidth || options.fillHeight) {
|
||||
setQuality = true;
|
||||
}
|
||||
|
||||
if (setQuality && !options.quality) {
|
||||
options.quality = 90;
|
||||
}
|
||||
}
|
||||
|
||||
getMaxBandwidth() {
|
||||
/* eslint-disable compat/compat */
|
||||
if (navigator.connection) {
|
||||
let max = navigator.connection.downlinkMax;
|
||||
if (max && max > 0 && max < Number.POSITIVE_INFINITY) {
|
||||
max /= 8;
|
||||
max *= 1000000;
|
||||
max *= 0.7;
|
||||
return parseInt(max, 10);
|
||||
}
|
||||
}
|
||||
/* eslint-enable compat/compat */
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onApiClientCreated(e, newApiClient) {
|
||||
newApiClient.normalizeImageOptions = this.normalizeImageOptions;
|
||||
newApiClient.getMaxBandwidth = this.getMaxBandwidth;
|
||||
|
||||
Events.off(newApiClient, 'requestfail', this.onRequestFail);
|
||||
Events.on(newApiClient, 'requestfail', this.onRequestFail);
|
||||
}
|
||||
|
||||
initApiClient(apiClient, instance) {
|
||||
instance.onApiClientCreated({}, apiClient);
|
||||
}
|
||||
|
||||
initApiClients() {
|
||||
ServerConnections.getApiClients().forEach((apiClient) => {
|
||||
this.initApiClient(apiClient, this);
|
||||
});
|
||||
|
||||
Events.on(ServerConnections, 'apiclientcreated', this.onApiClientCreated);
|
||||
}
|
||||
|
||||
onAppResume() {
|
||||
const apiClient = ServerConnections.currentApiClient();
|
||||
|
||||
if (apiClient) {
|
||||
apiClient.ensureWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
authenticate(ctx, route, callback) {
|
||||
#authenticate(ctx, route, callback) {
|
||||
const firstResult = this.firstConnectionResult;
|
||||
|
||||
this.firstConnectionResult = null;
|
||||
|
@ -497,9 +374,9 @@ class AppRouter {
|
|||
}).then(data => {
|
||||
if (data !== null && data.StartupWizardCompleted === false) {
|
||||
ServerConnections.setLocalApiClient(firstResult.ApiClient);
|
||||
Dashboard.navigate('wizardstart.html');
|
||||
this.show('wizardstart.html');
|
||||
} else {
|
||||
this.handleConnectionResult(firstResult);
|
||||
this.#handleConnectionResult(firstResult);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
|
@ -507,7 +384,7 @@ class AppRouter {
|
|||
|
||||
return;
|
||||
} else if (firstResult.State !== 'SignedIn') {
|
||||
this.handleConnectionResult(firstResult);
|
||||
this.#handleConnectionResult(firstResult);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -521,7 +398,7 @@ class AppRouter {
|
|||
|
||||
if (!shouldExitApp && (!apiClient || !apiClient.isLoggedIn()) && !route.anonymous) {
|
||||
console.debug('[appRouter] route does not allow anonymous access: redirecting to login');
|
||||
this.beginConnectionWizard();
|
||||
this.#beginConnectionWizard();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -541,9 +418,9 @@ class AppRouter {
|
|||
this.goHome();
|
||||
return;
|
||||
} else if (route.roles) {
|
||||
this.validateRoles(apiClient, route.roles).then(() => {
|
||||
this.#validateRoles(apiClient, route.roles).then(() => {
|
||||
callback();
|
||||
}, this.beginConnectionWizard);
|
||||
}, this.#beginConnectionWizard.bind(this));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -552,13 +429,13 @@ class AppRouter {
|
|||
callback();
|
||||
}
|
||||
|
||||
validateRoles(apiClient, roles) {
|
||||
#validateRoles(apiClient, roles) {
|
||||
return Promise.all(roles.split(',').map((role) => {
|
||||
return this.validateRole(apiClient, role);
|
||||
return this.#validateRole(apiClient, role);
|
||||
}));
|
||||
}
|
||||
|
||||
validateRole(apiClient, role) {
|
||||
#validateRole(apiClient, role) {
|
||||
if (role === 'admin') {
|
||||
return apiClient.getCurrentUser().then((user) => {
|
||||
if (user.Policy.IsAdministrator) {
|
||||
|
@ -572,7 +449,7 @@ class AppRouter {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
loadContent(ctx, route, html, request) {
|
||||
#loadContent(ctx, route, html, request) {
|
||||
html = globalize.translateHtml(html, route.dictionary);
|
||||
request.view = html;
|
||||
|
||||
|
@ -586,7 +463,7 @@ class AppRouter {
|
|||
ctx.handled = true;
|
||||
}
|
||||
|
||||
getRequestFile() {
|
||||
#getRequestFile() {
|
||||
let path = window.location.pathname || '';
|
||||
|
||||
const index = path.lastIndexOf('/');
|
||||
|
@ -613,34 +490,10 @@ class AppRouter {
|
|||
return;
|
||||
}
|
||||
|
||||
this.handleRoute(ctx, next, route);
|
||||
this.#handleRoute(ctx, next, route);
|
||||
};
|
||||
}
|
||||
|
||||
showGuide() {
|
||||
Dashboard.navigate('livetv.html?tab=1');
|
||||
}
|
||||
|
||||
goHome() {
|
||||
Dashboard.navigate('home.html');
|
||||
}
|
||||
|
||||
showSearch() {
|
||||
Dashboard.navigate('search.html');
|
||||
}
|
||||
|
||||
showLiveTV() {
|
||||
Dashboard.navigate('livetv.html');
|
||||
}
|
||||
|
||||
showRecordedTV() {
|
||||
Dashboard.navigate('livetv.html?tab=3');
|
||||
}
|
||||
|
||||
showFavorites() {
|
||||
Dashboard.navigate('home.html?tab=1');
|
||||
}
|
||||
|
||||
getRouteUrl(item, options) {
|
||||
if (!item) {
|
||||
throw new Error('item cannot be null');
|
||||
|
@ -835,10 +688,53 @@ class AppRouter {
|
|||
|
||||
return '#!/details?id=' + id + '&serverId=' + serverId;
|
||||
}
|
||||
|
||||
showLocalLogin(serverId) {
|
||||
return this.show('login.html?serverid=' + serverId);
|
||||
}
|
||||
|
||||
showVideoOsd() {
|
||||
return this.show('video');
|
||||
}
|
||||
|
||||
showSelectServer() {
|
||||
return this.show('selectserver.html');
|
||||
}
|
||||
|
||||
showSettings() {
|
||||
return this.show('mypreferencesmenu.html');
|
||||
}
|
||||
|
||||
showNowPlaying() {
|
||||
return this.show('queue');
|
||||
}
|
||||
|
||||
showGuide() {
|
||||
return this.show('livetv.html?tab=1');
|
||||
}
|
||||
|
||||
goHome() {
|
||||
return this.show('home.html');
|
||||
}
|
||||
|
||||
showSearch() {
|
||||
return this.show('search.html');
|
||||
}
|
||||
|
||||
showLiveTV() {
|
||||
return this.show('livetv.html');
|
||||
}
|
||||
|
||||
showRecordedTV() {
|
||||
return this.show('livetv.html?tab=3');
|
||||
}
|
||||
|
||||
showFavorites() {
|
||||
return this.show('home.html?tab=1');
|
||||
}
|
||||
}
|
||||
|
||||
export const appRouter = new AppRouter();
|
||||
|
||||
window.Emby = window.Emby || {};
|
||||
|
||||
window.Emby.Page = appRouter;
|
||||
|
|
|
@ -657,12 +657,12 @@ import { appRouter } from '../appRouter';
|
|||
|
||||
if (str) {
|
||||
const charIndex = Math.floor(str.length / 2);
|
||||
const character = String(str.substr(charIndex, 1).charCodeAt());
|
||||
const character = String(str.slice(charIndex, charIndex + 1).charCodeAt());
|
||||
let sum = 0;
|
||||
for (let i = 0; i < character.length; i++) {
|
||||
sum += parseInt(character.charAt(i));
|
||||
}
|
||||
const index = String(sum).substr(-1);
|
||||
const index = String(sum).slice(-1);
|
||||
|
||||
return (index % numRandomColors) + 1;
|
||||
} else {
|
||||
|
|
41
src/components/dashboard/users/AccessContainer.tsx
Normal file
41
src/components/dashboard/users/AccessContainer.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import CheckBoxElement from './CheckBoxElement';
|
||||
|
||||
type IProps = {
|
||||
containerClassName?: string;
|
||||
headerTitle?: string;
|
||||
checkBoxClassName?: string;
|
||||
checkBoxTitle?: string;
|
||||
listContainerClassName?: string;
|
||||
accessClassName?: string;
|
||||
listTitle?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
<h2>{globalize.translate(headerTitle)}</h2>
|
||||
<CheckBoxElement labelClassName='checkboxContainer' type='checkbox' className={checkBoxClassName} title={checkBoxTitle} />
|
||||
<div className={listContainerClassName}>
|
||||
<div className={accessClassName}>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate(listTitle)}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{
|
||||
padding: '.5em 1em'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate(description)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessContainer;
|
35
src/components/dashboard/users/SectionTitleContainer.tsx
Normal file
35
src/components/dashboard/users/SectionTitleContainer.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
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;
|
|
@ -121,10 +121,14 @@ import '../elements/emby-itemscontainer/emby-itemscontainer';
|
|||
}
|
||||
|
||||
if (!isSingleSection) {
|
||||
options.Limit = screenWidth >= 1920 ? 10 : screenWidth >= 1440 ? 8 : 6;
|
||||
options.Limit = 6;
|
||||
|
||||
if (enableScrollX()) {
|
||||
options.Limit = 20;
|
||||
} else if (screenWidth >= 1920) {
|
||||
options.Limit = 10;
|
||||
} else if (screenWidth >= 1440) {
|
||||
options.Limit = 8;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ import template from './filterdialog.template.html';
|
|||
context.querySelector('.chk3DFilter').checked = query.Is3D === true;
|
||||
context.querySelector('.chkHDFilter').checked = query.IsHD === true;
|
||||
context.querySelector('.chk4KFilter').checked = query.Is4K === true;
|
||||
context.querySelector('.chkSDFilter').checked = query.IsHD === true;
|
||||
context.querySelector('.chkSDFilter').checked = query.IsHD === false;
|
||||
context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true;
|
||||
context.querySelector('#chkTrailer').checked = query.HasTrailer === true;
|
||||
context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true;
|
||||
|
@ -272,15 +272,25 @@ import template from './filterdialog.template.html';
|
|||
triggerChange(this);
|
||||
});
|
||||
const chkHDFilter = context.querySelector('.chkHDFilter');
|
||||
const chkSDFilter = context.querySelector('.chkSDFilter');
|
||||
chkHDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsHD = chkHDFilter.checked ? true : null;
|
||||
if (chkHDFilter.checked) {
|
||||
chkSDFilter.checked = false;
|
||||
query.IsHD = true;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSDFilter = context.querySelector('.chkSDFilter');
|
||||
chkSDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsHD = chkSDFilter.checked ? false : null;
|
||||
if (chkSDFilter.checked) {
|
||||
chkHDFilter.checked = false;
|
||||
query.IsHD = false;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
for (const elem of context.querySelectorAll('.chkStatus')) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import dom from '../../scripts/dom';
|
||||
import layoutManager from '../layoutManager';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
@ -401,15 +400,8 @@ import ServerConnections from '../ServerConnections';
|
|||
function getItemsToResumeFn(mediaType, serverId) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const screenWidth = dom.getWindowSize().innerWidth;
|
||||
|
||||
let limit;
|
||||
if (enableScrollX()) {
|
||||
limit = 12;
|
||||
} else {
|
||||
limit = screenWidth >= 1920 ? 8 : (screenWidth >= 1600 ? 8 : (screenWidth >= 1200 ? 9 : 6));
|
||||
limit = Math.min(limit, 5);
|
||||
}
|
||||
const limit = enableScrollX() ? 12 : 5;
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
|
|
|
@ -520,7 +520,14 @@ import toast from './toast/toast';
|
|||
}
|
||||
|
||||
function play(item, resume, queue, queueNext) {
|
||||
const method = queue ? (queueNext ? 'queueNext' : 'queue') : 'play';
|
||||
let method = 'play';
|
||||
if (queue) {
|
||||
if (queueNext) {
|
||||
method = 'queueNext';
|
||||
} else {
|
||||
method = 'queue';
|
||||
}
|
||||
}
|
||||
|
||||
let startPosition = 0;
|
||||
if (resume && item.UserData && item.UserData.PlaybackPositionTicks) {
|
||||
|
|
|
@ -550,7 +550,9 @@ import template from './libraryoptionseditor.template.html';
|
|||
function getOrderedPlugins(plugins, configuredOrder) {
|
||||
plugins = plugins.slice(0);
|
||||
plugins.sort((a, b) => {
|
||||
return ((a = configuredOrder.indexOf(a.Name), b = configuredOrder.indexOf(b.Name)), a < b ? -1 : a > b ? 1 : 0);
|
||||
a = configuredOrder.indexOf(a.Name);
|
||||
b = configuredOrder.indexOf(b.Name);
|
||||
return a - b;
|
||||
});
|
||||
return plugins;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
will-change: transform;
|
||||
contain: layout style;
|
||||
transition: transform 200ms ease-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nowPlayingBar-hidden {
|
||||
|
|
|
@ -4,12 +4,11 @@ import Dashboard from '../../utils/dashboard';
|
|||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../loading/loading';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
|
||||
import InputElement from '../dashboard/users/InputElement';
|
||||
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
import AccessContainer from '../dashboard/users/AccessContainer';
|
||||
|
||||
type userInput = {
|
||||
Name?: string;
|
||||
|
@ -178,18 +177,10 @@ const NewUserPage: FunctionComponent = () => {
|
|||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle'>
|
||||
{globalize.translate('ButtonAddUser')}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTitleContainer
|
||||
title={globalize.translate('HeaderAddUser')}
|
||||
titleLink='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
<form className='newUserProfileForm'>
|
||||
<div className='inputContainer'>
|
||||
<InputElement
|
||||
|
@ -206,65 +197,47 @@ const NewUserPage: FunctionComponent = () => {
|
|||
label='LabelPassword'
|
||||
/>
|
||||
</div>
|
||||
<AccessContainer
|
||||
containerClassName='folderAccessContainer'
|
||||
headerTitle='HeaderLibraryAccess'
|
||||
checkBoxClassName='chkEnableAllFolders'
|
||||
checkBoxTitle='OptionEnableAccessToAllLibraries'
|
||||
listContainerClassName='folderAccessListContainer'
|
||||
accessClassName='folderAccess'
|
||||
listTitle='HeaderLibraries'
|
||||
description='LibraryAccessHelp'
|
||||
>
|
||||
{mediaFoldersItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
|
||||
<div className='folderAccessContainer'>
|
||||
<h2>{globalize.translate('HeaderLibraryAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableAllFolders'
|
||||
title='OptionEnableAccessToAllLibraries'
|
||||
/>
|
||||
<div className='folderAccessListContainer'>
|
||||
<div className='folderAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderLibraries')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{mediaFoldersItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LibraryAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='channelAccessContainer verticalSection-extrabottompadding hide'>
|
||||
<h2>{globalize.translate('HeaderChannelAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableAllChannels'
|
||||
title='OptionEnableAccessToAllChannels'
|
||||
/>
|
||||
<div className='channelAccessListContainer'>
|
||||
<div className='channelAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('Channels')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('ChannelAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessContainer
|
||||
containerClassName='channelAccessContainer verticalSection-extrabottompadding hide'
|
||||
headerTitle='HeaderChannelAccess'
|
||||
checkBoxClassName='chkEnableAllChannels'
|
||||
checkBoxTitle='OptionEnableAccessToAllChannels'
|
||||
listContainerClassName='channelAccessListContainer'
|
||||
accessClassName='channelAccess'
|
||||
listTitle='Channels'
|
||||
description='ChannelAccessHelp'
|
||||
>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
|
|
|
@ -8,7 +8,7 @@ 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 SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
|
||||
import SelectElement from '../dashboard/users/SelectElement';
|
||||
import SelectSyncPlayAccessElement from '../dashboard/users/SelectSyncPlayAccessElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
|
@ -278,18 +278,10 @@ const UserEditPage: FunctionComponent = () => {
|
|||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
titleLink='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
<SectionTabs activeTab='useredit'/>
|
||||
<div
|
||||
className='lnkEditUserPreferencesContainer'
|
||||
|
|
|
@ -5,12 +5,12 @@ import loading from '../loading/loading';
|
|||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import toast from '../toast/toast';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
|
||||
import AccessContainer from '../dashboard/users/AccessContainer';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
|
@ -109,7 +109,7 @@ const UserLibraryAccessPage: FunctionComponent = () => {
|
|||
itemsArr.push({
|
||||
Id: device.Id,
|
||||
Name: device.Name,
|
||||
AppName : device.AppName,
|
||||
AppName: device.AppName,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
@ -228,115 +228,75 @@ const UserLibraryAccessPage: FunctionComponent = () => {
|
|||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
titleLink='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
<SectionTabs activeTab='userlibraryaccess'/>
|
||||
<form className='userLibraryAccessForm'>
|
||||
<div className='folderAccessContainer'>
|
||||
<h2>{globalize.translate('HeaderLibraryAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllFolders'
|
||||
title='OptionEnableAccessToAllLibraries'
|
||||
/>
|
||||
<div className='folderAccessListContainer'>
|
||||
<div className='folderAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderLibraries')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList checkboxList-paperList'>
|
||||
{mediaFoldersItems.map(Item => {
|
||||
return (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LibraryAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='channelAccessContainer hide'>
|
||||
<h2>{globalize.translate('HeaderChannelAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllChannels'
|
||||
title='OptionEnableAccessToAllChannels'
|
||||
/>
|
||||
<div className='channelAccessListContainer'>
|
||||
<div className='channelAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('Channels')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('ChannelAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='deviceAccessContainer hide'>
|
||||
<h2>{globalize.translate('HeaderDeviceAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllDevices'
|
||||
title='OptionEnableAccessFromAllDevices'
|
||||
/>
|
||||
<div className='deviceAccessListContainer'>
|
||||
<div className='deviceAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderDevices')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{devicesItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkDevice'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
AppName={Item.AppName}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('DeviceAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<AccessContainer
|
||||
containerClassName='folderAccessContainer'
|
||||
headerTitle='HeaderLibraryAccess'
|
||||
checkBoxClassName='chkEnableAllFolders'
|
||||
checkBoxTitle='OptionEnableAccessToAllLibraries'
|
||||
listContainerClassName='folderAccessListContainer'
|
||||
accessClassName='folderAccess'
|
||||
listTitle='HeaderLibraries'
|
||||
description='LibraryAccessHelp'
|
||||
>
|
||||
{mediaFoldersItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
|
||||
<AccessContainer
|
||||
containerClassName='channelAccessContainer hide'
|
||||
headerTitle='HeaderChannelAccess'
|
||||
checkBoxClassName='chkEnableAllChannels'
|
||||
checkBoxTitle='OptionEnableAccessToAllChannels'
|
||||
listContainerClassName='channelAccessListContainer'
|
||||
accessClassName='channelAccess'
|
||||
listTitle='Channels'
|
||||
description='ChannelAccessHelp'
|
||||
>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
|
||||
<AccessContainer
|
||||
containerClassName='deviceAccessContainer hide'
|
||||
headerTitle='HeaderDeviceAccess'
|
||||
checkBoxClassName='chkEnableAllDevices'
|
||||
checkBoxTitle='OptionEnableAccessFromAllDevices'
|
||||
listContainerClassName='deviceAccessListContainer'
|
||||
accessClassName='deviceAccess'
|
||||
listTitle='HeaderDevices'
|
||||
description='DeviceAccessHelp'
|
||||
>
|
||||
{devicesItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkDevice'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
AppName={Item.AppName}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
<br />
|
||||
<div>
|
||||
<ButtonElement
|
||||
|
|
|
@ -7,7 +7,7 @@ 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 SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
|
||||
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import loading from '../loading/loading';
|
||||
|
@ -319,18 +319,10 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
titleLink='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
<SectionTabs activeTab='userparentalcontrol'/>
|
||||
<form className='userParentalControlForm'>
|
||||
<div className='selectContainer'>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
|
||||
|
||||
const UserPasswordPage: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
|
@ -23,18 +23,10 @@ const UserPasswordPage: FunctionComponent = () => {
|
|||
return (
|
||||
<div>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
titleLink='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
<SectionTabs activeTab='userpassword'/>
|
||||
<div className='readOnlyContent'>
|
||||
<UserPasswordForm
|
||||
|
|
|
@ -6,8 +6,7 @@ import loading from '../loading/loading';
|
|||
import dom from '../../scripts/dom';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import UserCardBox from '../dashboard/users/UserCardBox';
|
||||
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../components/cardbuilder/card.scss';
|
||||
|
@ -133,30 +132,16 @@ const UserProfilesPage: FunctionComponent = () => {
|
|||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection verticalSection-extrabottompadding'>
|
||||
<div
|
||||
className='sectionTitleContainer sectionTitleContainer-cards'
|
||||
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
|
||||
>
|
||||
<h2 className='sectionTitle sectionTitle-cards'>
|
||||
{globalize.translate('HeaderUsers')}
|
||||
</h2>
|
||||
<SectionTitleButtonElement
|
||||
className='fab btnAddUser submit sectionTitleButton'
|
||||
title='ButtonAddUser'
|
||||
icon='add'
|
||||
/>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
|
||||
/>
|
||||
</div>
|
||||
<div className='localUsers itemsContainer vertical-wrap'>
|
||||
{users.map(user => {
|
||||
return <UserCardBox key={user.Id} user={user} />;
|
||||
})}
|
||||
</div>
|
||||
<SectionTitleContainer
|
||||
title={globalize.translate('HeaderUsers')}
|
||||
isBtnVisible={true}
|
||||
titleLink='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
|
||||
/>
|
||||
|
||||
<div className='localUsers itemsContainer vertical-wrap'>
|
||||
{users.map(user => {
|
||||
return <UserCardBox key={user.Id} user={user} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1827,12 +1827,18 @@ class PlaybackManager {
|
|||
Limit: UNLIMITED_ITEMS
|
||||
}, queryOptions));
|
||||
} else if (firstItem.IsFolder) {
|
||||
let sortBy = null;
|
||||
if (options.shuffle) {
|
||||
sortBy = 'Random';
|
||||
} else if (firstItem.Type !== 'BoxSet') {
|
||||
sortBy = 'SortName';
|
||||
}
|
||||
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
|
||||
ParentId: firstItem.Id,
|
||||
Filters: 'IsNotFolder',
|
||||
Recursive: true,
|
||||
// These are pre-sorted
|
||||
SortBy: options.shuffle ? 'Random' : (['BoxSet'].indexOf(firstItem.Type) === -1 ? 'SortName' : null),
|
||||
SortBy: sortBy,
|
||||
MediaTypes: 'Audio,Video'
|
||||
}, queryOptions));
|
||||
} else if (firstItem.Type === 'Episode' && items.length === 1 && getPlayer(firstItem, options).supportsProgress !== false) {
|
||||
|
|
|
@ -61,7 +61,12 @@ import layoutManager from './layoutManager';
|
|||
* @return {number} Clamped value.
|
||||
*/
|
||||
function clamp(value, min, max) {
|
||||
return value <= min ? min : value >= max ? max : value;
|
||||
if (value <= min) {
|
||||
return min;
|
||||
} else if (value >= max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,6 +28,16 @@ function getTextStyles(settings, preview) {
|
|||
break;
|
||||
}
|
||||
|
||||
switch (settings.textWeight || '') {
|
||||
case 'bold':
|
||||
list.push({ name: 'font-weight', value: 'bold' });
|
||||
break;
|
||||
case 'normal':
|
||||
default:
|
||||
list.push({ name: 'font-weight', value: 'normal' });
|
||||
break;
|
||||
}
|
||||
|
||||
switch (settings.dropShadow || '') {
|
||||
case 'raised':
|
||||
list.push({ name: 'text-shadow', value: '-1px -1px white, 0px -1px white, -1px 0px white, 1px 1px black, 0px 1px black, 1px 0px black' });
|
||||
|
|
|
@ -28,6 +28,7 @@ function getSubtitleAppearanceObject(context) {
|
|||
const appearanceSettings = {};
|
||||
|
||||
appearanceSettings.textSize = context.querySelector('#selectTextSize').value;
|
||||
appearanceSettings.textWeight = context.querySelector('#selectTextWeight').value;
|
||||
appearanceSettings.dropShadow = context.querySelector('#selectDropShadow').value;
|
||||
appearanceSettings.font = context.querySelector('#selectFont').value;
|
||||
appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value;
|
||||
|
@ -53,6 +54,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
|
|||
context.querySelector('#selectSubtitlePlaybackMode').dispatchEvent(new CustomEvent('change', {}));
|
||||
|
||||
context.querySelector('#selectTextSize').value = appearanceSettings.textSize || '';
|
||||
context.querySelector('#selectTextWeight').value = appearanceSettings.textWeight || 'normal';
|
||||
context.querySelector('#selectDropShadow').value = appearanceSettings.dropShadow || '';
|
||||
context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent';
|
||||
context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff';
|
||||
|
@ -166,6 +168,7 @@ function embed(options, self) {
|
|||
|
||||
options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange);
|
||||
options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange);
|
||||
options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange);
|
||||
options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange);
|
||||
options.element.querySelector('#selectFont').addEventListener('change', onAppearanceFieldChange);
|
||||
options.element.querySelector('#inputTextColor').addEventListener('change', onAppearanceFieldChange);
|
||||
|
|
|
@ -72,6 +72,13 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTextWeight" label="${LabelTextWeight}">
|
||||
<option value="normal">${Normal}</option>
|
||||
<option value="bold">${Bold}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectFont" label="${LabelFont}">
|
||||
<option value="">${Default}</option>
|
||||
|
|
|
@ -173,12 +173,18 @@ export function translateItemsForPlayback(apiClient, items, options) {
|
|||
MediaTypes: 'Audio'
|
||||
});
|
||||
} else if (firstItem.IsFolder) {
|
||||
let sortBy = null;
|
||||
if (options.shuffle) {
|
||||
sortBy = 'Random';
|
||||
} else if (firstItem.Type === 'BoxSet') {
|
||||
sortBy = 'SortName';
|
||||
}
|
||||
promise = getItemsForPlayback(apiClient, mergePlaybackQueries({
|
||||
ParentId: firstItem.Id,
|
||||
Filters: 'IsNotFolder',
|
||||
Recursive: true,
|
||||
// These are pre-sorted.
|
||||
SortBy: options.shuffle ? 'Random' : (['BoxSet'].indexOf(firstItem.Type) === -1 ? 'SortName' : null),
|
||||
SortBy: sortBy,
|
||||
MediaTypes: 'Audio,Video'
|
||||
}, queryOptions));
|
||||
} else if (firstItem.Type === 'Episode' && items.length === 1) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue