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

Merge pull request #4535 from davidangel/tag-whitelist

Add "AllowedTags" option to parental controls (updated)
This commit is contained in:
Bill Thornton 2024-03-25 03:37:44 -04:00 committed by GitHub
commit 0202ac6c2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 159 additions and 69 deletions

View file

@ -83,6 +83,8 @@
- [Chris-Codes-It](https://github.com/Chris-Codes-It) - [Chris-Codes-It](https://github.com/Chris-Codes-It)
- [Vedant](https://github.com/viktory36) - [Vedant](https://github.com/viktory36)
- [GeorgeH005](https://github.com/GeorgeH005) - [GeorgeH005](https://github.com/GeorgeH005)
- [JPUC1143](https://github.com/Jpuc1143)
- [David Angel](https://github.com/davidangel)
## Emby Contributors ## Emby Contributors

View file

@ -6,7 +6,7 @@ import escapeHTML from 'escape-html';
import globalize from '../../../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../../../scripts/libraryMenu'; import LibraryMenu from '../../../../scripts/libraryMenu';
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList'; 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 ButtonElement from '../../../../elements/ButtonElement';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
@ -16,6 +16,8 @@ import { getParameterByName } from '../../../../utils/url';
import CheckBoxElement from '../../../../elements/CheckBoxElement'; import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement'; import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page'; import Page from '../../../../components/Page';
import prompt from '../../../../components/prompt/prompt';
import ServerConnections from 'components/ServerConnections';
type UnratedItem = { type UnratedItem = {
name: string; name: string;
@ -23,12 +25,44 @@ type UnratedItem = {
checkedAttribute: string 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 UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]); const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]); const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]); const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]); const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
@ -106,7 +140,28 @@ const UserParentalControl: FunctionComponent = () => {
blockUnratedItems.dispatchEvent(new CustomEvent('create')); blockUnratedItems.dispatchEvent(new CustomEvent('create'));
}, []); }, []);
const loadBlockedTags = useCallback((tags) => { const loadAllowedTags = useCallback((tags: string[]) => {
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(t => t !== tag);
loadAllowedTags(newTags);
});
}
}, []);
const loadBlockedTags = useCallback((tags: string[]) => {
const page = element.current; const page = element.current;
if (!page) { if (!page) {
@ -121,9 +176,7 @@ const UserParentalControl: FunctionComponent = () => {
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) { for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () { btnDeleteTag.addEventListener('click', function () {
const tag = btnDeleteTag.getAttribute('data-tag'); const tag = btnDeleteTag.getAttribute('data-tag');
const newTags = tags.filter(function (t: string) { const newTags = tags.filter(t => t !== tag);
return t != tag;
});
loadBlockedTags(newTags); loadBlockedTags(newTags);
}); });
} }
@ -145,15 +198,13 @@ const UserParentalControl: FunctionComponent = () => {
btnDelete.addEventListener('click', function () { btnDelete.addEventListener('click', function () {
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10); const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
schedules.splice(index, 1); schedules.splice(index, 1);
const newindex = schedules.filter(function (i: number) { const newindex = schedules.filter((i: number) => i != index);
return i != index;
});
renderAccessSchedule(newindex); renderAccessSchedule(newindex);
}); });
} }
}, []); }, []);
const loadUser = useCallback((user, allParentalRatings) => { const loadUser = useCallback((user: UserDto, allParentalRatings: ParentalRating[]) => {
const page = element.current; const page = element.current;
if (!page) { if (!page) {
@ -161,34 +212,33 @@ const UserParentalControl: FunctionComponent = () => {
return; return;
} }
setUserName(user.Name); setUserName(user.Name || '');
LibraryMenu.setTitle(user.Name); LibraryMenu.setTitle(user.Name);
loadUnratedItems(user); loadUnratedItems(user);
loadBlockedTags(user.Policy.BlockedTags); loadAllowedTags(user.Policy?.AllowedTags || []);
loadBlockedTags(user.Policy?.BlockedTags || []);
populateRatings(allParentalRatings); populateRatings(allParentalRatings);
let ratingValue = ''; let ratingValue = '';
if (user.Policy?.MaxParentalRating) {
if (user.Policy.MaxParentalRating != null) { allParentalRatings.forEach(rating => {
for (let i = 0, length = allParentalRatings.length; i < length; i++) { if (rating.Value && user.Policy?.MaxParentalRating && user.Policy.MaxParentalRating >= rating.Value) {
const rating = allParentalRatings[i]; ratingValue = `${rating.Value}`;
if (user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = rating.Value;
}
} }
});
} }
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue; (page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy.IsAdministrator) { if (user.Policy?.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide'); (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
} else { } else {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide'); (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
} }
renderAccessSchedule(user.Policy.AccessSchedules || []); renderAccessSchedule(user.Policy?.AccessSchedules || []);
loading.hide(); loading.hide();
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]); }, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
const loadData = useCallback(() => { const loadData = useCallback(() => {
loading.show(); loading.show();
@ -212,32 +262,6 @@ const UserParentalControl: FunctionComponent = () => {
loadData(); 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) => { const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {}; schedule = schedule || {};
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
@ -270,6 +294,27 @@ const UserParentalControl: FunctionComponent = () => {
}) as AccessSchedule[]; }) 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 = () => { const getBlockedTagsFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) { return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
return elem.getAttribute('data-tag'); return elem.getAttribute('data-tag');
@ -277,7 +322,6 @@ const UserParentalControl: FunctionComponent = () => {
}; };
const showBlockedTagPopup = () => { const showBlockedTagPopup = () => {
import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({ prompt({
label: globalize.translate('LabelTag') label: globalize.translate('LabelTag')
}).then(function (value) { }).then(function (value) {
@ -290,11 +334,15 @@ const UserParentalControl: FunctionComponent = () => {
}).catch(() => { }).catch(() => {
// prompt closed // prompt closed
}); });
}).catch(err => {
console.error('[userparentalcontrol] failed to load prompt', err);
});
}; };
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
};
const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
const onSubmit = (e: Event) => { const onSubmit = (e: Event) => {
loading.show(); loading.show();
const userId = getParameterByName('userId'); const userId = getParameterByName('userId');
@ -318,12 +366,16 @@ const UserParentalControl: FunctionComponent = () => {
}, -1); }, -1);
}); });
(page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', function () {
showAllowedTagPopup();
});
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () { (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup(); showBlockedTagPopup();
}); });
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]); }, [loadAllowedTags, loadBlockedTags, loadData, renderAccessSchedule]);
const optionMaxParentalRating = () => { const optionMaxParentalRating = () => {
let content = ''; let content = '';
@ -378,6 +430,27 @@ const UserParentalControl: FunctionComponent = () => {
</div> </div>
</div> </div>
<br /> <br />
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelAllowContentWithTags')}
isBtnVisible={true}
btnId='btnAddAllowedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<div className='allowedTags' style={{ marginTop: '.5em' }}>
{allowedTags?.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='allowedTag'
/>;
})}
</div>
</div>
<div className='verticalSection' style={{ marginBottom: '2em' }}> <div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer <SectionTitleContainer
SectionClassName='detailSectionHeader' SectionClassName='detailSectionHeader'
@ -391,9 +464,10 @@ const UserParentalControl: FunctionComponent = () => {
/> />
<div className='blockedTags' style={{ marginTop: '.5em' }}> <div className='blockedTags' style={{ marginTop: '.5em' }}>
{blockedTags.map(tag => { {blockedTags.map(tag => {
return <BlockedTagList return <TagList
key={tag} key={tag}
tag={tag} tag={tag}
tagType='blockedTag'
/>; />;
})} })}
</div> </div>

View file

@ -104,6 +104,18 @@ class ServerConnections extends ConnectionManager {
return apiClient; return apiClient;
} }
/**
* Gets the ApiClient that is currently connected or throws if not defined.
* @async
* @returns {Promise<ApiClient>} The current ApiClient instance.
*/
async getCurrentApiClientAsync() {
const apiClient = this.currentApiClient();
if (!apiClient) throw new Error('[ServerConnection] No current ApiClient instance');
return apiClient;
}
onLocalUserSignedIn(user) { onLocalUserSignedIn(user) {
const apiClient = this.getApiClient(user.ServerId); const apiClient = this.getApiClient(user.ServerId);
this.setLocalApiClient(apiClient); this.setLocalApiClient(apiClient);

View file

@ -2,10 +2,11 @@ import React, { FunctionComponent } from 'react';
import IconButtonElement from '../../../elements/IconButtonElement'; import IconButtonElement from '../../../elements/IconButtonElement';
type IProps = { type IProps = {
tag?: string; tag?: string,
tagType?: string;
}; };
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => { const TagList: FunctionComponent<IProps> = ({ tag, tagType }: IProps) => {
return ( return (
<div className='paperList'> <div className='paperList'>
<div className='listItem'> <div className='listItem'>
@ -16,7 +17,7 @@ const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
</div> </div>
<IconButtonElement <IconButtonElement
is='paper-icon-button-light' is='paper-icon-button-light'
className='blockedTag btnDeleteTag listItemButton' className={`${tagType} btnDeleteTag listItemButton`}
title='Delete' title='Delete'
icon='delete' icon='delete'
dataTag={tag} dataTag={tag}
@ -26,4 +27,4 @@ const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
); );
}; };
export default BlockedTagList; export default TagList;

View file

@ -528,6 +528,7 @@
"LabelAlbum": "Album", "LabelAlbum": "Album",
"LabelAlbumArtists": "Album artists", "LabelAlbumArtists": "Album artists",
"LabelAlbumGain": "Album Gain", "LabelAlbumGain": "Album Gain",
"LabelAllowContentWithTags": "Allow items with tags",
"LabelAllowedRemoteAddresses": "Remote IP address filter", "LabelAllowedRemoteAddresses": "Remote IP address filter",
"LabelAllowedRemoteAddressesMode": "Remote IP address filter mode", "LabelAllowedRemoteAddressesMode": "Remote IP address filter mode",
"LabelAllowHWTranscoding": "Allow hardware transcoding", "LabelAllowHWTranscoding": "Allow hardware transcoding",