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'],
'keyword-spacing': ['error'],
'max-statements-per-line': ['error'],
'no-empty-function': ['error'],
'no-floating-decimal': ['error'],
'no-multi-spaces': ['error'],
'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' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:

View file

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

View file

@ -13,10 +13,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup node environment
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v3.0.0
with:
node-version: 12
check-latest: true
@ -48,10 +48,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup node environment
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v3.0.0
with:
node-version: 12
check-latest: true
@ -86,10 +86,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup node environment
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v3.0.0
with:
node-version: 12
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
save-exact=true

View file

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

3963
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",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@babel/core": "^7.16.7",
"@babel/eslint-parser": "^7.16.5",
"@babel/eslint-plugin": "^7.16.5",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-private-methods": "^7.16.7",
"@babel/plugin-transform-modules-umd": "^7.16.7",
"@babel/preset-env": "^7.16.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@uupaa/dynamic-import-polyfill": "^1.0.2",
"autoprefixer": "^10.4.1",
"babel-loader": "^8.2.3",
"babel-plugin-dynamic-import-polyfill": "^1.0.0",
"clean-webpack-plugin": "^4.0.0",
"confusing-browser-globals": "^1.0.11",
"copy-webpack-plugin": "^10.2.0",
"css-loader": "^6.5.1",
"cssnano": "^5.0.14",
"eslint": "^7.32.0",
"eslint-plugin-compat": "^4.0.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"expose-loader": "^3.1.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"postcss": "^8.4.5",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.2.0",
"postcss-scss": "^4.0.2",
"sass": "^1.45.2",
"sass-loader": "^12.4.0",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"stylelint": "^14.2.0",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-no-browser-hacks": "^1.2.1",
"stylelint-order": "^5.0.0",
"stylelint-scss": "^4.1.0",
"ts-loader": "^9.2.6",
"typescript": "^4.5.4",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.2",
"webpack-merge": "^5.8.0",
"workbox-webpack-plugin": "^6.2.4",
"worker-loader": "^3.0.8"
"@babel/core": "7.17.5",
"@babel/eslint-parser": "7.17.0",
"@babel/eslint-plugin": "7.16.5",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-private-methods": "7.16.11",
"@babel/plugin-transform-modules-umd": "7.16.7",
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@thornbill/jellyfin-sdk": "0.4.1",
"@types/lodash-es": "4.17.6",
"@types/react": "17.0.39",
"@types/react-dom": "17.0.12",
"@typescript-eslint/eslint-plugin": "5.13.0",
"@typescript-eslint/parser": "5.13.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.2",
"babel-loader": "8.2.3",
"babel-plugin-dynamic-import-polyfill": "1.0.0",
"clean-webpack-plugin": "4.0.0",
"confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "10.2.4",
"css-loader": "6.6.0",
"cssnano": "5.1.0",
"eslint": "8.10.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-promise": "6.0.0",
"eslint-plugin-react": "7.29.2",
"eslint-plugin-react-hooks": "4.3.0",
"expose-loader": "3.1.0",
"html-loader": "3.1.0",
"html-webpack-plugin": "5.5.0",
"postcss": "8.4.7",
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.4.1",
"postcss-scss": "4.0.3",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"source-map-loader": "3.0.1",
"style-loader": "3.3.1",
"stylelint": "14.5.3",
"stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "5.0.0",
"stylelint-scss": "4.1.0",
"ts-loader": "9.2.7",
"typescript": "4.6.2",
"webpack": "5.69.1",
"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": {
"@fontsource/noto-sans": "^4.5.1",
"@fontsource/noto-sans-hk": "^4.5.2",
"@fontsource/noto-sans-jp": "^4.5.2",
"@fontsource/noto-sans-kr": "^4.5.2",
"@fontsource/noto-sans-sc": "^4.5.2",
"blurhash": "^1.1.4",
"@fontsource/noto-sans": "4.5.1",
"@fontsource/noto-sans-hk": "4.5.2",
"@fontsource/noto-sans-jp": "4.5.2",
"@fontsource/noto-sans-kr": "4.5.2",
"@fontsource/noto-sans-sc": "4.5.2",
"blurhash": "1.1.4",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "^2.3.1",
"core-js": "^3.20.2",
"date-fns": "^2.28.0",
"dompurify": "^2.3.4",
"epubjs": "^0.3.90",
"fast-text-encoding": "^1.0.3",
"flv.js": "^1.6.2",
"headroom.js": "^0.12.0",
"hls.js": "^0.14.17",
"intersection-observer": "^0.12.0",
"jellyfin-apiclient": "^1.10.0",
"jquery": "^3.5.1",
"jstree": "^3.3.12",
"libarchive.js": "^1.3.0",
"classnames": "2.3.1",
"core-js": "3.20.2",
"date-fns": "2.28.0",
"dompurify": "2.3.4",
"epubjs": "0.3.90",
"fast-text-encoding": "1.0.3",
"flv.js": "1.6.2",
"headroom.js": "0.12.0",
"hls.js": "0.14.17",
"intersection-observer": "0.12.0",
"jellyfin-apiclient": "1.10.0",
"jquery": "3.6.0",
"jstree": "3.3.12",
"libarchive.js": "1.3.0",
"libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4",
"lodash-es": "^4.17.21",
"marked": "^4.0.8",
"material-design-icons-iconfont": "^6.1.1",
"native-promise-only": "^0.8.0-a",
"page": "^1.11.6",
"lodash-es": "4.17.21",
"marked": "4.0.10",
"material-design-icons-iconfont": "6.1.1",
"native-promise-only": "0.8.1",
"page": "1.11.6",
"pdfjs-dist": "2.12.313",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^6.0.0",
"sortablejs": "^1.14.0",
"swiper": "^6.8.4",
"webcomponents.js": "^0.7.24",
"whatwg-fetch": "^3.6.2",
"workbox-core": "^6.2.4",
"workbox-precaching": "^6.2.4"
"react": "17.0.2",
"react-dom": "17.0.2",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.0",
"sortablejs": "1.14.0",
"swiper": "6.8.4",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.2",
"workbox-core": "6.2.4",
"workbox-precaching": "6.2.4"
},
"browserslist": [
"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 {
margin: 0.15em 0 0.2em;
// Leave room for a focused button
margin-left: -1em;
padding-left: 1em;
}
.layout-mobile .subtitle {
margin: 0.2em 0 0.2em;
padding-left: 0; // Reset padding for focused button since 'margin-left' is 0
}
.detailPagePrimaryContainer {
@ -987,6 +992,10 @@ div.itemDetailGalleryLink.defaultCardBackground {
border-collapse: collapse;
}
.mediaInfoContent .btnCopy .material-icons {
font-size: inherit;
}
.mediaInfoStream {
margin: 0 3em 0 0;
display: inline-block;
@ -995,6 +1004,10 @@ div.itemDetailGalleryLink.defaultCardBackground {
.mediaInfoStreamType {
display: block;
margin: 0.622em 0; /* copy button height compensation */
}
.layout-tv .mediaInfoStreamType {
margin: 1em 0;
}

View file

@ -142,7 +142,7 @@ export function show(options) {
if (layoutManager.tv) {
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>`;
}
@ -204,9 +204,9 @@ export function show(options) {
itemIcon = icons[i];
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) {
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">';

View file

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

View file

@ -23,12 +23,12 @@ import alert from './alert';
}
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',
tag: entry.UserPrimaryImageTag
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
} 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">';
@ -45,7 +45,7 @@ import alert from './alert';
if (entry.Overview) {
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>`;
}

View file

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

View file

@ -75,7 +75,7 @@ import 'material-design-icons-iconfont';
html += `<div class="${rowClassName}">`;
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 {
letters = ['#'];
html += mapLetters(letters, vertical).join('');
@ -85,7 +85,7 @@ import 'material-design-icons-iconfont';
html += mapLetters(letters, vertical).join('');
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>';
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.visible(query.SortBy.indexOf('SortName') === 0);
this.visible(query.SortBy.indexOf('SortName') !== -1);
}
visible(visible) {

View file

@ -34,16 +34,21 @@ class AppRouter {
constructor() {
// WebKit fires a popstate event on document load
// Skip it using timeout
// Skip it using boolean
// For Tizen 2.x
// https://stackoverflow.com/a/12214354
// See `page` node module
let loaded = document.readyState === 'complete';
if (!loaded) {
window.addEventListener('load', () => {
setTimeout(() => {
window.addEventListener('popstate', () => {
this.popstateOccurred = true;
});
loaded = true;
}, 0);
});
}
window.addEventListener('popstate', () => {
if (!loaded) return;
this.popstateOccurred = true;
});
document.addEventListener('viewshow', () => this.onViewShow());
@ -752,7 +757,13 @@ class AppRouter {
}
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') {

View file

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

View file

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

View file

@ -227,6 +227,10 @@ button::-moz-focus-inner {
background-color: transparent;
}
.cardPadder {
position: relative; // For centering the cardImageIcon
}
.cardBox:not(.visualCardBox) .cardPadder {
border-radius: 0.2em;
background-color: #242424;
@ -377,6 +381,18 @@ button::-moz-focus-inner {
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 {
right: 0.225em;
top: 0.225em;

View file

@ -145,7 +145,7 @@ import ServerConnections from '../ServerConnections';
return 100 / 14.2857142857;
}
if (screenWidth >= 1200) {
return 100 / 16.666666666666666666;
return 100 / 16.66666667;
}
if (screenWidth >= 1000) {
return 5;
@ -481,12 +481,20 @@ import ServerConnections from '../ServerConnections';
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.
* @param {Object} item - Item for which to generate a card.
* @param {Object} apiClient - API client object.
* @param {Object} options - Options of the card.
* @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) {
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.
* @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.
*/
function getDefaultColorIndex(str) {
@ -726,8 +734,8 @@ import ServerConnections from '../ServerConnections';
/**
* 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 {string} showAirDateTime - ISO8601 date for the start of the show.
* @param {string} showAirEndTime - ISO8601 date for the end of the show.
* @param {boolean} showAirDateTime - ISO8601 date for the start 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.
*/
function getAirTimeText(item, showAirDateTime, showAirEndTime) {
@ -782,7 +790,7 @@ import ServerConnections from '../ServerConnections';
if (isOuterFooter && options.cardLayout && layoutManager.mobile) {
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) {
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.
* @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.
*/
export function getDefaultBackgroundClass(str) {
@ -1297,15 +1309,15 @@ import ServerConnections from '../ServerConnections';
const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction';
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') {
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) {
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';
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>';
cardScalableClose = '</div>';
@ -1444,7 +1467,7 @@ import ServerConnections from '../ServerConnections';
const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';
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">';
@ -1454,7 +1477,7 @@ import ServerConnections from '../ServerConnections';
if (itemHelper.canMarkPlayed(item)) {
/* eslint-disable-next-line @babel/no-unused-expressions */
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)) {
@ -1462,10 +1485,10 @@ import ServerConnections from '../ServerConnections';
/* eslint-disable-next-line @babel/no-unused-expressions */
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>';
@ -1480,35 +1503,40 @@ import ServerConnections from '../ServerConnections';
*/
export function getDefaultText(item, options) {
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) {
case 'MusicAlbum':
return '<span class="cardImageIcon material-icons album"></span>';
return '<span class="cardImageIcon material-icons album" aria-hidden="true"></span>';
case 'MusicArtist':
case 'Person':
return '<span class="cardImageIcon material-icons person"></span>';
return '<span class="cardImageIcon material-icons person" aria-hidden="true"></span>';
case 'Audio':
return '<span class="cardImageIcon material-icons audiotrack"></span>';
return '<span class="cardImageIcon material-icons audiotrack" aria-hidden="true"></span>';
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':
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':
return '<span class="cardImageIcon material-icons book"></span>';
return '<span class="cardImageIcon material-icons book" aria-hidden="true"></span>';
case 'Folder':
return '<span class="cardImageIcon material-icons folder"></span>';
return '<span class="cardImageIcon material-icons folder" aria-hidden="true"></span>';
case 'BoxSet':
return '<span class="cardImageIcon material-icons collections"></span>';
return '<span class="cardImageIcon material-icons collections" aria-hidden="true"></span>';
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':
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) {
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '"></span>';
if (options?.defaultCardImageIcon) {
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '" aria-hidden="true"></span>';
}
const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item);
@ -1605,7 +1633,7 @@ import ServerConnections from '../ServerConnections';
indicatorsElem = ensureIndicators(card, indicatorsElem);
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 {
playedIndicator = card.querySelector('.playedIndicator');
if (playedIndicator) {
@ -1688,7 +1716,7 @@ import ServerConnections from '../ServerConnections';
const icon = cell.querySelector('.timerIndicator');
if (!icon) {
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);
}

View file

@ -94,7 +94,7 @@ import ServerConnections from '../ServerConnections';
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
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 = '';

View file

@ -72,7 +72,7 @@ export default class channelMapper {
function getTunerChannelHtml(channel, providerName) {
let html = '';
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 += '<h3 class="listItemBodyText">';
html += channel.Name;
@ -85,7 +85,7 @@ export default class channelMapper {
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>';
}
@ -127,7 +127,7 @@ export default class channelMapper {
let html = '';
const title = globalize.translate('MapChannels');
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 += title;
html += '</h3>';

View file

@ -229,7 +229,7 @@ import toast from '../toast/toast';
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
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 += title;
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 globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }) => ({
const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
__html: `<button
is="emby-button"
type="${type}"

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react';
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}">
<input
is="emby-checkbox"

View file

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

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react';
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
is="emby-input"
type="${type}"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -77,7 +77,7 @@ function getItem(cssClass, type, path, name) {
html += name;
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>';
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 += '</div>';
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>';
if (!readOnlyAttribute) {
@ -264,7 +264,7 @@ class DirectoryBrowser {
let html = '';
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 += options.header || globalize.translate('HeaderSelectPath');
html += '</h3>';

View file

@ -100,16 +100,6 @@ import template from './displaySettings.template.html';
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('#selectDashboardTheme'), userSettings.dashboardTheme());

View file

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

View file

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

View file

@ -228,7 +228,7 @@ class FilterMenu {
let html = '';
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 += '</div>';

View file

@ -166,7 +166,6 @@ function Guide(options) {
stopAutoRefresh();
Events.off(serverNotifications, 'TimerCreated', onTimerCreated);
Events.off(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.off(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);
@ -409,7 +408,7 @@ function Guide(options) {
let status;
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) {
status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') {
@ -420,13 +419,13 @@ function Guide(options) {
if (item.SeriesTimerId) {
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) {
@ -537,7 +536,7 @@ function Guide(options) {
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;
@ -1057,9 +1056,6 @@ function Guide(options) {
}
}
function onSeriesTimerCreated() {
}
function onTimerCancelled(e, apiClient, data) {
const id = data.Id;
// find guide cells by timer id, remove timer icon
@ -1186,7 +1182,6 @@ function Guide(options) {
Events.trigger(self, 'load');
Events.on(serverNotifications, 'TimerCreated', onTimerCreated);
Events.on(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.on(serverNotifications, 'TimerCancelled', onTimerCancelled);
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 += '<span class="material-icons listItemIcon folder_open"></span>';
currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
currentHtml += '<div class="listItemBody">';
@ -187,8 +187,8 @@ import template from './homeScreenSettings.template.html';
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="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="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" aria-hidden="true"></span></button>`;
currentHtml += '</div>';

View file

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

View file

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

View file

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

View file

@ -31,11 +31,16 @@ import template from './imageDownloader.template.html';
let browsableImageStartIndex = 0;
let browsableImageType = 'Primary';
let selectedProvider;
let browsableParentId;
function getBaseRemoteOptions() {
function getBaseRemoteOptions(page) {
const options = {};
if (page.querySelector('#chkShowParentImages').checked && browsableParentId) {
options.itemId = browsableParentId;
} else {
options.itemId = currentItemId;
}
return options;
}
@ -43,7 +48,7 @@ import template from './imageDownloader.template.html';
function reloadBrowsableImages(page, apiClient) {
loading.show();
const options = getBaseRemoteOptions();
const options = getBaseRemoteOptions(page);
options.type = browsableImageType;
options.startIndex = browsableImageStartIndex;
@ -124,8 +129,8 @@ import template from './imageDownloader.template.html';
if (showControls) {
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('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('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" aria-hidden="true"></span></button>`;
html += '</div>';
}
@ -135,7 +140,7 @@ import template from './imageDownloader.template.html';
}
function downloadRemoteImage(page, apiClient, url, type, provider) {
const options = getBaseRemoteOptions();
const options = getBaseRemoteOptions(page);
options.Type = type;
options.ImageUrl = url;
@ -259,7 +264,7 @@ import template from './imageDownloader.template.html';
if (enableFooterButtons) {
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>';
}
@ -273,26 +278,31 @@ import template from './imageDownloader.template.html';
return html;
}
function reloadBrowsableImagesFirstPage(page, apiClient) {
browsableImageStartIndex = 0;
reloadBrowsableImages(page, apiClient);
}
function initEditor(page, apiClient) {
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
browsableImageType = this.value;
browsableImageStartIndex = 0;
selectedProvider = null;
reloadBrowsableImages(page, apiClient);
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#selectImageProvider').addEventListener('change', function () {
browsableImageStartIndex = 0;
selectedProvider = this.value;
reloadBrowsableImages(page, apiClient);
reloadBrowsableImagesFirstPage(page, apiClient);
});
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) {
@ -336,6 +346,10 @@ import template from './imageDownloader.template.html';
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()
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) {
currentResolve = resolve;
currentReject = reject;
@ -374,6 +388,7 @@ export function show(itemId, serverId, itemType, imageType) {
browsableImageStartIndex = 0;
browsableImageType = imageType || 'Primary';
selectedProvider = null;
browsableParentId = parentId;
showEditor(itemId, serverId, itemType);
});
}

View file

@ -1,5 +1,5 @@
<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">
${Search}
</h3>
@ -39,6 +39,10 @@
<input id="chkAllLanguages" type="checkbox" is="emby-checkbox" />
<span>${AllLanguages}</span>
</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 class="availableImagesList vertical-wrap centered"></div>

View file

@ -1,5 +1,5 @@
<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">
${HeaderImageOptions}
</h3>

View file

@ -1,5 +1,5 @@
<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">
${HeaderUploadImage}
</h3>
@ -14,7 +14,7 @@
<h2 style="margin:0;">${HeaderAddUpdateImage}</h2>
<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>
</button>
</div>

View file

@ -165,21 +165,21 @@ import template from './imageeditor.template.html';
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>';
} 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) {
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 {
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 {
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>';
}
@ -280,7 +280,13 @@ import template from './imageeditor.template.html';
function showImageDownloader(page, imageType) {
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;
reload(page);
});

View file

@ -1,5 +1,5 @@
<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">
${HeaderEditImages}
</h3>
@ -12,10 +12,10 @@
<div class="imageEditor-buttons first-imageEditor-buttons">
<h2 style="margin:0;">${Images}</h2>
<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 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>
</div>
<div id="images" class="itemsContainer vertical-wrap">
@ -27,10 +27,10 @@
<div class="imageEditor-buttons">
<h2 style="margin:0;">${Backdrops}</h2>
<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 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>
</div>
<div id="backdrops" class="itemsContainer vertical-wrap">
@ -42,10 +42,10 @@
<div class="imageEditor-buttons">
<h2 style="margin: 0;">${Screenshots}</h2>
<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 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>
</div>
<div id="screenshots" class="itemsContainer vertical-wrap">

View file

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

View file

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

View file

@ -88,7 +88,7 @@ export function getPlayedIndicatorHtml(item) {
}
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;
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) {
status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') {
@ -120,20 +120,20 @@ export function getTimerIndicator(item) {
if (item.SeriesTimerId) {
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) {
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) {
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 '';
@ -148,7 +148,7 @@ export function getTypeIndicator(item) {
};
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) {

View file

@ -1,4 +1,5 @@
import browser from '../scripts/browser';
import { copy } from '../scripts/clipboard';
import globalize from '../scripts/globalize';
import actionsheet from './actionSheet/actionSheet';
import { appHost } from './apphost';
@ -366,32 +367,11 @@ import toast from './toast/toast';
break;
case 'copy-stream': {
const downloadHref = apiClient.getItemDownloadUrl(itemId);
const textAreaCopy = function () {
const textArea = document.createElement('textarea');
textArea.value = downloadHref;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
if (document.execCommand('copy')) {
copy(downloadHref).then(() => {
toast(globalize.translate('CopyStreamURLSuccess'));
} else {
}).catch(() => {
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)();
break;
}

View file

@ -7,6 +7,9 @@
import dialogHelper from '../dialogHelper/dialogHelper';
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 loading from '../loading/loading';
import '../../elements/emby-select/emby-select';
@ -19,6 +22,12 @@ import '../../assets/css/flexstyles.scss';
import ServerConnections from '../ServerConnections';
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) {
let html = item.MediaSources.map(version => {
return getMediaSourceHtml(user, item, version);
@ -28,12 +37,25 @@ import template from './itemMediaInfo.template.html';
}
const mediaInfoContent = page.querySelector('#mediaInfoContent');
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) {
let html = '';
let html = '<div class="mediaInfoSource">';
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) {
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
@ -69,7 +91,7 @@ import template from './itemMediaInfo.template.html';
}
const displayType = globalize.translate(translateString);
html += `<h2 class="mediaInfoStreamType">${displayType}</h2>`;
html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
const attributes = [];
if (stream.DisplayTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
@ -143,10 +165,8 @@ import template from './itemMediaInfo.template.html';
if (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')));
}
if (stream.Type === 'Subtitle') {
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? '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 += '</div>';
}
html += '</div>';
return html;
}
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) {

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader">
<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>
<h3 class="formDialogHeaderTitle">${MoreMediaInfo}</h3>
</div>

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader">
<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>
<h3 class="formDialogHeaderTitle">${Identify}</h3>
</div>

View file

@ -69,16 +69,16 @@ import template from './libraryoptionseditor.template.html';
for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i];
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 += '<h3 class="listItemBodyText">';
html += plugin.Name;
html += '</h3>';
html += '</div>';
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) {
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>';
}
@ -132,9 +132,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>';
html += '</div>';
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) {
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>';
});
@ -198,9 +198,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>';
html += '</div>';
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) {
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>';
}
@ -237,9 +237,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>';
html += '</div>';
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) {
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>';
}
@ -411,7 +411,13 @@ import template from './libraryoptionseditor.template.html';
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);
}
@ -509,6 +515,7 @@ import template from './libraryoptionseditor.template.html';
AutomaticRefreshIntervalDays: parseInt(parent.querySelector('#selectAutoRefreshInterval').value),
EnableEmbeddedTitles: parent.querySelector('#chkEnableEmbeddedTitles').checked,
EnableEmbeddedEpisodeInfos: parent.querySelector('#chkEnableEmbeddedEpisodeInfos').checked,
AllowEmbeddedSubtitles: parent.querySelector('#selectAllowEmbeddedSubtitles').value,
SkipSubtitlesIfEmbeddedSubtitlesPresent: parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked,
SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked,
SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked,
@ -560,7 +567,8 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata;
parent.querySelector('.chkAutomaticallyGroupSeries').checked = options.EnableAutomaticSeriesGrouping;
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('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia;
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;

View file

@ -30,6 +30,15 @@
</label>
<div class="fieldDescription checkboxFieldDescription">${PreferEmbeddedEpisodeInfosOverFileNamesHelp}</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">
<label>

View file

@ -166,7 +166,7 @@ import ServerConnections from '../ServerConnections';
for (let i = 0, length = options.rightButtons.length; i < length; 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;
@ -257,7 +257,7 @@ import ServerConnections from '../ServerConnections';
}
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) {
@ -282,6 +282,11 @@ import ServerConnections from '../ServerConnections';
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 = '';
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
@ -290,7 +295,7 @@ import ServerConnections from '../ServerConnections';
}
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, {
@ -450,11 +455,11 @@ import ServerConnections from '../ServerConnections';
if (!clickEntireItem) {
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) {
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) {
@ -466,16 +471,16 @@ import ServerConnections from '../ServerConnections';
const likes = userData.Likes == null ? '' : userData.Likes;
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) {
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) {
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>';

View file

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

View file

@ -73,7 +73,7 @@ import template from './mediaLibraryCreator.template.html';
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
const value = this.value;
const dlg = $(this).parents('.dialog')[0];
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value == 'mixed' ? '' : value);
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
if (value) {
dlg.querySelector('.libraryOptions').classList.remove('hide');
@ -87,12 +87,11 @@ import template from './mediaLibraryCreator.template.html';
if (index != -1) {
const name = this.options[index].innerHTML.replace('*', '').replace('&amp;', '&');
$('#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('.btnSubmit').addEventListener('click', onAddLibrary);
@ -128,7 +127,7 @@ import template from './mediaLibraryCreator.template.html';
}
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>';
return html;
}

View file

@ -1,5 +1,5 @@
<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>
</div>
@ -20,7 +20,7 @@
<div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1>
<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>
</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 += `<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>';
return html;
}

View file

@ -1,5 +1,5 @@
<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>
</div>
@ -13,7 +13,7 @@
<div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1>
<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>
</div>
<div class="paperList folderList" style="margin-bottom:2em;"></div>

View file

@ -13,7 +13,7 @@ import '../../elements/emby-button/emby-button';
let status;
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) {
status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') {
@ -24,13 +24,13 @@ import '../../elements/emby-button/emby-button';
if (item.SeriesTimerId) {
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) {
@ -358,7 +358,7 @@ import '../../elements/emby-button/emby-button';
if (item.CommunityRating) {
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 += '</div>';
}

View file

@ -460,7 +460,7 @@ import template from './metadataEditor.template.html';
html += '</div>';
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>';
@ -898,7 +898,7 @@ import template from './metadataEditor.template.html';
for (let i = 0; i < items.length; i++) {
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">';
@ -908,7 +908,7 @@ import template from './metadataEditor.template.html';
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>';
}
@ -945,7 +945,7 @@ import template from './metadataEditor.template.html';
html += '</button>';
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>';
}

View file

@ -1,18 +1,18 @@
<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">
${Edit}
</h3>
<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">
<span class="material-icons check"></span>
<span class="material-icons check" aria-hidden="true"></span>
<span>${Save}</span>
</button>
<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 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>
</div>
</div>
@ -195,7 +195,7 @@
${Genres}
</h2>
<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>
<div class="paperList" id="listGenres"></div>
</div>
@ -204,7 +204,7 @@
${People}
</h2>
<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>
<div id="peopleList" class="paperList">
</div>
@ -214,7 +214,7 @@
${Studios}
</h2>
<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>
<div class="paperList" id="listStudios"></div>
</div>
@ -223,7 +223,7 @@
${Tags}
</h2>
<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>
<div class="paperList" id="listTags"></div>
</div>

View file

@ -1,5 +1,5 @@
<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">
${Edit}
</h3>

View file

@ -125,11 +125,11 @@ import itemHelper from '../itemHelper';
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>';
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;

View file

@ -59,13 +59,13 @@ import { appRouter } from '../appRouter';
// The onclicks are needed due to the return false above
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) {
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>';
@ -73,23 +73,23 @@ import { appRouter } from '../appRouter';
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 += '<input type="range" is="emby-slider" pin step="1" min="0" max="100" value="0" class="slider-medium-thumb nowPlayingBarVolumeSlider"/>';
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="btnShuffleQueue mediaButton"><span class="material-icons shuffle"></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" aria-hidden="true"></span></button>';
html += '<div class="nowPlayingBarUserDataButtons">';
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) {
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 {
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>';
@ -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 {
@ -658,6 +658,11 @@ import { appRouter } from '../appRouter';
}
function onStateChanged(event, state) {
if (event.type === 'init') {
// skip non-ready state
return;
}
console.debug('nowplaying event: ' + event.type);
const player = this;
@ -728,10 +733,10 @@ import { appRouter } from '../appRouter';
updatePlayerVolumeState(player.isMuted(), player.getVolume());
}
function refreshFromPlayer(player) {
function refreshFromPlayer(player, type) {
const state = playbackManager.getPlayerState(player);
onStateChanged.call(player, { type: 'init' }, state);
onStateChanged.call(player, { type }, state);
}
function bindToPlayer(player) {
@ -747,7 +752,7 @@ import { appRouter } from '../appRouter';
return;
}
refreshFromPlayer(player);
refreshFromPlayer(player, 'init');
Events.on(player, 'playbackstart', onPlaybackStart);
Events.on(player, 'statechange', onPlaybackStart);
@ -775,7 +780,7 @@ import { appRouter } from '../appRouter';
} else if (!isVisibilityAllowed) {
isVisibilityAllowed = true;
if (currentPlayer) {
refreshFromPlayer(currentPlayer);
refreshFromPlayer(currentPlayer, 'refresh');
} else {
hideNowPlayingBar();
}

View file

@ -22,9 +22,9 @@ type ItemsArr = {
}
const NewUserPage: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState([]);
const element = useRef(null);
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
const getItemsResult = (items: ItemsArr[]) => {
return items.map(item =>
@ -36,33 +36,54 @@ const NewUserPage: FunctionComponent = () => {
};
const loadMediaFolders = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const mediaFolders = getItemsResult(result);
setMediaFoldersItems(mediaFolders);
const folderAccess = element?.current?.querySelector('.folderAccess');
const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement;
folderAccess.dispatchEvent(new CustomEvent('create'));
element.current.querySelector('.chkEnableAllFolders').checked = false;
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
}, []);
const loadChannels = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const channels = getItemsResult(result);
setChannelsItems(channels);
const channelAccess = element?.current?.querySelector('.channelAccess');
const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement;
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');
element.current.querySelector('.chkEnableAllChannels').checked = false;
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false;
}, []);
const loadUser = useCallback(() => {
element.current.querySelector('#txtUsername').value = '';
element.current.querySelector('#txtPassword').value = '';
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
(page.querySelector('#txtUsername') as HTMLInputElement).value = '';
(page.querySelector('#txtPassword') as HTMLInputElement).value = '';
loading.show();
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
@ -76,29 +97,44 @@ const NewUserPage: FunctionComponent = () => {
}, [loadChannels, loadMediaFolders]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadUser();
const saveUser = () => {
const userInput: userInput = {};
userInput.Name = element?.current?.querySelector('#txtUsername').value;
userInput.Password = element?.current?.querySelector('#txtPassword').value;
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
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 = [];
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;
}).map(function (i) {
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 = [];
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;
}).map(function (i) {
return i.getAttribute('data-id');
@ -114,7 +150,7 @@ const NewUserPage: FunctionComponent = () => {
});
};
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
saveUser();
e.preventDefault();
@ -122,19 +158,19 @@ const NewUserPage: FunctionComponent = () => {
return false;
};
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = element?.current?.querySelector('.channelAccessListContainer');
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement;
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
});
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = element?.current?.querySelector('.folderAccessListContainer');
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement;
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();
});
}, [loadUser]);

View file

@ -12,7 +12,7 @@ type SearchProps = {
};
const SearchPage: FunctionComponent<SearchProps> = ({ serverId, parentId, collectionType }: SearchProps) => {
const [ query, setQuery ] = useState(null);
const [ query, setQuery ] = useState<string>();
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 Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize';
@ -23,16 +24,16 @@ type ItemsArr = {
const UserEditPage: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]);
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ItemsArr[]>([]);
const [ authProviders, setAuthProviders ] = useState([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = 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');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
@ -44,7 +45,14 @@ const UserEditPage: FunctionComponent = () => {
};
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');
setAuthProviders(providers);
@ -54,7 +62,14 @@ const UserEditPage: FunctionComponent = () => {
}, []);
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');
setPasswordResetProviders(providers);
@ -64,6 +79,13 @@ const UserEditPage: FunctionComponent = () => {
}, []);
const loadDeleteFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
@ -93,13 +115,20 @@ const UserEditPage: FunctionComponent = () => {
setDeleteFoldersAccess(itemsArr);
const chkEnableDeleteAllFolders = element.current.querySelector('.chkEnableDeleteAllFolders');
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
triggerChange(chkEnableDeleteAllFolders);
});
}, []);
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) {
loadAuthProviders(user, providers);
});
@ -112,37 +141,38 @@ const UserEditPage: FunctionComponent = () => {
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');
const txtUserName = element?.current?.querySelector('#txtUserName');
txtUserName.disabled = '';
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false;
txtUserName.removeAttribute('disabled');
const lnkEditUserPreferences = element?.current?.querySelector('.lnkEditUserPreferences');
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
LibraryMenu.setTitle(user.Name);
setUserName(user.Name);
element.current.querySelector('#txtUserName').value = user.Name;
element.current.querySelector('.chkIsAdmin').checked = user.Policy.IsAdministrator;
element.current.querySelector('.chkDisabled').checked = user.Policy.IsDisabled;
element.current.querySelector('.chkIsHidden').checked = user.Policy.IsHidden;
element.current.querySelector('.chkRemoteControlSharedDevices').checked = user.Policy.EnableSharedDeviceControl;
element.current.querySelector('.chkEnableRemoteControlOtherUsers').checked = user.Policy.EnableRemoteControlOfOtherUsers;
element.current.querySelector('.chkEnableDownloading').checked = user.Policy.EnableContentDownloading;
element.current.querySelector('.chkManageLiveTv').checked = user.Policy.EnableLiveTvManagement;
element.current.querySelector('.chkEnableLiveTvAccess').checked = user.Policy.EnableLiveTvAccess;
element.current.querySelector('.chkEnableMediaPlayback').checked = user.Policy.EnableMediaPlayback;
element.current.querySelector('.chkEnableAudioPlaybackTranscoding').checked = user.Policy.EnableAudioPlaybackTranscoding;
element.current.querySelector('.chkEnableVideoPlaybackTranscoding').checked = user.Policy.EnableVideoPlaybackTranscoding;
element.current.querySelector('.chkEnableVideoPlaybackRemuxing').checked = user.Policy.EnablePlaybackRemuxing;
element.current.querySelector('.chkForceRemoteSourceTranscoding').checked = user.Policy.ForceRemoteSourceTranscoding;
element.current.querySelector('.chkRemoteAccess').checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
element.current.querySelector('#txtRemoteClientBitrateLimit').value = user.Policy.RemoteClientBitrateLimit / 1e6 || '';
element.current.querySelector('#txtLoginAttemptsBeforeLockout').value = user.Policy.LoginAttemptsBeforeLockout || '0';
element.current.querySelector('#txtMaxActiveSessions').value = user.Policy.MaxActiveSessions || '0';
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, {maximumFractionDigits: 6}) : '';
(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')) {
element.current.querySelector('#selectSyncPlayAccess').value = user.Policy.SyncPlayAccess;
(page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value = user.Policy.SyncPlayAccess;
}
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
@ -155,6 +185,13 @@ const UserEditPage: FunctionComponent = () => {
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
function onSaveComplete() {
@ -163,44 +200,52 @@ const UserEditPage: FunctionComponent = () => {
toast(globalize.translate('SettingsSaved'));
}
const saveUser = (user) => {
user.Name = element?.current?.querySelector('#txtUserName').value;
user.Policy.IsAdministrator = element?.current?.querySelector('.chkIsAdmin').checked;
user.Policy.IsHidden = element?.current?.querySelector('.chkIsHidden').checked;
user.Policy.IsDisabled = element?.current?.querySelector('.chkDisabled').checked;
user.Policy.EnableRemoteControlOfOtherUsers = element?.current?.querySelector('.chkEnableRemoteControlOtherUsers').checked;
user.Policy.EnableLiveTvManagement = element?.current?.querySelector('.chkManageLiveTv').checked;
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.Policy.EnableAudioPlaybackTranscoding = element?.current?.querySelector('.chkEnableAudioPlaybackTranscoding').checked;
user.Policy.EnableVideoPlaybackTranscoding = element?.current?.querySelector('.chkEnableVideoPlaybackTranscoding').checked;
user.Policy.EnablePlaybackRemuxing = element?.current?.querySelector('.chkEnableVideoPlaybackRemuxing').checked;
user.Policy.ForceRemoteSourceTranscoding = element?.current?.querySelector('.chkForceRemoteSourceTranscoding').checked;
user.Policy.EnableContentDownloading = element?.current?.querySelector('.chkEnableDownloading').checked;
user.Policy.EnableRemoteAccess = element?.current?.querySelector('.chkRemoteAccess').checked;
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat(element?.current?.querySelector('#txtRemoteClientBitrateLimit').value || '0'));
user.Policy.LoginAttemptsBeforeLockout = parseInt(element?.current?.querySelector('#txtLoginAttemptsBeforeLockout').value || '0');
user.Policy.MaxActiveSessions = parseInt(element?.current?.querySelector('#txtMaxActiveSessions').value || '0');
user.Policy.AuthenticationProviderId = element?.current?.querySelector('.selectLoginProvider').value;
user.Policy.PasswordResetProviderId = element?.current?.querySelector('.selectPasswordResetProvider').value;
user.Policy.EnableContentDeletion = element?.current?.querySelector('.chkEnableDeleteAllFolders').checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) {
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.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
user.Policy.IsAdministrator = (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked;
user.Policy.IsHidden = (page.querySelector('.chkIsHidden') as HTMLInputElement).checked;
user.Policy.IsDisabled = (page.querySelector('.chkDisabled') as HTMLInputElement).checked;
user.Policy.EnableRemoteControlOfOtherUsers = (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked;
user.Policy.EnableLiveTvManagement = (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked;
user.Policy.EnableLiveTvAccess = (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked;
user.Policy.EnableSharedDeviceControl = (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked;
user.Policy.EnableMediaPlayback = (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked;
user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
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;
}).map(function (c) {
return c.getAttribute('data-id');
});
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.updateUserPolicy(user.Id, user.Policy).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
onSaveComplete();
});
});
};
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
getUser().then(function (result) {
saveUser(result);
@ -210,22 +255,22 @@ const UserEditPage: FunctionComponent = () => {
return false;
};
element?.current?.querySelector('.chkEnableDeleteAllFolders').addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
if (this.checked) {
element?.current?.querySelector('.deleteAccess').classList.add('hide');
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
} else {
element?.current?.querySelector('.deleteAccess').classList.remove('hide');
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
}
});
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');
});
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();
});
}, [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 loading from '../loading/loading';
@ -20,19 +21,26 @@ type ItemsArr = {
const UserLibraryAccessPage: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState([]);
const [devicesItems, setDevicesItems] = useState([]);
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
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');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
};
const loadMediaFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const folder of mediaFolders) {
@ -47,12 +55,19 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setMediaFoldersItems(itemsArr);
const chkEnableAllFolders = element.current.querySelector('.chkEnableAllFolders');
const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders);
}, []);
const loadChannels = useCallback((user, channels) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const folder of channels) {
@ -68,17 +83,24 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setChannelsItems(itemsArr);
if (channels.length) {
element?.current?.querySelector('.channelAccessContainer').classList.remove('hide');
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.remove('hide');
} 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;
triggerChange(chkEnableAllChannels);
}, []);
const loadDevices = useCallback((user, devices) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const device of devices) {
@ -94,14 +116,14 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setDevicesItems(itemsArr);
const chkEnableAllDevices = element.current.querySelector('.chkEnableAllDevices');
const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices);
if (user.Policy.IsAdministrator) {
element?.current?.querySelector('.deviceAccessContainer').classList.add('hide');
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
} else {
element?.current?.querySelector('.deviceAccessContainer').classList.remove('hide');
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
}
}, []);
@ -129,9 +151,16 @@ const UserLibraryAccessPage: FunctionComponent = () => {
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
const userId = appRouter.param('userId');
window.ApiClient.getUser(userId).then(function (result) {
@ -142,21 +171,29 @@ const UserLibraryAccessPage: FunctionComponent = () => {
return false;
};
const saveUser = (user) => {
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked;
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) {
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.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;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (c) {
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllDevices = element?.current?.querySelector('.chkEnableAllDevices').checked;
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkDevice'), function (c) {
user.Policy.EnableAllDevices = (page.querySelector('.chkEnableAllDevices') as HTMLInputElement).checked;
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkDevice'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
@ -173,19 +210,19 @@ const UserLibraryAccessPage: FunctionComponent = () => {
toast(globalize.translate('SettingsSaved'));
};
element?.current?.querySelector('.chkEnableAllDevices').addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.deviceAccessListContainer').classList.toggle('hide', this.checked);
(page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.deviceAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.channelAccessListContainer').classList.toggle('hide', this.checked);
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.channelAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.folderAccessListContainer').classList.toggle('hide', this.checked);
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(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]);
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 Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize';
@ -21,9 +21,9 @@ type MenuEntry = {
}
const UserProfilesPage: FunctionComponent = () => {
const [ users, setUsers ] = useState([]);
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const loadData = () => {
loading.show();
@ -34,12 +34,24 @@ const UserProfilesPage: FunctionComponent = () => {
};
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const showUserMenu = (elem) => {
const showUserMenu = (elem: HTMLElement) => {
const card = dom.parentWithClass(elem, 'card');
const userId = card.getAttribute('data-userid');
if (!userId) {
console.error('Unexpected null user id');
return;
}
const menuItems: MenuEntry[] = [];
menuItems.push({
@ -67,7 +79,7 @@ const UserProfilesPage: FunctionComponent = () => {
actionsheet.show({
items: menuItems,
positionTo: card,
callback: function (id) {
callback: function (id: string) {
switch (id) {
case 'open':
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');
confirm({
@ -105,15 +117,15 @@ const UserProfilesPage: FunctionComponent = () => {
});
};
element?.current?.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target, 'btnUserMenu');
page.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
element?.current?.querySelector('.btnAddUser').addEventListener('click', function() {
(page.querySelector('.btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
}, []);

View file

@ -15,7 +15,7 @@ let enableAnimation;
function getOsdElementHtml() {
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>';

View file

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

View file

@ -3,6 +3,7 @@ import { Events } from 'jellyfin-apiclient';
import browser from '../../scripts/browser';
import loading from '../loading/loading';
import { playbackManager } from '../playback/playbackmanager';
import { pluginManager } from '../pluginManager';
import { appRouter } from '../appRouter';
import globalize from '../../scripts/globalize';
import { appHost } from '../apphost';
@ -130,6 +131,13 @@ export function show(button) {
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) {
const target = targets.filter(function (t) {
return t.id === id;

View file

@ -16,7 +16,7 @@ let enableAnimation;
function getOsdElementHtml() {
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>';

View file

@ -176,12 +176,6 @@ import template from './playbackSettings.template.html';
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('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();

View file

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

View file

@ -26,7 +26,7 @@ import ServerConnections from '../ServerConnections';
if (layoutManager.tv) {
button = '';
} 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';

View file

@ -242,7 +242,7 @@ import ServerConnections from '../ServerConnections';
const title = globalize.translate('HeaderAddToPlaylist');
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 += title;
html += '</h3>';

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader">
<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>
<h3 class="formDialogHeaderTitle"></h3>

View file

@ -1,5 +1,5 @@
<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>
</div>
<div class="formDialogContent smoothScrollY">

View file

@ -1,5 +1,5 @@
<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">
${HeaderRecordingOptions}
</h3>

View file

@ -2,7 +2,7 @@
<div class="recordSeriesContainer recordingFields-buttons flex align-items-center hide">
<div>
<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>
</button>
</div>
@ -14,7 +14,7 @@
<div class="recordingFields-buttons flex align-items-center">
<div>
<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>
</button>
</div>

View file

@ -1,5 +1,5 @@
<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">
${HeaderSeriesOptions}
</h3>

View file

@ -120,7 +120,7 @@ class RefreshDialog {
const title = globalize.translate('RefreshMetadata');
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 += title;
html += '</h3>';

View file

@ -233,8 +233,8 @@ function updateNowPlayingInfo(context, state, serverId) {
apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) {
const userData = fullItem.UserData || {};
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('.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('.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" aria-hidden="true"></span></button>';
});
} else {
backdrop.clearBackdrop();
@ -248,15 +248,11 @@ function setImageUrl(context, state, url) {
if (url) {
imgContainer.innerHTML = '<img class="nowPlayingPageImage" src="' + url + '" />';
if (item.Type == 'Audio') {
context.querySelector('.nowPlayingPageImage').classList.add('nowPlayingPageImageAudio');
context.querySelector('.nowPlayingPageImageContainer').classList.remove('nowPlayingPageImageAudio');
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImageAudio', item.Type === 'Audio');
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio');
} else {
context.querySelector('.nowPlayingPageImageContainer').classList.add('nowPlayingPageImagePoster');
context.querySelector('.nowPlayingPageImage').classList.remove('nowPlayingPageImageAudio');
}
} 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 toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton');
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;
switch (repeatMode) {
case 'RepeatAll':
break;
case 'RepeatOne':
innHtml = '<span class="material-icons repeat_one"></span>';
innHtml = '<span class="material-icons repeat_one" aria-hidden="true"></span>';
break;
case 'RepeatNone':
default:
@ -889,7 +885,7 @@ export default function () {
function init(ownerView, context) {
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>';
const optionsSection = context.querySelector('.playlistSectionButton');

View file

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

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