// Shared Custom Items editor (wizard + advanced section editor) (function() { 'use strict'; const CUSTOM_IMAGE_FIELDS = [ { key: 'posterUrl', label: 'Poster URL', required: true }, { key: 'thumbUrl', label: 'Thumb URL' }, { key: 'backdropUrl', label: 'Backdrop URL' }, { key: 'squareUrl', label: 'Square URL' }, { key: 'bannerUrl', label: 'Banner URL' }, { key: 'logoUrl', label: 'Logo URL' } ]; const CUSTOM_POSTER_FIELD = CUSTOM_IMAGE_FIELDS[0]; const CUSTOM_ADDITIONAL_IMAGE_FIELDS = CUSTOM_IMAGE_FIELDS.slice(1); function escapeHtml(str) { if (str == null) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderMaterialIcon(name, className = '') { return ``; } function getOptions(options = {}) { return { idPrefix: options.idPrefix || 'hsse-custom', actionAttr: options.actionAttr || 'data-hsse-action', actions: { openEditor: 'wizard-open-custom-item-editor', closeEditor: 'wizard-close-custom-item-editor', addItem: 'wizard-add-custom-item', removeItem: 'wizard-remove-custom-item', toggleImages: 'wizard-toggle-custom-images', ...(options.actions || {}) }, intro: options.intro !== false }; } function actionAttr(options, action) { return `${options.actionAttr}="${escapeHtml(options.actions[action])}"`; } function fieldId(options, index, key) { return `${options.idPrefix}-${index}-${key}`; } function createBlankCustomItem() { return { name: '', cardFooter: '', cardUrl: '', posterUrl: '', thumbUrl: '', backdropUrl: '', squareUrl: '', bannerUrl: '', logoUrl: '' }; } function staticItemToDraft(item) { if (!item) return createBlankCustomItem(); const draft = createBlankCustomItem(); draft.name = item.Name || item.name || ''; draft.cardFooter = item.cardFooter || ''; draft.cardUrl = item.cardUrl || ''; draft.posterUrl = item.posterUrl || item.imageUrl || ''; draft.thumbUrl = item.thumbUrl || ''; draft.backdropUrl = item.backdropUrl || ''; draft.squareUrl = item.squareUrl || ''; draft.bannerUrl = item.bannerUrl || ''; draft.logoUrl = item.logoUrl || ''; return draft; } function staticItemsToDrafts(items) { const list = (items || []).map(staticItemToDraft).filter(Boolean); return list.length ? list : [createBlankCustomItem()]; } function ensureCustomItemsInitialized(state) { if (!state.customItems?.length) { state.customItems = [createBlankCustomItem()]; } } function hasAdditionalCustomImages(item) { return CUSTOM_ADDITIONAL_IMAGE_FIELDS.some(field => item[field.key]?.trim()); } function isCustomItemAdditionalExpanded(state, index, item) { const explicit = state.customItemAdditionalExpanded?.[index]; if (explicit === true) return true; if (explicit === false) return false; return hasAdditionalCustomImages(item); } function reindexCustomItemAdditionalExpanded(state, removedIndex) { if (!state.customItemAdditionalExpanded) return; const next = {}; Object.entries(state.customItemAdditionalExpanded).forEach(([key, value]) => { const i = parseInt(key, 10); if (Number.isNaN(i) || value === undefined) return; if (i < removedIndex) next[i] = value; else if (i > removedIndex) next[i - 1] = value; }); state.customItemAdditionalExpanded = next; } function hasValidCustomItems(items) { return (items || []).some(item => item.name?.trim() && item.posterUrl?.trim()); } function hasCustomItemContent(item) { if (!item) return false; return [ item.name, item.cardFooter, item.cardUrl, item.posterUrl, item.thumbUrl, item.backdropUrl, item.squareUrl, item.bannerUrl, item.logoUrl ].some(value => value?.trim()); } function buildStaticItemFromDraft(draft) { const name = draft.name?.trim(); const posterUrl = draft.posterUrl?.trim(); if (!name || !posterUrl) return null; const imageKeys = ['posterUrl', 'thumbUrl', 'backdropUrl', 'squareUrl', 'bannerUrl', 'logoUrl']; const provided = {}; imageKeys.forEach(key => { const value = draft[key]?.trim(); if (value) provided[key] = value; }); const item = { Name: name, Type: 'Folder' }; const cardUrl = draft.cardUrl?.trim(); if (cardUrl) item.cardUrl = cardUrl; const cardFooter = draft.cardFooter?.trim(); if (cardFooter) item.cardFooter = cardFooter; const keys = Object.keys(provided); if (keys.length === 1 && provided.posterUrl) { item.imageUrl = provided.posterUrl; } else { Object.assign(item, provided); } return item; } function collectCustomItemFromForm(root, state, index, options) { if (!root || !state.customItems?.[index]) return; const opts = getOptions(options); const item = state.customItems[index]; const nameEl = root.querySelector(`#${fieldId(opts, index, 'name')}`); const cardFooterEl = root.querySelector(`#${fieldId(opts, index, 'cardFooter')}`); const cardUrlEl = root.querySelector(`#${fieldId(opts, index, 'cardUrl')}`); if (nameEl) item.name = nameEl.value; if (cardFooterEl) item.cardFooter = cardFooterEl.value; if (cardUrlEl) item.cardUrl = cardUrlEl.value; CUSTOM_IMAGE_FIELDS.forEach(field => { const el = root.querySelector(`#${fieldId(opts, index, field.key)}`); if (el) item[field.key] = el.value; }); } function collectCustomItemsFromForm(root, state, options) { if (!root || !state.customItems) return; const editorIndex = state.customItemEditorIndex; if (editorIndex != null && !Number.isNaN(editorIndex)) { collectCustomItemFromForm(root, state, editorIndex, options); } } function updateCustomImagePreview(imgEl, url) { if (!imgEl) return; const trimmed = (url || '').trim(); if (!trimmed) { imgEl.removeAttribute('src'); imgEl.style.display = 'none'; imgEl.dataset.previewState = 'empty'; return; } imgEl.style.display = ''; imgEl.dataset.previewState = 'loading'; imgEl.onerror = () => { imgEl.style.display = 'none'; imgEl.dataset.previewState = 'error'; }; imgEl.onload = () => { imgEl.style.display = ''; imgEl.dataset.previewState = 'loaded'; }; imgEl.src = trimmed; } function refreshPreviews(root, state, options) { if (!root || !state.customItems) return; state.customItems.forEach((item, index) => { const tileImg = root.querySelector(`img[data-tile-index="${index}"]`); updateCustomImagePreview(tileImg, item.posterUrl); }); root.querySelectorAll('[data-hsse-custom-preview], [data-hsae-custom-preview]').forEach(input => { const img = root.querySelector(`img[data-preview-for="${input.id}"]`); updateCustomImagePreview(img, input.value); }); } function buildCustomImageFieldHTML(index, field, value, options, fieldOptions = {}) { const opts = getOptions(options); const { isPoster = false } = fieldOptions; const requiredMark = field.required ? ' *' : ''; const inputId = fieldId(opts, index, field.key); const previewAttr = isPoster ? ' data-hsse-custom-preview="true" data-hsae-custom-preview="true"' : ''; const posterHint = isPoster ? `
You can place images in your "Jellyfin\\Server\\jellyfin-web" folder and use the "/web/image_name.png" path to access them.
` : ''; return `Name and poster required. Link and other images optional.