mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #6184 from thornbill/playlist-editor
Add playlist editing
This commit is contained in:
commit
9c405e9360
8 changed files with 179 additions and 29 deletions
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
@ -113,6 +113,7 @@ const MoreCommandsButton: FC<MoreCommandsButtonProps> = ({
|
||||||
itemId: selectedItemId || itemId || ''
|
itemId: selectedItemId || itemId || ''
|
||||||
});
|
});
|
||||||
const parentId = item?.SeasonId || item?.SeriesId || item?.ParentId;
|
const parentId = item?.SeasonId || item?.SeriesId || item?.ParentId;
|
||||||
|
const [ hasCommands, setHasCommands ] = useState(false);
|
||||||
|
|
||||||
const playlistItem = useMemo(() => {
|
const playlistItem = useMemo(() => {
|
||||||
let PlaylistItemId: string | null = null;
|
let PlaylistItemId: string | null = null;
|
||||||
|
@ -198,10 +199,15 @@ const MoreCommandsButton: FC<MoreCommandsButtonProps> = ({
|
||||||
[defaultMenuOptions, item, itemId, items, parentId, queryClient, queryKey]
|
[defaultMenuOptions, item, itemId, items, parentId, queryClient, queryKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
useEffect(() => {
|
||||||
item
|
const getCommands = async () => {
|
||||||
&& itemContextMenu.getCommands(defaultMenuOptions).length
|
const commands = await itemContextMenu.getCommands(defaultMenuOptions);
|
||||||
) {
|
setHasCommands(commands.length > 0);
|
||||||
|
};
|
||||||
|
void getCommands();
|
||||||
|
}, [ defaultMenuOptions ]);
|
||||||
|
|
||||||
|
if (item && hasCommands) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='button-flat btnMoreCommands'
|
className='button-flat btnMoreCommands'
|
||||||
|
|
|
@ -5,7 +5,7 @@ import globalize from '../lib/globalize';
|
||||||
import actionsheet from './actionSheet/actionSheet';
|
import actionsheet from './actionSheet/actionSheet';
|
||||||
import { appHost } from './apphost';
|
import { appHost } from './apphost';
|
||||||
import { appRouter } from './router/appRouter';
|
import { appRouter } from './router/appRouter';
|
||||||
import itemHelper from './itemHelper';
|
import itemHelper, { canEditPlaylist } from './itemHelper';
|
||||||
import { playbackManager } from './playback/playbackmanager';
|
import { playbackManager } from './playback/playbackmanager';
|
||||||
import ServerConnections from './ServerConnections';
|
import ServerConnections from './ServerConnections';
|
||||||
import toast from './toast/toast';
|
import toast from './toast/toast';
|
||||||
|
@ -29,7 +29,7 @@ function getDeleteLabel(type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommands(options) {
|
export async function getCommands(options) {
|
||||||
const item = options.item;
|
const item = options.item;
|
||||||
const user = options.user;
|
const user = options.user;
|
||||||
|
|
||||||
|
@ -209,6 +209,17 @@ export function getCommands(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.Type === BaseItemKind.Playlist) {
|
||||||
|
const _canEditPlaylist = await canEditPlaylist(user, item);
|
||||||
|
if (_canEditPlaylist) {
|
||||||
|
commands.push({
|
||||||
|
name: globalize.translate('Edit'),
|
||||||
|
id: 'editplaylist',
|
||||||
|
icon: 'edit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canEdit = itemHelper.canEdit(user, item);
|
const canEdit = itemHelper.canEdit(user, item);
|
||||||
if (canEdit && options.edit !== false && item.Type !== 'SeriesTimer') {
|
if (canEdit && options.edit !== false && item.Type !== 'SeriesTimer') {
|
||||||
const text = (item.Type === 'Timer' || item.Type === 'SeriesTimer') ? globalize.translate('Edit') : globalize.translate('EditMetadata');
|
const text = (item.Type === 'Timer' || item.Type === 'SeriesTimer') ? globalize.translate('Edit') : globalize.translate('EditMetadata');
|
||||||
|
@ -466,6 +477,15 @@ function executeCommand(item, id, options) {
|
||||||
case 'edit':
|
case 'edit':
|
||||||
editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
break;
|
break;
|
||||||
|
case 'editplaylist':
|
||||||
|
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
|
const playlistEditor = new PlaylistEditor();
|
||||||
|
playlistEditor.show({
|
||||||
|
id: itemId,
|
||||||
|
serverId
|
||||||
|
}).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'editimages':
|
case 'editimages':
|
||||||
import('./imageeditor/imageeditor').then((imageEditor) => {
|
import('./imageeditor/imageeditor').then((imageEditor) => {
|
||||||
imageEditor.show({
|
imageEditor.show({
|
||||||
|
@ -712,19 +732,19 @@ function refresh(apiClient, item) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function show(options) {
|
export async function show(options) {
|
||||||
const commands = getCommands(options);
|
const commands = await getCommands(options);
|
||||||
if (!commands.length) {
|
if (!commands.length) {
|
||||||
return Promise.reject();
|
throw new Error('No item commands present');
|
||||||
}
|
}
|
||||||
|
|
||||||
return actionsheet.show({
|
const id = await actionsheet.show({
|
||||||
items: commands,
|
items: commands,
|
||||||
positionTo: options.positionTo,
|
positionTo: options.positionTo,
|
||||||
resolveOnClick: ['share']
|
resolveOnClick: ['share']
|
||||||
}).then(function (id) {
|
|
||||||
return executeCommand(options.item, id, options);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return executeCommand(options.item, id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { appHost } from './apphost';
|
|
||||||
import globalize from 'lib/globalize';
|
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type';
|
import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type';
|
||||||
import { RecordingStatus } from '@jellyfin/sdk/lib/generated-client/models/recording-status';
|
import { RecordingStatus } from '@jellyfin/sdk/lib/generated-client/models/recording-status';
|
||||||
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
|
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
|
||||||
|
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||||
|
|
||||||
|
import { appHost } from './apphost';
|
||||||
|
import globalize from 'lib/globalize';
|
||||||
|
import ServerConnections from './ServerConnections';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
|
||||||
export function getDisplayName(item, options = {}) {
|
export function getDisplayName(item, options = {}) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -159,6 +163,25 @@ export function canEditImages (user, item) {
|
||||||
return itemType !== 'Timer' && itemType !== 'SeriesTimer' && canEdit(user, item) && !isLocalItem(item);
|
return itemType !== 'Timer' && itemType !== 'SeriesTimer' && canEdit(user, item) && !isLocalItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function canEditPlaylist(user, item) {
|
||||||
|
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||||
|
const api = toApi(apiClient);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: permissions } = await getPlaylistsApi(api)
|
||||||
|
.getPlaylistUser({
|
||||||
|
userId: user.Id,
|
||||||
|
playlistId: item.Id
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!permissions.CanEdit;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get playlist permissions', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function canEditSubtitles (user, item) {
|
export function canEditSubtitles (user, item) {
|
||||||
if (item.MediaType !== MediaType.Video) {
|
if (item.MediaType !== MediaType.Video) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||||
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
import toast from 'components/toast/toast';
|
import toast from 'components/toast/toast';
|
||||||
|
@ -10,6 +11,7 @@ import globalize from 'lib/globalize';
|
||||||
import { currentSettings as userSettings } from 'scripts/settings/userSettings';
|
import { currentSettings as userSettings } from 'scripts/settings/userSettings';
|
||||||
import { PluginType } from 'types/plugin';
|
import { PluginType } from 'types/plugin';
|
||||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import { isBlank } from 'utils/string';
|
||||||
|
|
||||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||||
import loading from '../loading/loading';
|
import loading from '../loading/loading';
|
||||||
|
@ -28,11 +30,13 @@ import 'material-design-icons-iconfont';
|
||||||
import '../formdialog.scss';
|
import '../formdialog.scss';
|
||||||
|
|
||||||
interface DialogElement extends HTMLDivElement {
|
interface DialogElement extends HTMLDivElement {
|
||||||
|
playlistId?: string
|
||||||
submitted?: boolean
|
submitted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaylistEditorOptions {
|
interface PlaylistEditorOptions {
|
||||||
items: string[],
|
items: string[],
|
||||||
|
id?: string,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
enableAddToPlayQueue?: boolean,
|
enableAddToPlayQueue?: boolean,
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
|
@ -56,6 +60,13 @@ function onSubmit(this: HTMLElement, e: Event) {
|
||||||
toast(globalize.translate('PlaylistError.AddFailed'));
|
toast(globalize.translate('PlaylistError.AddFailed'));
|
||||||
})
|
})
|
||||||
.finally(loading.hide);
|
.finally(loading.hide);
|
||||||
|
} else if (panel.playlistId) {
|
||||||
|
updatePlaylist(panel)
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[PlaylistEditor] Failed to update to playlist %s', panel.playlistId, err);
|
||||||
|
toast(globalize.translate('PlaylistError.UpdateFailed'));
|
||||||
|
})
|
||||||
|
.finally(loading.hide);
|
||||||
} else {
|
} else {
|
||||||
createPlaylist(panel)
|
createPlaylist(panel)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -73,6 +84,9 @@ function onSubmit(this: HTMLElement, e: Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPlaylist(dlg: DialogElement) {
|
function createPlaylist(dlg: DialogElement) {
|
||||||
|
const name = dlg.querySelector<HTMLInputElement>('#txtNewPlaylistName')?.value;
|
||||||
|
if (isBlank(name)) return Promise.reject(new Error('Playlist name should not be blank'));
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||||
const api = toApi(apiClient);
|
const api = toApi(apiClient);
|
||||||
|
|
||||||
|
@ -81,7 +95,7 @@ function createPlaylist(dlg: DialogElement) {
|
||||||
return getPlaylistsApi(api)
|
return getPlaylistsApi(api)
|
||||||
.createPlaylist({
|
.createPlaylist({
|
||||||
createPlaylistDto: {
|
createPlaylistDto: {
|
||||||
Name: dlg.querySelector<HTMLInputElement>('#txtNewPlaylistName')?.value,
|
Name: name,
|
||||||
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked,
|
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked,
|
||||||
Ids: itemIds?.split(','),
|
Ids: itemIds?.split(','),
|
||||||
UserId: apiClient.getCurrentUserId()
|
UserId: apiClient.getCurrentUserId()
|
||||||
|
@ -99,6 +113,29 @@ function redirectToPlaylist(id: string | undefined) {
|
||||||
appRouter.showItem(id, currentServerId);
|
appRouter.showItem(id, currentServerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePlaylist(dlg: DialogElement) {
|
||||||
|
if (!dlg.playlistId) return Promise.reject(new Error('Missing playlist ID'));
|
||||||
|
|
||||||
|
const name = dlg.querySelector<HTMLInputElement>('#txtNewPlaylistName')?.value;
|
||||||
|
if (isBlank(name)) return Promise.reject(new Error('Playlist name should not be blank'));
|
||||||
|
|
||||||
|
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||||
|
const api = toApi(apiClient);
|
||||||
|
|
||||||
|
return getPlaylistsApi(api)
|
||||||
|
.updatePlaylist({
|
||||||
|
playlistId: dlg.playlistId,
|
||||||
|
updatePlaylistDto: {
|
||||||
|
Name: name,
|
||||||
|
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dlg.submitted = true;
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addToPlaylist(dlg: DialogElement, id: string) {
|
function addToPlaylist(dlg: DialogElement, id: string) {
|
||||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||||
const api = toApi(apiClient);
|
const api = toApi(apiClient);
|
||||||
|
@ -210,7 +247,7 @@ function populatePlaylists(editorOptions: PlaylistEditorOptions, panel: DialogEl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditorHtml(items: string[]) {
|
function getEditorHtml(items: string[], options: PlaylistEditorOptions) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||||
|
@ -232,7 +269,7 @@ function getEditorHtml(items: string[]) {
|
||||||
html += `
|
html += `
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkPlaylistPublic" checked />
|
<input type="checkbox" is="emby-checkbox" id="chkPlaylistPublic" />
|
||||||
<span>${globalize.translate('PlaylistPublic')}</span>
|
<span>${globalize.translate('PlaylistPublic')}</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription checkboxFieldDescription">
|
<div class="fieldDescription checkboxFieldDescription">
|
||||||
|
@ -244,7 +281,7 @@ function getEditorHtml(items: string[]) {
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
html += '<div class="formDialogFooter">';
|
html += '<div class="formDialogFooter">';
|
||||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
|
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${options.id ? globalize.translate('Save') : globalize.translate('Add')}</button>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||||
|
@ -281,6 +318,34 @@ function initEditor(content: DialogElement, options: PlaylistEditorOptions, item
|
||||||
console.error('[PlaylistEditor] failed to populate playlists', err);
|
console.error('[PlaylistEditor] failed to populate playlists', err);
|
||||||
})
|
})
|
||||||
.finally(loading.hide);
|
.finally(loading.hide);
|
||||||
|
} else if (options.id) {
|
||||||
|
content.querySelector('.fldSelectPlaylist')?.classList.add('hide');
|
||||||
|
const panel = dom.parentWithClass(content, 'dialog') as DialogElement | null;
|
||||||
|
if (!panel) {
|
||||||
|
console.error('[PlaylistEditor] could not find dialog element');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||||
|
const api = toApi(apiClient);
|
||||||
|
Promise.all([
|
||||||
|
getUserLibraryApi(api)
|
||||||
|
.getItem({ itemId: options.id }),
|
||||||
|
getPlaylistsApi(api)
|
||||||
|
.getPlaylist({ playlistId: options.id })
|
||||||
|
])
|
||||||
|
.then(([ { data: playlistItem }, { data: playlist } ]) => {
|
||||||
|
panel.playlistId = options.id;
|
||||||
|
|
||||||
|
const nameField = panel.querySelector<HTMLInputElement>('#txtNewPlaylistName');
|
||||||
|
if (nameField) nameField.value = playlistItem.Name || '';
|
||||||
|
|
||||||
|
const publicField = panel.querySelector<HTMLInputElement>('#chkPlaylistPublic');
|
||||||
|
if (publicField) publicField.checked = !!playlist.OpenAccess;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[playlistEditor] failed to get playlist details', err);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
content.querySelector('.fldSelectPlaylist')?.classList.add('hide');
|
content.querySelector('.fldSelectPlaylist')?.classList.add('hide');
|
||||||
|
|
||||||
|
@ -325,17 +390,21 @@ export class PlaylistEditor {
|
||||||
dlg.classList.add('formDialog');
|
dlg.classList.add('formDialog');
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
const title = globalize.translate('HeaderAddToPlaylist');
|
|
||||||
|
|
||||||
html += '<div class="formDialogHeader">';
|
html += '<div class="formDialogHeader">';
|
||||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||||
html += '<h3 class="formDialogHeaderTitle">';
|
html += '<h3 class="formDialogHeaderTitle">';
|
||||||
html += title;
|
if (items.length) {
|
||||||
|
html += globalize.translate('HeaderAddToPlaylist');
|
||||||
|
} else if (options.id) {
|
||||||
|
html += globalize.translate('HeaderEditPlaylist');
|
||||||
|
} else {
|
||||||
|
html += globalize.translate('HeaderNewPlaylist');
|
||||||
|
}
|
||||||
html += '</h3>';
|
html += '</h3>';
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
html += getEditorHtml(items);
|
html += getEditorHtml(items, options);
|
||||||
|
|
||||||
dlg.innerHTML = html;
|
dlg.innerHTML = html;
|
||||||
|
|
||||||
|
|
|
@ -582,11 +582,13 @@ function reloadFromItem(instance, page, params, item, user) {
|
||||||
page.querySelector('.btnSplitVersions').classList.add('hide');
|
page.querySelector('.btnSplitVersions').classList.add('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) {
|
itemContextMenu.getCommands(getContextMenuOptions(item, user)).then(commands => {
|
||||||
|
if (commands.length) {
|
||||||
hideAll(page, 'btnMoreCommands', true);
|
hideAll(page, 'btnMoreCommands', true);
|
||||||
} else {
|
} else {
|
||||||
hideAll(page, 'btnMoreCommands');
|
hideAll(page, 'btnMoreCommands');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const itemBirthday = page.querySelector('#itemBirthday');
|
const itemBirthday = page.querySelector('#itemBirthday');
|
||||||
|
|
||||||
|
|
|
@ -411,6 +411,7 @@
|
||||||
"HeaderDummyChapter": "Chapter Images",
|
"HeaderDummyChapter": "Chapter Images",
|
||||||
"HeaderDVR": "DVR",
|
"HeaderDVR": "DVR",
|
||||||
"HeaderEditImages": "Edit Images",
|
"HeaderEditImages": "Edit Images",
|
||||||
|
"HeaderEditPlaylist": "Edit Playlist",
|
||||||
"HeaderEnabledFields": "Enabled Fields",
|
"HeaderEnabledFields": "Enabled Fields",
|
||||||
"HeaderEnabledFieldsHelp": "Uncheck a field to lock it and prevent its data from being changed.",
|
"HeaderEnabledFieldsHelp": "Uncheck a field to lock it and prevent its data from being changed.",
|
||||||
"HeaderEpisodesStatus": "Episodes Status",
|
"HeaderEpisodesStatus": "Episodes Status",
|
||||||
|
@ -457,6 +458,7 @@
|
||||||
"HeaderNetworking": "IP Protocols",
|
"HeaderNetworking": "IP Protocols",
|
||||||
"HeaderNewApiKey": "New API Key",
|
"HeaderNewApiKey": "New API Key",
|
||||||
"HeaderNewDevices": "New Devices",
|
"HeaderNewDevices": "New Devices",
|
||||||
|
"HeaderNewPlaylist": "New Playlist",
|
||||||
"HeaderNewRepository": "New Repository",
|
"HeaderNewRepository": "New Repository",
|
||||||
"HeaderNextItem": "Next {0}",
|
"HeaderNextItem": "Next {0}",
|
||||||
"HeaderNextItemPlayingInValue": "Next {0} Playing in {1}",
|
"HeaderNextItemPlayingInValue": "Next {0} Playing in {1}",
|
||||||
|
@ -1335,6 +1337,7 @@
|
||||||
"PlayFromBeginning": "Play from beginning",
|
"PlayFromBeginning": "Play from beginning",
|
||||||
"PlaylistError.AddFailed": "Error adding to playlist",
|
"PlaylistError.AddFailed": "Error adding to playlist",
|
||||||
"PlaylistError.CreateFailed": "Error creating playlist",
|
"PlaylistError.CreateFailed": "Error creating playlist",
|
||||||
|
"PlaylistError.UpdateFailed": "Error updating playlist",
|
||||||
"PlaylistPublic": "Allow public access",
|
"PlaylistPublic": "Allow public access",
|
||||||
"PlaylistPublicDescription": "Allow this playlist to be viewed by any logged in user.",
|
"PlaylistPublicDescription": "Allow this playlist to be viewed by any logged in user.",
|
||||||
"Playlists": "Playlists",
|
"Playlists": "Playlists",
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { toBoolean, toFloat } from './string';
|
import { isBlank, toBoolean, toFloat } from './string';
|
||||||
|
|
||||||
|
describe('isBlank', () => {
|
||||||
|
it('Should return true if the string is blank', () => {
|
||||||
|
let check = isBlank(undefined);
|
||||||
|
expect(check).toBe(true);
|
||||||
|
check = isBlank(null);
|
||||||
|
expect(check).toBe(true);
|
||||||
|
check = isBlank('');
|
||||||
|
expect(check).toBe(true);
|
||||||
|
check = isBlank(' \t\t ');
|
||||||
|
expect(check).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return false if the string is not blank', () => {
|
||||||
|
const check = isBlank('not an empty string');
|
||||||
|
expect(check).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('toBoolean', () => {
|
describe('toBoolean', () => {
|
||||||
it('Should return the boolean represented by the string', () => {
|
it('Should return the boolean represented by the string', () => {
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Checks if a string is empty or contains only whitespace.
|
||||||
|
* @param {string} value The string to test.
|
||||||
|
* @returns {boolean} True if the string is blank.
|
||||||
|
*/
|
||||||
|
export function isBlank(value: string | undefined | null) {
|
||||||
|
return !value?.trim().length;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of a string as boolean.
|
* Gets the value of a string as boolean.
|
||||||
* @param {string} name The value as a string.
|
* @param {string} name The value as a string.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue