1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge branch 'master' into additional-episode-orders

This commit is contained in:
oledfish 2022-03-03 12:01:58 -03:00 committed by GitHub
commit 0e606ed69d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
275 changed files with 6941 additions and 4435 deletions

View file

@ -42,6 +42,7 @@ module.exports = {
'jsx-quotes': ['error', 'prefer-single'], 'jsx-quotes': ['error', 'prefer-single'],
'keyword-spacing': ['error'], 'keyword-spacing': ['error'],
'max-statements-per-line': ['error'], 'max-statements-per-line': ['error'],
'no-empty-function': ['error'],
'no-floating-decimal': ['error'], 'no-floating-decimal': ['error'],
'no-multi-spaces': ['error'], 'no-multi-spaces': ['error'],
'no-multiple-empty-lines': ['error', { 'max': 1 }], 'no-multiple-empty-lines': ['error', { 'max': 1 }],

View file

@ -1,16 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10
ignore:
- dependency-name: hls.js
update-types: [ version-update:semver-major ]
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10

42
.github/renovate.json vendored Normal file
View file

@ -0,0 +1,42 @@
{
"packageRules": [
{
"matchManagers": ["npm"],
"addLabels": ["javascript"]
},
{
"matchDepTypes": ["devDependencies"],
"groupName": "development dependencies",
"groupSlug": "dev-deps"
},
{
"matchDepTypes": ["dependencies"],
"groupName": "dependencies",
"groupSlug": "deps"
},
{
"matchDepTypes": ["action"],
"addLabels": ["github_actions"],
"groupName": "CI dependencies",
"groupSlug": "ci-deps"
},
{
"matchPackageNames": ["hls.js"],
"matchUpdateTypes": "major",
"enabled": false
}
],
"vulnerabilityAlerts": {
"addLabels": ["security"]
},
"dependencyDashboard": false,
"ignoreDeps": ["npm", "node"],
"lockFileMaintenance": {
"enabled": false
},
"enabledManagers": ["npm", "github-actions"],
"labels": ["dependencies"],
"prHourlyLimit": 2,
"rebaseWhen": "conflicted",
"rangeStrategy": "pin"
}

27
.github/stale.yml vendored
View file

@ -1,27 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 120
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 21
# Issues with these labels will never be considered stale
exemptLabels:
- regression
- security
- roadmap
- future
- feature
- enhancement
- confirmed
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
# Disable automatic closing of pull requests
pulls:
daysUntilClose: false

View file

@ -19,7 +19,7 @@ jobs:
language: [ 'javascript' ] language: [ 'javascript' ]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: with:

View file

@ -18,7 +18,7 @@ jobs:
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@v2.3.4 uses: actions/checkout@v3.0.0
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0

View file

@ -13,10 +13,10 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v3.0.0
with: with:
node-version: 12 node-version: 12
check-latest: true check-latest: true
@ -48,10 +48,10 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v3.0.0
with: with:
node-version: 12 node-version: 12
check-latest: true check-latest: true
@ -86,10 +86,10 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v3.0.0
with: with:
node-version: 12 node-version: 12
check-latest: true check-latest: true

27
.github/workflows/repo-stale.yaml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Issue Stale Check
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@v5.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).

1
.npmrc
View file

@ -1 +1,2 @@
fund=false fund=false
save-exact=true

View file

@ -50,6 +50,7 @@
- [Keegan Dahm](https://github.com/keegandahm) - [Keegan Dahm](https://github.com/keegandahm)
- [GodTamIt](https://github.com/GodTamIt) - [GodTamIt](https://github.com/GodTamIt)
- [MinecraftPlaye](https://github.com/MinecraftPlaye) - [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [Matthew Jones](https://github.com/matthew-jones-uk)
# Emby Contributors # Emby Contributors

3961
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,98 +5,102 @@
"repository": "https://github.com/jellyfin/jellyfin-web", "repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.7", "@babel/core": "7.17.5",
"@babel/eslint-parser": "^7.16.5", "@babel/eslint-parser": "7.17.0",
"@babel/eslint-plugin": "^7.16.5", "@babel/eslint-plugin": "7.16.5",
"@babel/plugin-proposal-class-properties": "^7.16.7", "@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-private-methods": "^7.16.7", "@babel/plugin-proposal-private-methods": "7.16.11",
"@babel/plugin-transform-modules-umd": "^7.16.7", "@babel/plugin-transform-modules-umd": "7.16.7",
"@babel/preset-env": "^7.16.7", "@babel/preset-env": "7.16.11",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "7.16.7",
"@typescript-eslint/eslint-plugin": "^4.33.0", "@thornbill/jellyfin-sdk": "0.4.1",
"@typescript-eslint/parser": "^4.33.0", "@types/lodash-es": "4.17.6",
"@uupaa/dynamic-import-polyfill": "^1.0.2", "@types/react": "17.0.39",
"autoprefixer": "^10.4.1", "@types/react-dom": "17.0.12",
"babel-loader": "^8.2.3", "@typescript-eslint/eslint-plugin": "5.13.0",
"babel-plugin-dynamic-import-polyfill": "^1.0.0", "@typescript-eslint/parser": "5.13.0",
"clean-webpack-plugin": "^4.0.0", "@uupaa/dynamic-import-polyfill": "1.0.2",
"confusing-browser-globals": "^1.0.11", "autoprefixer": "10.4.2",
"copy-webpack-plugin": "^10.2.0", "babel-loader": "8.2.3",
"css-loader": "^6.5.1", "babel-plugin-dynamic-import-polyfill": "1.0.0",
"cssnano": "^5.0.14", "clean-webpack-plugin": "4.0.0",
"eslint": "^7.32.0", "confusing-browser-globals": "1.0.11",
"eslint-plugin-compat": "^4.0.0", "copy-webpack-plugin": "10.2.4",
"eslint-plugin-eslint-comments": "^3.2.0", "css-loader": "6.6.0",
"eslint-plugin-import": "^2.25.4", "cssnano": "5.1.0",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint": "8.10.0",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-compat": "4.0.2",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-import": "2.25.4",
"expose-loader": "^3.1.0", "eslint-plugin-jsx-a11y": "6.5.1",
"html-loader": "^3.0.1", "eslint-plugin-promise": "6.0.0",
"html-webpack-plugin": "^5.5.0", "eslint-plugin-react": "7.29.2",
"postcss": "^8.4.5", "eslint-plugin-react-hooks": "4.3.0",
"postcss-loader": "^6.2.1", "expose-loader": "3.1.0",
"postcss-preset-env": "^7.2.0", "html-loader": "3.1.0",
"postcss-scss": "^4.0.2", "html-webpack-plugin": "5.5.0",
"sass": "^1.45.2", "postcss": "8.4.7",
"sass-loader": "^12.4.0", "postcss-loader": "6.2.1",
"source-map-loader": "^3.0.1", "postcss-preset-env": "7.4.1",
"style-loader": "^3.3.1", "postcss-scss": "4.0.3",
"stylelint": "^14.2.0", "sass": "1.49.9",
"stylelint-config-rational-order": "^0.1.2", "sass-loader": "12.6.0",
"stylelint-no-browser-hacks": "^1.2.1", "source-map-loader": "3.0.1",
"stylelint-order": "^5.0.0", "style-loader": "3.3.1",
"stylelint-scss": "^4.1.0", "stylelint": "14.5.3",
"ts-loader": "^9.2.6", "stylelint-config-rational-order": "0.1.2",
"typescript": "^4.5.4", "stylelint-no-browser-hacks": "1.2.1",
"webpack": "^5.65.0", "stylelint-order": "5.0.0",
"webpack-cli": "^4.9.1", "stylelint-scss": "4.1.0",
"webpack-dev-server": "^4.7.2", "ts-loader": "9.2.7",
"webpack-merge": "^5.8.0", "typescript": "4.6.2",
"workbox-webpack-plugin": "^6.2.4", "webpack": "5.69.1",
"worker-loader": "^3.0.8" "webpack-cli": "4.9.2",
"webpack-dev-server": "4.7.4",
"webpack-merge": "5.8.0",
"workbox-webpack-plugin": "6.5.0",
"worker-loader": "3.0.8"
}, },
"dependencies": { "dependencies": {
"@fontsource/noto-sans": "^4.5.1", "@fontsource/noto-sans": "4.5.1",
"@fontsource/noto-sans-hk": "^4.5.2", "@fontsource/noto-sans-hk": "4.5.2",
"@fontsource/noto-sans-jp": "^4.5.2", "@fontsource/noto-sans-jp": "4.5.2",
"@fontsource/noto-sans-kr": "^4.5.2", "@fontsource/noto-sans-kr": "4.5.2",
"@fontsource/noto-sans-sc": "^4.5.2", "@fontsource/noto-sans-sc": "4.5.2",
"blurhash": "^1.1.4", "blurhash": "1.1.4",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "^2.3.1", "classnames": "2.3.1",
"core-js": "^3.20.2", "core-js": "3.20.2",
"date-fns": "^2.28.0", "date-fns": "2.28.0",
"dompurify": "^2.3.4", "dompurify": "2.3.4",
"epubjs": "^0.3.90", "epubjs": "0.3.90",
"fast-text-encoding": "^1.0.3", "fast-text-encoding": "1.0.3",
"flv.js": "^1.6.2", "flv.js": "1.6.2",
"headroom.js": "^0.12.0", "headroom.js": "0.12.0",
"hls.js": "^0.14.17", "hls.js": "0.14.17",
"intersection-observer": "^0.12.0", "intersection-observer": "0.12.0",
"jellyfin-apiclient": "^1.10.0", "jellyfin-apiclient": "1.10.0",
"jquery": "^3.5.1", "jquery": "3.6.0",
"jstree": "^3.3.12", "jstree": "3.3.12",
"libarchive.js": "^1.3.0", "libarchive.js": "1.3.0",
"libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4", "libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4",
"lodash-es": "^4.17.21", "lodash-es": "4.17.21",
"marked": "^4.0.8", "marked": "4.0.10",
"material-design-icons-iconfont": "^6.1.1", "material-design-icons-iconfont": "6.1.1",
"native-promise-only": "^0.8.0-a", "native-promise-only": "0.8.1",
"page": "^1.11.6", "page": "1.11.6",
"pdfjs-dist": "2.12.313", "pdfjs-dist": "2.12.313",
"react": "^17.0.2", "react": "17.0.2",
"react-dom": "^17.0.2", "react-dom": "17.0.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "1.5.1",
"screenfull": "^6.0.0", "screenfull": "6.0.0",
"sortablejs": "^1.14.0", "sortablejs": "1.14.0",
"swiper": "^6.8.4", "swiper": "6.8.4",
"webcomponents.js": "^0.7.24", "webcomponents.js": "0.7.24",
"whatwg-fetch": "^3.6.2", "whatwg-fetch": "3.6.2",
"workbox-core": "^6.2.4", "workbox-core": "6.2.4",
"workbox-precaching": "^6.2.4" "workbox-precaching": "6.2.4"
}, },
"browserslist": [ "browserslist": [
"last 2 Firefox versions", "last 2 Firefox versions",

352
src/apiclient.d.ts vendored Normal file
View file

@ -0,0 +1,352 @@
// TODO: Move to jellyfin-apiclient
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'jellyfin-apiclient' {
import {
AllThemeMediaResult,
AuthenticationResult,
BaseItemDto,
BaseItemDtoQueryResult,
BufferRequestDto,
ClientCapabilities,
CountryInfo,
CultureDto,
DeviceOptions,
DisplayPreferencesDto,
EndPointInfo,
FileSystemEntryInfo,
GeneralCommand,
GroupInfoDto,
GuideInfo,
IgnoreWaitRequestDto,
ImageInfo,
ImageProviderInfo,
ImageType,
ItemCounts,
LiveTvInfo,
MovePlaylistItemRequestDto,
NewGroupRequestDto,
NextItemRequestDto,
NotificationResultDto,
NotificationsSummaryDto,
ParentalRating,
PingRequestDto,
PlaybackInfoResponse,
PlaybackProgressInfo,
PlaybackStartInfo,
PlaybackStopInfo,
PlayCommand,
PlaystateCommand,
PluginInfo,
PluginSecurityInfo,
PreviousItemRequestDto,
QueryFiltersLegacy,
QueueRequestDto,
QuickConnectResult,
QuickConnectState,
ReadyRequestDto,
RecommendationDto,
RemoteImageResult,
RemoveFromPlaylistRequestDto,
SearchHintResult,
SeekRequestDto,
SeriesTimerInfoDto,
SeriesTimerInfoDtoQueryResult,
ServerConfiguration,
SessionInfo,
SetPlaylistItemRequestDto,
SetRepeatModeRequestDto,
SetShuffleModeRequestDto,
SystemInfo,
TaskInfo,
TaskTriggerInfo,
TimerInfoDto,
TimerInfoDtoQueryResult,
UserConfiguration,
UserDto,
UserItemDataDto,
UserPolicy,
UtcTimeResponse,
VirtualFolderInfo
} from '@thornbill/jellyfin-sdk/dist/generated-client';
class ApiClient {
constructor(serverAddress: string, appName: string, appVersion: string, deviceName: string, deviceId: string);
accessToken(): string;
addMediaPath(virtualFolderName: string, mediaPath: string, networkSharePath: string, refreshLibrary?: boolean): Promise<void>;
addVirtualFolder(name: string, type?: string, refreshLibrary?: boolean, libraryOptions?: any): Promise<void>;
appName(): string;
appVersion(): string;
authenticateUserByName(name: string, password: string): Promise<AuthenticationResult>;
cancelLiveTvSeriesTimer(id: string): Promise<void>;
cancelLiveTvTimer(id: string): Promise<void>;
cancelSyncItems(itemIds: string[], targetId?: string): Promise<void>;
clearAuthenticationInfo(): void;
clearUserItemRating(userId: string, itemId: string): Promise<UserItemDataDto>;
closeWebSocket(): void;
createLiveTvSeriesTimer(item: string): Promise<void>;
createLiveTvTimer(item: string): Promise<void>;
createPackageReview(review: any): Promise<any>;
createSyncPlayGroup(options?: NewGroupRequestDto): Promise<void>;
createUser(user: UserDto): Promise<UserDto>;
deleteDevice(deviceId: string): Promise<void>;
deleteItemImage(itemId: string, imageType: ImageType, imageIndex?: number): Promise<void>;
deleteItem(itemId: string): Promise<void>;
deleteLiveTvRecording(id: string): Promise<void>;
deleteUserImage(userId: string, imageType: ImageType, imageIndex?: number): Promise<void>;
deleteUser(userId: string): Promise<void>;
detectBitrate(force: boolean): Promise<number>;
deviceId(): string;
deviceName(): string;
disablePlugin(id: string, version: string): Promise<void>;
downloadRemoteImage(options: any): Promise<void>;
enablePlugin(id: string, version: string): Promise<void>;
encodeName(name: string): string;
ensureWebSocket(): void;
fetch(request: any, includeAuthorization?: boolean): Promise<any>;
fetchWithFailover(request: any, enableReconnection?: boolean): Promise<any>;
getAdditionalVideoParts(userId?: string, itemId: string): Promise<BaseItemDtoQueryResult>;
getAlbumArtists(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getAncestorItems(itemId: string, userId?: string): Promise<BaseItemDto[]>;
getArtist(name: string, userId?: string): Promise<BaseItemDto>;
getArtists(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getAvailablePlugins(options?: any): Promise<PluginInfo[]>;
getAvailableRemoteImages(options: any): Promise<RemoteImageResult>;
getContentUploadHistory(): Promise<any>;
getCountries(): Promise<CountryInfo[]>;
getCriticReviews(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getCultures(): Promise<CultureDto[]>;
getCurrentUserId(): string;
getDateParamValue(date: Date): string;
getDefaultImageQuality(imageType: ImageType): number;
getDevicesOptions(): Promise<DeviceOptions>;
getDirectoryContents(path: string, options?: any): Promise<FileSystemEntryInfo[]>;
getDisplayPreferences(id: string, userId: string, app: string): Promise<DisplayPreferencesDto>;
getDownloadSpeed(byteSize: number): Promise<number>;
getDrives(): Promise<FileSystemEntryInfo[]>;
getEndpointInfo(): Promise<EndPointInfo>;
getEpisodes(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getFilters(options?: any): Promise<QueryFiltersLegacy>;
getGenre(name: string, userId?: string): Promise<BaseItemDto>;
getGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getImageUrl(itemId: string, options?: any): string;
getInstalledPlugins(): Promise<PluginInfo[]>;
getInstantMixFromItem(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getIntros(itemId: string): Promise<BaseItemDtoQueryResult>;
getItemCounts(userId?: string): Promise<ItemCounts>;
getItemDownloadUrl(itemId: string): string;
getItemImageInfos(itemId: string): Promise<ImageInfo[]>;
getItems(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getItem(userId: string, itemId: string): Promise<BaseItemDto>;
getJSON(url: string, includeAuthorization?: boolean): Promise<any>;
getLatestItems(options?: any): Promise<BaseItemDto[]>;
getLiveStreamMediaInfo(liveStreamId: string): Promise<any>;
getLiveTvChannel(id: string, userId?: string): Promise<BaseItemDto>;
getLiveTvChannels(options?: any): Promise<BaseItemDtoQueryResult>;
getLiveTvGuideInfo(userId: string): Promise<GuideInfo>;
getLiveTvInfo(userId: string): Promise<LiveTvInfo>;
getLiveTvProgram(id: string, userId?: string): Promise<BaseItemDto>;
getLiveTvPrograms(options?: any): Promise<BaseItemDtoQueryResult>;
getLiveTvRecommendedPrograms(options?: any): Promise<BaseItemDtoQueryResult>;
getLiveTvRecordingGroup(id: string): Promise<BaseItemDto>;
getLiveTvRecordingGroups(options?: any): Promise<BaseItemDtoQueryResult>;
getLiveTvRecording(id: string, userId?: string): Promise<BaseItemDto>;
getLiveTvRecordingSeries(options?: any): Promise<BaseItemDtoQueryResult>;
getLiveTvRecordings(options?: any): Promise<BaseItemDtoQueryResult>;
getLiveTvSeriesTimer(id: string): Promise<SeriesTimerInfoDto>;
getLiveTvSeriesTimers(options?: any): Promise<SeriesTimerInfoDtoQueryResult>;
getLiveTvTimer(id: string): Promise<TimerInfoDto>;
getLiveTvTimers(options?: any): Promise<TimerInfoDtoQueryResult>;
getLocalTrailers(userId: string, itemId: string): Promise<BaseItemDto[]>;
getMovieRecommendations(options?: any): Promise<RecommendationDto[]>;
getMusicGenre(name: string, userId?: string): Promise<BaseItemDto>;
getMusicGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getNamedConfiguration(name: string): Promise<any>;
getNetworkDevices(): Promise<any>;
getNetworkShares(path: string): Promise<FileSystemEntryInfo[]>;
getNewLiveTvTimerDefaults(options?: any): Promise<SeriesTimerInfoDto>;
getNextUpEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
getNotificationSummary(userId: string): Promise<NotificationsSummaryDto>;
getNotifications(userId: string, options?: any): Promise<NotificationResultDto>;
getPackageInfo(name: string, guid: string): Promise<PackageInfo>;
getPackageReviews(packageId: string, minRating?: string, maxRating?: string, limit?: string): Promise<any>;
getParentalRatings(): Promise<ParentalRating[]>;
getParentPath(path: string): Promise<string>;
getPeople(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getPerson(name: string, userId?: string): Promise<BaseItemDto>;
getPhysicalPaths(): Promise<string[]>;
getPlaybackInfo(itemId: string, options: any, deviceProfile: any): Promise<PlaybackInfoResponse>;
getPluginConfiguration(id: string): Promise<any>;
getPublicSystemInfo(): Promise<PublicSystemInfo>;
getPublicUsers(): Promise<UserDto[]>;
getQuickConnect(verb: string): Promise<void|boolean|number|QuickConnectResult|QuickConnectState>;
getReadySyncItems(deviceId: string): Promise<any>;
getRecordingFolders(userId: string): Promise<BaseItemDtoQueryResult>;
getRegistrationInfo(feature: string): Promise<any>;
getRemoteImageProviders(options: any): Promise<ImageProviderInfo[]>;
getResumableItems(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getRootFolder(userId: string): Promise<BaseItemDto>;
getSavedEndpointInfo(): EndPointInfo;
getScaledImageUrl(itemId: string, options?: any): string;
getScheduledTask(id: string): Promise<TaskInfo>;
getScheduledTasks(options?: any): Promise<TaskInfo[]>;
getSearchHints(options?: any): Promise<SearchHintResult>;
getSeasons(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getServerConfiguration(): Promise<ServerConfiguration>;
getServerTime(): Promise<UtcTimeResponse>;
getSessions(options?: any): Promise<SessionInfo[]>;
getSimilarItems(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getSpecialFeatures(userId: string, itemId: string): Promise<BaseItemDto[]>;
getStudio(name: string, userId?: string): Promise<BaseItemDto>;
getStudios(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getSyncPlayGroups(): Promise<GroupInfoDto[]>;
getSyncStatus(itemId: string): Promise<any>;
getSystemInfo(): Promise<SystemInfo>;
getThemeMedia(userId?: string, itemId: string, inherit?: boolean): Promise<AllThemeMediaResult>;
getThumbImageUrl(item: BaseItemDto, options?: any): string;
getUpcomingEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
getUrl(name: string, params?: any, serverAddress?: string): string;
get(url: string): Promise<any>;
getUserImageUrl(userId: string, options?: any): string;
getUsers(options?: any): Promise<UserDto[]>;
getUser(userId: string): Promise<UserDto>;
getUserViews(options?: any, userId: string): Promise<BaseItemDtoQueryResult>;
getVirtualFolders(): Promise<VirtualFolderInfo[]>;
handleMessageReceived(msg: any): void;
installPlugin(name: string, guid: string, version?: string): Promise<void>;
isLoggedIn(): boolean;
isMessageChannelOpen(): boolean;
isMinServerVersion(version: string): boolean;
isWebSocketOpen(): boolean;
isWebSocketOpenOrConnecting(): boolean;
isWebSocketSupported(): boolean;
joinSyncPlayGroup(options?: any): Promise<void>;
leaveSyncPlayGroup(): Promise<void>;
logout(): Promise<void>;
markNotificationsRead(userId: string, idList: string[], isRead: boolean): Promise<void>;
markPlayed(userId: string, itemId: string, date: Date): Promise<UserItemDataDto>;
markUnplayed(userId: string, itemId: string, date: Date): Promise<UserItemDataDto>;
openWebSocket(): void;
quickConnect(secret: string): Promise<AuthenticationResult>;
refreshItem(itemId: string, options?: any): Promise<void>;
removeMediaPath(virtualFolderName: string, mediaPath: string, refreshLibrary?: boolean): Promise<void>;
removeVirtualFolder(name: string, refreshLibrary?: boolean): Promise<void>;
renameVirtualFolder(name: string, newName: string, refreshLibrary?: boolean): Promise<void>;
reportCapabilities(capabilities: ClientCapabilities): Promise<void>;
reportOfflineActions(actions: any): Promise<any>;
reportPlaybackProgress(options: PlaybackProgressInfo): Promise<void>;
reportPlaybackStart(options: PlaybackStartInfo): Promise<void>;
reportPlaybackStopped(options: PlaybackStopInfo): Promise<void>;
reportSyncJobItemTransferred(syncJobItemId: string): Promise<any>;
requestSyncPlayBuffering(options?: BufferRequestDto): Promise<void>;
requestSyncPlayMovePlaylistItem(options?: MovePlaylistItemRequestDto): Promise<void>;
requestSyncPlayNextItem(options?: NextItemRequestDto): Promise<void>;
requestSyncPlayPause(): Promise<void>;
requestSyncPlayPreviousItem(options?: PreviousItemRequestDto): Promise<void>;
requestSyncPlayQueue(options?: QueueRequestDto): Promise<void>;
requestSyncPlayReady(options?: ReadyRequestDto): Promise<void>;
requestSyncPlayRemoveFromPlaylist(options?: RemoveFromPlaylistRequestDto): Promise<void>;
requestSyncPlaySeek(options?: SeekRequestDto): Promise<void>;
requestSyncPlaySetIgnoreWait(options?: IgnoreWaitRequestDto): Promise<void>;
requestSyncPlaySetNewQueue(options?: NewGroupRequestDto): Promise<void>;
requestSyncPlaySetPlaylistItem(options?: SetPlaylistItemRequestDto): Promise<void>;
requestSyncPlaySetRepeatMode(options?: SetRepeatModeRequestDto): Promise<void>;
requestSyncPlaySetShuffleMode(options?: SetShuffleModeRequestDto): Promise<void>;
requestSyncPlayUnpause(): Promise<void>;
resetEasyPassword(userId: string): Promise<void>;
resetLiveTvTuner(id: string): Promise<void>;
resetUserPassword(userId: string): Promise<void>;
restartServer(): Promise<void>;
sendCommand(sessionId: string, command: any): Promise<void>;
sendMessageCommand(sessionId: string, options: GeneralCommand): Promise<void>;
sendMessage(name: string, data: any): void;
sendPlayCommand(sessionId: string, options: PlayCommand): Promise<void>;
sendPlayStateCommand(sessionId: string, command: PlaystateCommand, options?: any): Promise<void>;
sendSyncPlayPing(options?: PingRequestDto): Promise<void>;
sendWebSocketMessage(name: string, data: any): void;
serverAddress(val?: string): string;
serverId(): string;
serverVersion(): string
setAuthenticationInfo(accessKey?: string, userId?: string): void;
setRequestHeaders(headers: any): void;
setSystemInfo(info: SystemInfo): void;
shutdownServer(): Promise<void>;
startScheduledTask(id: string): Promise<void>;
stopActiveEncodings(playSessionId: string): Promise<void>;
stopScheduledTask(id: string): Promise<void>;
syncData(data: any): Promise<any>;
uninstallPluginByVersion(id: string, version: string): Promise<void>;
uninstallPlugin(id: string): Promise<void>;
updateDisplayPreferences(id: string, obj: DisplayPreferencesDto, userId: string, app: string): Promise<void>;
updateEasyPassword(userId: string, newPassword: string): Promise<void>;
updateFavoriteStatus(userId: string, itemId: string, isFavorite: boolean): Promise<UserItemDataDto>;
updateItemImageIndex(itemId: string, imageType: ImageType, imageIndex: number, newIndex: number): Promise<any>;
updateItem(item: BaseItemDto): Promise<void>;
updateLiveTvSeriesTimer(item: SeriesTimerInfoDto): Promise<void>;
updateLiveTvTimer(item: TimerInfoDto): Promise<void>;
updateMediaPath(virtualFolderName: string, pathInfo: any): Promise<void>;
updateNamedConfiguration(name: string, configuration: any): Promise<void>;
updatePluginConfiguration(id: string, configuration: any): Promise<void>;
updatePluginSecurityInfo(info: PluginSecurityInfo): Promise<void>;
updateScheduledTaskTriggers(id: string, triggers: TaskTriggerInfo[]): Promise<void>;
updateServerConfiguration(configuration: ServerConfiguration): Promise<void>;
updateServerInfo(server: any, serverUrl: string): void;
updateUserConfiguration(userId: string, configuration: UserConfiguration): Promise<void>;
updateUserItemRating(userId: string, itemId: string, likes: boolean): Promise<UserItemDataDto>;
updateUserPassword(userId: string, currentPassword: string, newPassword: string): Promise<void>;
updateUserPolicy(userId: string, policy: UserPolicy): Promise<void>;
updateUser(user: UserDto): Promise<void>;
updateVirtualFolderOptions(id: string, libraryOptions?: any): Promise<void>;
uploadItemImage(itemId: string, imageType: ImageType, file: File): Promise<void>;
uploadItemSubtitle(itemId: string, language: string, isForced: boolean, file: File): Promise<void>;
uploadUserImage(userId: string, imageType: ImageType, file: File): Promise<void>;
}
class AppStore {
constructor();
getItem(name: string): string|null;
removeItem(name: string): void;
setItem(name: string, value: string): void;
}
class ConnectionManager {
constructor(credentialProvider: Credentials, appName: string, appVersion: string, deviceName: string, deviceId: string, capabilities: ClientCapabilities);
addApiClient(apiClient: ApiClient): void;
clearData(): void;
connect(options?: any): Promise<any>;
connectToAddress(address: string, options?: any): Promise<any>;
connectToServer(server: any, options?: any): Promise<any>;
connectToServers(servers: any[], options?: any): Promise<any>;
deleteServer(serverId: string): Promise<void>;
getApiClient(item: BaseItemDto|string): ApiClient;
getApiClients(): ApiClient[];
getAvailableServers(): any[];
getOrCreateApiClient(serverId: string): ApiClient;
getSavedServers(): any[];
handleMessageReceived(msg: any): void;
logout(): Promise<void>;
minServerVersion(val?: string): string;
user(apiClient: ApiClient): Promise<any>;
}
class Credentials {
constructor(key?: string);
addOrUpdateServer(list: any[], server: any): any;
clear(): void;
credentials(data?: any): any;
}
interface Event {
type: string;
}
const Events: {
off(obj: any, eventName: string, fn: (e: Event, ...args: any[]) => void): void;
on(obj: any, eventName: string, fn: (e: Event, ...args: any[]) => void): void;
trigger(obj: any, eventName: string, ...args: any[]): void;
};
}
/* eslint-enable @typescript-eslint/no-explicit-any */

View file

@ -627,10 +627,15 @@
.subtitle { .subtitle {
margin: 0.15em 0 0.2em; margin: 0.15em 0 0.2em;
// Leave room for a focused button
margin-left: -1em;
padding-left: 1em;
} }
.layout-mobile .subtitle { .layout-mobile .subtitle {
margin: 0.2em 0 0.2em; margin: 0.2em 0 0.2em;
padding-left: 0; // Reset padding for focused button since 'margin-left' is 0
} }
.detailPagePrimaryContainer { .detailPagePrimaryContainer {
@ -987,6 +992,10 @@ div.itemDetailGalleryLink.defaultCardBackground {
border-collapse: collapse; border-collapse: collapse;
} }
.mediaInfoContent .btnCopy .material-icons {
font-size: inherit;
}
.mediaInfoStream { .mediaInfoStream {
margin: 0 3em 0 0; margin: 0 3em 0 0;
display: inline-block; display: inline-block;
@ -995,6 +1004,10 @@ div.itemDetailGalleryLink.defaultCardBackground {
.mediaInfoStreamType { .mediaInfoStreamType {
display: block; display: block;
margin: 0.622em 0; /* copy button height compensation */
}
.layout-tv .mediaInfoStreamType {
margin: 1em 0; margin: 1em 0;
} }

View file

@ -142,7 +142,7 @@ export function show(options) {
if (layoutManager.tv) { if (layoutManager.tv) {
html += `<button is="paper-icon-button-light" class="btnCloseActionSheet hide-mouse-idle-tv" tabindex="-1"> html += `<button is="paper-icon-button-light" class="btnCloseActionSheet hide-mouse-idle-tv" tabindex="-1">
<span class="material-icons arrow_back"></span> <span class="material-icons arrow_back" aria-hidden="true"></span>
</button>`; </button>`;
} }
@ -204,9 +204,9 @@ export function show(options) {
itemIcon = icons[i]; itemIcon = icons[i];
if (itemIcon) { if (itemIcon) {
html += `<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons ${itemIcon}"></span>`; html += `<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons ${itemIcon}" aria-hidden="true"></span>`;
} else if (renderIcon && !center) { } else if (renderIcon && !center) {
html += '<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" style="visibility:hidden;"></span>'; html += '<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" aria-hidden="true" style="visibility:hidden;"></span>';
} }
html += '<div class="listItemBody actionsheetListItemBody">'; html += '<div class="listItemBody actionsheetListItemBody">';

View file

@ -90,12 +90,13 @@
.actionSheetTitle { .actionSheetTitle {
margin: 0.6em 0 0.7em !important; margin: 0.6em 0 0.7em !important;
padding: 0 0.9em; padding: 0 0.75rem;
flex-grow: 0; flex-grow: 0;
} }
.actionSheetText { .actionSheetText {
padding: 0 1em; margin-top: 0;
padding: 0 0.75rem;
flex-grow: 0; flex-grow: 0;
} }

View file

@ -23,12 +23,12 @@ import alert from './alert';
} }
if (entry.UserId && entry.UserPrimaryImageTag) { if (entry.UserId && entry.UserPrimaryImageTag) {
html += '<span class="listItemIcon material-icons dvr" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, { html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
type: 'Primary', type: 'Primary',
tag: entry.UserPrimaryImageTag tag: entry.UserPrimaryImageTag
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>"; }) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
} else { } else {
html += '<span class="listItemIcon material-icons ' + icon + '" style="background-color:' + color + '"></span>'; html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
} }
html += '<div class="listItemBody three-line">'; html += '<div class="listItemBody three-line">';
@ -45,7 +45,7 @@ import alert from './alert';
if (entry.Overview) { if (entry.Overview) {
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}"> html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
<span class="material-icons info"></span> <span class="material-icons info" aria-hidden="true"></span>
</button>`; </button>`;
} }

View file

@ -3,14 +3,14 @@ import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import AlphaPicker from './alphaPicker'; import AlphaPicker from './alphaPicker';
type AlphaPickerProps = { type AlphaPickerProps = {
onAlphaPicked?: () => void onAlphaPicked?: (e: Event) => void
}; };
// React compatibility wrapper component for alphaPicker.js // React compatibility wrapper component for alphaPicker.js
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
const AlphaPickerComponent: FunctionComponent<AlphaPickerProps> = ({ onAlphaPicked = () => {} }: AlphaPickerProps) => { const AlphaPickerComponent: FunctionComponent<AlphaPickerProps> = ({ onAlphaPicked = () => {} }: AlphaPickerProps) => {
const [ alphaPicker, setAlphaPicker ] = useState(null); const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
const element = useRef(null); const element = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
setAlphaPicker(new AlphaPicker({ setAlphaPicker(new AlphaPicker({

View file

@ -75,7 +75,7 @@ import 'material-design-icons-iconfont';
html += `<div class="${rowClassName}">`; html += `<div class="${rowClassName}">`;
if (options.mode === 'keyboard') { if (options.mode === 'keyboard') {
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon space_bar"></span></button>`; html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
} else { } else {
letters = ['#']; letters = ['#'];
html += mapLetters(letters, vertical).join(''); html += mapLetters(letters, vertical).join('');
@ -85,7 +85,7 @@ import 'material-design-icons-iconfont';
html += mapLetters(letters, vertical).join(''); html += mapLetters(letters, vertical).join('');
if (options.mode === 'keyboard') { if (options.mode === 'keyboard') {
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon backspace"></span></button>`; html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
html += '</div>'; html += '</div>';
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
@ -287,7 +287,7 @@ import 'material-design-icons-iconfont';
this.value(query.NameStartsWith); this.value(query.NameStartsWith);
} }
this.visible(query.SortBy.indexOf('SortName') === 0); this.visible(query.SortBy.indexOf('SortName') !== -1);
} }
visible(visible) { visible(visible) {

View file

@ -34,15 +34,20 @@ class AppRouter {
constructor() { constructor() {
// WebKit fires a popstate event on document load // WebKit fires a popstate event on document load
// Skip it using timeout // Skip it using boolean
// For Tizen 2.x // For Tizen 2.x
// https://stackoverflow.com/a/12214354 // See `page` node module
window.addEventListener('load', () => { let loaded = document.readyState === 'complete';
setTimeout(() => { if (!loaded) {
window.addEventListener('popstate', () => { window.addEventListener('load', () => {
this.popstateOccurred = true; setTimeout(() => {
}); loaded = true;
}, 0); }, 0);
});
}
window.addEventListener('popstate', () => {
if (!loaded) return;
this.popstateOccurred = true;
}); });
document.addEventListener('viewshow', () => this.onViewShow()); document.addEventListener('viewshow', () => this.onViewShow());
@ -752,7 +757,13 @@ class AppRouter {
} }
if (item === 'nextup') { if (item === 'nextup') {
return '#!/list.html?type=nextup&serverId=' + options.serverId; url = '#!/list.html?type=nextup&serverId=' + options.serverId;
if (options.rewatching) {
url += '&rewatching=' + options.rewatching;
}
return url;
} }
if (item === 'list') { if (item === 'list') {

View file

@ -199,7 +199,6 @@ const supportedFeatures = function () {
if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) { if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) {
features.push('exit'); features.push('exit');
} else { } else {
features.push('exitmenu');
features.push('plugins'); features.push('plugins');
} }

View file

@ -64,7 +64,6 @@ import layoutManager from './layoutManager';
candidates.push(activeElement); candidates.push(activeElement);
} }
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnResume')));
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay'))); candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
let focusedElement; let focusedElement;

View file

@ -227,6 +227,10 @@ button::-moz-focus-inner {
background-color: transparent; background-color: transparent;
} }
.cardPadder {
position: relative; // For centering the cardImageIcon
}
.cardBox:not(.visualCardBox) .cardPadder { .cardBox:not(.visualCardBox) .cardPadder {
border-radius: 0.2em; border-radius: 0.2em;
background-color: #242424; background-color: #242424;
@ -377,6 +381,18 @@ button::-moz-focus-inner {
color: inherit; color: inherit;
} }
.cardPadder .cardImageIcon {
color: #111;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.cardImageContainer .cardImageIcon {
margin: auto; /* 'justify-content: center' doesn't work in Safari 10 */
}
.cardIndicators { .cardIndicators {
right: 0.225em; right: 0.225em;
top: 0.225em; top: 0.225em;

View file

@ -145,7 +145,7 @@ import ServerConnections from '../ServerConnections';
return 100 / 14.2857142857; return 100 / 14.2857142857;
} }
if (screenWidth >= 1200) { if (screenWidth >= 1200) {
return 100 / 16.666666666666666666; return 100 / 16.66666667;
} }
if (screenWidth >= 1000) { if (screenWidth >= 1000) {
return 5; return 5;
@ -481,12 +481,20 @@ import ServerConnections from '../ServerConnections';
return null; return null;
} }
/**
* @typedef {Object} CardImageUrl
* @property {string} imgUrl - Image URL.
* @property {string} blurhash - Image blurhash.
* @property {boolean} forceName - Force name.
* @property {boolean} coverImage - Use cover style.
*/
/** Get the URL of the card's image. /** Get the URL of the card's image.
* @param {Object} item - Item for which to generate a card. * @param {Object} item - Item for which to generate a card.
* @param {Object} apiClient - API client object. * @param {Object} apiClient - API client object.
* @param {Object} options - Options of the card. * @param {Object} options - Options of the card.
* @param {string} shape - Shape of the desired image. * @param {string} shape - Shape of the desired image.
* @returns {Object} Object representing the URL of the card's image. * @returns {CardImageUrl} Object representing the URL of the card's image.
*/ */
function getCardImageUrl(item, apiClient, options, shape) { function getCardImageUrl(item, apiClient, options, shape) {
item = item.ProgramInfo || item; item = item.ProgramInfo || item;
@ -639,7 +647,7 @@ import ServerConnections from '../ServerConnections';
/** /**
* Generates an index used to select the default color of a card based on a string. * Generates an index used to select the default color of a card based on a string.
* @param {string} str - String to use for generating the index. * @param {?string} [str] - String to use for generating the index.
* @returns {number} Index of the color. * @returns {number} Index of the color.
*/ */
function getDefaultColorIndex(str) { function getDefaultColorIndex(str) {
@ -726,8 +734,8 @@ import ServerConnections from '../ServerConnections';
/** /**
* Returns the air time text for the item based on the given times. * Returns the air time text for the item based on the given times.
* @param {object} item - Item used to generate the air time text. * @param {object} item - Item used to generate the air time text.
* @param {string} showAirDateTime - ISO8601 date for the start of the show. * @param {boolean} showAirDateTime - ISO8601 date for the start of the show.
* @param {string} showAirEndTime - ISO8601 date for the end of the show. * @param {boolean} showAirEndTime - ISO8601 date for the end of the show.
* @returns {string} The air time text for the item based on the given dates. * @returns {string} The air time text for the item based on the given dates.
*/ */
function getAirTimeText(item, showAirDateTime, showAirEndTime) { function getAirTimeText(item, showAirDateTime, showAirEndTime) {
@ -782,7 +790,7 @@ import ServerConnections from '../ServerConnections';
if (isOuterFooter && options.cardLayout && layoutManager.mobile) { if (isOuterFooter && options.cardLayout && layoutManager.mobile) {
if (options.cardFooterAside !== 'none') { if (options.cardFooterAside !== 'none') {
html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions cardText-secondary" data-action="menu"><span class="material-icons more_vert"></span></button>'; html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions cardText-secondary" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
} }
} }
@ -856,6 +864,10 @@ import ServerConnections from '../ServerConnections';
} }
} }
if (item.ExtraType && item.ExtraType !== 'Unknown') {
lines.push(globalize.translate(item.ExtraType));
}
if (options.showItemCounts) { if (options.showItemCounts) {
lines.push(getItemCountsHtml(options, item)); lines.push(getItemCountsHtml(options, item));
} }
@ -1125,7 +1137,7 @@ import ServerConnections from '../ServerConnections';
/** /**
* Returns the default background class for a card based on a string. * Returns the default background class for a card based on a string.
* @param {string} str - Text used to generate the background class. * @param {?string} [str] - Text used to generate the background class.
* @returns {string} CSS classes for default card backgrounds. * @returns {string} CSS classes for default card backgrounds.
*/ */
export function getDefaultBackgroundClass(str) { export function getDefaultBackgroundClass(str) {
@ -1297,15 +1309,15 @@ import ServerConnections from '../ServerConnections';
const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction'; const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction';
if (options.centerPlayButton) { if (options.centerPlayButton) {
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayButton-centered" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow"></span></button>'; overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayButton-centered" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>';
} }
if (overlayPlayButton && !item.IsPlaceHolder && (item.LocationType !== 'Virtual' || !item.MediaType || item.Type === 'Program') && item.Type !== 'Person') { if (overlayPlayButton && !item.IsPlaceHolder && (item.LocationType !== 'Virtual' || !item.MediaType || item.Type === 'Program') && item.Type !== 'Person') {
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow"></span></button>'; overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>';
} }
if (options.overlayMoreButton) { if (options.overlayMoreButton) {
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon more_vert"></span></button>'; overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon more_vert" aria-hidden="true"></span></button>';
} }
} }
@ -1340,7 +1352,18 @@ import ServerConnections from '../ServerConnections';
const cardScalableClass = 'cardScalable'; const cardScalableClass = 'cardScalable';
cardImageContainerOpen = '<div class="' + cardBoxClass + '"><div class="' + cardScalableClass + '"><div class="cardPadder cardPadder-' + shape + '"></div>' + cardImageContainerOpen; let cardPadderIcon = '';
// TV Channel logos are transparent so skip the placeholder to avoid overlapping
if (imgUrl && item.Type !== 'TvChannel') {
cardPadderIcon = getDefaultText(item, {
// Always use an icon
defaultCardImageIcon: 'folder',
...options
});
}
cardImageContainerOpen = `<div class="${cardBoxClass}"><div class="${cardScalableClass}"><div class="cardPadder cardPadder-${shape}">${cardPadderIcon}</div>${cardImageContainerOpen}`;
cardBoxClose = '</div>'; cardBoxClose = '</div>';
cardScalableClose = '</div>'; cardScalableClose = '</div>';
@ -1444,7 +1467,7 @@ import ServerConnections from '../ServerConnections';
const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light'; const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';
if (playbackManager.canPlay(item)) { if (playbackManager.canPlay(item)) {
html += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayFab-primary" data-action="resume"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow"></span></button>'; html += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayFab-primary" data-action="resume"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow" aria-hidden="true"></span></button>';
} }
html += '<div class="cardOverlayButton-br flex">'; html += '<div class="cardOverlayButton-br flex">';
@ -1454,7 +1477,7 @@ import ServerConnections from '../ServerConnections';
if (itemHelper.canMarkPlayed(item)) { if (itemHelper.canMarkPlayed(item)) {
/* eslint-disable-next-line @babel/no-unused-expressions */ /* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-playstatebutton/emby-playstatebutton'); import('../../elements/emby-playstatebutton/emby-playstatebutton');
html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check"></span></button>'; html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>';
} }
if (itemHelper.canRate(item)) { if (itemHelper.canRate(item)) {
@ -1462,10 +1485,10 @@ import ServerConnections from '../ServerConnections';
/* eslint-disable-next-line @babel/no-unused-expressions */ /* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-ratingbutton/emby-ratingbutton'); import('../../elements/emby-ratingbutton/emby-ratingbutton');
html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite"></span></button>'; html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>';
} }
html += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover more_vert"></span></button>'; html += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover more_vert" aria-hidden="true"></span></button>';
html += '</div>'; html += '</div>';
html += '</div>'; html += '</div>';
@ -1480,35 +1503,40 @@ import ServerConnections from '../ServerConnections';
*/ */
export function getDefaultText(item, options) { export function getDefaultText(item, options) {
if (item.CollectionType) { if (item.CollectionType) {
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '"></span>'; return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '" aria-hidden="true"></span>';
} }
switch (item.Type) { switch (item.Type) {
case 'MusicAlbum': case 'MusicAlbum':
return '<span class="cardImageIcon material-icons album"></span>'; return '<span class="cardImageIcon material-icons album" aria-hidden="true"></span>';
case 'MusicArtist': case 'MusicArtist':
case 'Person': case 'Person':
return '<span class="cardImageIcon material-icons person"></span>'; return '<span class="cardImageIcon material-icons person" aria-hidden="true"></span>';
case 'Audio': case 'Audio':
return '<span class="cardImageIcon material-icons audiotrack"></span>'; return '<span class="cardImageIcon material-icons audiotrack" aria-hidden="true"></span>';
case 'Movie': case 'Movie':
return '<span class="cardImageIcon material-icons movie"></span>'; return '<span class="cardImageIcon material-icons movie" aria-hidden="true"></span>';
case 'Episode':
case 'Series': case 'Series':
return '<span class="cardImageIcon material-icons tv"></span>'; return '<span class="cardImageIcon material-icons tv" aria-hidden="true"></span>';
case 'Program':
return '<span class="cardImageIcon material-icons live_tv" aria-hidden="true"></span>';
case 'Book': case 'Book':
return '<span class="cardImageIcon material-icons book"></span>'; return '<span class="cardImageIcon material-icons book" aria-hidden="true"></span>';
case 'Folder': case 'Folder':
return '<span class="cardImageIcon material-icons folder"></span>'; return '<span class="cardImageIcon material-icons folder" aria-hidden="true"></span>';
case 'BoxSet': case 'BoxSet':
return '<span class="cardImageIcon material-icons collections"></span>'; return '<span class="cardImageIcon material-icons collections" aria-hidden="true"></span>';
case 'Playlist': case 'Playlist':
return '<span class="cardImageIcon material-icons view_list"></span>'; return '<span class="cardImageIcon material-icons view_list" aria-hidden="true"></span>';
case 'Photo':
return '<span class="cardImageIcon material-icons photo" aria-hidden="true"></span>';
case 'PhotoAlbum': case 'PhotoAlbum':
return '<span class="cardImageIcon material-icons photo_album"></span>'; return '<span class="cardImageIcon material-icons photo_album" aria-hidden="true"></span>';
} }
if (options && options.defaultCardImageIcon) { if (options?.defaultCardImageIcon) {
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '"></span>'; return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '" aria-hidden="true"></span>';
} }
const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item); const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item);
@ -1605,7 +1633,7 @@ import ServerConnections from '../ServerConnections';
indicatorsElem = ensureIndicators(card, indicatorsElem); indicatorsElem = ensureIndicators(card, indicatorsElem);
indicatorsElem.appendChild(playedIndicator); indicatorsElem.appendChild(playedIndicator);
} }
playedIndicator.innerHTML = '<span class="material-icons indicatorIcon check"></span>'; playedIndicator.innerHTML = '<span class="material-icons indicatorIcon check" aria-hidden="true"></span>';
} else { } else {
playedIndicator = card.querySelector('.playedIndicator'); playedIndicator = card.querySelector('.playedIndicator');
if (playedIndicator) { if (playedIndicator) {
@ -1688,7 +1716,7 @@ import ServerConnections from '../ServerConnections';
const icon = cell.querySelector('.timerIndicator'); const icon = cell.querySelector('.timerIndicator');
if (!icon) { if (!icon) {
const indicatorsElem = ensureIndicators(cell); const indicatorsElem = ensureIndicators(cell);
indicatorsElem.insertAdjacentHTML('beforeend', '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record"></span>'); indicatorsElem.insertAdjacentHTML('beforeend', '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record" aria-hidden="true"></span>');
} }
cell.setAttribute('data-timerid', newTimerId); cell.setAttribute('data-timerid', newTimerId);
} }

View file

@ -94,7 +94,7 @@ import ServerConnections from '../ServerConnections';
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`); let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
if (!imgUrl) { if (!imgUrl) {
cardImageContainer += '<span class="material-icons cardImageIcon local_movies"></span>'; cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
} }
let nameHtml = ''; let nameHtml = '';

View file

@ -72,7 +72,7 @@ export default class channelMapper {
function getTunerChannelHtml(channel, providerName) { function getTunerChannelHtml(channel, providerName) {
let html = ''; let html = '';
html += '<div class="listItem">'; html += '<div class="listItem">';
html += '<span class="material-icons listItemIcon dvr"></span>'; html += '<span class="material-icons listItemIcon dvr" aria-hidden="true"></span>';
html += '<div class="listItemBody two-line">'; html += '<div class="listItemBody two-line">';
html += '<h3 class="listItemBodyText">'; html += '<h3 class="listItemBodyText">';
html += channel.Name; html += channel.Name;
@ -85,7 +85,7 @@ export default class channelMapper {
html += '</div>'; html += '</div>';
html += '</div>'; html += '</div>';
html += `<button class="btnMap autoSize" is="paper-icon-button-light" type="button" data-id="${channel.Id}" data-providerid="${channel.ProviderChannelId}"><span class="material-icons mode_edit"></span></button>`; html += `<button class="btnMap autoSize" is="paper-icon-button-light" type="button" data-id="${channel.Id}" data-providerid="${channel.ProviderChannelId}"><span class="material-icons mode_edit" aria-hidden="true"></span></button>`;
return html += '</div>'; return html += '</div>';
} }
@ -127,7 +127,7 @@ export default class channelMapper {
let html = ''; let html = '';
const title = globalize.translate('MapChannels'); const title = globalize.translate('MapChannels');
html += '<div class="formDialogHeader">'; html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">'; html += '<h3 class="formDialogHeaderTitle">';
html += title; html += title;
html += '</h3>'; html += '</h3>';

View file

@ -229,7 +229,7 @@ import toast from '../toast/toast';
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection'); const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
html += '<div class="formDialogHeader">'; html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">'; html += '<h3 class="formDialogHeaderTitle">';
html += title; html += title;
html += '</h3>'; html += '</h3>';

View file

@ -0,0 +1,58 @@
import React, { FunctionComponent } from 'react';
import datetime from '../../../scripts/datetime';
import globalize from '../../../scripts/globalize';
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 = {
index: number;
Id: number;
DayOfWeek?: string;
StartHour?: number ;
EndHour?: number;
}
function getDisplayTime(hours = 0) {
let minutes = 0;
const pct = hours % 1;
if (pct) {
minutes = Math.floor(60 * pct);
}
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
return (
<div
className='liSchedule listItem'
data-day={ DayOfWeek}
data-start={ StartHour}
data-end={ EndHour}
>
<div className='listItemBody two-line'>
<h3 className='listItemBodyText'>
{globalize.translate(DayOfWeek)}
</h3>
<div className='listItemBodyText secondary'>
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
</div>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(index)}
/>
</div>
);
};
export default AccessScheduleList;

View file

@ -0,0 +1,36 @@
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>`
});
type IProps = {
tag?: string;
}
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
return (
<div className='paperList'>
<div className='listItem'>
<div className='listItemBody'>
<h3 className='listItemBodyText'>
{tag}
</h3>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(tag)}
/>
</div>
</div>
);
};
export default BlockedTagList;

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }) => ({ const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
__html: `<button __html: `<button
is="emby-button" is="emby-button"
type="${type}" type="${type}"

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, type, className, title }) => ({ const createCheckBoxElement = ({ labelClassName, type, className, title }: { labelClassName?: string, type?: string, className?: string, title?: string }) => ({
__html: `<label class="${labelClassName}"> __html: `<label class="${labelClassName}">
<input <input
is="emby-checkbox" is="emby-checkbox"

View file

@ -4,30 +4,31 @@ type IProps = {
className?: string; className?: string;
Name?: string; Name?: string;
Id?: string; Id?: string;
ItemType?: string;
AppName?: string; AppName?: string;
checkedAttribute?: string; checkedAttribute?: string;
} }
const createCheckBoxElement = ({className, Name, Id, AppName, checkedAttribute}) => ({ const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
__html: `<label> __html: `<label>
<input <input
type="checkbox" type="checkbox"
is="emby-checkbox" is="emby-checkbox"
class="${className}" class="${className}"
data-id="${Id}" ${checkedAttribute} ${dataAttributes} ${checkedAttribute}
/> />
<span>${Name} ${AppName}</span> <span>${Name} ${AppName}</span>
</label>` </label>`
}); });
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, AppName, checkedAttribute}: IProps) => { const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
return ( return (
<div <div
className='sectioncheckbox' className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({ dangerouslySetInnerHTML={createCheckBoxElement({
className: className, className: className,
Name: Name, Name: Name,
Id: Id, dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
AppName: AppName ? `- ${AppName}` : '', AppName: AppName ? `- ${AppName}` : '',
checkedAttribute: checkedAttribute checkedAttribute: checkedAttribute
})} })}

View file

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

View file

@ -6,7 +6,7 @@ type IProps = {
className?: string; className?: string;
} }
const createLinkElement = ({ className, title }) => ({ const createLinkElement = ({ className, title }: IProps) => ({
__html: `<a __html: `<a
is="emby-linkbutton" is="emby-linkbutton"
class="${className}" class="${className}"

View file

@ -5,7 +5,7 @@ type IProps = {
activeTab: string; activeTab: string;
} }
const createLinkElement = ({ activeTab }) => ({ const createLinkElement = (activeTab: string) => ({
__html: `<a href="#" __html: `<a href="#"
is="emby-linkbutton" is="emby-linkbutton"
data-role="button" data-role="button"
@ -42,9 +42,7 @@ const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
data-role='controlgroup' data-role='controlgroup'
data-type='horizontal' data-type='horizontal'
className='localnav' className='localnav'
dangerouslySetInnerHTML={createLinkElement({ dangerouslySetInnerHTML={createLinkElement(activeTab)}
activeTab: activeTab
})}
/> />
); );
}; };

View file

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

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
const createLinkElement = ({ className, title, href }) => ({ const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
__html: `<a __html: `<a
is="emby-linkbutton" is="emby-linkbutton"
rel="noopener noreferrer" rel="noopener noreferrer"

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }) => ({ const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string[] }) => ({
__html: `<select __html: `<select
class="${className}" class="${className}"
is="emby-select" is="emby-select"

View file

@ -0,0 +1,46 @@
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}'>${rating.Name}</option>`;
}
return content;
};
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption()
})}
/>
);
};
export default SelectMaxParentalRating;

View file

@ -1,9 +1,9 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, id, label }) => ({ const createSelectElement = ({ className, id, label }: { className?: string, id?: string, label: string }) => ({
__html: `<select __html: `<select
className="${className}" class="${className}"
is="emby-select" is="emby-select"
id="${id}" id="${id}"
label="${label}" label="${label}"

View file

@ -1,10 +1,11 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { localeWithSuffix } from '../../../scripts/dfnshelper'; import { localeWithSuffix } from '../../../scripts/dfnshelper';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
import cardBuilder from '../../cardbuilder/cardBuilder'; import cardBuilder from '../../cardbuilder/cardBuilder';
const createLinkElement = ({ user, renderImgUrl }) => ({ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a __html: `<a
is="emby-linkbutton" is="emby-linkbutton"
class="cardContent" class="cardContent"
@ -20,15 +21,15 @@ const createButtonElement = () => ({
type="button" type="button"
class="btnUserMenu flex-shrink-zero" class="btnUserMenu flex-shrink-zero"
> >
<span class="material-icons more_vert"></span> <span class="material-icons more_vert" aria-hidden="true"></span>
</button>` </button>`
}); });
type IProps = { type IProps = {
user?: Record<string, any>; user?: UserDto;
} }
const getLastSeenText = (lastActivityDate) => { const getLastSeenText = (lastActivityDate?: string | null) => {
if (lastActivityDate) { if (lastActivityDate) {
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix)); return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
} }
@ -36,16 +37,16 @@ const getLastSeenText = (lastActivityDate) => {
return ''; return '';
}; };
const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => { const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
let cssClass = 'card squareCard scalableCard squareCard-scalable'; let cssClass = 'card squareCard scalableCard squareCard-scalable';
if (user.Policy.IsDisabled) { if (user.Policy?.IsDisabled) {
cssClass += ' grayscale'; cssClass += ' grayscale';
} }
let imgUrl; let imgUrl;
if (user.PrimaryImageTag) { if (user.PrimaryImageTag && user.Id) {
imgUrl = window.ApiClient.getUserImageUrl(user.Id, { imgUrl = window.ApiClient.getUserImageUrl(user.Id, {
width: 300, width: 300,
tag: user.PrimaryImageTag, tag: user.PrimaryImageTag,
@ -55,7 +56,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
let imageClass = 'cardImage'; let imageClass = 'cardImage';
if (user.Policy.IsDisabled) { if (user.Policy?.IsDisabled) {
imageClass += ' disabledUser'; imageClass += ' disabledUser';
} }
@ -64,7 +65,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
const renderImgUrl = imgUrl ? const renderImgUrl = imgUrl ?
`<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` : `<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` :
`<div class='${imageClass} ${cardBuilder.getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'> `<div class='${imageClass} ${cardBuilder.getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'>
<span class='material-icons cardImageIcon person'></span> <span class='material-icons cardImageIcon person' aria-hidden='true'></span>
</div>`; </div>`;
return ( return (

View file

@ -77,7 +77,7 @@ function getItem(cssClass, type, path, name) {
html += name; html += name;
html += '</div>'; html += '</div>';
html += '</div>'; html += '</div>';
html += '<span class="material-icons arrow_forward" style="font-size:inherit;"></span>'; html += '<span class="material-icons arrow_forward" aria-hidden="true" style="font-size:inherit;"></span>';
html += '</div>'; html += '</div>';
return html; return html;
} }
@ -116,7 +116,7 @@ function getEditorHtml(options, systemInfo) {
html += `<input is="emby-input" id="txtDirectoryPickerPath" type="text" required="required" ${readOnlyAttribute} label="${globalize.translate(labelKey)}"/>`; html += `<input is="emby-input" id="txtDirectoryPickerPath" type="text" required="required" ${readOnlyAttribute} label="${globalize.translate(labelKey)}"/>`;
html += '</div>'; html += '</div>';
if (!readOnlyAttribute) { if (!readOnlyAttribute) {
html += `<button type="button" is="paper-icon-button-light" class="btnRefreshDirectories emby-input-iconbutton" title="${globalize.translate('Refresh')}"><span class="material-icons search"></span></button>`; html += `<button type="button" is="paper-icon-button-light" class="btnRefreshDirectories emby-input-iconbutton" title="${globalize.translate('Refresh')}"><span class="material-icons search" aria-hidden="true"></span></button>`;
} }
html += '</div>'; html += '</div>';
if (!readOnlyAttribute) { if (!readOnlyAttribute) {
@ -235,66 +235,66 @@ function getDefaultPath(options) {
let systemInfo; let systemInfo;
class DirectoryBrowser { class DirectoryBrowser {
currentDialog; currentDialog;
show = options => { show = options => {
options = options || {}; options = options || {};
const fileOptions = { const fileOptions = {
includeDirectories: true includeDirectories: true
}; };
if (options.includeDirectories != null) { if (options.includeDirectories != null) {
fileOptions.includeDirectories = options.includeDirectories; fileOptions.includeDirectories = options.includeDirectories;
} }
if (options.includeFiles != null) { if (options.includeFiles != null) {
fileOptions.includeFiles = options.includeFiles; fileOptions.includeFiles = options.includeFiles;
} }
Promise.all([getSystemInfo(), getDefaultPath(options)]).then( Promise.all([getSystemInfo(), getDefaultPath(options)]).then(
responses => { responses => {
const systemInfo = responses[0]; const systemInfo = responses[0];
const initialPath = responses[1]; const initialPath = responses[1];
const dlg = dialogHelper.createDialog({ const dlg = dialogHelper.createDialog({
size: 'small', size: 'small',
removeOnClose: true, removeOnClose: true,
scrollY: false scrollY: false
}); });
dlg.classList.add('ui-body-a'); dlg.classList.add('ui-body-a');
dlg.classList.add('background-theme-a'); dlg.classList.add('background-theme-a');
dlg.classList.add('directoryPicker'); dlg.classList.add('directoryPicker');
dlg.classList.add('formDialog'); dlg.classList.add('formDialog');
let html = ''; let html = '';
html += '<div class="formDialogHeader">'; html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCloseDialog autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCloseDialog autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">'; html += '<h3 class="formDialogHeaderTitle">';
html += options.header || globalize.translate('HeaderSelectPath'); html += options.header || globalize.translate('HeaderSelectPath');
html += '</h3>'; html += '</h3>';
html += '</div>'; html += '</div>';
html += getEditorHtml(options, systemInfo); html += getEditorHtml(options, systemInfo);
dlg.innerHTML = html; dlg.innerHTML = html;
initEditor(dlg, options, fileOptions); initEditor(dlg, options, fileOptions);
dlg.addEventListener('close', onDialogClosed); dlg.addEventListener('close', onDialogClosed);
dialogHelper.open(dlg); dialogHelper.open(dlg);
dlg.querySelector('.btnCloseDialog').addEventListener('click', () => { dlg.querySelector('.btnCloseDialog').addEventListener('click', () => {
dialogHelper.close(dlg); dialogHelper.close(dlg);
}); });
this.currentDialog = dlg; this.currentDialog = dlg;
dlg.querySelector('#txtDirectoryPickerPath').value = initialPath; dlg.querySelector('#txtDirectoryPickerPath').value = initialPath;
const txtNetworkPath = dlg.querySelector('#txtNetworkPath'); const txtNetworkPath = dlg.querySelector('#txtNetworkPath');
if (txtNetworkPath) { if (txtNetworkPath) {
txtNetworkPath.value = options.networkSharePath || ''; txtNetworkPath.value = options.networkSharePath || '';
} }
if (!options.pathReadOnly) { if (!options.pathReadOnly) {
refreshDirectoryBrowser(dlg, initialPath, fileOptions, true); refreshDirectoryBrowser(dlg, initialPath, fileOptions, true);
}
} }
);
};
close = () => {
if (this.currentDialog) {
dialogHelper.close(this.currentDialog);
} }
}; );
};
close = () => {
if (this.currentDialog) {
dialogHelper.close(this.currentDialog);
}
};
} }
export default DirectoryBrowser; export default DirectoryBrowser;

View file

@ -100,16 +100,6 @@ import template from './displaySettings.template.html';
context.querySelector('.fldDateTimeLocale').classList.add('hide'); context.querySelector('.fldDateTimeLocale').classList.add('hide');
} }
if (!browser.tizen && !browser.web0s) {
context.querySelector('.fldBackdrops').classList.remove('hide');
context.querySelector('.fldThemeSong').classList.remove('hide');
context.querySelector('.fldThemeVideo').classList.remove('hide');
} else {
context.querySelector('.fldBackdrops').classList.add('hide');
context.querySelector('.fldThemeSong').classList.add('hide');
context.querySelector('.fldThemeVideo').classList.add('hide');
}
fillThemes(context.querySelector('#selectTheme'), userSettings.theme()); fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme()); fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());

View file

@ -231,7 +231,7 @@
<div class="fieldDescription checkboxFieldDescription">${EnableDetailsBannerHelp}</div> <div class="fieldDescription checkboxFieldDescription">${EnableDetailsBannerHelp}</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription fldBackdrops hide"> <div class="checkboxContainer checkboxContainer-withDescription fldBackdrops">
<label> <label>
<input type="checkbox" is="emby-checkbox" id="chkBackdrops" /> <input type="checkbox" is="emby-checkbox" id="chkBackdrops" />
<span>${Backdrops}</span> <span>${Backdrops}</span>
@ -239,7 +239,7 @@
<div class="fieldDescription checkboxFieldDescription">${EnableBackdropsHelp}</div> <div class="fieldDescription checkboxFieldDescription">${EnableBackdropsHelp}</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription fldThemeSong hide"> <div class="checkboxContainer checkboxContainer-withDescription fldThemeSong">
<label> <label>
<input type="checkbox" is="emby-checkbox" id="chkThemeSong" /> <input type="checkbox" is="emby-checkbox" id="chkThemeSong" />
<span>${ThemeSongs}</span> <span>${ThemeSongs}</span>
@ -247,7 +247,7 @@
<div class="fieldDescription checkboxFieldDescription">${EnableThemeSongsHelp}</div> <div class="fieldDescription checkboxFieldDescription">${EnableThemeSongsHelp}</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription fldThemeVideo hide"> <div class="checkboxContainer checkboxContainer-withDescription fldThemeVideo">
<label> <label>
<input type="checkbox" is="emby-checkbox" id="chkThemeVideo" /> <input type="checkbox" is="emby-checkbox" id="chkThemeVideo" />
<span>${ThemeVideos}</span> <span>${ThemeVideos}</span>

View file

@ -145,7 +145,7 @@ import '../elements/emby-itemscontainer/emby-itemscontainer';
html += '<h2 class="sectionTitle sectionTitle-cards">'; html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate(section.name); html += globalize.translate(section.name);
html += '</h2>'; html += '</h2>';
html += '<span class="material-icons chevron_right"></span>'; html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>'; html += '</a>';
} else { } else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>'; html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';

View file

@ -228,7 +228,7 @@ class FilterMenu {
let html = ''; let html = '';
html += '<div class="formDialogHeader">'; html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel hide-mouse-idle-tv" tabindex="-1"><span class="material-icons arrow_back"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCancel hide-mouse-idle-tv" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">${Filters}</h3>'; html += '<h3 class="formDialogHeaderTitle">${Filters}</h3>';
html += '</div>'; html += '</div>';

View file

@ -166,7 +166,6 @@ function Guide(options) {
stopAutoRefresh(); stopAutoRefresh();
Events.off(serverNotifications, 'TimerCreated', onTimerCreated); Events.off(serverNotifications, 'TimerCreated', onTimerCreated);
Events.off(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.off(serverNotifications, 'TimerCancelled', onTimerCancelled); Events.off(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); Events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);
@ -409,7 +408,7 @@ function Guide(options) {
let status; let status;
if (item.Type === 'SeriesTimer') { if (item.Type === 'SeriesTimer') {
return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record"></span>'; return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record" aria-hidden="true"></span>';
} else if (item.TimerId || item.SeriesTimerId) { } else if (item.TimerId || item.SeriesTimerId) {
status = item.Status || 'Cancelled'; status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') { } else if (item.Type === 'Timer') {
@ -420,13 +419,13 @@ function Guide(options) {
if (item.SeriesTimerId) { if (item.SeriesTimerId) {
if (status !== 'Cancelled') { if (status !== 'Cancelled') {
return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record"></span>'; return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record" aria-hidden="true"></span>';
} }
return '<span class="material-icons programIcon seriesTimerIcon seriesTimerIcon-inactive fiber_smart_record"></span>'; return '<span class="material-icons programIcon seriesTimerIcon seriesTimerIcon-inactive fiber_smart_record" aria-hidden="true"></span>';
} }
return '<span class="material-icons programIcon timerIcon fiber_manual_record"></span>'; return '<span class="material-icons programIcon timerIcon fiber_manual_record" aria-hidden="true"></span>';
} }
function getChannelProgramsHtml(context, date, channel, programs, options, listInfo) { function getChannelProgramsHtml(context, date, channel, programs, options, listInfo) {
@ -537,7 +536,7 @@ function Guide(options) {
html += '<div class="' + guideProgramNameClass + '">'; html += '<div class="' + guideProgramNameClass + '">';
html += '<div class="guide-programNameCaret hide"><span class="guideProgramNameCaretIcon material-icons keyboard_arrow_left"></span></div>'; html += '<div class="guide-programNameCaret hide"><span class="guideProgramNameCaretIcon material-icons keyboard_arrow_left" aria-hidden="true"></span></div>';
html += '<div class="guideProgramNameText">' + program.Name; html += '<div class="guideProgramNameText">' + program.Name;
@ -1057,9 +1056,6 @@ function Guide(options) {
} }
} }
function onSeriesTimerCreated() {
}
function onTimerCancelled(e, apiClient, data) { function onTimerCancelled(e, apiClient, data) {
const id = data.Id; const id = data.Id;
// find guide cells by timer id, remove timer icon // find guide cells by timer id, remove timer icon
@ -1186,7 +1182,6 @@ function Guide(options) {
Events.trigger(self, 'load'); Events.trigger(self, 'load');
Events.on(serverNotifications, 'TimerCreated', onTimerCreated); Events.on(serverNotifications, 'TimerCreated', onTimerCreated);
Events.on(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.on(serverNotifications, 'TimerCancelled', onTimerCancelled); Events.on(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled); Events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);

View file

@ -177,7 +177,7 @@ import template from './homeScreenSettings.template.html';
currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`; currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`;
currentHtml += '<span class="material-icons listItemIcon folder_open"></span>'; currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
currentHtml += '<div class="listItemBody">'; currentHtml += '<div class="listItemBody">';
@ -187,8 +187,8 @@ import template from './homeScreenSettings.template.html';
currentHtml += '</div>'; currentHtml += '</div>';
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up"></span></button>`; currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down"></span></button>`; currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
currentHtml += '</div>'; currentHtml += '</div>';

View file

@ -27,6 +27,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>
@ -41,6 +42,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>
@ -55,6 +57,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>
@ -69,6 +72,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>
@ -83,6 +87,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>
@ -97,6 +102,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>
@ -111,6 +117,7 @@
<option value="resumebook">${HeaderContinueReading}</option> <option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option> <option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option> <option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option> <option value="livetv">${LiveTV}</option>
<option value="none">${None}</option> <option value="none">${None}</option>
</select> </select>

View file

@ -73,8 +73,7 @@ import ServerConnections from '../ServerConnections';
return Promise.all(promises).then(function () { return Promise.all(promises).then(function () {
return resume(elem, { return resume(elem, {
refresh: true, refresh: true
returnPromise: false
}); });
}); });
} else { } else {
@ -127,10 +126,7 @@ import ServerConnections from '../ServerConnections';
promises.push(elems[i].resume(options)); promises.push(elems[i].resume(options));
} }
const promise = Promise.all(promises); return Promise.all(promises);
if (!options || options.returnPromise !== false) {
return promise;
}
} }
function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) { function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) {
@ -151,6 +147,8 @@ import ServerConnections from '../ServerConnections';
loadLatestLiveTvRecordings(elem, true, apiClient); loadLatestLiveTvRecordings(elem, true, apiClient);
} else if (section === 'nextup') { } else if (section === 'nextup') {
loadNextUp(elem, apiClient, userSettings); loadNextUp(elem, apiClient, userSettings);
} else if (section === 'rewatching') {
loadNextUp(elem, apiClient, userSettings, true);
} else if (section === 'onnow' || section === 'livetv') { } else if (section === 'onnow' || section === 'livetv') {
return loadOnNow(elem, apiClient, user); return loadOnNow(elem, apiClient, user);
} else if (section === 'resumebook') { } else if (section === 'resumebook') {
@ -196,7 +194,7 @@ import ServerConnections from '../ServerConnections';
for (let i = 0, length = items.length; i < length; i++) { for (let i = 0, length = items.length; i < length; i++) {
const item = items[i]; const item = items[i];
const icon = imageHelper.getLibraryIcon(item.CollectionType); const icon = imageHelper.getLibraryIcon(item.CollectionType);
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '"></span><span class="homeLibraryText">' + item.Name + '</span></a>'; html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + item.Name + '</span></a>';
} }
html += '</div>'; html += '</div>';
@ -287,7 +285,7 @@ import ServerConnections from '../ServerConnections';
html += '<h2 class="sectionTitle sectionTitle-cards">'; html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate('LatestFromLibrary', parent.Name); html += globalize.translate('LatestFromLibrary', parent.Name);
html += '</h2>'; html += '</h2>';
html += '<span class="material-icons chevron_right"></span>'; html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>'; html += '</a>';
} else { } else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', parent.Name) + '</h2>'; html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', parent.Name) + '</h2>';
@ -569,7 +567,7 @@ import ServerConnections from '../ServerConnections';
html += '<h2 class="sectionTitle sectionTitle-cards">'; html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate('HeaderOnNow'); html += globalize.translate('HeaderOnNow');
html += '</h2>'; html += '</h2>';
html += '<span class="material-icons chevron_right"></span>'; html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>'; html += '</a>';
} else { } else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>'; html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
@ -600,20 +598,21 @@ import ServerConnections from '../ServerConnections';
}); });
} }
function getNextUpFetchFn(serverId, userSettings) { function getNextUpFetchFn(serverId, userSettings, rewatching) {
return function () { return function () {
const apiClient = ServerConnections.getApiClient(serverId); const apiClient = ServerConnections.getApiClient(serverId);
const oldestDateForNextUp = new Date(); const oldestDateForNextUp = new Date();
oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp()); oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
return apiClient.getNextUpEpisodes({ return apiClient.getNextUpEpisodes({
Limit: enableScrollX() ? 24 : 15, Limit: enableScrollX() ? 24 : 15,
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path', Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
UserId: apiClient.getCurrentUserId(), UserId: apiClient.getCurrentUserId(),
ImageTypeLimit: 1, ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false, EnableTotalRecordCount: false,
DisableFirstEpisode: false, DisableFirstEpisode: false,
NextUpDateCutoff: oldestDateForNextUp.toISOString() NextUpDateCutoff: oldestDateForNextUp.toISOString(),
Rewatching: rewatching
}); });
}; };
} }
@ -639,21 +638,32 @@ import ServerConnections from '../ServerConnections';
}; };
} }
function loadNextUp(elem, apiClient, userSettings) { function loadNextUp(elem, apiClient, userSettings, rewatching = false) {
let html = ''; let html = '';
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">'; html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
if (!layoutManager.tv) { if (!layoutManager.tv) {
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', { html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
serverId: apiClient.serverId() serverId: apiClient.serverId(),
rewatching: rewatching
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">'; }) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
html += '<h2 class="sectionTitle sectionTitle-cards">'; html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate('NextUp'); if (rewatching) {
html += globalize.translate('NextUpRewatching');
} else {
html += globalize.translate('NextUp');
}
html += '</h2>'; html += '</h2>';
html += '<span class="material-icons chevron_right"></span>'; html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>'; html += '</a>';
} else { } else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('NextUp') + '</h2>'; html += '<h2 class="sectionTitle sectionTitle-cards">';
if (rewatching) {
html += globalize.translate('NextUpRewatching');
} else {
html += globalize.translate('NextUp');
}
html += '</h2>';
} }
html += '</div>'; html += '</div>';
@ -673,7 +683,7 @@ import ServerConnections from '../ServerConnections';
elem.innerHTML = html; elem.innerHTML = html;
const itemsContainer = elem.querySelector('.itemsContainer'); const itemsContainer = elem.querySelector('.itemsContainer');
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings); itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, rewatching);
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume()); itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
itemsContainer.parentContainer = elem; itemsContainer.parentContainer = elem;
} }

View file

@ -197,24 +197,21 @@ import { Events } from 'jellyfin-apiclient';
export function playWithPromise(elem, onErrorFn) { export function playWithPromise(elem, onErrorFn) {
try { try {
const promise = elem.play(); return elem.play()
if (promise && promise.then) { .catch((e) => {
// Chrome now returns a promise
return promise.catch(function (e) {
const errorName = (e.name || '').toLowerCase(); const errorName = (e.name || '').toLowerCase();
// safari uses aborterror // safari uses aborterror
if (errorName === 'notallowederror' || if (errorName === 'notallowederror' ||
errorName === 'aborterror') { errorName === 'aborterror') {
// swallow this error because the user can still click the play button on the video element // swallow this error because the user can still click the play button on the video element
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject(); return Promise.reject();
})
.then(() => {
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
}); });
} else {
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
}
} catch (err) { } catch (err) {
console.error('error calling video.play: ' + err); console.error('error calling video.play: ' + err);
return Promise.reject(); return Promise.reject();

View file

@ -31,11 +31,16 @@ import template from './imageDownloader.template.html';
let browsableImageStartIndex = 0; let browsableImageStartIndex = 0;
let browsableImageType = 'Primary'; let browsableImageType = 'Primary';
let selectedProvider; let selectedProvider;
let browsableParentId;
function getBaseRemoteOptions() { function getBaseRemoteOptions(page) {
const options = {}; const options = {};
options.itemId = currentItemId; if (page.querySelector('#chkShowParentImages').checked && browsableParentId) {
options.itemId = browsableParentId;
} else {
options.itemId = currentItemId;
}
return options; return options;
} }
@ -43,7 +48,7 @@ import template from './imageDownloader.template.html';
function reloadBrowsableImages(page, apiClient) { function reloadBrowsableImages(page, apiClient) {
loading.show(); loading.show();
const options = getBaseRemoteOptions(); const options = getBaseRemoteOptions(page);
options.type = browsableImageType; options.type = browsableImageType;
options.startIndex = browsableImageStartIndex; options.startIndex = browsableImageStartIndex;
@ -124,8 +129,8 @@ import template from './imageDownloader.template.html';
if (showControls) { if (showControls) {
html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">'; html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">';
html += '<button is="paper-icon-button-light" title="' + globalize.translate('Previous') + '" class="btnPreviousPage autoSize" ' + (startIndex ? '' : 'disabled') + '><span class="material-icons arrow_back"></span></button>'; html += `<button is="paper-icon-button-light" title="${globalize.translate('Previous')}" class="btnPreviousPage autoSize" ${(startIndex ? '' : 'disabled')}><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
html += '<button is="paper-icon-button-light" title="' + globalize.translate('Next') + '" class="btnNextPage autoSize" ' + (startIndex + limit >= totalRecordCount ? 'disabled' : '') + '><span class="material-icons arrow_forward"></span></button>'; html += `<button is="paper-icon-button-light" title="${globalize.translate('Next')}" class="btnNextPage autoSize" ${(startIndex + limit >= totalRecordCount ? 'disabled' : '')}><span class="material-icons arrow_forward" aria-hidden="true"></span></button>`;
html += '</div>'; html += '</div>';
} }
@ -135,7 +140,7 @@ import template from './imageDownloader.template.html';
} }
function downloadRemoteImage(page, apiClient, url, type, provider) { function downloadRemoteImage(page, apiClient, url, type, provider) {
const options = getBaseRemoteOptions(); const options = getBaseRemoteOptions(page);
options.Type = type; options.Type = type;
options.ImageUrl = url; options.ImageUrl = url;
@ -259,7 +264,7 @@ import template from './imageDownloader.template.html';
if (enableFooterButtons) { if (enableFooterButtons) {
html += '<div class="cardText cardTextCentered">'; html += '<div class="cardText cardTextCentered">';
html += '<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="' + globalize.translate('Download') + '"><span class="material-icons cloud_download"></span></button>'; html += `<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="${globalize.translate('Download')}"><span class="material-icons cloud_download" aria-hidden="true"></span></button>`;
html += '</div>'; html += '</div>';
} }
@ -273,26 +278,31 @@ import template from './imageDownloader.template.html';
return html; return html;
} }
function reloadBrowsableImagesFirstPage(page, apiClient) {
browsableImageStartIndex = 0;
reloadBrowsableImages(page, apiClient);
}
function initEditor(page, apiClient) { function initEditor(page, apiClient) {
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () { page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
browsableImageType = this.value; browsableImageType = this.value;
browsableImageStartIndex = 0;
selectedProvider = null; selectedProvider = null;
reloadBrowsableImages(page, apiClient); reloadBrowsableImagesFirstPage(page, apiClient);
}); });
page.querySelector('#selectImageProvider').addEventListener('change', function () { page.querySelector('#selectImageProvider').addEventListener('change', function () {
browsableImageStartIndex = 0;
selectedProvider = this.value; selectedProvider = this.value;
reloadBrowsableImages(page, apiClient); reloadBrowsableImagesFirstPage(page, apiClient);
}); });
page.querySelector('#chkAllLanguages').addEventListener('change', function () { page.querySelector('#chkAllLanguages').addEventListener('change', function () {
browsableImageStartIndex = 0; reloadBrowsableImagesFirstPage(page, apiClient);
});
reloadBrowsableImages(page, apiClient); page.querySelector('#chkShowParentImages').addEventListener('change', function () {
reloadBrowsableImagesFirstPage(page, apiClient);
}); });
page.addEventListener('click', function (e) { page.addEventListener('click', function (e) {
@ -336,6 +346,10 @@ import template from './imageDownloader.template.html';
scrollHelper.centerFocus.on(dlg, false); scrollHelper.centerFocus.on(dlg, false);
} }
if (browsableParentId) {
dlg.querySelector('#lblShowParentImages').classList.remove('hide');
}
// Has to be assigned a z-index after the call to .open() // Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', onDialogClosed); dlg.addEventListener('close', onDialogClosed);
@ -366,7 +380,7 @@ import template from './imageDownloader.template.html';
} }
} }
export function show(itemId, serverId, itemType, imageType) { export function show(itemId, serverId, itemType, imageType, parentId) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
currentResolve = resolve; currentResolve = resolve;
currentReject = reject; currentReject = reject;
@ -374,6 +388,7 @@ export function show(itemId, serverId, itemType, imageType) {
browsableImageStartIndex = 0; browsableImageStartIndex = 0;
browsableImageType = imageType || 'Primary'; browsableImageType = imageType || 'Primary';
selectedProvider = null; selectedProvider = null;
browsableParentId = parentId;
showEditor(itemId, serverId, itemType); showEditor(itemId, serverId, itemType);
}); });
} }

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${Search} ${Search}
</h3> </h3>
@ -39,6 +39,10 @@
<input id="chkAllLanguages" type="checkbox" is="emby-checkbox" /> <input id="chkAllLanguages" type="checkbox" is="emby-checkbox" />
<span>${AllLanguages}</span> <span>${AllLanguages}</span>
</label> </label>
<label id="lblShowParentImages" class="hide" style="margin: 0 0 0 1em;width:auto;">
<input id="chkShowParentImages" type="checkbox" is="emby-checkbox" />
<span>${ShowParentImages}</span>
</label>
</div> </div>
<div class="availableImagesList vertical-wrap centered"></div> <div class="availableImagesList vertical-wrap centered"></div>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${HeaderImageOptions} ${HeaderImageOptions}
</h3> </h3>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${HeaderUploadImage} ${HeaderUploadImage}
</h3> </h3>
@ -14,7 +14,7 @@
<h2 style="margin:0;">${HeaderAddUpdateImage}</h2> <h2 style="margin:0;">${HeaderAddUpdateImage}</h2>
<button is="emby-button" type="button" class="raised raised-mini btnBrowse" style="margin-left:1.5em;"> <button is="emby-button" type="button" class="raised raised-mini btnBrowse" style="margin-left:1.5em;">
<span class="material-icons folder"></span> <span class="material-icons folder" aria-hidden="true"></span>
<span>${Browse}</span> <span>${Browse}</span>
</button> </button>
</div> </div>

View file

@ -165,21 +165,21 @@ import template from './imageeditor.template.html';
if (index > 0) { if (index > 0) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>'; html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
} else { } else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>'; html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left" aria-hidden="true"></span></button>';
} }
if (index < numImages - 1) { if (index < numImages - 1) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right"></span></button>'; html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
} else { } else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right"></span></button>'; html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
} }
} else { } else {
if (imageProviders.length) { if (imageProviders.length) {
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search"></span></button>'; html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
} }
} }
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete"></span></button>'; html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>'; html += '</div>';
} }
@ -280,7 +280,13 @@ import template from './imageeditor.template.html';
function showImageDownloader(page, imageType) { function showImageDownloader(page, imageType) {
import('../imageDownloader/imageDownloader').then((ImageDownloader) => { import('../imageDownloader/imageDownloader').then((ImageDownloader) => {
ImageDownloader.show(currentItem.Id, currentItem.ServerId, currentItem.Type, imageType).then(function () { ImageDownloader.show(
currentItem.Id,
currentItem.ServerId,
currentItem.Type,
imageType,
currentItem.Type == 'Season' ? currentItem.ParentId : null
).then(function () {
hasChanges = true; hasChanges = true;
reload(page); reload(page);
}); });

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${HeaderEditImages} ${HeaderEditImages}
</h3> </h3>
@ -12,10 +12,10 @@
<div class="imageEditor-buttons first-imageEditor-buttons"> <div class="imageEditor-buttons first-imageEditor-buttons">
<h2 style="margin:0;">${Images}</h2> <h2 style="margin:0;">${Images}</h2>
<button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;"> <button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;">
<span class="material-icons search"></span> <span class="material-icons search" aria-hidden="true"></span>
</button> </button>
<button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;"> <button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
</div> </div>
<div id="images" class="itemsContainer vertical-wrap"> <div id="images" class="itemsContainer vertical-wrap">
@ -27,10 +27,10 @@
<div class="imageEditor-buttons"> <div class="imageEditor-buttons">
<h2 style="margin:0;">${Backdrops}</h2> <h2 style="margin:0;">${Backdrops}</h2>
<button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;" data-imagetype="Backdrop"> <button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;" data-imagetype="Backdrop">
<span class="material-icons search"></span> <span class="material-icons search" aria-hidden="true"></span>
</button> </button>
<button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;" data-imagetype="Backdrop"> <button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;" data-imagetype="Backdrop">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
</div> </div>
<div id="backdrops" class="itemsContainer vertical-wrap"> <div id="backdrops" class="itemsContainer vertical-wrap">
@ -42,10 +42,10 @@
<div class="imageEditor-buttons"> <div class="imageEditor-buttons">
<h2 style="margin: 0;">${Screenshots}</h2> <h2 style="margin: 0;">${Screenshots}</h2>
<button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;" data-imagetype="Screenshot"> <button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;" data-imagetype="Screenshot">
<span class="material-icons search"></span> <span class="material-icons search" aria-hidden="true"></span>
</button> </button>
<button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;" data-imagetype="Screenshot"> <button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;" data-imagetype="Screenshot">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
</div> </div>
<div id="screenshots" class="itemsContainer vertical-wrap"> <div id="screenshots" class="itemsContainer vertical-wrap">

View file

@ -95,9 +95,12 @@ worker.addEventListener(
const elem = event.target; const elem = event.target;
requestAnimationFrame(() => { requestAnimationFrame(() => {
const canvas = elem.previousSibling; const canvas = elem.previousSibling;
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') { if (elem.classList.contains('blurhashed') && canvas?.tagName === 'CANVAS') {
canvas.classList.add('lazy-hidden'); canvas.classList.add('lazy-hidden');
} }
// HACK: Hide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.add('lazy-hidden-children');
}); });
elem.removeEventListener('animationend', onAnimationEnd); elem.removeEventListener('animationend', onAnimationEnd);
} }
@ -135,10 +138,13 @@ worker.addEventListener(
function emptyImageElement(elem) { function emptyImageElement(elem) {
elem.removeEventListener('animationend', onAnimationEnd); elem.removeEventListener('animationend', onAnimationEnd);
const canvas = elem.previousSibling; const canvas = elem.previousSibling;
if (canvas && canvas.tagName === 'CANVAS') { if (canvas?.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden'); canvas.classList.remove('lazy-hidden');
} }
// HACK: Unhide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.remove('lazy-hidden-children');
let url; let url;
if (elem.tagName !== 'IMG') { if (elem.tagName !== 'IMG') {

View file

@ -18,7 +18,8 @@
animation: fadein 0.1s; animation: fadein 0.1s;
} }
.lazy-hidden { .lazy-hidden,
.lazy-hidden-children * {
opacity: 0; opacity: 0;
} }

View file

@ -88,7 +88,7 @@ export function getPlayedIndicatorHtml(item) {
} }
if (userData.PlayedPercentage && userData.PlayedPercentage >= 100 || (userData.Played)) { if (userData.PlayedPercentage && userData.PlayedPercentage >= 100 || (userData.Played)) {
return '<div class="playedIndicator indicator"><span class="material-icons indicatorIcon check"></span></div>'; return '<div class="playedIndicator indicator"><span class="material-icons indicatorIcon check" aria-hidden="true"></span></div>';
} }
} }
@ -109,7 +109,7 @@ export function getTimerIndicator(item) {
let status; let status;
if (item.Type === 'SeriesTimer') { if (item.Type === 'SeriesTimer') {
return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record"></span>'; return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record" aria-hidden="true"></span>';
} else if (item.TimerId || item.SeriesTimerId) { } else if (item.TimerId || item.SeriesTimerId) {
status = item.Status || 'Cancelled'; status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') { } else if (item.Type === 'Timer') {
@ -120,20 +120,20 @@ export function getTimerIndicator(item) {
if (item.SeriesTimerId) { if (item.SeriesTimerId) {
if (status !== 'Cancelled') { if (status !== 'Cancelled') {
return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record"></span>'; return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record" aria-hidden="true"></span>';
} }
return '<span class="material-icons timerIndicator timerIndicator-inactive indicatorIcon fiber_smart_record"></span>'; return '<span class="material-icons timerIndicator timerIndicator-inactive indicatorIcon fiber_smart_record" aria-hidden="true"></span>';
} }
return '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record"></span>'; return '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record" aria-hidden="true"></span>';
} }
export function getSyncIndicator(item) { export function getSyncIndicator(item) {
if (item.SyncPercent === 100) { if (item.SyncPercent === 100) {
return '<div class="syncIndicator indicator fullSyncIndicator"><span class="material-icons indicatorIcon file_download"></span></div>'; return '<div class="syncIndicator indicator fullSyncIndicator"><span class="material-icons indicatorIcon file_download" aria-hidden="true"></span></div>';
} else if (item.SyncPercent != null) { } else if (item.SyncPercent != null) {
return '<div class="syncIndicator indicator emptySyncIndicator"><span class="material-icons indicatorIcon file_download"></span></div>'; return '<div class="syncIndicator indicator emptySyncIndicator"><span class="material-icons indicatorIcon file_download" aria-hidden="true"></span></div>';
} }
return ''; return '';
@ -148,7 +148,7 @@ export function getTypeIndicator(item) {
}; };
const icon = iconT[item.Type]; const icon = iconT[item.Type];
return icon ? '<div class="indicator videoIndicator"><span class="material-icons indicatorIcon ' + icon + '"></span></div>' : ''; return icon ? '<div class="indicator videoIndicator"><span class="material-icons indicatorIcon ' + icon + '" aria-hidden="true"></span></div>' : '';
} }
export function getMissingIndicator(item) { export function getMissingIndicator(item) {

View file

@ -1,4 +1,5 @@
import browser from '../scripts/browser'; import browser from '../scripts/browser';
import { copy } from '../scripts/clipboard';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
import actionsheet from './actionSheet/actionSheet'; import actionsheet from './actionSheet/actionSheet';
import { appHost } from './apphost'; import { appHost } from './apphost';
@ -366,32 +367,11 @@ import toast from './toast/toast';
break; break;
case 'copy-stream': { case 'copy-stream': {
const downloadHref = apiClient.getItemDownloadUrl(itemId); const downloadHref = apiClient.getItemDownloadUrl(itemId);
const textAreaCopy = function () { copy(downloadHref).then(() => {
const textArea = document.createElement('textarea'); toast(globalize.translate('CopyStreamURLSuccess'));
textArea.value = downloadHref; }).catch(() => {
document.body.appendChild(textArea); prompt(globalize.translate('CopyStreamURL'), downloadHref);
textArea.focus(); });
textArea.select();
if (document.execCommand('copy')) {
toast(globalize.translate('CopyStreamURLSuccess'));
} else {
prompt(globalize.translate('CopyStreamURL'), downloadHref);
}
document.body.removeChild(textArea);
};
/* eslint-disable-next-line compat/compat */
if (navigator.clipboard === undefined) {
textAreaCopy();
} else {
/* eslint-disable-next-line compat/compat */
navigator.clipboard.writeText(downloadHref).then(function () {
toast(globalize.translate('CopyStreamURLSuccess'));
}).catch(function () {
textAreaCopy();
});
}
getResolveFunction(resolve, id)(); getResolveFunction(resolve, id)();
break; break;
} }

View file

@ -7,6 +7,9 @@
import dialogHelper from '../dialogHelper/dialogHelper'; import dialogHelper from '../dialogHelper/dialogHelper';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import toast from '../toast/toast';
import { copy } from '../../scripts/clipboard';
import dom from '../../scripts/dom';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import loading from '../loading/loading'; import loading from '../loading/loading';
import '../../elements/emby-select/emby-select'; import '../../elements/emby-select/emby-select';
@ -19,6 +22,12 @@ import '../../assets/css/flexstyles.scss';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import template from './itemMediaInfo.template.html'; import template from './itemMediaInfo.template.html';
// Do not add extra spaces between tags - they will be copied into the result
const copyButtonHtml = layoutManager.tv ? '' :
`<button is="paper-icon-button-light" class="btnCopy" title="${globalize.translate('Copy')}" aria-label="${globalize.translate('Copy')}"
><span class="material-icons content_copy" aria-hidden="true"></span></button>`;
const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </span>';
function setMediaInfo(user, page, item) { function setMediaInfo(user, page, item) {
let html = item.MediaSources.map(version => { let html = item.MediaSources.map(version => {
return getMediaSourceHtml(user, item, version); return getMediaSourceHtml(user, item, version);
@ -28,12 +37,25 @@ import template from './itemMediaInfo.template.html';
} }
const mediaInfoContent = page.querySelector('#mediaInfoContent'); const mediaInfoContent = page.querySelector('#mediaInfoContent');
mediaInfoContent.innerHTML = html; mediaInfoContent.innerHTML = html;
for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) {
btn.addEventListener('click', () => {
const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent;
copy(infoBlock.textContent).then(() => {
toast(globalize.translate('Copied'));
}).catch(() => {
console.error('Could not copy text');
toast(globalize.translate('CopyFailed'));
});
});
}
} }
function getMediaSourceHtml(user, item, version) { function getMediaSourceHtml(user, item, version) {
let html = ''; let html = '<div class="mediaInfoSource">';
if (version.Name) { if (version.Name) {
html += `<div><h2 class="mediaInfoStreamType">${version.Name}</h2></div>`; html += `<div><h2 class="mediaInfoStreamType">${version.Name}${copyButtonHtml}</h2></div>\n`;
} }
if (version.Container) { if (version.Container) {
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`; html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
@ -69,7 +91,7 @@ import template from './itemMediaInfo.template.html';
} }
const displayType = globalize.translate(translateString); const displayType = globalize.translate(translateString);
html += `<h2 class="mediaInfoStreamType">${displayType}</h2>`; html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
const attributes = []; const attributes = [];
if (stream.DisplayTitle) { if (stream.DisplayTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle)); attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
@ -143,10 +165,8 @@ import template from './itemMediaInfo.template.html';
if (stream.NalLengthSize) { if (stream.NalLengthSize) {
attributes.push(createAttribute('NAL', stream.NalLengthSize)); attributes.push(createAttribute('NAL', stream.NalLengthSize));
} }
if (stream.Type !== 'Video') { if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No'))); attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
}
if (stream.Type === 'Subtitle') {
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No'))); attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No'))); attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
} }
@ -156,11 +176,12 @@ import template from './itemMediaInfo.template.html';
html += attributes.join('<br/>'); html += attributes.join('<br/>');
html += '</div>'; html += '</div>';
} }
html += '</div>';
return html; return html;
} }
function createAttribute(label, value) { function createAttribute(label, value) {
return `<span class="mediaInfoLabel">${label}</span><span class="mediaInfoAttribute">${value}</span>`; return `<span class="mediaInfoLabel">${label}</span>${attributeDelimiterHtml}<span class="mediaInfoAttribute">${value}</span>\n`;
} }
function loadMediaInfo(itemId, serverId) { function loadMediaInfo(itemId, serverId) {

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
<span class="material-icons arrow_back"></span> <span class="material-icons arrow_back" aria-hidden="true"></span>
</button> </button>
<h3 class="formDialogHeaderTitle">${MoreMediaInfo}</h3> <h3 class="formDialogHeaderTitle">${MoreMediaInfo}</h3>
</div> </div>

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
<span class="material-icons arrow_back"></span> <span class="material-icons arrow_back" aria-hidden="true"></span>
</button> </button>
<h3 class="formDialogHeaderTitle">${Identify}</h3> <h3 class="formDialogHeaderTitle">${Identify}</h3>
</div> </div>

View file

@ -69,16 +69,16 @@ import template from './libraryoptionseditor.template.html';
for (let i = 0; i < plugins.length; i++) { for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i]; const plugin = plugins[i];
html += `<div class="listItem localReaderOption sortableOption" data-pluginname="${plugin.Name}">`; html += `<div class="listItem localReaderOption sortableOption" data-pluginname="${plugin.Name}">`;
html += '<span class="listItemIcon material-icons live_tv"></span>'; html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
html += '<div class="listItemBody">'; html += '<div class="listItemBody">';
html += '<h3 class="listItemBodyText">'; html += '<h3 class="listItemBodyText">';
html += plugin.Name; html += plugin.Name;
html += '</h3>'; html += '</h3>';
html += '</div>'; html += '</div>';
if (i > 0) { if (i > 0) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up"></span></button>`; html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
} else if (plugins.length > 1) { } else if (plugins.length > 1) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down"></span></button>`; html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
} }
html += '</div>'; html += '</div>';
} }
@ -132,9 +132,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>'; html += '</h3>';
html += '</div>'; html += '</div>';
if (index > 0) { if (index > 0) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_up"></span></button>'; html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>';
} else if (plugins.length > 1) { } else if (plugins.length > 1) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_down"></span></button>'; html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>';
} }
html += '</div>'; html += '</div>';
}); });
@ -198,9 +198,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>'; html += '</h3>';
html += '</div>'; html += '</div>';
if (i > 0) { if (i > 0) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up"></span></button>`; html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
} else if (plugins.length > 1) { } else if (plugins.length > 1) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down"></span></button>`; html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
} }
html += '</div>'; html += '</div>';
} }
@ -237,9 +237,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>'; html += '</h3>';
html += '</div>'; html += '</div>';
if (i > 0) { if (i > 0) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_up"></span></button>'; html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>';
} else if (plugins.length > 1) { } else if (plugins.length > 1) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_down"></span></button>'; html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>';
} }
html += '</div>'; html += '</div>';
} }
@ -411,7 +411,13 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('.chkEnableEmbeddedEpisodeInfosContainer').classList.add('hide'); parent.querySelector('.chkEnableEmbeddedEpisodeInfosContainer').classList.add('hide');
} }
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies'); if (contentType === 'tvshows' || contentType === 'movies' || contentType === 'musicvideos' || contentType === 'mixed') {
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.remove('hide');
} else {
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.add('hide');
}
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies' && contentType !== 'mixed');
return populateMetadataSettings(parent, contentType); return populateMetadataSettings(parent, contentType);
} }
@ -509,6 +515,7 @@ import template from './libraryoptionseditor.template.html';
AutomaticRefreshIntervalDays: parseInt(parent.querySelector('#selectAutoRefreshInterval').value), AutomaticRefreshIntervalDays: parseInt(parent.querySelector('#selectAutoRefreshInterval').value),
EnableEmbeddedTitles: parent.querySelector('#chkEnableEmbeddedTitles').checked, EnableEmbeddedTitles: parent.querySelector('#chkEnableEmbeddedTitles').checked,
EnableEmbeddedEpisodeInfos: parent.querySelector('#chkEnableEmbeddedEpisodeInfos').checked, EnableEmbeddedEpisodeInfos: parent.querySelector('#chkEnableEmbeddedEpisodeInfos').checked,
AllowEmbeddedSubtitles: parent.querySelector('#selectAllowEmbeddedSubtitles').value,
SkipSubtitlesIfEmbeddedSubtitlesPresent: parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked, SkipSubtitlesIfEmbeddedSubtitlesPresent: parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked,
SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked, SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked,
SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked, SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked,
@ -560,7 +567,8 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata; parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata;
parent.querySelector('.chkAutomaticallyGroupSeries').checked = options.EnableAutomaticSeriesGrouping; parent.querySelector('.chkAutomaticallyGroupSeries').checked = options.EnableAutomaticSeriesGrouping;
parent.querySelector('#chkEnableEmbeddedTitles').checked = options.EnableEmbeddedTitles; parent.querySelector('#chkEnableEmbeddedTitles').checked = options.EnableEmbeddedTitles;
parent.querySelector('#chkEnableEmbeddedEpisodeInfos').checked = options.EnableEmbeddedEpisodeInfos; parent.querySelector('#chkEnableEmbeddedEpisodeInfos').value = options.EnableEmbeddedEpisodeInfos;
parent.querySelector('#selectAllowEmbeddedSubtitles').value = options.AllowEmbeddedSubtitles;
parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked = options.SkipSubtitlesIfEmbeddedSubtitlesPresent; parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked = options.SkipSubtitlesIfEmbeddedSubtitlesPresent;
parent.querySelector('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia; parent.querySelector('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia;
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches; parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;

View file

@ -30,6 +30,15 @@
</label> </label>
<div class="fieldDescription checkboxFieldDescription">${PreferEmbeddedEpisodeInfosOverFileNamesHelp}</div> <div class="fieldDescription checkboxFieldDescription">${PreferEmbeddedEpisodeInfosOverFileNamesHelp}</div>
</div> </div>
<div class="selectContainer fldAllowEmbeddedSubtitlesContainer hide advanced" style="margin: 2em 0;">
<select is="emby-select" id="selectAllowEmbeddedSubtitles" label="${AllowEmbeddedSubtitles}">
<option value="AllowAll">${AllowEmbeddedSubtitlesAllowAllOption}</option>
<option value="AllowText">${AllowEmbeddedSubtitlesAllowTextOption}</option>
<option value="AllowImage">${AllowEmbeddedSubtitlesAllowImageOption}</option>
<option value="AllowNone">${AllowEmbeddedSubtitlesAllowNoneOption}</option>
</select>
<div class="fieldDescription">${AllowEmbeddedSubtitlesHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription advanced"> <div class="checkboxContainer checkboxContainer-withDescription advanced">
<label> <label>

View file

@ -166,7 +166,7 @@ import ServerConnections from '../ServerConnections';
for (let i = 0, length = options.rightButtons.length; i < length; i++) { for (let i = 0, length = options.rightButtons.length; i < length; i++) {
const button = options.rightButtons[i]; const button = options.rightButtons[i];
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}"></span></button>`; html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
} }
return html; return html;
@ -257,7 +257,7 @@ import ServerConnections from '../ServerConnections';
} }
if (!clickEntireItem && options.dragHandle) { if (!clickEntireItem && options.dragHandle) {
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle"></span>'; html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle" aria-hidden="true"></span>';
} }
if (options.image !== false) { if (options.image !== false) {
@ -282,6 +282,11 @@ import ServerConnections from '../ServerConnections';
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options); html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
} }
const mediaSourceCount = item.MediaSourceCount || 1;
if (mediaSourceCount > 1 && options.disableIndicators !== true) {
html += '<div class="mediaSourceIndicator">' + mediaSourceCount + '</div>';
}
let indicatorsHtml = ''; let indicatorsHtml = '';
indicatorsHtml += indicators.getPlayedIndicatorHtml(item); indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
@ -290,7 +295,7 @@ import ServerConnections from '../ServerConnections';
} }
if (playOnImageClick) { if (playOnImageClick) {
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow"></span></button>'; html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>';
} }
const progressHtml = indicators.getProgressBarHtml(item, { const progressHtml = indicators.getProgressBarHtml(item, {
@ -450,11 +455,11 @@ import ServerConnections from '../ServerConnections';
if (!clickEntireItem) { if (!clickEntireItem) {
if (options.addToListButton) { if (options.addToListButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add"></span></button>'; html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>';
} }
if (options.infoButton) { if (options.infoButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline"></span></button>'; html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>';
} }
if (options.rightButtons) { if (options.rightButtons) {
@ -466,16 +471,16 @@ import ServerConnections from '../ServerConnections';
const likes = userData.Likes == null ? '' : userData.Likes; const likes = userData.Likes == null ? '' : userData.Likes;
if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) { if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) {
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check"></span></button>'; html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check" aria-hidden="true"></span></button>';
} }
if (itemHelper.canRate(item) && options.enableRatingButton !== false) { if (itemHelper.canRate(item) && options.enableRatingButton !== false) {
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite"></span></button>'; html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
} }
} }
if (options.moreButton !== false) { if (options.moreButton !== false) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert"></span></button>'; html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
} }
} }
html += '</div>'; html += '</div>';

View file

@ -164,6 +164,10 @@
padding: 0.2em; padding: 0.2em;
} }
.listItemImage .cardImageIcon {
font-size: 3em;
}
@media all and (max-width: 64em) { @media all and (max-width: 64em) {
.listItemImage-large { .listItemImage-large {
width: 22vw; width: 22vw;

View file

@ -73,7 +73,7 @@ import template from './mediaLibraryCreator.template.html';
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () { $('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
const value = this.value; const value = this.value;
const dlg = $(this).parents('.dialog')[0]; const dlg = $(this).parents('.dialog')[0];
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value == 'mixed' ? '' : value); libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
if (value) { if (value) {
dlg.querySelector('.libraryOptions').classList.remove('hide'); dlg.querySelector('.libraryOptions').classList.remove('hide');
@ -87,12 +87,11 @@ import template from './mediaLibraryCreator.template.html';
if (index != -1) { if (index != -1) {
const name = this.options[index].innerHTML.replace('*', '').replace('&amp;', '&'); const name = this.options[index].innerHTML.replace('*', '').replace('&amp;', '&');
$('#txtValue', dlg).val(name); $('#txtValue', dlg).val(name);
const folderOption = collectionTypeOptions.filter(i => {
return i.value == value;
})[0];
$('.collectionTypeFieldDescription', dlg).html(folderOption.message || '');
} }
} }
const folderOption = collectionTypeOptions.find(i => i.value === value);
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
}); });
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick); page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
page.querySelector('.btnSubmit').addEventListener('click', onAddLibrary); page.querySelector('.btnSubmit').addEventListener('click', onAddLibrary);
@ -128,7 +127,7 @@ import template from './mediaLibraryCreator.template.html';
} }
html += '</div>'; html += '</div>';
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle"></span></button>`; html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>'; html += '</div>';
return html; return html;
} }

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3> <h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
</div> </div>
@ -20,7 +20,7 @@
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1> <h1 style="margin: .5em 0;">${Folders}</h1>
<button is="emby-button" type="button" class="fab btnAddFolder submit" style="margin-left:1em;" title="${Add}"> <button is="emby-button" type="button" class="fab btnAddFolder submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
</div> </div>
<div class="paperList folderList hide" style="margin-bottom:2em;"></div> <div class="paperList folderList hide" style="margin-bottom:2em;"></div>

View file

@ -119,7 +119,7 @@ import template from './mediaLibraryEditor.template.html';
} }
html += '</div>'; html += '</div>';
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle"></span></button>`; html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>'; html += '</div>';
return html; return html;
} }

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"></h3> <h3 class="formDialogHeaderTitle"></h3>
</div> </div>
@ -13,7 +13,7 @@
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1> <h1 style="margin: .5em 0;">${Folders}</h1>
<button is="emby-button" type="button" class="fab btnAddFolder submit" style="margin-left:1em;" title="${Add}"> <button is="emby-button" type="button" class="fab btnAddFolder submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
</div> </div>
<div class="paperList folderList" style="margin-bottom:2em;"></div> <div class="paperList folderList" style="margin-bottom:2em;"></div>

View file

@ -13,7 +13,7 @@ import '../../elements/emby-button/emby-button';
let status; let status;
if (item.Type === 'SeriesTimer') { if (item.Type === 'SeriesTimer') {
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record"></span>'; return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record" aria-hidden="true"></span>';
} else if (item.TimerId || item.SeriesTimerId) { } else if (item.TimerId || item.SeriesTimerId) {
status = item.Status || 'Cancelled'; status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') { } else if (item.Type === 'Timer') {
@ -24,13 +24,13 @@ import '../../elements/emby-button/emby-button';
if (item.SeriesTimerId) { if (item.SeriesTimerId) {
if (status !== 'Cancelled') { if (status !== 'Cancelled') {
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record"></span>'; return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record" aria-hidden="true"></span>';
} }
return '<span class="material-icons mediaInfoItem mediaInfoIconItem fiber_smart_record"></span>'; return '<span class="material-icons mediaInfoItem mediaInfoIconItem fiber_smart_record" aria-hidden="true"></span>';
} }
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_manual_record"></span>'; return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_manual_record" aria-hidden="true"></span>';
} }
function getProgramInfoHtml(item, options) { function getProgramInfoHtml(item, options) {
@ -358,7 +358,7 @@ import '../../elements/emby-button/emby-button';
if (item.CommunityRating) { if (item.CommunityRating) {
html += '<div class="starRatingContainer mediaInfoItem">'; html += '<div class="starRatingContainer mediaInfoItem">';
html += '<span class="material-icons starIcon star"></span>'; html += '<span class="material-icons starIcon star" aria-hidden="true"></span>';
html += item.CommunityRating.toFixed(1); html += item.CommunityRating.toFixed(1);
html += '</div>'; html += '</div>';
} }

View file

@ -460,7 +460,7 @@ import template from './metadataEditor.template.html';
html += '</div>'; html += '</div>';
if (formatString) { if (formatString) {
html += '<button type="button" is="paper-icon-button-light" class="btnOpenExternalId align-self-flex-end" data-fieldid="' + id + '"><span class="material-icons open_in_browser"></span></button>'; html += '<button type="button" is="paper-icon-button-light" class="btnOpenExternalId align-self-flex-end" data-fieldid="' + id + '"><span class="material-icons open_in_browser" aria-hidden="true"></span></button>';
} }
html += '</div>'; html += '</div>';
@ -898,7 +898,7 @@ import template from './metadataEditor.template.html';
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
html += '<div class="listItem">'; html += '<div class="listItem">';
html += '<span class="material-icons listItemIcon live_tv" style="background-color:#333;"></span>'; html += '<span class="material-icons listItemIcon live_tv" aria-hidden="true" style="background-color:#333;"></span>';
html += '<div class="listItemBody">'; html += '<div class="listItemBody">';
@ -908,7 +908,7 @@ import template from './metadataEditor.template.html';
html += '</div>'; html += '</div>';
html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnRemoveFromEditorList autoSize"><span class="material-icons delete"></span></button>'; html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnRemoveFromEditorList autoSize"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>'; html += '</div>';
} }
@ -945,7 +945,7 @@ import template from './metadataEditor.template.html';
html += '</button>'; html += '</button>';
html += '</div>'; html += '</div>';
html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnDeletePerson autoSize"><span class="material-icons delete"></span></button>'; html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnDeletePerson autoSize"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>'; html += '</div>';
} }

View file

@ -1,18 +1,18 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel btnBack autoSize hide" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel btnBack autoSize hide" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${Edit} ${Edit}
</h3> </h3>
<div style="margin-left: auto;" class="flex align-items-center justify-content-center"> <div style="margin-left: auto;" class="flex align-items-center justify-content-center">
<button is="emby-button" type="button" class="btnHeaderSave button-accent-flat button-flat hide" tabindex="-1"> <button is="emby-button" type="button" class="btnHeaderSave button-accent-flat button-flat hide" tabindex="-1">
<span class="material-icons check"></span> <span class="material-icons check" aria-hidden="true"></span>
<span>${Save}</span> <span>${Save}</span>
</button> </button>
<button is="paper-icon-button-light" class="btnMore autoSize" tabindex="-1"> <button is="paper-icon-button-light" class="btnMore autoSize" tabindex="-1">
<span class="material-icons more_vert"></span> <span class="material-icons more_vert" aria-hidden="true"></span>
</button> </button>
<button is="paper-icon-button-light" class="btnCancel btnClose autoSize" tabindex="-1"> <button is="paper-icon-button-light" class="btnCancel btnClose autoSize" tabindex="-1">
<span class="material-icons close"></span> <span class="material-icons close" aria-hidden="true"></span>
</button> </button>
</div> </div>
</div> </div>
@ -195,7 +195,7 @@
${Genres} ${Genres}
</h2> </h2>
<button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}"> <button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
<div class="paperList" id="listGenres"></div> <div class="paperList" id="listGenres"></div>
</div> </div>
@ -204,7 +204,7 @@
${People} ${People}
</h2> </h2>
<button is="emby-button" type="button" id="btnAddPerson" class="fab btnAddPerson" style="margin-left:1em;" title="${Add}"> <button is="emby-button" type="button" id="btnAddPerson" class="fab btnAddPerson" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
<div id="peopleList" class="paperList"> <div id="peopleList" class="paperList">
</div> </div>
@ -214,7 +214,7 @@
${Studios} ${Studios}
</h2> </h2>
<button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}"> <button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
<div class="paperList" id="listStudios"></div> <div class="paperList" id="listStudios"></div>
</div> </div>
@ -223,7 +223,7 @@
${Tags} ${Tags}
</h2> </h2>
<button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}"> <button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span> <span class="material-icons add" aria-hidden="true"></span>
</button> </button>
<div class="paperList" id="listTags"></div> <div class="paperList" id="listTags"></div>
</div> </div>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${Edit} ${Edit}
</h3> </h3>

View file

@ -125,11 +125,11 @@ import itemHelper from '../itemHelper';
let html = ''; let html = '';
html += '<button is="paper-icon-button-light" class="btnCloseSelectionPanel autoSize"><span class="material-icons close"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCloseSelectionPanel autoSize"><span class="material-icons close" aria-hidden="true"></span></button>';
html += '<h1 class="itemSelectionCount"></h1>'; html += '<h1 class="itemSelectionCount"></h1>';
const moreIcon = 'more_vert'; const moreIcon = 'more_vert';
html += `<button is="paper-icon-button-light" class="btnSelectionPanelOptions autoSize" style="margin-left:auto;"><span class="material-icons ${moreIcon}"></span></button>`; html += `<button is="paper-icon-button-light" class="btnSelectionPanelOptions autoSize" style="margin-left:auto;"><span class="material-icons ${moreIcon}" aria-hidden="true"></span></button>`;
selectionCommandsPanel.innerHTML = html; selectionCommandsPanel.innerHTML = html;

View file

@ -59,13 +59,13 @@ import { appRouter } from '../appRouter';
// The onclicks are needed due to the return false above // The onclicks are needed due to the return false above
html += '<div class="nowPlayingBarCenter">'; html += '<div class="nowPlayingBarCenter">';
html += '<button is="paper-icon-button-light" class="previousTrackButton mediaButton"><span class="material-icons skip_previous"></span></button>'; html += '<button is="paper-icon-button-light" class="previousTrackButton mediaButton"><span class="material-icons skip_previous" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause"></span></button>'; html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="stopButton mediaButton"><span class="material-icons stop"></span></button>'; html += '<button is="paper-icon-button-light" class="stopButton mediaButton"><span class="material-icons stop" aria-hidden="true"></span></button>';
if (!layoutManager.mobile) { if (!layoutManager.mobile) {
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next"></span></button>'; html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next" aria-hidden="true"></span></button>';
} }
html += '<div class="nowPlayingBarCurrentTime"></div>'; html += '<div class="nowPlayingBarCurrentTime"></div>';
@ -73,23 +73,23 @@ import { appRouter } from '../appRouter';
html += '<div class="nowPlayingBarRight">'; html += '<div class="nowPlayingBarRight">';
html += '<button is="paper-icon-button-light" class="muteButton mediaButton"><span class="material-icons volume_up"></span></button>'; html += '<button is="paper-icon-button-light" class="muteButton mediaButton"><span class="material-icons volume_up" aria-hidden="true"></span></button>';
html += '<div class="sliderContainer nowPlayingBarVolumeSliderContainer hide" style="width:9em;vertical-align:middle;display:inline-flex;">'; html += '<div class="sliderContainer nowPlayingBarVolumeSliderContainer hide" style="width:9em;vertical-align:middle;display:inline-flex;">';
html += '<input type="range" is="emby-slider" pin step="1" min="0" max="100" value="0" class="slider-medium-thumb nowPlayingBarVolumeSlider"/>'; html += '<input type="range" is="emby-slider" pin step="1" min="0" max="100" value="0" class="slider-medium-thumb nowPlayingBarVolumeSlider"/>';
html += '</div>'; html += '</div>';
html += '<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton"><span class="material-icons repeat"></span></button>'; html += '<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton"><span class="material-icons repeat" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton"><span class="material-icons shuffle"></span></button>'; html += '<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton"><span class="material-icons shuffle" aria-hidden="true"></span></button>';
html += '<div class="nowPlayingBarUserDataButtons">'; html += '<div class="nowPlayingBarUserDataButtons">';
html += '</div>'; html += '</div>';
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause"></span></button>'; html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause" aria-hidden="true"></span></button>';
if (layoutManager.mobile) { if (layoutManager.mobile) {
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next"></span></button>'; html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next" aria-hidden="true"></span></button>';
} else { } else {
html += '<button is="paper-icon-button-light" class="btnToggleContextMenu mediaButton"><span class="material-icons more_vert"></span></button>'; html += '<button is="paper-icon-button-light" class="btnToggleContextMenu mediaButton"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
} }
html += '</div>'; html += '</div>';
@ -569,7 +569,7 @@ import { appRouter } from '../appRouter';
}); });
}); });
} }
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite"></span></button>'; nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
}); });
} }
} else { } else {
@ -658,6 +658,11 @@ import { appRouter } from '../appRouter';
} }
function onStateChanged(event, state) { function onStateChanged(event, state) {
if (event.type === 'init') {
// skip non-ready state
return;
}
console.debug('nowplaying event: ' + event.type); console.debug('nowplaying event: ' + event.type);
const player = this; const player = this;
@ -728,10 +733,10 @@ import { appRouter } from '../appRouter';
updatePlayerVolumeState(player.isMuted(), player.getVolume()); updatePlayerVolumeState(player.isMuted(), player.getVolume());
} }
function refreshFromPlayer(player) { function refreshFromPlayer(player, type) {
const state = playbackManager.getPlayerState(player); const state = playbackManager.getPlayerState(player);
onStateChanged.call(player, { type: 'init' }, state); onStateChanged.call(player, { type }, state);
} }
function bindToPlayer(player) { function bindToPlayer(player) {
@ -747,7 +752,7 @@ import { appRouter } from '../appRouter';
return; return;
} }
refreshFromPlayer(player); refreshFromPlayer(player, 'init');
Events.on(player, 'playbackstart', onPlaybackStart); Events.on(player, 'playbackstart', onPlaybackStart);
Events.on(player, 'statechange', onPlaybackStart); Events.on(player, 'statechange', onPlaybackStart);
@ -775,7 +780,7 @@ import { appRouter } from '../appRouter';
} else if (!isVisibilityAllowed) { } else if (!isVisibilityAllowed) {
isVisibilityAllowed = true; isVisibilityAllowed = true;
if (currentPlayer) { if (currentPlayer) {
refreshFromPlayer(currentPlayer); refreshFromPlayer(currentPlayer, 'refresh');
} else { } else {
hideNowPlayingBar(); hideNowPlayingBar();
} }

View file

@ -22,9 +22,9 @@ type ItemsArr = {
} }
const NewUserPage: FunctionComponent = () => { const NewUserPage: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState([]); const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState([]); const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef(null); const element = useRef<HTMLDivElement>(null);
const getItemsResult = (items: ItemsArr[]) => { const getItemsResult = (items: ItemsArr[]) => {
return items.map(item => return items.map(item =>
@ -36,33 +36,54 @@ const NewUserPage: FunctionComponent = () => {
}; };
const loadMediaFolders = useCallback((result) => { const loadMediaFolders = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const mediaFolders = getItemsResult(result); const mediaFolders = getItemsResult(result);
setMediaFoldersItems(mediaFolders); setMediaFoldersItems(mediaFolders);
const folderAccess = element?.current?.querySelector('.folderAccess'); const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement;
folderAccess.dispatchEvent(new CustomEvent('create')); folderAccess.dispatchEvent(new CustomEvent('create'));
element.current.querySelector('.chkEnableAllFolders').checked = false; (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
}, []); }, []);
const loadChannels = useCallback((result) => { const loadChannels = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const channels = getItemsResult(result); const channels = getItemsResult(result);
setChannelsItems(channels); setChannelsItems(channels);
const channelAccess = element?.current?.querySelector('.channelAccess'); const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement;
channelAccess.dispatchEvent(new CustomEvent('create')); channelAccess.dispatchEvent(new CustomEvent('create'));
const channelAccessContainer = element?.current?.querySelector('.channelAccessContainer'); const channelAccessContainer = page.querySelector('.channelAccessContainer') as HTMLDivElement;
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide'); channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
element.current.querySelector('.chkEnableAllChannels').checked = false; (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false;
}, []); }, []);
const loadUser = useCallback(() => { const loadUser = useCallback(() => {
element.current.querySelector('#txtUsername').value = ''; const page = element.current;
element.current.querySelector('#txtPassword').value = '';
if (!page) {
console.error('Unexpected null reference');
return;
}
(page.querySelector('#txtUsername') as HTMLInputElement).value = '';
(page.querySelector('#txtPassword') as HTMLInputElement).value = '';
loading.show(); loading.show();
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false IsHidden: false
@ -76,29 +97,44 @@ const NewUserPage: FunctionComponent = () => {
}, [loadChannels, loadMediaFolders]); }, [loadChannels, loadMediaFolders]);
useEffect(() => { useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadUser(); loadUser();
const saveUser = () => { const saveUser = () => {
const userInput: userInput = {}; const userInput: userInput = {};
userInput.Name = element?.current?.querySelector('#txtUsername').value; userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
userInput.Password = element?.current?.querySelector('#txtPassword').value; userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
window.ApiClient.createUser(userInput).then(function (user) { window.ApiClient.createUser(userInput).then(function (user) {
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked; if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = []; user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) { if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (i) { user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) {
return i.checked; return i.checked;
}).map(function (i) { }).map(function (i) {
return i.getAttribute('data-id'); return i.getAttribute('data-id');
}); });
} }
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked; user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = []; user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) { if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (i) { user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) {
return i.checked; return i.checked;
}).map(function (i) { }).map(function (i) {
return i.getAttribute('data-id'); return i.getAttribute('data-id');
@ -114,7 +150,7 @@ const NewUserPage: FunctionComponent = () => {
}); });
}; };
const onSubmit = (e) => { const onSubmit = (e: Event) => {
loading.show(); loading.show();
saveUser(); saveUser();
e.preventDefault(); e.preventDefault();
@ -122,19 +158,19 @@ const NewUserPage: FunctionComponent = () => {
return false; return false;
}; };
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = element?.current?.querySelector('.channelAccessListContainer'); const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement;
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide'); this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
}); });
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = element?.current?.querySelector('.folderAccessListContainer'); const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement;
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide'); this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
}); });
element?.current?.querySelector('.newUserProfileForm').addEventListener('submit', onSubmit); (page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
element?.current?.querySelector('.button-cancel').addEventListener('click', function() { (page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back(); window.history.back();
}); });
}, [loadUser]); }, [loadUser]);

View file

@ -12,7 +12,7 @@ type SearchProps = {
}; };
const SearchPage: FunctionComponent<SearchProps> = ({ serverId, parentId, collectionType }: SearchProps) => { const SearchPage: FunctionComponent<SearchProps> = ({ serverId, parentId, collectionType }: SearchProps) => {
const [ query, setQuery ] = useState(null); const [ query, setQuery ] = useState<string>();
return ( return (
<> <>

View file

@ -1,3 +1,4 @@
import { SyncPlayUserAccessType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../scripts/clientUtils'; import Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
@ -23,16 +24,16 @@ type ItemsArr = {
const UserEditPage: FunctionComponent = () => { const UserEditPage: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ItemsArr[]>([]);
const [ authProviders, setAuthProviders ] = useState([]); const [ authProviders, setAuthProviders ] = useState([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]); const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState(''); const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState(''); const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
const element = useRef(null); const element = useRef<HTMLDivElement>(null);
const triggerChange = (select) => { const triggerChange = (select: HTMLInputElement) => {
const evt = document.createEvent('HTMLEvents'); const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true); evt.initEvent('change', false, true);
select.dispatchEvent(evt); select.dispatchEvent(evt);
@ -44,7 +45,14 @@ const UserEditPage: FunctionComponent = () => {
}; };
const loadAuthProviders = useCallback((user, providers) => { const loadAuthProviders = useCallback((user, providers) => {
const fldSelectLoginProvider = element?.current?.querySelector('.fldSelectLoginProvider'); const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide'); providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
setAuthProviders(providers); setAuthProviders(providers);
@ -54,7 +62,14 @@ const UserEditPage: FunctionComponent = () => {
}, []); }, []);
const loadPasswordResetProviders = useCallback((user, providers) => { const loadPasswordResetProviders = useCallback((user, providers) => {
const fldSelectPasswordResetProvider = element?.current?.querySelector('.fldSelectPasswordResetProvider'); const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide'); providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
setPasswordResetProviders(providers); setPasswordResetProviders(providers);
@ -64,6 +79,13 @@ const UserEditPage: FunctionComponent = () => {
}, []); }, []);
const loadDeleteFolders = useCallback((user, mediaFolders) => { const loadDeleteFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', { window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true SupportsMediaDeletion: true
})).then(function (channelsResult) { })).then(function (channelsResult) {
@ -93,13 +115,20 @@ const UserEditPage: FunctionComponent = () => {
setDeleteFoldersAccess(itemsArr); setDeleteFoldersAccess(itemsArr);
const chkEnableDeleteAllFolders = element.current.querySelector('.chkEnableDeleteAllFolders'); const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion; chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
triggerChange(chkEnableDeleteAllFolders); triggerChange(chkEnableDeleteAllFolders);
}); });
}, []); }, []);
const loadUser = useCallback((user) => { const loadUser = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) { window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(user, providers); loadAuthProviders(user, providers);
}); });
@ -112,37 +141,38 @@ const UserEditPage: FunctionComponent = () => {
loadDeleteFolders(user, folders.Items); loadDeleteFolders(user, folders.Items);
}); });
const disabledUserBanner = element?.current?.querySelector('.disabledUserBanner'); const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide'); user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
const txtUserName = element?.current?.querySelector('#txtUserName'); const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = ''; txtUserName.disabled = false;
txtUserName.removeAttribute('disabled'); txtUserName.removeAttribute('disabled');
const lnkEditUserPreferences = element?.current?.querySelector('.lnkEditUserPreferences'); const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id); lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
LibraryMenu.setTitle(user.Name); LibraryMenu.setTitle(user.Name);
setUserName(user.Name); setUserName(user.Name);
element.current.querySelector('#txtUserName').value = user.Name; (page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
element.current.querySelector('.chkIsAdmin').checked = user.Policy.IsAdministrator; (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
element.current.querySelector('.chkDisabled').checked = user.Policy.IsDisabled; (page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
element.current.querySelector('.chkIsHidden').checked = user.Policy.IsHidden; (page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
element.current.querySelector('.chkRemoteControlSharedDevices').checked = user.Policy.EnableSharedDeviceControl; (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
element.current.querySelector('.chkEnableRemoteControlOtherUsers').checked = user.Policy.EnableRemoteControlOfOtherUsers; (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
element.current.querySelector('.chkEnableDownloading').checked = user.Policy.EnableContentDownloading; (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
element.current.querySelector('.chkManageLiveTv').checked = user.Policy.EnableLiveTvManagement; (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
element.current.querySelector('.chkEnableLiveTvAccess').checked = user.Policy.EnableLiveTvAccess; (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
element.current.querySelector('.chkEnableMediaPlayback').checked = user.Policy.EnableMediaPlayback; (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
element.current.querySelector('.chkEnableAudioPlaybackTranscoding').checked = user.Policy.EnableAudioPlaybackTranscoding; (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
element.current.querySelector('.chkEnableVideoPlaybackTranscoding').checked = user.Policy.EnableVideoPlaybackTranscoding; (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
element.current.querySelector('.chkEnableVideoPlaybackRemuxing').checked = user.Policy.EnablePlaybackRemuxing; (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
element.current.querySelector('.chkForceRemoteSourceTranscoding').checked = user.Policy.ForceRemoteSourceTranscoding; (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
element.current.querySelector('.chkRemoteAccess').checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess; (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
element.current.querySelector('#txtRemoteClientBitrateLimit').value = user.Policy.RemoteClientBitrateLimit / 1e6 || ''; (page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
element.current.querySelector('#txtLoginAttemptsBeforeLockout').value = user.Policy.LoginAttemptsBeforeLockout || '0'; (user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, {maximumFractionDigits: 6}) : '';
element.current.querySelector('#txtMaxActiveSessions').value = user.Policy.MaxActiveSessions || '0'; (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')) { if (window.ApiClient.isMinServerVersion('10.6.0')) {
element.current.querySelector('#selectSyncPlayAccess').value = user.Policy.SyncPlayAccess; (page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value = user.Policy.SyncPlayAccess;
} }
loading.hide(); loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]); }, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
@ -155,6 +185,13 @@ const UserEditPage: FunctionComponent = () => {
}, [loadUser]); }, [loadUser]);
useEffect(() => { useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData(); loadData();
function onSaveComplete() { function onSaveComplete() {
@ -163,44 +200,52 @@ const UserEditPage: FunctionComponent = () => {
toast(globalize.translate('SettingsSaved')); toast(globalize.translate('SettingsSaved'));
} }
const saveUser = (user) => { const saveUser = (user: UserDto) => {
user.Name = element?.current?.querySelector('#txtUserName').value; if (!user.Id) {
user.Policy.IsAdministrator = element?.current?.querySelector('.chkIsAdmin').checked; throw new Error('Unexpected null user.Id');
user.Policy.IsHidden = element?.current?.querySelector('.chkIsHidden').checked; }
user.Policy.IsDisabled = element?.current?.querySelector('.chkDisabled').checked;
user.Policy.EnableRemoteControlOfOtherUsers = element?.current?.querySelector('.chkEnableRemoteControlOtherUsers').checked; if (!user.Policy) {
user.Policy.EnableLiveTvManagement = element?.current?.querySelector('.chkManageLiveTv').checked; throw new Error('Unexpected null user.Policy');
user.Policy.EnableLiveTvAccess = element?.current?.querySelector('.chkEnableLiveTvAccess').checked; }
user.Policy.EnableSharedDeviceControl = element?.current?.querySelector('.chkRemoteControlSharedDevices').checked;
user.Policy.EnableMediaPlayback = element?.current?.querySelector('.chkEnableMediaPlayback').checked; user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
user.Policy.EnableAudioPlaybackTranscoding = element?.current?.querySelector('.chkEnableAudioPlaybackTranscoding').checked; user.Policy.IsAdministrator = (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked;
user.Policy.EnableVideoPlaybackTranscoding = element?.current?.querySelector('.chkEnableVideoPlaybackTranscoding').checked; user.Policy.IsHidden = (page.querySelector('.chkIsHidden') as HTMLInputElement).checked;
user.Policy.EnablePlaybackRemuxing = element?.current?.querySelector('.chkEnableVideoPlaybackRemuxing').checked; user.Policy.IsDisabled = (page.querySelector('.chkDisabled') as HTMLInputElement).checked;
user.Policy.ForceRemoteSourceTranscoding = element?.current?.querySelector('.chkForceRemoteSourceTranscoding').checked; user.Policy.EnableRemoteControlOfOtherUsers = (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked;
user.Policy.EnableContentDownloading = element?.current?.querySelector('.chkEnableDownloading').checked; user.Policy.EnableLiveTvManagement = (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked;
user.Policy.EnableRemoteAccess = element?.current?.querySelector('.chkRemoteAccess').checked; user.Policy.EnableLiveTvAccess = (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked;
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat(element?.current?.querySelector('#txtRemoteClientBitrateLimit').value || '0')); user.Policy.EnableSharedDeviceControl = (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked;
user.Policy.LoginAttemptsBeforeLockout = parseInt(element?.current?.querySelector('#txtLoginAttemptsBeforeLockout').value || '0'); user.Policy.EnableMediaPlayback = (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked;
user.Policy.MaxActiveSessions = parseInt(element?.current?.querySelector('#txtMaxActiveSessions').value || '0'); user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.AuthenticationProviderId = element?.current?.querySelector('.selectLoginProvider').value; user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.PasswordResetProviderId = element?.current?.querySelector('.selectPasswordResetProvider').value; user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
user.Policy.EnableContentDeletion = element?.current?.querySelector('.chkEnableDeleteAllFolders').checked; user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) { user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
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.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; return c.checked;
}).map(function (c) { }).map(function (c) {
return c.getAttribute('data-id'); return c.getAttribute('data-id');
}); });
if (window.ApiClient.isMinServerVersion('10.6.0')) { if (window.ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = element?.current?.querySelector('#selectSyncPlayAccess').value; user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value as SyncPlayUserAccessType;
} }
window.ApiClient.updateUser(user).then(function () { window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
onSaveComplete(); onSaveComplete();
}); });
}); });
}; };
const onSubmit = (e) => { const onSubmit = (e: Event) => {
loading.show(); loading.show();
getUser().then(function (result) { getUser().then(function (result) {
saveUser(result); saveUser(result);
@ -210,22 +255,22 @@ const UserEditPage: FunctionComponent = () => {
return false; return false;
}; };
element?.current?.querySelector('.chkEnableDeleteAllFolders').addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
if (this.checked) { if (this.checked) {
element?.current?.querySelector('.deleteAccess').classList.add('hide'); (page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
} else { } else {
element?.current?.querySelector('.deleteAccess').classList.remove('hide'); (page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
} }
}); });
window.ApiClient.getServerConfiguration().then(function (config) { window.ApiClient.getServerConfiguration().then(function (config) {
const fldRemoteAccess = element?.current?.querySelector('.fldRemoteAccess'); const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide'); config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
}); });
element?.current?.querySelector('.editUserProfileForm').addEventListener('submit', onSubmit); (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
element?.current?.querySelector('.button-cancel').addEventListener('click', function() { (page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back(); window.history.back();
}); });
}, [loadData]); }, [loadData]);

View file

@ -1,3 +1,4 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import loading from '../loading/loading'; import loading from '../loading/loading';
@ -20,19 +21,26 @@ type ItemsArr = {
const UserLibraryAccessPage: FunctionComponent = () => { const UserLibraryAccessPage: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState([]); const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState([]); const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
const [devicesItems, setDevicesItems] = useState([]); const [devicesItems, setDevicesItems] = useState<ItemsArr[]>([]);
const element = useRef(null); const element = useRef<HTMLDivElement>(null);
const triggerChange = (select) => { const triggerChange = (select: HTMLInputElement) => {
const evt = document.createEvent('HTMLEvents'); const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true); evt.initEvent('change', false, true);
select.dispatchEvent(evt); select.dispatchEvent(evt);
}; };
const loadMediaFolders = useCallback((user, mediaFolders) => { const loadMediaFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = []; const itemsArr: ItemsArr[] = [];
for (const folder of mediaFolders) { for (const folder of mediaFolders) {
@ -47,12 +55,19 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setMediaFoldersItems(itemsArr); setMediaFoldersItems(itemsArr);
const chkEnableAllFolders = element.current.querySelector('.chkEnableAllFolders'); const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
chkEnableAllFolders.checked = user.Policy.EnableAllFolders; chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders); triggerChange(chkEnableAllFolders);
}, []); }, []);
const loadChannels = useCallback((user, channels) => { const loadChannels = useCallback((user, channels) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = []; const itemsArr: ItemsArr[] = [];
for (const folder of channels) { for (const folder of channels) {
@ -68,17 +83,24 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setChannelsItems(itemsArr); setChannelsItems(itemsArr);
if (channels.length) { if (channels.length) {
element?.current?.querySelector('.channelAccessContainer').classList.remove('hide'); (page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.remove('hide');
} else { } else {
element?.current?.querySelector('.channelAccessContainer').classList.add('hide'); (page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.add('hide');
} }
const chkEnableAllChannels = element.current.querySelector('.chkEnableAllChannels'); const chkEnableAllChannels = page.querySelector('.chkEnableAllChannels') as HTMLInputElement;
chkEnableAllChannels.checked = user.Policy.EnableAllChannels; chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
triggerChange(chkEnableAllChannels); triggerChange(chkEnableAllChannels);
}, []); }, []);
const loadDevices = useCallback((user, devices) => { const loadDevices = useCallback((user, devices) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = []; const itemsArr: ItemsArr[] = [];
for (const device of devices) { for (const device of devices) {
@ -94,14 +116,14 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setDevicesItems(itemsArr); setDevicesItems(itemsArr);
const chkEnableAllDevices = element.current.querySelector('.chkEnableAllDevices'); const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
chkEnableAllDevices.checked = user.Policy.EnableAllDevices; chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices); triggerChange(chkEnableAllDevices);
if (user.Policy.IsAdministrator) { if (user.Policy.IsAdministrator) {
element?.current?.querySelector('.deviceAccessContainer').classList.add('hide'); (page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
} else { } else {
element?.current?.querySelector('.deviceAccessContainer').classList.remove('hide'); (page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
} }
}, []); }, []);
@ -129,9 +151,16 @@ const UserLibraryAccessPage: FunctionComponent = () => {
}, [loadUser]); }, [loadUser]);
useEffect(() => { useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData(); loadData();
const onSubmit = (e) => { const onSubmit = (e: Event) => {
loading.show(); loading.show();
const userId = appRouter.param('userId'); const userId = appRouter.param('userId');
window.ApiClient.getUser(userId).then(function (result) { window.ApiClient.getUser(userId).then(function (result) {
@ -142,21 +171,29 @@ const UserLibraryAccessPage: FunctionComponent = () => {
return false; return false;
}; };
const saveUser = (user) => { const saveUser = (user: UserDto) => {
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked; if (!user.Id) {
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) { throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked; return c.checked;
}).map(function (c) { }).map(function (c) {
return c.getAttribute('data-id'); return c.getAttribute('data-id');
}); });
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked; user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (c) { user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (c) {
return c.checked; return c.checked;
}).map(function (c) { }).map(function (c) {
return c.getAttribute('data-id'); return c.getAttribute('data-id');
}); });
user.Policy.EnableAllDevices = element?.current?.querySelector('.chkEnableAllDevices').checked; user.Policy.EnableAllDevices = (page.querySelector('.chkEnableAllDevices') as HTMLInputElement).checked;
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkDevice'), function (c) { user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkDevice'), function (c) {
return c.checked; return c.checked;
}).map(function (c) { }).map(function (c) {
return c.getAttribute('data-id'); return c.getAttribute('data-id');
@ -173,19 +210,19 @@ const UserLibraryAccessPage: FunctionComponent = () => {
toast(globalize.translate('SettingsSaved')); toast(globalize.translate('SettingsSaved'));
}; };
element?.current?.querySelector('.chkEnableAllDevices').addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.deviceAccessListContainer').classList.toggle('hide', this.checked); (page.querySelector('.deviceAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
}); });
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.channelAccessListContainer').classList.toggle('hide', this.checked); (page.querySelector('.channelAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
}); });
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.folderAccessListContainer').classList.toggle('hide', this.checked); (page.querySelector('.folderAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
}); });
element?.current?.querySelector('.userLibraryAccessForm').addEventListener('submit', onSubmit); (page.querySelector('.userLibraryAccessForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadData]); }, [loadData]);
return ( return (

View file

@ -0,0 +1,429 @@
import { AccessSchedule, DynamicDayOfWeek, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import { appRouter } from '../appRouter';
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 SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
type RatingsArr = {
Name: string;
Value: number;
}
type ItemsArr = {
name: string;
value: string;
checkedAttribute: string
}
const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<RatingsArr[]>([]);
const [ unratedItems, setUnratedItems ] = useState<ItemsArr[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]);
const element = useRef<HTMLDivElement>(null);
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: RatingsArr[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
setParentalRatings(ratings);
}, []);
const loadUnratedItems = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const items = [{
name: globalize.translate('Books'),
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: 'Series'
}];
const itemsArr: ItemsArr[] = [];
for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
value: item.value,
name: item.name,
checkedAttribute: checkedAttribute
});
}
setUnratedItems(itemsArr);
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
}, []);
const loadBlockedTags = useCallback((tags) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setBlockedTags(tags);
const blockedTagsElem = page.querySelector('.blockedTags') as HTMLDivElement;
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = btnDeleteTag.getAttribute('data-tag');
const newTags = tags.filter(function (t: string) {
return t != tag;
});
loadBlockedTags(newTags);
});
}
}, []);
const renderAccessSchedule = useCallback((schedules) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setAccessSchedules(schedules);
const accessScheduleList = page.querySelector('.accessScheduleList') as HTMLDivElement;
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
btnDelete.addEventListener('click', function () {
const index = parseInt(btnDelete.getAttribute('data-index') || '0', 10);
schedules.splice(index, 1);
const newindex = schedules.filter(function (i: number) {
return i != index;
});
renderAccessSchedule(newindex);
});
}
}, []);
const loadUser = useCallback((user, allParentalRatings) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setUserName(user.Name);
LibraryMenu.setTitle(user.Name);
loadUnratedItems(user);
loadBlockedTags(user.Policy.BlockedTags);
populateRatings(allParentalRatings);
let ratingValue = '';
if (user.Policy.MaxParentalRating) {
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
const rating = allParentalRatings[i];
if (user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = rating.Value;
}
}
}
(page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value = ratingValue;
if (user.Policy.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
}
renderAccessSchedule(user.Policy.AccessSchedules || []);
loading.hide();
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
const loadData = useCallback(() => {
loading.show();
const userId = appRouter.param('userId');
const promise1 = window.ApiClient.getUser(userId);
const promise2 = window.ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
loadUser(responses[0], responses[1]);
});
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
};
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.MaxParentalRating = parseInt((page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value || '0', 10) || null;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-itemtype');
});
user.Policy.AccessSchedules = getSchedulesFromPage();
user.Policy.BlockedTags = getBlockedTagsFromPage();
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
};
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {};
import('../../components/accessSchedule/accessSchedule').then(({default: accessschedule}) => {
accessschedule.show({
schedule: schedule
}).then(function (updatedSchedule) {
const schedules = getSchedulesFromPage();
if (index == -1) {
index = schedules.length;
}
schedules[index] = updatedSchedule;
renderAccessSchedule(schedules);
});
});
};
const getSchedulesFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.liSchedule'), function (elem) {
return {
DayOfWeek: elem.getAttribute('data-day'),
StartHour: elem.getAttribute('data-start'),
EndHour: elem.getAttribute('data-end')
};
}) as AccessSchedule[];
};
const getBlockedTagsFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
return elem.getAttribute('data-tag');
}) as string[];
};
const showBlockedTagPopup = () => {
import('../../components/prompt/prompt').then(({default: prompt}) => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getBlockedTagsFromPage();
if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(tags);
}
});
});
};
const onSubmit = (e: Event) => {
loading.show();
const userId = appRouter.param('userId');
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('.btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
DayOfWeek: DynamicDayOfWeek.Sunday,
StartHour: 0,
EndHour: 0
}, -1);
});
(page.querySelector('.btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]);
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>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectMaxParentalRating
className= 'selectMaxParentalRating'
label= 'LabelMaxParentalRating'
parentalRatings={parentalRatings}
/>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
</div>
<div>
<div className='blockUnratedItems'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderBlockItemsWithNoRating')}
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxListItem
key={Item.value}
className='chkUnratedItem'
ItemType={Item.value}
Name={Item.name}
checkedAttribute={Item.checkedAttribute}
/>;
})}
</div>
</div>
</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>
<div className='blockedTags' style={{marginTop: '.5em'}}>
{blockedTags.map((tag, index) => {
return <BlockedTagList
key={index}
tag={tag}
/>;
})}
</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>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
return <AccessScheduleList
key={index}
index={index}
Id={accessSchedule.Id}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
/>;
})}
</div>
</div>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</div>
);
};
export default UserParentalControl;

View file

@ -1,4 +1,4 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, {FunctionComponent, useEffect, useState, useRef} from 'react'; import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import Dashboard from '../../scripts/clientUtils'; import Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
@ -21,9 +21,9 @@ type MenuEntry = {
} }
const UserProfilesPage: FunctionComponent = () => { const UserProfilesPage: FunctionComponent = () => {
const [ users, setUsers ] = useState([]); const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef(null); const element = useRef<HTMLDivElement>(null);
const loadData = () => { const loadData = () => {
loading.show(); loading.show();
@ -34,12 +34,24 @@ const UserProfilesPage: FunctionComponent = () => {
}; };
useEffect(() => { useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData(); loadData();
const showUserMenu = (elem) => { const showUserMenu = (elem: HTMLElement) => {
const card = dom.parentWithClass(elem, 'card'); const card = dom.parentWithClass(elem, 'card');
const userId = card.getAttribute('data-userid'); const userId = card.getAttribute('data-userid');
if (!userId) {
console.error('Unexpected null user id');
return;
}
const menuItems: MenuEntry[] = []; const menuItems: MenuEntry[] = [];
menuItems.push({ menuItems.push({
@ -67,7 +79,7 @@ const UserProfilesPage: FunctionComponent = () => {
actionsheet.show({ actionsheet.show({
items: menuItems, items: menuItems,
positionTo: card, positionTo: card,
callback: function (id) { callback: function (id: string) {
switch (id) { switch (id) {
case 'open': case 'open':
Dashboard.navigate('useredit.html?userId=' + userId); Dashboard.navigate('useredit.html?userId=' + userId);
@ -89,7 +101,7 @@ const UserProfilesPage: FunctionComponent = () => {
}); });
}; };
const deleteUser = (id) => { const deleteUser = (id: string) => {
const msg = globalize.translate('DeleteUserConfirmation'); const msg = globalize.translate('DeleteUserConfirmation');
confirm({ confirm({
@ -105,15 +117,15 @@ const UserProfilesPage: FunctionComponent = () => {
}); });
}; };
element?.current?.addEventListener('click', function (e) { page.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target, 'btnUserMenu'); const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
if (btnUserMenu) { if (btnUserMenu) {
showUserMenu(btnUserMenu); showUserMenu(btnUserMenu);
} }
}); });
element?.current?.querySelector('.btnAddUser').addEventListener('click', function() { (page.querySelector('.btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html'); Dashboard.navigate('usernew.html');
}); });
}, []); }, []);

View file

@ -15,7 +15,7 @@ let enableAnimation;
function getOsdElementHtml() { function getOsdElementHtml() {
let html = ''; let html = '';
html += '<span class="material-icons iconOsdIcon brightness_high"></span>'; html += '<span class="material-icons iconOsdIcon brightness_high" aria-hidden="true"></span>';
html += '<div class="iconOsdProgressOuter"><div class="iconOsdProgressInner brightnessOsdProgressInner"></div></div>'; html += '<div class="iconOsdProgressOuter"><div class="iconOsdProgressInner brightnessOsdProgressInner"></div></div>';

View file

@ -12,6 +12,8 @@ import Screenfull from 'screenfull';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import alert from '../alert'; import alert from '../alert';
const UNLIMITED_ITEMS = -1;
function enableLocalPlaylistManagement(player) { function enableLocalPlaylistManagement(player) {
if (player.getPlaylist) { if (player.getPlaylist) {
return false; return false;
@ -119,7 +121,11 @@ function getItemsForPlayback(serverId, query) {
}; };
}); });
} else { } else {
query.Limit = query.Limit || 300; if (query.Limit === UNLIMITED_ITEMS) {
delete query.Limit;
} else {
query.Limit = query.Limit || 300;
}
query.Fields = 'Chapters'; query.Fields = 'Chapters';
query.ExcludeLocationTypes = 'Virtual'; query.ExcludeLocationTypes = 'Virtual';
query.EnableTotalRecordCount = false; query.EnableTotalRecordCount = false;
@ -1774,7 +1780,8 @@ class PlaybackManager {
// Setting this to true may cause some incorrect sorting // Setting this to true may cause some incorrect sorting
Recursive: false, Recursive: false,
SortBy: options.shuffle ? 'Random' : 'SortName', SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Photo,Video' MediaTypes: 'Photo,Video',
Limit: UNLIMITED_ITEMS
}).then(function (result) { }).then(function (result) {
const items = result.Items; const items = result.Items;
@ -1799,7 +1806,7 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName', SortBy: options.shuffle ? 'Random' : 'SortName',
// Only include Photos because we do not handle mixed queues currently // Only include Photos because we do not handle mixed queues currently
MediaTypes: 'Photo', MediaTypes: 'Photo',
Limit: 1000 Limit: UNLIMITED_ITEMS
}); });
} else if (firstItem.Type === 'MusicGenre') { } else if (firstItem.Type === 'MusicGenre') {
promise = getItemsForPlayback(serverId, { promise = getItemsForPlayback(serverId, {
@ -1817,7 +1824,7 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName', SortBy: options.shuffle ? 'Random' : 'SortName',
// Only include Photos because we do not handle mixed queues currently // Only include Photos because we do not handle mixed queues currently
MediaTypes: 'Photo', MediaTypes: 'Photo',
Limit: 1000 Limit: UNLIMITED_ITEMS
}, queryOptions)); }, queryOptions));
} else if (firstItem.IsFolder) { } else if (firstItem.IsFolder) {
promise = getItemsForPlayback(serverId, mergePlaybackQueries({ promise = getItemsForPlayback(serverId, mergePlaybackQueries({
@ -2385,8 +2392,11 @@ class PlaybackManager {
streamInfo.fullscreen = playOptions.fullscreen; streamInfo.fullscreen = playOptions.fullscreen;
getPlayerData(player).isChangingStream = false; const playerData = getPlayerData(player);
getPlayerData(player).maxStreamingBitrate = maxBitrate;
playerData.isChangingStream = false;
playerData.maxStreamingBitrate = maxBitrate;
playerData.streamInfo = streamInfo;
return player.play(streamInfo).then(function () { return player.play(streamInfo).then(function () {
loading.hide(); loading.hide();
@ -3312,6 +3322,7 @@ class PlaybackManager {
mediaSource.MediaStreams = info.MediaStreams; mediaSource.MediaStreams = info.MediaStreams;
Events.trigger(player, 'mediastreamschange'); Events.trigger(player, 'mediastreamschange');
}, function () { }, function () {
// Swallow errors
}); });
} }

View file

@ -3,6 +3,7 @@ import { Events } from 'jellyfin-apiclient';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import loading from '../loading/loading'; import loading from '../loading/loading';
import { playbackManager } from '../playback/playbackmanager'; import { playbackManager } from '../playback/playbackmanager';
import { pluginManager } from '../pluginManager';
import { appRouter } from '../appRouter'; import { appRouter } from '../appRouter';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
@ -130,6 +131,13 @@ export function show(button) {
menuOptions.enableHistory = false; menuOptions.enableHistory = false;
} }
// Add message when Google Cast is not supported
const isChromecastPluginLoaded = !!pluginManager.plugins.find(plugin => plugin.id === 'chromecast');
// TODO: Add other checks for support (Android app, secure context, etc)
if (!isChromecastPluginLoaded) {
menuOptions.text = `(${globalize.translate('GoogleCastUnsupported')})`;
}
actionsheet.show(menuOptions).then(function (id) { actionsheet.show(menuOptions).then(function (id) {
const target = targets.filter(function (t) { const target = targets.filter(function (t) {
return t.id === id; return t.id === id;

View file

@ -16,7 +16,7 @@ let enableAnimation;
function getOsdElementHtml() { function getOsdElementHtml() {
let html = ''; let html = '';
html += '<span class="material-icons iconOsdIcon volume_up"></span>'; html += '<span class="material-icons iconOsdIcon volume_up" aria-hidden="true"></span>';
html += '<div class="iconOsdProgressOuter"><div class="iconOsdProgressInner"></div></div>'; html += '<div class="iconOsdProgressOuter"><div class="iconOsdProgressInner"></div></div>';

View file

@ -176,12 +176,6 @@ import template from './playbackSettings.template.html';
context.querySelector('.fldChromecastQuality').classList.add('hide'); context.querySelector('.fldChromecastQuality').classList.add('hide');
} }
if (browser.tizen || browser.web0s) {
context.querySelector('.fldEnableNextVideoOverlay').classList.add('hide');
} else {
context.querySelector('.fldEnableNextVideoOverlay').classList.remove('hide');
}
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false; context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer(); context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode(); context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();

View file

@ -90,7 +90,7 @@
<div class="fieldDescription checkboxFieldDescription">${SetUsingLastTracksHelp}</div> <div class="fieldDescription checkboxFieldDescription">${SetUsingLastTracksHelp}</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription fldEnableNextVideoOverlay hide"> <div class="checkboxContainer checkboxContainer-withDescription fldEnableNextVideoOverlay">
<label> <label>
<input type="checkbox" is="emby-checkbox" class="chkEnableNextVideoOverlay" /> <input type="checkbox" is="emby-checkbox" class="chkEnableNextVideoOverlay" />
<span>${EnableNextVideoInfoOverlay}</span> <span>${EnableNextVideoInfoOverlay}</span>

View file

@ -26,7 +26,7 @@ import ServerConnections from '../ServerConnections';
if (layoutManager.tv) { if (layoutManager.tv) {
button = ''; button = '';
} else { } else {
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close"></span></button>'; button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>';
} }
const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content'; const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content';

View file

@ -242,7 +242,7 @@ import ServerConnections from '../ServerConnections';
const title = globalize.translate('HeaderAddToPlaylist'); const title = globalize.translate('HeaderAddToPlaylist');
html += '<div class="formDialogHeader">'; html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">'; html += '<h3 class="formDialogHeaderTitle">';
html += title; html += title;
html += '</h3>'; html += '</h3>';

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
<span class="material-icons arrow_back"></span> <span class="material-icons arrow_back" aria-hidden="true"></span>
</button> </button>
<h3 class="formDialogHeaderTitle"></h3> <h3 class="formDialogHeaderTitle"></h3>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"></h3> <h3 class="formDialogHeaderTitle"></h3>
</div> </div>
<div class="formDialogContent smoothScrollY"> <div class="formDialogContent smoothScrollY">

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${HeaderRecordingOptions} ${HeaderRecordingOptions}
</h3> </h3>

View file

@ -2,7 +2,7 @@
<div class="recordSeriesContainer recordingFields-buttons flex align-items-center hide"> <div class="recordSeriesContainer recordingFields-buttons flex align-items-center hide">
<div> <div>
<button is="emby-button" type="button" class="raised recordingButton seriesRecordingButton"> <button is="emby-button" type="button" class="raised recordingButton seriesRecordingButton">
<span class="material-icons recordingIcon fiber_smart_record"></span> <span class="material-icons recordingIcon fiber_smart_record" aria-hidden="true"></span>
<span class="buttonText">${RecordSeries}</span> <span class="buttonText">${RecordSeries}</span>
</button> </button>
</div> </div>
@ -14,7 +14,7 @@
<div class="recordingFields-buttons flex align-items-center"> <div class="recordingFields-buttons flex align-items-center">
<div> <div>
<button is="emby-button" type="button" class="raised recordingButton singleRecordingButton"> <button is="emby-button" type="button" class="raised recordingButton singleRecordingButton">
<span class="material-icons recordingIcon fiber_manual_record"></span> <span class="material-icons recordingIcon fiber_manual_record" aria-hidden="true"></span>
<span class="buttonText">${Record}</span> <span class="buttonText">${Record}</span>
</button> </button>
</div> </div>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader"> <div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button> <button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"> <h3 class="formDialogHeaderTitle">
${HeaderSeriesOptions} ${HeaderSeriesOptions}
</h3> </h3>

View file

@ -120,7 +120,7 @@ class RefreshDialog {
const title = globalize.translate('RefreshMetadata'); const title = globalize.translate('RefreshMetadata');
html += '<div class="formDialogHeader">'; html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>'; html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">'; html += '<h3 class="formDialogHeaderTitle">';
html += title; html += title;
html += '</h3>'; html += '</h3>';

View file

@ -233,8 +233,8 @@ function updateNowPlayingInfo(context, state, serverId) {
apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) {
const userData = fullItem.UserData || {}; const userData = fullItem.UserData || {};
const likes = userData.Likes == null ? '' : userData.Likes; const likes = userData.Likes == null ? '' : userData.Likes;
context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite"></span></button>'; context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite"></span></button>'; context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
}); });
} else { } else {
backdrop.clearBackdrop(); backdrop.clearBackdrop();
@ -248,15 +248,11 @@ function setImageUrl(context, state, url) {
if (url) { if (url) {
imgContainer.innerHTML = '<img class="nowPlayingPageImage" src="' + url + '" />'; imgContainer.innerHTML = '<img class="nowPlayingPageImage" src="' + url + '" />';
if (item.Type == 'Audio') {
context.querySelector('.nowPlayingPageImage').classList.add('nowPlayingPageImageAudio'); context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImageAudio', item.Type === 'Audio');
context.querySelector('.nowPlayingPageImageContainer').classList.remove('nowPlayingPageImageAudio'); context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio');
} else {
context.querySelector('.nowPlayingPageImageContainer').classList.add('nowPlayingPageImagePoster');
context.querySelector('.nowPlayingPageImage').classList.remove('nowPlayingPageImageAudio');
}
} else { } else {
imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + cardBuilder.getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album"></span></button></div>'; imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + cardBuilder.getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album" aria-hidden="true"></span></button></div>';
} }
} }
@ -385,14 +381,14 @@ export default function () {
const context = dlg; const context = dlg;
const toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton'); const toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton');
const cssClass = 'buttonActive'; const cssClass = 'buttonActive';
let innHtml = '<span class="material-icons repeat"></span>'; let innHtml = '<span class="material-icons repeat" aria-hidden="true"></span>';
let repeatOn = true; let repeatOn = true;
switch (repeatMode) { switch (repeatMode) {
case 'RepeatAll': case 'RepeatAll':
break; break;
case 'RepeatOne': case 'RepeatOne':
innHtml = '<span class="material-icons repeat_one"></span>'; innHtml = '<span class="material-icons repeat_one" aria-hidden="true"></span>';
break; break;
case 'RepeatNone': case 'RepeatNone':
default: default:
@ -889,7 +885,7 @@ export default function () {
function init(ownerView, context) { function init(ownerView, context) {
let volumecontrolHtml = '<div class="volumecontrol flex align-items-center flex-wrap-wrap justify-content-center">'; let volumecontrolHtml = '<div class="volumecontrol flex align-items-center flex-wrap-wrap justify-content-center">';
volumecontrolHtml += `<button is="paper-icon-button-light" class="buttonMute autoSize" title=${globalize.translate('Mute')}><span class="xlargePaperIconButton material-icons volume_up"></span></button>`; volumecontrolHtml += `<button is="paper-icon-button-light" class="buttonMute autoSize" title=${globalize.translate('Mute')}><span class="xlargePaperIconButton material-icons volume_up" aria-hidden="true"></span></button>`;
volumecontrolHtml += '<div class="sliderContainer nowPlayingVolumeSliderContainer"><input is="emby-slider" type="range" step="1" min="0" max="100" value="0" class="nowPlayingVolumeSlider"/></div>'; volumecontrolHtml += '<div class="sliderContainer nowPlayingVolumeSliderContainer"><input is="emby-slider" type="range" step="1" min="0" max="100" value="0" class="nowPlayingVolumeSlider"/></div>';
volumecontrolHtml += '</div>'; volumecontrolHtml += '</div>';
const optionsSection = context.querySelector('.playlistSectionButton'); const optionsSection = context.querySelector('.playlistSectionButton');

View file

@ -81,8 +81,6 @@
-webkit-box-align: center; -webkit-box-align: center;
-webkit-align-items: center; -webkit-align-items: center;
align-items: center; align-items: center;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-flex-shrink: 0; -webkit-flex-shrink: 0;
flex-shrink: 0; flex-shrink: 0;
} }
@ -145,7 +143,7 @@
width: 100%; width: 100%;
-webkit-box-shadow: 0 0 1.9vh #000; -webkit-box-shadow: 0 0 1.9vh #000;
box-shadow: 0 0 1.9vh #000; box-shadow: 0 0 1.9vh #000;
border: 0.1em solid #222; border-radius: 0.2em;
user-select: none; user-select: none;
-moz-user-select: none; -moz-user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
@ -203,6 +201,7 @@
.layout-desktop .playlistSectionButton, .layout-desktop .playlistSectionButton,
.layout-tv .playlistSectionButton { .layout-tv .playlistSectionButton {
background: none; background: none;
color: inherit;
} }
.layout-desktop .nowPlayingPlaylist, .layout-desktop .nowPlayingPlaylist,
@ -274,9 +273,7 @@
.remoteControlContent { .remoteControlContent {
padding-left: 7.3% !important; padding-left: 7.3% !important;
padding-right: 7.3% !important; padding-right: 7.3% !important;
display: flex;
height: 100%; height: 100%;
flex-direction: column;
} }
.layout-desktop .nowPlayingPageUserDataButtons { .layout-desktop .nowPlayingPageUserDataButtons {
@ -296,7 +293,6 @@
} }
.nowPlayingPageTitle { .nowPlayingPageTitle {
/* text-align: center; */
margin: 0; margin: 0;
} }
@ -317,17 +313,24 @@
} }
.nowPlayingInfoButtons { .nowPlayingInfoButtons {
/* margin: 1.5em 0 0 0; */
-webkit-box-pack: center; -webkit-box-pack: center;
-webkit-justify-content: center; -webkit-justify-content: center;
justify-content: center; justify-content: center;
font-size: 1.5em; font-size: 1.5em;
height: 100%; margin-left: -0.5em;
margin-right: -0.5em;
} }
.nowPlayingPageImageContainer { .nowPlayingPageImageContainer {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
align-items: center;
justify-content: center;
width: 100%; width: 100%;
margin: auto auto 0.5em; min-height: 0;
margin: 0 auto 0.5em;
} }
.nowPlayingPageImageContainerNoAlbum .cardImageContainer .cardImageIcon { .nowPlayingPageImageContainerNoAlbum .cardImageContainer .cardImageIcon {
@ -336,7 +339,9 @@
} }
.nowPlayingInfoControls { .nowPlayingInfoControls {
margin: 0.5em 0 1em 0; flex-grow: 0;
flex-shrink: 1;
margin: 0.5em 0 0;
width: 100%; width: 100%;
-webkit-box-pack: start !important; -webkit-box-pack: start !important;
-webkit-justify-content: start !important; -webkit-justify-content: start !important;
@ -365,19 +370,15 @@
.nowPlayingInfoButtons .btnRepeat, .nowPlayingInfoButtons .btnRepeat,
.nowPlayingInfoButtons .btnRewind { .nowPlayingInfoButtons .btnRewind {
position: absolute;
left: 0;
margin-left: 0; margin-left: 0;
padding-left: 7.3%; margin-right: auto;
font-size: smaller; font-size: smaller;
} }
.nowPlayingInfoButtons .btnShuffleQueue, .nowPlayingInfoButtons .btnShuffleQueue,
.nowPlayingInfoButtons .btnFastForward { .nowPlayingInfoButtons .btnFastForward {
position: absolute; margin-left: auto;
right: 0;
margin-right: 0; margin-right: 0;
padding-right: 7.3%;
font-size: smaller; font-size: smaller;
} }
@ -391,25 +392,11 @@
font-size: 2em; font-size: 2em;
} }
.nowPlayingPageImage { .nowPlayingPageImage.nowPlayingPageImageAudio,
/* width: inherit; */ .nowPlayingPageImage.nowPlayingPageImagePoster {
overflow-y: hidden;
overflow: hidden;
margin: 0 auto;
}
.nowPlayingPageImage.nowPlayingPageImageAudio {
width: 100%;
}
.nowPlayingPageImageContainer.nowPlayingPageImagePoster {
height: 50%;
overflow: hidden;
}
.nowPlayingPageImageContainer.nowPlayingPageImagePoster img {
height: 100%;
width: auto; width: auto;
max-width: 100%;
max-height: 100%;
} }
.playlistSectionButton .volumecontrol { .playlistSectionButton .volumecontrol {
@ -423,7 +410,6 @@
.nowPlayingButtonsContainer { .nowPlayingButtonsContainer {
display: flex; display: flex;
height: 100%;
flex-direction: column; flex-direction: column;
} }
} }

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