Update sections in place when editing section server defaults from the home screen

This commit is contained in:
HighImKevin committed 2026-10-15 15:38:32 -04:00
1 parent a3094caf70
commit a27bd54072
2 files changed
+213 -31

No files matched your search

+52 -31
View File
@@ -2746,6 +2746,8 @@
getActiveViewMoreUrl: getActiveViewMoreUrl,
refreshSectionQueries: refreshSectionQueries,
loadActiveSectionQueryItems: loadActiveSectionQueryItems,
createSectionScrollableContainer: createSectionScrollableContainer,
applySectionPresentationAttrs: applySectionPresentationAttrs,
attachSectionControlButtons: attachSectionControlButtons,
ensureCardBorders: ensureCardBorders,
applyItemsLayoutState: applyItemsLayoutState,
@@ -4464,11 +4466,26 @@
// Overlays: desktop = hover FAB + br actions; mobile = native bottom-right play only
const NON_PLAYABLE_TYPES = ['CollectionFolder', 'Folder', 'UserView', 'Virtual', 'Studio', 'Genre', 'Person'];
const NO_RATE_TYPES = ['UserView', 'CollectionFolder', 'Folder', 'Program', 'Timer', 'SeriesTimer', 'Channel'];
const canShowPlayOverlay =
!isCustomCard
&& !NON_PLAYABLE_TYPES.includes(item.Type)
&& item.LocationType !== 'Virtual'
&& item.Type !== 'Person';
// Match jellyfin-web itemHelper.canMarkPlayed (cards: no Folder/CollectionFolder/UserView)
const canMarkPlayed =
item.Type !== 'Program'
&& (
(item.MediaType === 'Video' && item.Type !== 'TvChannel')
|| (item.MediaType === 'Audio' && item.Type === 'AudioBook')
|| item.Type === 'Series'
|| item.Type === 'Season'
|| item.Type === 'BoxSet'
|| item.MediaType === 'Book'
)
&& !NON_PLAYABLE_TYPES.includes(item.Type);
// Match jellyfin-web itemHelper.canRate type denylist (do not gate on missing UserData)
const canRate = !NO_RATE_TYPES.includes(item.Type);
let cardOverlayContainer = null;
let mobileOverlayPlayButton = null;
@@ -4535,40 +4552,44 @@
buttonContainer.appendChild(watchlistButton);
}
const watchedButton = document.createElement('button');
watchedButton.setAttribute('is', 'emby-playstatebutton');
watchedButton.type = 'button';
watchedButton.setAttribute('data-action', 'none');
watchedButton.className = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button';
watchedButton.setAttribute('data-id', itemId);
watchedButton.setAttribute('data-serverid', serverId);
watchedButton.setAttribute('data-itemtype', itemType);
watchedButton.setAttribute('data-played', item.UserData?.Played || 'false');
watchedButton.title = 'Mark played';
if (canMarkPlayed) {
const watchedButton = document.createElement('button');
watchedButton.setAttribute('is', 'emby-playstatebutton');
watchedButton.type = 'button';
watchedButton.setAttribute('data-action', 'none');
watchedButton.className = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button';
watchedButton.setAttribute('data-id', itemId);
watchedButton.setAttribute('data-serverid', serverId);
watchedButton.setAttribute('data-itemtype', itemType);
watchedButton.setAttribute('data-played', item.UserData?.Played || 'false');
watchedButton.title = 'Mark played';
const watchedIcon = document.createElement('span');
watchedIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check playstatebutton-icon-unplayed';
watchedIcon.setAttribute('aria-hidden', 'true');
watchedButton.appendChild(watchedIcon);
buttonContainer.appendChild(watchedButton);
const watchedIcon = document.createElement('span');
watchedIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check playstatebutton-icon-unplayed';
watchedIcon.setAttribute('aria-hidden', 'true');
watchedButton.appendChild(watchedIcon);
buttonContainer.appendChild(watchedButton);
}
const favoriteButton = document.createElement('button');
favoriteButton.setAttribute('is', 'emby-ratingbutton');
favoriteButton.type = 'button';
favoriteButton.setAttribute('data-action', 'none');
favoriteButton.className = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button';
favoriteButton.setAttribute('data-id', itemId);
favoriteButton.setAttribute('data-serverid', serverId);
favoriteButton.setAttribute('data-itemtype', itemType);
favoriteButton.setAttribute('data-likes', '');
favoriteButton.setAttribute('data-isfavorite', item.UserData?.IsFavorite || 'false');
favoriteButton.title = 'Add to favorites';
if (canRate) {
const favoriteButton = document.createElement('button');
favoriteButton.setAttribute('is', 'emby-ratingbutton');
favoriteButton.type = 'button';
favoriteButton.setAttribute('data-action', 'none');
favoriteButton.className = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button';
favoriteButton.setAttribute('data-id', itemId);
favoriteButton.setAttribute('data-serverid', serverId);
favoriteButton.setAttribute('data-itemtype', itemType);
favoriteButton.setAttribute('data-likes', '');
favoriteButton.setAttribute('data-isfavorite', item.UserData?.IsFavorite || 'false');
favoriteButton.title = 'Add to favorites';
const favoriteIcon = document.createElement('span');
favoriteIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite';
favoriteIcon.setAttribute('aria-hidden', 'true');
favoriteButton.appendChild(favoriteIcon);
buttonContainer.appendChild(favoriteButton);
const favoriteIcon = document.createElement('span');
favoriteIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite';
favoriteIcon.setAttribute('aria-hidden', 'true');
favoriteButton.appendChild(favoriteIcon);
buttonContainer.appendChild(favoriteButton);
}
const moreButton = document.createElement('button');
moreButton.className = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';
+161
View File
@@ -1957,6 +1957,166 @@
return enqueueHomeViewJob({ type: 'home', hash: window.location.hash || '' });
}
/**
* Full re-render + replace a single mounted home section (admin editor save).
* If the section is not mounted, no-ops (does not refresh the whole home).
* @param {string} sectionId
* @param {Object} [sectionConfig] saved section config from the editor
* @returns {Promise<boolean>} true if replaced
*/
async function refreshHomeSectionById(sectionId, sectionConfig) {
if (!sectionId) return false;
invalidateHomeScreenConfig('refreshHomeSectionById');
const escaped = typeof CSS !== 'undefined' && CSS.escape
? CSS.escape(String(sectionId))
: String(sectionId).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const sectionElement =
document.querySelector(`.homePage:not(.hide) #homeTab .sections [data-section-id="${escaped}"]`)
|| document.querySelector(`[data-section-id="${escaped}"]`);
if (!sectionElement?.isConnected) {
LOG('refreshHomeSectionById: section not mounted', sectionId);
return false;
}
const runtime = window.KefinHomeScreenSectionRuntime?.get?.(sectionId);
const cfg = sectionConfig
? { ...(runtime?.effectiveConfig || {}), ...sectionConfig }
: (runtime?.effectiveConfig || null);
if (!cfg) {
WARN('refreshHomeSectionById: no section config for', sectionId);
return false;
}
const cardBuilder = window.cardBuilder;
if (!cardBuilder?.createSectionScrollableContainer || !cardBuilder?.renderSpotlightSection) {
WARN('refreshHomeSectionById: cardBuilder rebuild APIs unavailable');
return false;
}
let items = [];
try {
if (Array.isArray(cfg.items) && cfg.items.length > 0) {
items = cardBuilder.postProcessItems
? cardBuilder.postProcessItems(cfg, cfg.items)
: cfg.items.slice();
} else if (window.sectionHelper?.loadSectionForRendering) {
const loaded = await window.sectionHelper.loadSectionForRendering(cfg);
const result = loaded?.result;
const raw = typeof result?.ensureData === 'function'
? await result.ensureData()
: await (result?.dataPromise ?? result?.data);
if (Array.isArray(raw)) items = raw;
else if (Array.isArray(raw?.Items)) items = raw.Items;
else items = [];
} else if (cardBuilder.refreshSectionQueries) {
items = await cardBuilder.refreshSectionQueries(cfg);
}
} catch (e) {
ERR('refreshHomeSectionById: failed to load items', sectionId, e);
return false;
}
if (!items.length) {
WARN('refreshHomeSectionById: no items after load', sectionId);
return false;
}
const title = cardBuilder.getSectionTitleForMultiQuery?.(cfg) || cfg.name || '';
const viewMoreUrl = cardBuilder.getActiveViewMoreUrl?.(cfg) || cfg.viewMoreUrl || null;
const isSpotlight = cfg.renderMode === 'Spotlight' || cfg.spotlight === true;
let newContent = null;
if (isSpotlight) {
newContent = cardBuilder.renderSpotlightSection(items, title, {
viewMoreUrl,
...(cfg.spotlightConfig || {}),
});
} else {
let cardFormat = cfg.cardFormat;
if (cardFormat === 'random' || cardFormat === 'Random') {
cardFormat = cardBuilder.getRandomCardFormat?.() || 'Poster';
}
newContent = cardBuilder.createSectionScrollableContainer(
items,
title,
viewMoreUrl,
cfg.overflowCard !== false,
cardFormat,
cfg,
);
}
if (!newContent) {
WARN('refreshHomeSectionById: failed to build section DOM', sectionId);
return false;
}
newContent.setAttribute('data-section-id', cfg.id);
if (cfg.order != null) {
newContent.dataset.order = String(cfg.order);
newContent.style.order = cfg.order;
}
if (cfg.cardFormat) {
newContent.setAttribute('data-card-format', cfg.cardFormat);
}
// Preserve non-mode layout classes from the previous root (e.g. category filters)
const SPOTLIGHT_MODE_CLASSES = ['spotlight-section', 'padded-left'];
const NORMAL_MODE_CLASSES = ['verticalSection', 'emby-scroller-container', 'custom-scroller-container'];
const wasSpotlight = sectionElement.classList.contains('spotlight-section');
const modeChanged = wasSpotlight !== isSpotlight;
if (modeChanged) {
const stale = wasSpotlight ? SPOTLIGHT_MODE_CLASSES : NORMAL_MODE_CLASSES;
sectionElement.classList.forEach((cls) => {
if (!stale.includes(cls)) newContent.classList.add(cls);
});
}
if (typeof cardBuilder.applySectionPresentationAttrs === 'function') {
cardBuilder.applySectionPresentationAttrs(newContent, cfg);
}
const layout = cardBuilder.resolveItemsLayout?.(cfg) || 'row';
const gapless = cardBuilder.resolveUseGaplessCards?.(cfg) === true;
sectionElement.replaceWith(newContent);
if (!isSpotlight && cardBuilder.applyItemsLayoutState) {
cardBuilder.applyItemsLayoutState(newContent, layout, gapless);
}
if (cardBuilder.ensureCardBorders) {
cardBuilder.ensureCardBorders(newContent);
}
if (cardBuilder.attachSectionControlButtons) {
cardBuilder.attachSectionControlButtons(cfg, newContent, items);
}
if (cardBuilder.registerLazyImagesInSection) {
cardBuilder.registerLazyImagesInSection(newContent);
} else if (cardBuilder.registerLazyImagesIn) {
cardBuilder.registerLazyImagesIn(newContent);
}
if (runtime) {
runtime.element = newContent;
runtime.effectiveConfig = cfg;
runtime.cachedItems = items;
window.KefinHomeScreenSectionRuntime?.register?.(runtime);
} else if (window.KefinHomeScreenSectionRuntime?.register) {
window.KefinHomeScreenSectionRuntime.register({
sectionId: cfg.id,
element: newContent,
effectiveConfig: cfg,
cachedItems: items,
serverDefaults: {},
});
}
LOG('refreshHomeSectionById: replaced section', sectionId);
return true;
}
const HOME_CHROME_CREATE_ID = 'kefin-home-create-section-btn';
const HOME_CHROME_CATEGORY_RAIL_ID = 'kefin-home-category-rail';
const HOME_CHROME_CATEGORY_STYLE_ID = 'kefin-home-category-filter-styles';
@@ -2731,6 +2891,7 @@
window.homeScreen3 = {
init: () => enqueueHomeViewJob({ type: 'home', hash: window.location.hash || '' }),
refreshHomeSections,
refreshHomeSectionById,
resolveDiscoverySection: buildDiscoverySectionInstance,
sectionHelper: () => window.sectionHelper,
getActiveHomeCategory: () => homeChromeActiveCategory || 'none',