diff --git a/configuration/homeScreen-configuration.js b/configuration/homeScreen-configuration.js index f97a65b..df8baf3 100644 --- a/configuration/homeScreen-configuration.js +++ b/configuration/homeScreen-configuration.js @@ -167,7 +167,7 @@ 'itemsLayout', 'useGaplessCards', 'sectionCssClass', 'hideName' ]; const RESTORE_ADVANCED_KEYS = [ - 'ttl', 'sortBy', 'sortOrder', 'sortOrderDirection', + 'ttl', 'sortBy', 'sortOrder', 'minimumItems', 'itemLimit', 'flattenSeries', 'limitBeforeSort', 'hideWatched' ]; const TTL_UNIT_OPTIONS = [ @@ -1444,6 +1444,63 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; return true; // flag always newly stamped → needs save } + // TEMPORARY: one-time overwrite of saved Kefin discovery sort fields from the new + // sortBy + sortOrder defaults. Remove in a future release once existing installs + // have picked up the patch (see discoverySortOrdersPatched). + // TODO(remove): patchDiscoverySortOrdersOnce + call site + discoverySortPatch log field + function patchDiscoverySortOrdersOnce(config) { + if (!window.KefinTweaksConfig) window.KefinTweaksConfig = {}; + if (window.KefinTweaksConfig.discoverySortOrdersPatched === true) return false; + if (!Array.isArray(window.KefinHomeConfig2?.DISCOVERY_SECTION_GROUPS)) return false; + + const canonicalMap = buildCanonicalSectionMapForGroups('DISCOVERY_SECTION_GROUPS'); + const isDirection = (value) => value === 'Ascending' || value === 'Descending'; + const topRatedQueryResetIds = new Set(['spotlightGenre', 'spotlightGenreSeries', 'spotlightNetwork']); + + const applySortPair = (target, canonical, fieldKey, directionKey) => { + if (!target || !canonical) return; + if (canonical[fieldKey]) target[fieldKey] = canonical[fieldKey]; + else delete target[fieldKey]; + + if (isDirection(canonical[directionKey])) target[directionKey] = canonical[directionKey]; + else delete target[directionKey]; + }; + + for (const group of config.DISCOVERY_SECTION_GROUPS || []) { + for (const section of group.sections || []) { + if (!section?.id) continue; + const canonical = canonicalMap.get(section.id); + if (!canonical) continue; + + applySortPair(section, canonical, 'sortBy', 'sortOrder'); + delete section.sortOrderDirection; + + const savedQueries = section.queries || []; + const canonicalQueries = canonical.queries || []; + const count = Math.min(savedQueries.length, canonicalQueries.length); + const replaceQueryOptions = topRatedQueryResetIds.has(section.id); + for (let i = 0; i < count; i++) { + const savedQuery = savedQueries[i]; + const canonicalQuery = canonicalQueries[i]; + if (!savedQuery || !canonicalQuery) continue; + if (replaceQueryOptions) { + savedQuery.queryOptions = JSON.parse(JSON.stringify(canonicalQuery.queryOptions || {})); + continue; + } + const canonicalOpts = canonicalQuery.queryOptions || {}; + if (!savedQuery.queryOptions) { + if (!canonicalOpts.SortBy && !isDirection(canonicalOpts.SortOrder)) continue; + savedQuery.queryOptions = {}; + } + applySortPair(savedQuery.queryOptions, canonicalOpts, 'SortBy', 'SortOrder'); + } + } + } + + window.KefinTweaksConfig.discoverySortOrdersPatched = true; + return true; // flag always newly stamped → needs save + } + async function ensureKefinTweaksDefaultSections() { try { if (!window.ApiClient || !window.ApiClient._loggedIn) { @@ -1477,8 +1534,10 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; const seasonalThemeChanges = syncSeasonalThemesFromDefaults(config); // TEMPORARY / TODO(remove): one-time liveTv-on-now Limit patch const liveTvLimitPatch = patchLiveTvOnNowLimitOnce(config); + // TEMPORARY / TODO(remove): one-time discovery sortBy/sortOrder overwrite + const discoverySortPatch = patchDiscoverySortOrdersOnce(config); - if (libraryChanges || addedDefaults || prunedOrphans || defaultSyncChanges || seasonalThemeChanges || liveTvLimitPatch) { + if (libraryChanges || addedDefaults || prunedOrphans || defaultSyncChanges || seasonalThemeChanges || liveTvLimitPatch || discoverySortPatch) { await saveConfig(config); LOG('KefinTweaks default home sections synced on startup', { libraryChanges, @@ -1486,7 +1545,8 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; prunedOrphans, defaultSyncChanges, seasonalThemeChanges, - liveTvLimitPatch + liveTvLimitPatch, + discoverySortPatch }); } } catch (error) { @@ -1919,6 +1979,11 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; ...(savedQuery.queryOptions || {}), ...currentQuery.queryOptions }; + ['SortBy', 'SortOrder'].forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(currentQuery.queryOptions, key)) { + delete merged.queryOptions[key]; + } + }); } else if (savedQuery.queryOptions) { merged.queryOptions = JSON.parse(JSON.stringify(savedQuery.queryOptions)); } @@ -1992,6 +2057,12 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; merged[key] = currentSection[key]; } }); + + ['sortBy', 'sortOrder'].forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(currentSection, key)) { + delete merged[key]; + } + }); // Ensure id is preserved from currentSection if (currentSection.id) { diff --git a/configuration/homeScreen-migration.js b/configuration/homeScreen-migration.js index 1a990ff..0a99698 100644 --- a/configuration/homeScreen-migration.js +++ b/configuration/homeScreen-migration.js @@ -628,8 +628,25 @@ found.section.isPlayed = legacySectionConfig.isPlayed; } if (legacySectionConfig.minimumItems !== undefined) found.section.minimumItems = legacySectionConfig.minimumItems; - if (legacySectionConfig.sortOrder !== undefined) found.section.sortOrder = legacySectionConfig.sortOrder; - if (legacySectionConfig.sortOrderDirection !== undefined) found.section.sortOrderDirection = legacySectionConfig.sortOrderDirection; + if (legacySectionConfig.sortOrder !== undefined) { + const legacySort = String(legacySectionConfig.sortOrder).trim(); + if (legacySort === 'Ascending' || legacySort === 'Descending') { + found.section.sortOrder = legacySort; + } else if (legacySort) { + found.section.sortBy = legacySort; + const existingOrder = found.section.sortOrder == null ? '' : String(found.section.sortOrder).trim(); + if (existingOrder !== 'Ascending' && existingOrder !== 'Descending') { + delete found.section.sortOrder; + } + } + } + if (legacySectionConfig.sortOrderDirection !== undefined) { + const legacyDirection = String(legacySectionConfig.sortOrderDirection).trim(); + if (legacyDirection === 'Ascending' || legacyDirection === 'Descending') { + found.section.sortOrder = legacyDirection; + } + } + delete found.section.sortOrderDirection; const isRecentlyWatchedPerson = RECENTLY_WATCHED_PERSON_IDS.has(sectionKey); if (legacySectionConfig.name !== undefined) { diff --git a/configuration/homeScreenAdvancedEditor.js b/configuration/homeScreenAdvancedEditor.js index a943133..f46b635 100644 --- a/configuration/homeScreenAdvancedEditor.js +++ b/configuration/homeScreenAdvancedEditor.js @@ -452,11 +452,26 @@ .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'); } + const SORT_BY_NONE = 'None'; + function toSortBySelectOptions(sortOrders) { - return (sortOrders || []).map(value => ({ - value, - label: humanizeSortOrderLabel(value) - })); + return [ + { value: SORT_BY_NONE, label: 'None' }, + ...(sortOrders || []).map(value => ({ + value, + label: humanizeSortOrderLabel(value) + })) + ]; + } + + function sortBySelectValue(stored) { + const value = stored == null ? '' : String(stored).trim(); + return value || SORT_BY_NONE; + } + + function isSortByNone(value) { + const by = value == null ? '' : String(value).trim(); + return !by || by === SORT_BY_NONE; } function getSectionSortOrderDirectionOptions(directions) { @@ -484,15 +499,63 @@ return allowed.includes(profile); } + const SECTION_SORT_DIRECTIONS = new Set(['Ascending', 'Descending']); + + function isSectionSortDirection(value) { + return SECTION_SORT_DIRECTIONS.has(String(value || '').trim()); + } + + /** + * Legacy sortOrder stored the field (Random, CommunityRating, …) and + * sortOrderDirection stored Ascending/Descending. Current model is + * sortBy = field, sortOrder = direction only. + */ + function normalizeSectionSortFields(section) { + if (!section || typeof section !== 'object') return section; + const order = section.sortOrder == null ? '' : String(section.sortOrder).trim(); + const legacyDirection = section.sortOrderDirection == null ? '' : String(section.sortOrderDirection).trim(); + + if (order && !isSectionSortDirection(order)) { + if (!section.sortBy) section.sortBy = order; + delete section.sortOrder; + } + if (isSectionSortDirection(legacyDirection)) { + section.sortOrder = legacyDirection; + } + delete section.sortOrderDirection; + return section; + } + function applySectionLevelSort(section, sortBy, sortOrder) { const by = sortBy == null ? '' : String(sortBy).trim(); const order = sortOrder == null ? '' : String(sortOrder).trim(); + delete section.sortOrderDirection; - if (!by || by === 'Default') delete section.sortBy; - else section.sortBy = by; + if (isSortByNone(by)) { + delete section.sortBy; + delete section.sortOrder; + return; + } - if (!order || order === 'Default') delete section.sortOrder; - else section.sortOrder = order; + section.sortBy = by; + if (isSectionSortDirection(order)) section.sortOrder = order; + else delete section.sortOrder; + } + + function applyCollectedQuerySort(queryOptions, sortBy, sortOrder) { + if (!queryOptions) return; + const by = sortBy == null ? '' : String(sortBy).trim(); + const order = sortOrder == null ? '' : String(sortOrder).trim(); + + if (isSortByNone(by)) { + delete queryOptions.SortBy; + delete queryOptions.SortOrder; + return; + } + + queryOptions.SortBy = by; + if (isSectionSortDirection(order)) queryOptions.SortOrder = order; + else delete queryOptions.SortOrder; } function resolvePremiereAgeFields(section) { @@ -513,19 +576,14 @@ } } - const sortByEl = dialog.querySelector('#hsae-advanced-sortBy'); - if (isProfileFieldActive(root, sortByEl)) { - const sortVal = sortByEl?.value; - delete section.sortBy; - if (sortVal && sortVal !== 'Default') section.sortOrder = sortVal; - else delete section.sortOrder; - } + normalizeSectionSortFields(section); + const sortByEl = dialog.querySelector('#hsae-advanced-sortBy'); const sortDirEl = dialog.querySelector('#hsae-advanced-sortOrder'); - if (isProfileFieldActive(root, sortDirEl)) { - const dirVal = sortDirEl?.value; - if (dirVal && dirVal !== 'Default') section.sortOrderDirection = dirVal; - else delete section.sortOrderDirection; + if (isProfileFieldActive(root, sortByEl)) { + applySectionLevelSort(section, sortByEl?.value, sortDirEl?.value); + } else { + delete section.sortOrderDirection; } } @@ -545,10 +603,12 @@ const sortByEl = dialog.querySelector('#hsae-advanced-sortBy'); const sortOrderEl = dialog.querySelector('#hsae-advanced-sortOrder'); + normalizeSectionSortFields(section); if (isProfileFieldActive(root, sortByEl) && !isDiscoverySection(section)) { if (section.useRandomQuery) { delete section.sortBy; delete section.sortOrder; + delete section.sortOrderDirection; } else { applySectionLevelSort(section, sortByEl?.value, sortOrderEl?.value); } @@ -991,9 +1051,13 @@ const renderMode = section.renderMode || (section.spotlight ? 'Spotlight' : 'Normal'); const ttlMatch = matchTtlPreset(section.ttl); const groupSelection = resolveGroupSelection(section, context); - const isDiscovery = isDiscoverySection(section); - const sectionSortBy = section.sortBy || (isDiscovery ? section.sortOrder : '') || ''; - const sectionSortOrder = isDiscovery ? (section.sortOrderDirection || '') : (section.sortOrder || ''); + const normalizedSort = normalizeSectionSortFields({ + sortBy: section.sortBy, + sortOrder: section.sortOrder, + sortOrderDirection: section.sortOrderDirection + }); + const sectionSortBy = normalizedSort.sortBy || ''; + const sectionSortOrder = isSectionSortDirection(normalizedSort.sortOrder) ? normalizedSort.sortOrder : ''; return { section, @@ -1540,7 +1604,7 @@