// KefinTweaks Item Version Preferences Configuration UI (function () { 'use strict'; const LOG = (...args) => console.log('[KefinTweaks VersionPreferences Config]', ...args); const ERR = (...args) => console.error('[KefinTweaks VersionPreferences Config]', ...args); const MODAL_ID = 'kefin-versionpreferences-config'; const NONE_SENTINEL = 'None'; const DEFAULT_VERSIONS = ['4k', '2160p', '1080p', '720p', '576p', '480p', '360p', NONE_SENTINEL]; const DEFAULT_EDITIONS = ['Theatrical', "Director's Cut", 'Extended Edition', 'Fan Edit', NONE_SENTINEL]; function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function isNoneSentinel(term) { return typeof term === 'string' && term.trim().toLowerCase() === NONE_SENTINEL.toLowerCase(); } function ensureNoneSentinel(list) { const out = []; let noneSeen = false; (Array.isArray(list) ? list : []).forEach((term) => { if (typeof term !== 'string' || !term.trim()) return; if (isNoneSentinel(term)) { if (!noneSeen) { out.push(NONE_SENTINEL); noneSeen = true; } return; } out.push(term.trim()); }); if (!noneSeen) out.push(NONE_SENTINEL); return out; } function getDefaults() { return { versions: DEFAULT_VERSIONS.slice(), editions: DEFAULT_EDITIONS.slice(), showVersionBeforeEdition: false }; } async function loadConfig() { const defaults = getDefaults(); const cfg = window.KefinTweaksConfig?.versionPreferences || {}; return { versions: ensureNoneSentinel( Array.isArray(cfg.versions) ? cfg.versions : defaults.versions ), editions: ensureNoneSentinel( Array.isArray(cfg.editions) ? cfg.editions : defaults.editions ), showVersionBeforeEdition: cfg.showVersionBeforeEdition === true }; } async function saveConfig(versionPreferences) { try { if (!window.KefinTweaksConfig) { window.KefinTweaksConfig = {}; } window.KefinTweaksConfig.versionPreferences = { versions: ensureNoneSentinel(versionPreferences.versions), editions: ensureNoneSentinel(versionPreferences.editions), showVersionBeforeEdition: versionPreferences.showVersionBeforeEdition === true }; if (window.KefinTweaksUtils?.saveConfigToJavaScriptInjector) { await window.KefinTweaksUtils.saveConfigToJavaScriptInjector(); LOG('Configuration saved to JS Injector'); return true; } if (window.KefinTweaksConfiguration?.findJavaScriptInjectorPlugin) { const pluginId = await window.KefinTweaksConfiguration.findJavaScriptInjectorPlugin(); if (pluginId) { await window.KefinTweaksConfiguration.saveConfigToJavaScriptInjector(window.KefinTweaksConfig); LOG('Configuration saved to JS Injector (fallback)'); return true; } } ERR('saveConfigToJavaScriptInjector not available'); return false; } catch (error) { ERR('Error saving config:', error); return false; } } function buildTermRowHTML(term, index, listKey) { const isNone = isNoneSentinel(term); const termCell = isNone ? ` ${escapeHtml(NONE_SENTINEL)} (no match) ` : ``; const removeBtn = isNone ? '' : ``; return `
${index + 1}. ${termCell} ${removeBtn}
`; } function buildListSectionHTML(title, hint, listKey, terms) { return `
${escapeHtml(title)}
${escapeHtml(hint)}
${terms.map((term, i) => buildTermRowHTML(term, i, listKey)).join('')}
`; } function ensureLayoutStyles() { if (document.getElementById('kefin-vp-config-styles')) return; const style = document.createElement('style'); style.id = 'kefin-vp-config-styles'; style.textContent = ` .kefin-vp-lists-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1em; align-items: start; height: calc(100% - 40px); } @media (max-width: 899px) { .kefin-vp-lists-grid { grid-template-columns: 1fr; } } [data-name="kefin-modal-content"]:has(.kefin-vp-lists-grid) > div { height: 100% !important; } .listItem.kefin-vp-list-section { align-items: start; } .kefin-vp-label-order-card { display: flex; align-items: center; gap: 1em; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 0.85em 1em; margin-bottom: 1em; } .kefin-vp-label-order-text { flex: 1; min-width: 0; } .kefin-vp-label-order-card .kefin-vp-toggle-switch { position: relative; width: 52px; height: 28px; border-radius: 14px; border: none; background: rgba(158, 158, 158, 0.5); cursor: pointer; flex-shrink: 0; padding: 0; transition: background-color 0.3s ease; } .kefin-vp-label-order-card .kefin-vp-toggle-switch[data-enabled="true"] { background: rgba(0, 164, 220, 0.8); } .kefin-vp-label-order-card .kefin-vp-toggle-switch::after { content: ""; position: absolute; width: 22px; height: 22px; border-radius: 50%; background: white; top: 3px; left: 3px; transition: left 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .kefin-vp-label-order-card .kefin-vp-toggle-switch[data-enabled="true"]::after { left: calc(100% - 25px); } `; document.head.appendChild(style); } function getLabelOrderDescription(showVersionBeforeEdition) { return showVersionBeforeEdition ? 'Version will be listed before the edition (1080p - Theatrical)' : 'Edition will be listed before the version (Theatrical - 1080p)'; } function buildLabelOrderToggleHTML(showVersionBeforeEdition) { const enabled = showVersionBeforeEdition === true; return `
List Version Before Edition
${escapeHtml(getLabelOrderDescription(enabled))}
`; } function updateLabelOrderToggleUI(root, checked) { const checkbox = root.querySelector('#kefin-vp-show-version-before-edition'); const toggleBtn = root.querySelector('.kefin-vp-toggle-switch'); const desc = root.querySelector('.kefin-vp-label-order-desc'); if (checkbox) checkbox.checked = checked === true; if (toggleBtn) { toggleBtn.dataset.enabled = checked === true; toggleBtn.setAttribute('aria-pressed', String(checked === true)); } if (desc) desc.textContent = getLabelOrderDescription(checked === true); } function buildConfigHTML(config) { return `
Manage how different versions and editions of your items are displayed. Terms are matched as case-insensitive substrings of the media source label. Higher list positions are prioritized first.
${buildLabelOrderToggleHTML(config.showVersionBeforeEdition)}
${buildListSectionHTML( 'Version Terms', 'Resolution / quality labels (e.g. 4k, 1080p).', 'versions', config.versions )} ${buildListSectionHTML( 'Edition Terms', 'Edition labels (e.g. Theatrical, Director\'s Cut).', 'editions', config.editions )}
`; } function readTermsFromDom(root, listKey) { const rows = root.querySelectorAll(`.kefin-vp-term-row[data-list="${listKey}"]`); const terms = []; rows.forEach((row) => { if (row.dataset.isNone === 'true') { terms.push(NONE_SENTINEL); return; } const input = row.querySelector('.kefin-vp-term-input'); const value = input?.value?.trim(); if (value) terms.push(value); }); return ensureNoneSentinel(terms); } function syncStateFromDom(root, state) { state.versions = readTermsFromDom(root, 'versions'); state.editions = readTermsFromDom(root, 'editions'); const checkbox = root.querySelector('#kefin-vp-show-version-before-edition'); state.showVersionBeforeEdition = checkbox?.checked === true; } function refreshList(root, state, listKey) { const listEl = root.querySelector(`.kefin-vp-term-list[data-list="${listKey}"]`); if (!listEl) return; state[listKey] = ensureNoneSentinel(state[listKey] || []); const terms = state[listKey]; listEl.innerHTML = terms.map((term, i) => buildTermRowHTML(term, i, listKey)).join(''); const rows = listEl.querySelectorAll('.kefin-vp-term-row'); rows.forEach((row, i) => { const down = row.querySelector('.kefin-vp-move-down'); if (down) down.disabled = i === rows.length - 1; const up = row.querySelector('.kefin-vp-move-up'); if (up) up.disabled = i === 0; }); } function collectConfig(root) { return { versions: readTermsFromDom(root, 'versions'), editions: readTermsFromDom(root, 'editions'), showVersionBeforeEdition: root.querySelector('#kefin-vp-show-version-before-edition')?.checked === true }; } function bindLabelOrderToggle(root, state) { root.addEventListener('click', (e) => { const toggleBtn = e.target.closest('.kefin-vp-toggle-switch'); if (!toggleBtn || !root.contains(toggleBtn)) return; e.preventDefault(); const checkbox = root.querySelector('#kefin-vp-show-version-before-edition'); if (!checkbox) return; checkbox.checked = !checkbox.checked; state.showVersionBeforeEdition = checkbox.checked; updateLabelOrderToggleUI(root, checkbox.checked); }); } function bindListActions(root, state) { root.addEventListener('click', (e) => { const addBtn = e.target.closest('.kefin-vp-add-btn'); if (addBtn) { const listKey = addBtn.dataset.list; const input = root.querySelector(`.kefin-vp-add-input[data-list="${listKey}"]`); const term = input?.value?.trim(); if (!term) return; if (isNoneSentinel(term)) { window.KefinTweaksToaster?.toast?.('"None" is a built-in priority slot and cannot be added as a term.'); if (input) input.value = ''; return; } syncStateFromDom(root, state); state[listKey].push(term); state[listKey] = ensureNoneSentinel(state[listKey]); if (input) input.value = ''; refreshList(root, state, listKey); return; } const removeBtn = e.target.closest('.kefin-vp-remove'); if (removeBtn) { const listKey = removeBtn.dataset.list; const index = parseInt(removeBtn.dataset.index, 10); syncStateFromDom(root, state); if (Number.isNaN(index)) return; if (isNoneSentinel(state[listKey][index])) return; state[listKey].splice(index, 1); state[listKey] = ensureNoneSentinel(state[listKey]); refreshList(root, state, listKey); return; } const upBtn = e.target.closest('.kefin-vp-move-up'); if (upBtn) { const listKey = upBtn.dataset.list; const index = parseInt(upBtn.dataset.index, 10); syncStateFromDom(root, state); if (Number.isNaN(index) || index <= 0) return; const list = state[listKey]; [list[index - 1], list[index]] = [list[index], list[index - 1]]; refreshList(root, state, listKey); return; } const downBtn = e.target.closest('.kefin-vp-move-down'); if (downBtn) { const listKey = downBtn.dataset.list; const index = parseInt(downBtn.dataset.index, 10); syncStateFromDom(root, state); if (Number.isNaN(index) || index >= state[listKey].length - 1) return; const list = state[listKey]; [list[index], list[index + 1]] = [list[index + 1], list[index]]; refreshList(root, state, listKey); } }); root.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; const addInput = e.target.closest('.kefin-vp-add-input'); if (!addInput) return; e.preventDefault(); const listKey = addInput.dataset.list; const btn = root.querySelector(`.kefin-vp-add-btn[data-list="${listKey}"]`); btn?.click(); }); } async function openConfigModal() { try { ensureLayoutStyles(); const config = await loadConfig(); const state = { versions: config.versions.slice(), editions: config.editions.slice(), showVersionBeforeEdition: config.showVersionBeforeEdition === true }; const content = document.createElement('div'); content.innerHTML = buildConfigHTML(state); bindLabelOrderToggle(content, state); bindListActions(content, state); refreshList(content, state, 'versions'); refreshList(content, state, 'editions'); const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.gap = '0.75em'; footer.style.justifyContent = 'flex-end'; footer.innerHTML = ` `; window.ModalSystem.create({ id: MODAL_ID, title: 'Item Version Preferences', content, footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, dialogStyle: window.innerWidth >= 900 ? { maxWidth: '90vw', width: '1400px', height: '90vh' } : undefined, onOpen: (modal) => { const saveBtn = modal.dialogFooter.querySelector('#save-versionpreferences-config-btn'); saveBtn?.addEventListener('click', async () => { const root = modal.dialogContent || content; syncStateFromDom(root, state); const toSave = collectConfig(root); const success = await saveConfig(toSave); if (success) { window.KefinTweaksToaster?.toast?.('Configuration saved!'); window.ModalSystem.close(MODAL_ID); } else { alert('Error saving configuration. Please ensure the JavaScript Injector plugin is installed and you have administrator permissions.'); } }); } }); LOG('Version Preferences configuration modal opened'); } catch (error) { ERR('Error opening version preferences config modal:', error); alert('Error opening configuration: ' + error.message); } } window.KefinTweaksFeatureConfigs = window.KefinTweaksFeatureConfigs || {}; window.KefinTweaksFeatureConfigs.versionPreferences = { openConfigModal }; console.log('[KefinTweaks VersionPreferences Config] Script loaded'); })();