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

Merge pull request #3433 from dmitrylyzo/types-react

[TypeScript] Disable implicit any
This commit is contained in:
Bill Thornton 2022-02-25 15:17:32 -05:00 committed by GitHub
commit bb46b32402
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 883 additions and 254 deletions

79
package-lock.json generated
View file

@ -2513,6 +2513,17 @@
"string.prototype.matchall": "^4.0.6"
}
},
"@thornbill/jellyfin-sdk": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@thornbill/jellyfin-sdk/-/jellyfin-sdk-0.4.1.tgz",
"integrity": "sha512-DuUeSiIvk1qcEYp9oxnKc/e4T0zj3LujXhITQ6L6TqGOo8miNiBgU8OjmidAUwPc2ibCLAK2rM5BcNVAEwaFmw==",
"dev": true,
"requires": {
"axios": "^0.26.0",
"compare-versions": "^4.0.0",
"normalize-url": "^6.1.0"
}
},
"@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@ -2651,6 +2662,21 @@
"localforage": "*"
}
},
"@types/lodash": {
"version": "4.14.178",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
"dev": true
},
"@types/lodash-es": {
"version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz",
"integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -2687,6 +2713,12 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -2699,6 +2731,26 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"@types/react": {
"version": "17.0.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz",
"integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/react-dom": {
"version": "17.0.11",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
"integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -2714,6 +2766,12 @@
"integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==",
"dev": true
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/serve-index": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
@ -3540,6 +3598,15 @@
"integrity": "sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==",
"dev": true
},
"axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.8"
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -4152,6 +4219,12 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"compare-versions": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz",
"integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==",
"dev": true
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -4686,6 +4759,12 @@
"css-tree": "^1.1.2"
}
},
"csstype": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
"dev": true
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",

View file

@ -14,6 +14,10 @@
"@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.11",
"@typescript-eslint/eslint-plugin": "5.12.0",
"@typescript-eslint/parser": "5.12.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",

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

@ -0,0 +1,350 @@
// TODO: Move to jellyfin-apiclient
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;
};
}

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

@ -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) {
@ -1129,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) {

View file

@ -2,7 +2,7 @@ import React, { FunctionComponent } from 'react';
import datetime from '../../../scripts/datetime';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({index}) => ({
const createButtonElement = (index: number) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
@ -21,7 +21,7 @@ type IProps = {
EndHour?: number;
}
function getDisplayTime(hours) {
function getDisplayTime(hours = 0) {
let minutes = 0;
const pct = hours % 1;
@ -49,9 +49,7 @@ const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartH
</div>
</div>
<div
dangerouslySetInnerHTML={createButtonElement({
index: index
})}
dangerouslySetInnerHTML={createButtonElement(index)}
/>
</div>
);

View file

@ -1,6 +1,6 @@
import React, { FunctionComponent } from 'react';
const createButtonElement = ({tag}) => ({
const createButtonElement = (tag?: string) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
@ -25,9 +25,7 @@ const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
</h3>
</div>
<div
dangerouslySetInnerHTML={createButtonElement({
tag: tag
})}
dangerouslySetInnerHTML={createButtonElement(tag)}
/>
</div>

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

@ -9,7 +9,7 @@ type IProps = {
checkedAttribute?: string;
}
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}) => ({
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
__html: `<label>
<input
type="checkbox"

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

@ -7,7 +7,7 @@ type IProps = {
icon: string,
}
const createButtonElement = ({ className, title, icon }) => ({
const createButtonElement = ({ className, title, icon }: { className?: string, title: string, icon: string }) => ({
__html: `<button
is="emby-button"
type="button"

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

@ -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

@ -1,7 +1,7 @@
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
class="${className}"
is="emby-select"

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"
@ -28,7 +29,7 @@ type IProps = {
user?: Record<string, any>;
}
const getLastSeenText = (lastActivityDate) => {
const getLastSeenText = (lastActivityDate?: string) => {
if (lastActivityDate) {
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
}

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

@ -1,3 +1,4 @@
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';
@ -26,12 +27,12 @@ type ItemsArr = {
const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState([]);
const [ unratedItems, setUnratedItems ] = useState([]);
const [ accessSchedules, setAccessSchedules ] = useState([]);
const [ parentalRatings, setParentalRatings ] = useState<RatingsArr[]>([]);
const [ unratedItems, setUnratedItems ] = useState<ItemsArr[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]);
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const populateRatings = useCallback((allParentalRatings) => {
let rating;
@ -59,6 +60,13 @@ const UserParentalControl: FunctionComponent = () => {
}, []);
const loadUnratedItems = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const items = [{
name: globalize.translate('Books'),
value: 'Book'
@ -96,19 +104,26 @@ const UserParentalControl: FunctionComponent = () => {
setUnratedItems(itemsArr);
const blockUnratedItems = element?.current?.querySelector('.blockUnratedItems');
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 = element?.current?.querySelector('.blockedTags');
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) {
const newTags = tags.filter(function (t: string) {
return t != tag;
});
loadBlockedTags(newTags);
@ -117,15 +132,22 @@ const UserParentalControl: FunctionComponent = () => {
}, []);
const renderAccessSchedule = useCallback((schedules) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setAccessSchedules(schedules);
const accessScheduleList = element?.current?.querySelector('.accessScheduleList');
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'));
const index = parseInt(btnDelete.getAttribute('data-index') || '0', 10);
schedules.splice(index, 1);
const newindex = schedules.filter(function (i) {
const newindex = schedules.filter(function (i: number) {
return i != index;
});
renderAccessSchedule(newindex);
@ -134,6 +156,13 @@ const UserParentalControl: FunctionComponent = () => {
}, []);
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);
@ -152,12 +181,12 @@ const UserParentalControl: FunctionComponent = () => {
}
}
element.current.querySelector('.selectMaxParentalRating').value = ratingValue;
(page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value = ratingValue;
if (user.Policy.IsAdministrator) {
element?.current?.querySelector('.accessScheduleSection').classList.add('hide');
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
} else {
element?.current?.querySelector('.accessScheduleSection').classList.remove('hide');
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
}
renderAccessSchedule(user.Policy.AccessSchedules || []);
loading.hide();
@ -174,6 +203,13 @@ const UserParentalControl: FunctionComponent = () => {
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSaveComplete = () => {
@ -181,9 +217,17 @@ const UserParentalControl: FunctionComponent = () => {
toast(globalize.translate('SettingsSaved'));
};
const saveUser = (user) => {
user.Policy.MaxParentalRating = element?.current?.querySelector('.selectMaxParentalRating').value || null;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkUnratedItem'), function (i) {
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');
@ -195,7 +239,7 @@ const UserParentalControl: FunctionComponent = () => {
});
};
const showSchedulePopup = (schedule, index) => {
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {};
import('../../components/accessSchedule/accessSchedule').then(({default: accessschedule}) => {
accessschedule.show({
@ -214,19 +258,19 @@ const UserParentalControl: FunctionComponent = () => {
};
const getSchedulesFromPage = () => {
return Array.prototype.map.call(element?.current?.querySelectorAll('.liSchedule'), function (elem) {
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(element?.current?.querySelectorAll('.blockedTag'), function (elem) {
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
return elem.getAttribute('data-tag');
});
}) as string[];
};
const showBlockedTagPopup = () => {
@ -244,7 +288,7 @@ const UserParentalControl: FunctionComponent = () => {
});
};
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
const userId = appRouter.param('userId');
window.ApiClient.getUser(userId).then(function (result) {
@ -255,15 +299,21 @@ const UserParentalControl: FunctionComponent = () => {
return false;
};
element?.current?.querySelector('.btnAddSchedule').addEventListener('click', function () {
showSchedulePopup({}, -1);
(page.querySelector('.btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
DayOfWeek: DynamicDayOfWeek.Sunday,
StartHour: 0,
EndHour: 0
}, -1);
});
element?.current?.querySelector('.btnAddBlockedTag').addEventListener('click', function () {
(page.querySelector('.btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
element?.current?.querySelector('.userParentalControlForm').addEventListener('submit', onSubmit);
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]);
return (
@ -355,6 +405,7 @@ const UserParentalControl: FunctionComponent = () => {
return <AccessScheduleList
key={index}
index={index}
Id={accessSchedule.Id}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}

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

@ -1,4 +1,6 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize';
@ -27,14 +29,14 @@ type LiveTVSearchResultsProps = {
/*
* React component to display search result rows for live tv library search
*/
const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serverId, parentId, collectionType, query }: LiveTVSearchResultsProps) => {
const [ movies, setMovies ] = useState([]);
const [ episodes, setEpisodes ] = useState([]);
const [ sports, setSports ] = useState([]);
const [ kids, setKids ] = useState([]);
const [ news, setNews ] = useState([]);
const [ programs, setPrograms ] = useState([]);
const [ channels, setChannels ] = useState([]);
const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
const [ kids, setKids ] = useState<BaseItemDto[]>([]);
const [ news, setNews ] = useState<BaseItemDto[]>([]);
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
@ -53,7 +55,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
});
// FIXME: This query does not support Live TV filters
const fetchItems = (apiClient, params = {}) => apiClient?.getItems(
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -72,8 +74,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
setChannels([]);
if (query && collectionType === 'livetv') {
// TODO: Remove type casting once we're using a properly typed API client
const apiClient = (ServerConnections as any).getApiClient(serverId);
const apiClient = ServerConnections.getApiClient(serverId);
// Movies row
fetchItems(apiClient, {
@ -83,7 +84,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: false
}).then(result => setMovies(result.Items));
}).then(result => setMovies(result.Items || []));
// Episodes row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -92,7 +93,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: false
}).then(result => setEpisodes(result.Items));
}).then(result => setEpisodes(result.Items || []));
// Sports row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -101,7 +102,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: true,
IsKids: false,
IsNews: false
}).then(result => setSports(result.Items));
}).then(result => setSports(result.Items || []));
// Kids row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -110,7 +111,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: true,
IsNews: false
}).then(result => setKids(result.Items));
}).then(result => setKids(result.Items || []));
// News row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -119,7 +120,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: true
}).then(result => setNews(result.Items));
}).then(result => setNews(result.Items || []));
// Programs row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -128,10 +129,10 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: false
}).then(result => setPrograms(result.Items));
}).then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items));
.then(result => setChannels(result.Items || []));
}
}, [collectionType, parentId, query, serverId]);

View file

@ -31,20 +31,20 @@ const createInputElement = () => ({
const normalizeInput = (value = '') => value.trim();
type SearchFieldsProps = {
onSearch?: () => void
onSearch?: (query: string) => void
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {} }: SearchFieldsProps) => {
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch');
const getSearchInput = () => element?.current?.querySelector<HTMLInputElement>('.searchfields-txtSearch');
const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), [onSearch]);
useEffect(() => {
getSearchInput()?.addEventListener('input', e => {
debouncedOnSearch(normalizeInput(e.target?.value));
debouncedOnSearch(normalizeInput((e.target as HTMLInputElement).value));
});
getSearchInput()?.focus();
@ -53,10 +53,15 @@ const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {
};
}, [debouncedOnSearch]);
const onAlphaPicked = e => {
const value = e.detail.value;
const onAlphaPicked = (e: Event) => {
const value = (e as CustomEvent).detail.value;
const searchInput = getSearchInput();
if (!searchInput) {
console.error('Unexpected null reference');
return;
}
if (value === 'backspace') {
const currentValue = searchInput.value;
searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : '';

View file

@ -1,4 +1,6 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize';
@ -15,22 +17,22 @@ type SearchResultsProps = {
/*
* React component to display search result rows for global search and non-live tv library search
*/
const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parentId, collectionType, query }: SearchResultsProps) => {
const [ movies, setMovies ] = useState([]);
const [ shows, setShows ] = useState([]);
const [ episodes, setEpisodes ] = useState([]);
const [ videos, setVideos ] = useState([]);
const [ programs, setPrograms ] = useState([]);
const [ channels, setChannels ] = useState([]);
const [ playlists, setPlaylists ] = useState([]);
const [ artists, setArtists ] = useState([]);
const [ albums, setAlbums ] = useState([]);
const [ songs, setSongs ] = useState([]);
const [ photoAlbums, setPhotoAlbums ] = useState([]);
const [ photos, setPhotos ] = useState([]);
const [ audioBooks, setAudioBooks ] = useState([]);
const [ books, setBooks ] = useState([]);
const [ people, setPeople ] = useState([]);
const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
const [ videos, setVideos ] = useState<BaseItemDto[]>([]);
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
const [ playlists, setPlaylists ] = useState<BaseItemDto[]>([]);
const [ artists, setArtists ] = useState<BaseItemDto[]>([]);
const [ albums, setAlbums ] = useState<BaseItemDto[]>([]);
const [ songs, setSongs ] = useState<BaseItemDto[]>([]);
const [ photoAlbums, setPhotoAlbums ] = useState<BaseItemDto[]>([]);
const [ photos, setPhotos ] = useState<BaseItemDto[]>([]);
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
@ -48,7 +50,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
IncludeArtists: false
});
const fetchArtists = (apiClient, params = {}) => apiClient?.getArtists(
const fetchArtists = (apiClient: ApiClient, params = {}) => apiClient?.getArtists(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -57,7 +59,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
}
);
const fetchItems = (apiClient, params = {}) => apiClient?.getItems(
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -66,7 +68,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
}
);
const fetchPeople = (apiClient, params = {}) => apiClient?.getPeople(
const fetchPeople = (apiClient: ApiClient, params = {}) => apiClient?.getPeople(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -99,45 +101,44 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
setPeople([]);
if (query) {
// TODO: Remove type casting once we're using a properly typed API client
const apiClient = (ServerConnections as any).getApiClient(serverId);
const apiClient = ServerConnections.getApiClient(serverId);
// Movie libraries
if (!collectionType || isMovies()) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items));
.then(result => setMovies(result.Items || []));
}
// TV Show libraries
if (!collectionType || isTVShows()) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items));
.then(result => setShows(result.Items || []));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items));
.then(result => setEpisodes(result.Items || []));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies() || isTVShows()) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items));
fetchPeople(apiClient).then(result => setPeople(result.Items || []));
}
// Music libraries
if (!collectionType || isMusic()) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items));
.then(results => setPlaylists(results.Items || []));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items));
fetchArtists(apiClient).then(result => setArtists(result.Items || []));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items));
.then(result => setAlbums(result.Items || []));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items));
.then(result => setSongs(result.Items || []));
}
// Other libraries do not support in-library search currently
@ -146,25 +147,25 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items));
}).then(result => setVideos(result.Items || []));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items));
.then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items));
.then(result => setChannels(result.Items || []));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(results => setPhotoAlbums(results.Items));
.then(results => setPhotoAlbums(results.Items || []));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(results => setPhotos(results.Items));
.then(results => setPhotos(results.Items || []));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(results => setAudioBooks(results.Items));
.then(results => setAudioBooks(results.Items || []));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items));
.then(results => setBooks(results.Items || []));
}
}
}, [collectionType, parentId, query, serverId]);

View file

@ -1,3 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useEffect, useRef } from 'react';
import cardBuilder from '../cardbuilder/cardBuilder';
@ -17,12 +18,12 @@ const createScroller = ({ title = '' }) => ({
type SearchResultsRowProps = {
title?: string;
items?: Array<any>; // TODO: Should be Array<BaseItemDto> once we have a typed API client
items?: BaseItemDto[];
cardOptions?: Record<string, any>;
}
const SearchResultsRow: FunctionComponent<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => {
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
cardBuilder.buildCards(items, {

View file

@ -1,3 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { appRouter } from '../appRouter';
@ -9,7 +10,7 @@ import '../../elements/emby-button/emby-button';
// There seems to be some compatibility issues here between
// React and our legacy web components, so we need to inject
// them as an html string for now =/
const createSuggestionLink = ({name, href}) => ({
const createSuggestionLink = ({ name, href }: { name: string, href: string }) => ({
__html: `<a
is='emby-linkbutton'
class='button-link'
@ -23,12 +24,11 @@ type SearchSuggestionsProps = {
parentId?: string;
}
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId, parentId }: SearchSuggestionsProps) => {
const [ suggestions, setSuggestions ] = useState([]);
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => {
const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]);
useEffect(() => {
// TODO: Remove type casting once we're using a properly typed API client
const apiClient = (ServerConnections as any).getApiClient(serverId);
const apiClient = ServerConnections.getApiClient(serverId);
apiClient.getItems(apiClient.getCurrentUserId(), {
SortBy: 'IsFavoriteOrLiked,Random',
@ -39,7 +39,7 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId
EnableImages: false,
ParentId: parentId,
EnableTotalRecordCount: false
}).then(result => setSuggestions(result.Items));
}).then(result => setSuggestions(result.Items || []));
}, [parentId, serverId]);
return (
@ -58,7 +58,7 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId
<div
key={`suggestion-${item.Id}`}
dangerouslySetInnerHTML={createSuggestionLink({
name: item.Name,
name: item.Name || '',
href: appRouter.getRouteUrl(item)
})}
/>

5
src/global.d.ts vendored
View file

@ -1,5 +1,8 @@
export declare global {
import { ApiClient, Events } from 'jellyfin-apiclient';
interface Window {
ApiClient: any;
ApiClient: ApiClient;
Events: Events;
}
}

View file

@ -9,7 +9,7 @@
* Returns parent of element with specified attribute value.
* @param {HTMLElement} elem - Element whose parent need to find.
* @param {string} name - Attribute name.
* @param {mixed} value - Attribute value.
* @param {mixed} [value] - Attribute value.
* @returns {HTMLElement} Parent with specified attribute value.
*/
export function parentWithAttribute(elem, name, value) {

View file

@ -1,11 +1,11 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "ES2015"],
"lib": ["DOM", "DOM.Iterable", "ES2015"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"noImplicitAny": false,
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"module": "ESNext",
@ -13,7 +13,8 @@
"outDir": "./dist/",
"checkJs": false,
"strictNullChecks": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"src"