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:
commit
0202ac6c2a
5 changed files with 159 additions and 69 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,24 +322,27 @@ 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) {
|
const tags = getBlockedTagsFromPage();
|
||||||
const tags = getBlockedTagsFromPage();
|
|
||||||
|
|
||||||
if (tags.indexOf(value) == -1) {
|
if (tags.indexOf(value) == -1) {
|
||||||
tags.push(value);
|
tags.push(value);
|
||||||
loadBlockedTags(tags);
|
loadBlockedTags(tags);
|
||||||
}
|
}
|
||||||
}).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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue