From adb22abdc22ecc9222b367344cbf09830e093664 Mon Sep 17 00:00:00 2001 From: Jpuc1143 Date: Wed, 22 Feb 2023 15:10:20 -0300 Subject: [PATCH 1/3] Add "AllowedTags" option to parental controls --- CONTRIBUTORS.md | 2 + .../routes/users/parentalcontrol.tsx | 162 +++++++++++++----- .../users/{BlockedTagList.tsx => TagList.tsx} | 9 +- src/strings/en-us.json | 1 + 4 files changed, 126 insertions(+), 48 deletions(-) rename src/components/dashboard/users/{BlockedTagList.tsx => TagList.tsx} (75%) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b2c6b246e9..a758d06f42 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -83,6 +83,8 @@ - [Chris-Codes-It](https://github.com/Chris-Codes-It) - [Vedant](https://github.com/viktory36) - [GeorgeH005](https://github.com/GeorgeH005) +- [JPUC1143](https://github.com/Jpuc1143) +- [David Angel](https://github.com/davidangel) ## Emby Contributors diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index 52a94df6c8..c5ce145fd1 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/routes/users/parentalcontrol.tsx @@ -6,7 +6,7 @@ import escapeHTML from 'escape-html'; import globalize from '../../../../scripts/globalize'; import LibraryMenu from '../../../../scripts/libraryMenu'; import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList'; -import BlockedTagList from '../../../../components/dashboard/users/BlockedTagList'; +import TagList from '../../../../components/dashboard/users/TagList'; import ButtonElement from '../../../../elements/ButtonElement'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; @@ -16,6 +16,7 @@ import { getParameterByName } from '../../../../utils/url'; import CheckBoxElement from '../../../../elements/CheckBoxElement'; import SelectElement from '../../../../elements/SelectElement'; import Page from '../../../../components/Page'; +import prompt from '../../../../components/prompt/prompt'; type UnratedItem = { name: string; @@ -28,6 +29,7 @@ const UserParentalControl: FunctionComponent = () => { const [ parentalRatings, setParentalRatings ] = useState([]); const [ unratedItems, setUnratedItems ] = useState([]); const [ accessSchedules, setAccessSchedules ] = useState([]); + const [ allowedTags, setAllowedTags ] = useState([]); const [ blockedTags, setBlockedTags ] = useState([]); const element = useRef(null); @@ -106,6 +108,29 @@ const UserParentalControl: FunctionComponent = () => { blockUnratedItems.dispatchEvent(new CustomEvent('create')); }, []); + const loadAllowedTags = useCallback((tags) => { + const page = element.current; + + if (!page) { + console.error('Unexpected null reference'); + return; + } + + setAllowedTags(tags); + + const allowedTagsElem = page.querySelector('.allowedTags') as HTMLDivElement; + + for (const btnDeleteTag of allowedTagsElem.querySelectorAll('.btnDeleteTag')) { + btnDeleteTag.addEventListener('click', function () { + const tag = btnDeleteTag.getAttribute('data-tag'); + const newTags = tags.filter(function (t: string) { + return t != tag; + }); + loadAllowedTags(newTags); + }); + } + }, []); + const loadBlockedTags = useCallback((tags) => { const page = element.current; @@ -165,6 +190,7 @@ const UserParentalControl: FunctionComponent = () => { LibraryMenu.setTitle(user.Name); loadUnratedItems(user); + loadAllowedTags(user.Policy.AllowedTags); loadBlockedTags(user.Policy.BlockedTags); populateRatings(allParentalRatings); let ratingValue = ''; @@ -188,7 +214,7 @@ const UserParentalControl: FunctionComponent = () => { } renderAccessSchedule(user.Policy.AccessSchedules || []); loading.hide(); - }, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]); + }, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]); const loadData = useCallback(() => { loading.show(); @@ -212,32 +238,6 @@ const UserParentalControl: FunctionComponent = () => { loadData(); - const onSaveComplete = () => { - loading.hide(); - toast(globalize.translate('SettingsSaved')); - }; - - const saveUser = (user: UserDto) => { - if (!user.Id || !user.Policy) { - throw new Error('Unexpected null user id or policy'); - } - - const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10); - user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating; - user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) { - return i.checked; - }).map(function (i) { - return i.getAttribute('data-itemtype'); - }); - user.Policy.AccessSchedules = getSchedulesFromPage(); - user.Policy.BlockedTags = getBlockedTagsFromPage(); - window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - onSaveComplete(); - }).catch(err => { - console.error('[userparentalcontrol] failed to update user policy', err); - }); - }; - const showSchedulePopup = (schedule: AccessSchedule, index: number) => { schedule = schedule || {}; import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { @@ -270,6 +270,27 @@ const UserParentalControl: FunctionComponent = () => { }) 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); + loadAllowedTags(tags); + } + }).catch(() => { + // prompt closed + }); + }; + const getBlockedTagsFromPage = () => { return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) { return elem.getAttribute('data-tag'); @@ -277,24 +298,27 @@ const UserParentalControl: FunctionComponent = () => { }; const showBlockedTagPopup = () => { - import('../../../../components/prompt/prompt').then(({ default: prompt }) => { - prompt({ - label: globalize.translate('LabelTag') - }).then(function (value) { - const tags = getBlockedTagsFromPage(); + prompt({ + label: globalize.translate('LabelTag') + }).then(function (value) { + const tags = getBlockedTagsFromPage(); - if (tags.indexOf(value) == -1) { - tags.push(value); - loadBlockedTags(tags); - } - }).catch(() => { - // prompt closed - }); - }).catch(err => { - console.error('[userparentalcontrol] failed to load prompt', err); + if (tags.indexOf(value) == -1) { + tags.push(value); + loadBlockedTags(tags); + } + }).catch(() => { + // prompt closed }); }; + const onSaveComplete = () => { + loading.hide(); + toast(globalize.translate('SettingsSaved')); + }; + + const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete); + const onSubmit = (e: Event) => { loading.show(); const userId = getParameterByName('userId'); @@ -318,12 +342,16 @@ const UserParentalControl: FunctionComponent = () => { }, -1); }); + (page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', function () { + showAllowedTagPopup(); + }); + (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () { showBlockedTagPopup(); }); (page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit); - }, [loadBlockedTags, loadData, renderAccessSchedule]); + }, [loadAllowedTags, loadBlockedTags, loadData, renderAccessSchedule]); const optionMaxParentalRating = () => { let content = ''; @@ -378,6 +406,27 @@ const UserParentalControl: FunctionComponent = () => {
+
+ +
+ {allowedTags?.map(tag => { + return ; + })} +
+
{ />
{blockedTags.map(tag => { - return ; })}
@@ -435,4 +485,28 @@ const UserParentalControl: FunctionComponent = () => { ); }; +function handleSaveUser(page: HTMLDivElement, getSchedulesFromPage: () => AccessSchedule[], getAllowedTagsFromPage: () => string[], getBlockedTagsFromPage: () => string[], onSaveComplete: () => void) { + return (user: UserDto) => { + if (!user.Id || !user.Policy) { + throw new Error('Unexpected null user id or policy'); + } + + const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10); + user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating; + user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) { + return i.checked; + }).map(function (i) { + return i.getAttribute('data-itemtype'); + }); + user.Policy.AccessSchedules = getSchedulesFromPage(); + user.Policy.AllowedTags = getAllowedTagsFromPage(); + user.Policy.BlockedTags = getBlockedTagsFromPage(); + window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { + onSaveComplete(); + }).catch(err => { + console.error('[userparentalcontrol] failed to update user policy', err); + }); + }; +} + export default UserParentalControl; diff --git a/src/components/dashboard/users/BlockedTagList.tsx b/src/components/dashboard/users/TagList.tsx similarity index 75% rename from src/components/dashboard/users/BlockedTagList.tsx rename to src/components/dashboard/users/TagList.tsx index 5158a63e97..531ee2f6e6 100644 --- a/src/components/dashboard/users/BlockedTagList.tsx +++ b/src/components/dashboard/users/TagList.tsx @@ -2,10 +2,11 @@ import React, { FunctionComponent } from 'react'; import IconButtonElement from '../../../elements/IconButtonElement'; type IProps = { - tag?: string; + tag?: string, + tagType?: string; }; -const BlockedTagList: FunctionComponent = ({ tag }: IProps) => { +const TagList: FunctionComponent = ({ tag, tagType }: IProps) => { return (
@@ -16,7 +17,7 @@ const BlockedTagList: FunctionComponent = ({ tag }: IProps) => {
= ({ tag }: IProps) => { ); }; -export default BlockedTagList; +export default TagList; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 01237cb45d..1d48742604 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -528,6 +528,7 @@ "LabelAlbum": "Album", "LabelAlbumArtists": "Album artists", "LabelAlbumGain": "Album Gain", + "LabelAllowContentWithTags": "Allow items with tags", "LabelAllowedRemoteAddresses": "Remote IP address filter", "LabelAllowedRemoteAddressesMode": "Remote IP address filter mode", "LabelAllowHWTranscoding": "Allow hardware transcoding", From d52c56eb2ed21476982661c101f95f3813587b06 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sat, 23 Mar 2024 03:46:05 -0400 Subject: [PATCH 2/3] Remove global ApiClient reference --- .../routes/users/parentalcontrol.tsx | 26 +++++++++++-------- src/components/ServerConnections.js | 12 +++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index c5ce145fd1..cca19e719c 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/routes/users/parentalcontrol.tsx @@ -17,6 +17,7 @@ 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 UnratedItem = { name: string; @@ -487,25 +488,28 @@ const UserParentalControl: FunctionComponent = () => { function handleSaveUser(page: HTMLDivElement, getSchedulesFromPage: () => AccessSchedule[], getAllowedTagsFromPage: () => string[], getBlockedTagsFromPage: () => string[], onSaveComplete: () => void) { return (user: UserDto) => { - if (!user.Id || !user.Policy) { + 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); - user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating; - user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) { + userPolicy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating; + userPolicy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) { return i.checked; }).map(function (i) { return i.getAttribute('data-itemtype'); }); - user.Policy.AccessSchedules = getSchedulesFromPage(); - user.Policy.AllowedTags = getAllowedTagsFromPage(); - user.Policy.BlockedTags = getBlockedTagsFromPage(); - window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - onSaveComplete(); - }).catch(err => { - console.error('[userparentalcontrol] failed to update user policy', err); - }); + 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); + }); }; } diff --git a/src/components/ServerConnections.js b/src/components/ServerConnections.js index 9bdb82fc60..70dcc931d0 100644 --- a/src/components/ServerConnections.js +++ b/src/components/ServerConnections.js @@ -104,6 +104,18 @@ class ServerConnections extends ConnectionManager { return apiClient; } + /** + * Gets the ApiClient that is currently connected or throws if not defined. + * @async + * @returns {Promise} The current ApiClient instance. + */ + async getCurrentApiClientAsync() { + const apiClient = this.currentApiClient(); + if (!apiClient) throw new Error('[ServerConnection] No current ApiClient instance'); + + return apiClient; + } + onLocalUserSignedIn(user) { const apiClient = this.getApiClient(user.ServerId); this.setLocalApiClient(apiClient); From 2e855382fe0aaf28c74ed39d83c466fe220cd11f Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sun, 24 Mar 2024 03:43:40 -0400 Subject: [PATCH 3/3] Cleanup types and functions --- .../routes/users/parentalcontrol.tsx | 104 +++++++++--------- 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index cca19e719c..5efa0f93dd 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/routes/users/parentalcontrol.tsx @@ -25,13 +25,44 @@ type UnratedItem = { 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: FunctionComponent = () => { const [ userName, setUserName ] = useState(''); const [ parentalRatings, setParentalRatings ] = useState([]); const [ unratedItems, setUnratedItems ] = useState([]); const [ accessSchedules, setAccessSchedules ] = useState([]); - const [ allowedTags, setAllowedTags ] = useState([]); - const [ blockedTags, setBlockedTags ] = useState([]); + const [ allowedTags, setAllowedTags ] = useState([]); + const [ blockedTags, setBlockedTags ] = useState([]); const element = useRef(null); @@ -109,7 +140,7 @@ const UserParentalControl: FunctionComponent = () => { blockUnratedItems.dispatchEvent(new CustomEvent('create')); }, []); - const loadAllowedTags = useCallback((tags) => { + const loadAllowedTags = useCallback((tags: string[]) => { const page = element.current; if (!page) { @@ -124,15 +155,13 @@ const UserParentalControl: FunctionComponent = () => { for (const btnDeleteTag of allowedTagsElem.querySelectorAll('.btnDeleteTag')) { btnDeleteTag.addEventListener('click', function () { const tag = btnDeleteTag.getAttribute('data-tag'); - const newTags = tags.filter(function (t: string) { - return t != tag; - }); + const newTags = tags.filter(t => t !== tag); loadAllowedTags(newTags); }); } }, []); - const loadBlockedTags = useCallback((tags) => { + const loadBlockedTags = useCallback((tags: string[]) => { const page = element.current; if (!page) { @@ -147,9 +176,7 @@ const UserParentalControl: FunctionComponent = () => { for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) { btnDeleteTag.addEventListener('click', function () { const tag = btnDeleteTag.getAttribute('data-tag'); - const newTags = tags.filter(function (t: string) { - return t != tag; - }); + const newTags = tags.filter(t => t !== tag); loadBlockedTags(newTags); }); } @@ -171,15 +198,13 @@ const UserParentalControl: FunctionComponent = () => { btnDelete.addEventListener('click', function () { const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10); schedules.splice(index, 1); - const newindex = schedules.filter(function (i: number) { - return i != index; - }); + const newindex = schedules.filter((i: number) => i != index); renderAccessSchedule(newindex); }); } }, []); - const loadUser = useCallback((user, allParentalRatings) => { + const loadUser = useCallback((user: UserDto, allParentalRatings: ParentalRating[]) => { const page = element.current; if (!page) { @@ -187,33 +212,31 @@ const UserParentalControl: FunctionComponent = () => { return; } - setUserName(user.Name); + setUserName(user.Name || ''); LibraryMenu.setTitle(user.Name); loadUnratedItems(user); - loadAllowedTags(user.Policy.AllowedTags); - loadBlockedTags(user.Policy.BlockedTags); + loadAllowedTags(user.Policy?.AllowedTags || []); + loadBlockedTags(user.Policy?.BlockedTags || []); populateRatings(allParentalRatings); + let ratingValue = ''; - - if (user.Policy.MaxParentalRating != null) { - for (let i = 0, length = allParentalRatings.length; i < length; i++) { - const rating = allParentalRatings[i]; - - if (user.Policy.MaxParentalRating >= rating.Value) { - ratingValue = rating.Value; + if (user.Policy?.MaxParentalRating) { + allParentalRatings.forEach(rating => { + if (rating.Value && user.Policy?.MaxParentalRating && user.Policy.MaxParentalRating >= rating.Value) { + ratingValue = `${rating.Value}`; } - } + }); } (page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue; - if (user.Policy.IsAdministrator) { + if (user.Policy?.IsAdministrator) { (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide'); } else { (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide'); } - renderAccessSchedule(user.Policy.AccessSchedules || []); + renderAccessSchedule(user.Policy?.AccessSchedules || []); loading.hide(); }, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]); @@ -486,31 +509,4 @@ const UserParentalControl: FunctionComponent = () => { ); }; -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'), function (i) { - return i.checked; - }).map(function (i) { - return 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); - }); - }; -} - export default UserParentalControl;