import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client'; import { UnratedItem } from '@jellyfin/sdk/lib/generated-client/models/unrated-item'; import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week'; import escapeHTML from 'escape-html'; import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import globalize from '../../../../lib/globalize'; import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList'; import TagList from '../../../../components/dashboard/users/TagList'; import ButtonElement from '../../../../elements/ButtonElement'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import loading from '../../../../components/loading/loading'; import toast from '../../../../components/toast/toast'; import CheckBoxElement from '../../../../elements/CheckBoxElement'; import SelectElement from '../../../../elements/SelectElement'; import Page from '../../../../components/Page'; import prompt from '../../../../components/prompt/prompt'; import ServerConnections from 'components/ServerConnections'; type NamedItem = { name: string; value: UnratedItem; }; type UnratedNamedItem = NamedItem & { checkedAttribute: string }; function handleSaveUser( page: HTMLDivElement, getSchedulesFromPage: () => AccessSchedule[], getAllowedTagsFromPage: () => string[], getBlockedTagsFromPage: () => string[], onSaveComplete: () => void ) { return (user: UserDto) => { const userId = user.Id; const userPolicy = user.Policy; if (!userId || !userPolicy) { throw new Error('Unexpected null user id or policy'); } const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10); userPolicy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating; userPolicy.BlockUnratedItems = Array.prototype.filter .call(page.querySelectorAll('.chkUnratedItem'), i => i.checked) .map(i => i.getAttribute('data-itemtype')); userPolicy.AccessSchedules = getSchedulesFromPage(); userPolicy.AllowedTags = getAllowedTagsFromPage(); userPolicy.BlockedTags = getBlockedTagsFromPage(); ServerConnections.getCurrentApiClientAsync() .then(apiClient => apiClient.updateUserPolicy(userId, userPolicy)) .then(() => onSaveComplete()) .catch(err => { console.error('[userparentalcontrol] failed to update user policy', err); }); }; } const UserParentalControl = () => { const [ searchParams ] = useSearchParams(); const userId = searchParams.get('userId'); const [ userName, setUserName ] = useState(''); const [ parentalRatings, setParentalRatings ] = useState([]); const [ unratedItems, setUnratedItems ] = useState([]); const [ maxParentalRating, setMaxParentalRating ] = useState(); const [ accessSchedules, setAccessSchedules ] = useState([]); const [ allowedTags, setAllowedTags ] = useState([]); const [ blockedTags, setBlockedTags ] = useState([]); const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); const element = useRef(null); const populateRatings = useCallback((allParentalRatings: ParentalRating[]) => { let rating; const ratings: ParentalRating[] = []; for (let i = 0, length = allParentalRatings.length; i < length; i++) { rating = allParentalRatings[i]; if (!rating) continue; if (ratings.length) { const lastRating = ratings[ratings.length - 1]; if (lastRating && lastRating.Value === rating.Value) { lastRating.Name += '/' + rating.Name; continue; } } ratings.push({ Name: rating.Name, Value: rating.Value }); } setParentalRatings(ratings); }, []); const loadUnratedItems = useCallback((user: UserDto) => { const page = element.current; if (!page) { console.error('[userparentalcontrol] Unexpected null page reference'); return; } const items: NamedItem[] = [{ name: globalize.translate('Books'), value: UnratedItem.Book }, { name: globalize.translate('Channels'), value: UnratedItem.ChannelContent }, { name: globalize.translate('LiveTV'), value: UnratedItem.LiveTvChannel }, { name: globalize.translate('Movies'), value: UnratedItem.Movie }, { name: globalize.translate('Music'), value: UnratedItem.Music }, { name: globalize.translate('Trailers'), value: UnratedItem.Trailer }, { name: globalize.translate('Shows'), value: UnratedItem.Series }]; const unratedNamedItem: UnratedNamedItem[] = []; for (const item of items) { const isChecked = user.Policy?.BlockUnratedItems?.indexOf(item.value) != -1; const checkedAttribute = isChecked ? ' checked="checked"' : ''; unratedNamedItem.push({ value: item.value, name: item.name, checkedAttribute: checkedAttribute }); } setUnratedItems(unratedNamedItem); const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement; blockUnratedItems.dispatchEvent(new CustomEvent('create')); }, []); const loadUser = useCallback((user: UserDto, allParentalRatings: ParentalRating[]) => { const page = element.current; if (!page) { console.error('[userparentalcontrol] Unexpected null page reference'); return; } setUserName(user.Name || ''); void libraryMenu.then(menu => menu.setTitle(user.Name)); loadUnratedItems(user); setAllowedTags(user.Policy?.AllowedTags || []); setBlockedTags(user.Policy?.BlockedTags || []); populateRatings(allParentalRatings); let ratingValue = ''; allParentalRatings.forEach(rating => { if (rating.Value != null && user.Policy?.MaxParentalRating != null && user.Policy.MaxParentalRating >= rating.Value) { ratingValue = `${rating.Value}`; } }); setMaxParentalRating(ratingValue); if (user.Policy?.IsAdministrator) { (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide'); } else { (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide'); } setAccessSchedules(user.Policy?.AccessSchedules || []); loading.hide(); }, [libraryMenu, setAllowedTags, setBlockedTags, loadUnratedItems, populateRatings]); const loadData = useCallback(() => { if (!userId) { console.error('[userparentalcontrol.loadData] missing user id'); return; } loading.show(); const promise1 = window.ApiClient.getUser(userId); const promise2 = window.ApiClient.getParentalRatings(); Promise.all([promise1, promise2]).then(function (responses) { loadUser(responses[0], responses[1]); }).catch(err => { console.error('[userparentalcontrol] failed to load data', err); }); }, [loadUser, userId]); useEffect(() => { const page = element.current; if (!page) { console.error('[userparentalcontrol] Unexpected null page reference'); return; } loadData(); 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; setAccessSchedules(schedules); }).catch(() => { // access schedule closed }); }).catch(err => { console.error('[userparentalcontrol] failed to load access schedule', err); }); }; 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 getAllowedTagsFromPage = () => { return Array.prototype.map.call(page.querySelectorAll('.allowedTag'), function (elem) { return elem.getAttribute('data-tag'); }) as string[]; }; const showAllowedTagPopup = () => { prompt({ label: globalize.translate('LabelTag') }).then(function (value) { const tags = getAllowedTagsFromPage(); if (tags.indexOf(value) == -1) { tags.push(value); setAllowedTags(tags); } }).catch(() => { // prompt closed }); }; const getBlockedTagsFromPage = () => { return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) { return elem.getAttribute('data-tag'); }) as string[]; }; const showBlockedTagPopup = () => { prompt({ label: globalize.translate('LabelTag') }).then(function (value) { const tags = getBlockedTagsFromPage(); if (tags.indexOf(value) == -1) { tags.push(value); setBlockedTags(tags); } }).catch(() => { // prompt closed }); }; const onSaveComplete = () => { loading.hide(); toast(globalize.translate('SettingsSaved')); }; const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete); const onSubmit = (e: Event) => { if (!userId) { console.error('[userparentalcontrol.onSubmit] missing user id'); return; } loading.show(); window.ApiClient.getUser(userId).then(function (result) { saveUser(result); }).catch(err => { console.error('[userparentalcontrol] failed to fetch user', err); }); e.preventDefault(); e.stopPropagation(); return false; }; // The following is still hacky and should migrate to pure react implementation for callbacks in the future const accessSchedulesPopupCallback = function () { showSchedulePopup({ Id: 0, UserId: '', DayOfWeek: DynamicDayOfWeek.Sunday, StartHour: 0, EndHour: 0 }, -1); }; (page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', accessSchedulesPopupCallback); (page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', showAllowedTagPopup); (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', showBlockedTagPopup); (page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit); return () => { (page.querySelector('#btnAddSchedule') as HTMLButtonElement).removeEventListener('click', accessSchedulesPopupCallback); (page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).removeEventListener('click', showAllowedTagPopup); (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).removeEventListener('click', showBlockedTagPopup); (page.querySelector('.userParentalControlForm') as HTMLFormElement).removeEventListener('submit', onSubmit); }; }, [setAllowedTags, setBlockedTags, loadData, userId]); useEffect(() => { const page = element.current; if (!page) { console.error('[userparentalcontrol] Unexpected null page reference'); return; } (page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = String(maxParentalRating); }, [maxParentalRating, parentalRatings]); const optionMaxParentalRating = () => { let content = ''; content += ''; for (const rating of parentalRatings) { if (rating.Value != null) { content += ``; } } return content; }; const removeAllowedTagsCallback = useCallback((tag: string) => { const newTags = allowedTags.filter(t => t !== tag); setAllowedTags(newTags); }, [allowedTags, setAllowedTags]); const removeBlockedTagsTagsCallback = useCallback((tag: string) => { const newTags = blockedTags.filter(t => t !== tag); setBlockedTags(newTags); }, [blockedTags, setBlockedTags]); const removeScheduleCallback = useCallback((index: number) => { const newSchedules = accessSchedules.filter((_e, i) => i != index); setAccessSchedules(newSchedules); }, [accessSchedules, setAccessSchedules]); return (
{optionMaxParentalRating()}
{globalize.translate('MaxParentalRatingHelp')}

{globalize.translate('HeaderBlockItemsWithNoRating')}

{unratedItems.map(Item => { return ; })}

{globalize.translate('AllowContentWithTagsHelp')}
{allowedTags?.map(tag => { return ; })}
{globalize.translate('BlockContentWithTagsHelp')}
{blockedTags.map(tag => { return ; })}

{globalize.translate('HeaderAccessScheduleHelp')}

{accessSchedules.map((accessSchedule, index) => { return ; })}
); }; export default UserParentalControl;