// KefinTweaks User Manager Configuration UI — User Templates (function () { 'use strict'; const LOG = (...args) => console.log('[KefinTweaks UserManager Config]', ...args); const ERR = (...args) => console.error('[KefinTweaks UserManager Config]', ...args); const WARN = (...args) => console.warn('[KefinTweaks UserManager Config]', ...args); const CONFIG_MODAL_ID = 'kefin-usermanager-config'; const EDITOR_MODAL_ID = 'kefin-usermanager-template-editor'; const TAG_MODAL_ID = 'kefin-usermanager-tag-modal'; const SCHEDULE_MODAL_ID = 'kefin-usermanager-schedule-modal'; const DEFAULT_AUTH = 'Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider'; const DEFAULT_PASSWORD_RESET = 'Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider'; const UNRATED_TYPES = [ { type: 'Book', label: 'Books' }, { type: 'ChannelContent', label: 'Channels' }, { type: 'LiveTvChannel', label: 'Live TV' }, { type: 'Movie', label: 'Movies' }, { type: 'Music', label: 'Music' }, { type: 'Trailer', label: 'Trailers' }, { type: 'Series', label: 'Shows' } ]; const DAYS = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Everyday', 'Weekday', 'Weekend' ]; function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function clone(obj) { return JSON.parse(JSON.stringify(obj ?? {})); } function getSeedPolicy() { return { IsAdministrator: false, IsHidden: false, EnableCollectionManagement: false, EnableSubtitleManagement: false, EnableLyricManagement: false, IsDisabled: false, BlockedTags: [], AllowedTags: [], EnableUserPreferenceAccess: true, AccessSchedules: [], BlockUnratedItems: [], EnableRemoteControlOfOtherUsers: false, EnableSharedDeviceControl: true, EnableRemoteAccess: true, EnableLiveTvManagement: true, EnableLiveTvAccess: true, EnableMediaPlayback: true, EnableAudioPlaybackTranscoding: true, EnableVideoPlaybackTranscoding: true, EnablePlaybackRemuxing: true, ForceRemoteSourceTranscoding: false, EnableContentDeletion: false, EnableContentDeletionFromFolders: [], EnableContentDownloading: true, EnableSyncTranscoding: true, EnableMediaConversion: true, EnabledDevices: [], EnableAllDevices: true, EnabledChannels: [], EnableAllChannels: true, EnabledFolders: [], EnableAllFolders: true, InvalidLoginAttemptCount: 0, LoginAttemptsBeforeLockout: -1, MaxActiveSessions: 0, BlockedMediaFolders: [], BlockedChannels: [], RemoteClientBitrateLimit: 0, AuthenticationProviderId: DEFAULT_AUTH, PasswordResetProviderId: DEFAULT_PASSWORD_RESET, SyncPlayAccess: 'CreateAndJoinGroups' }; } function normalizeConfig(raw) { const cfg = raw && typeof raw === 'object' ? raw : {}; const templates = Array.isArray(cfg.templates) ? cfg.templates.filter((t) => t && typeof t.name === 'string' && t.name.trim()) : []; let defaultTemplateName = typeof cfg.defaultTemplateName === 'string' ? cfg.defaultTemplateName : ''; if (defaultTemplateName && !templates.some((t) => t.name === defaultTemplateName)) { defaultTemplateName = ''; } return { defaultTemplateName, templates }; } function loadConfig() { return normalizeConfig(window.KefinTweaksConfig?.userManager); } async function saveConfig(next) { try { if (!window.KefinTweaksConfig) window.KefinTweaksConfig = {}; window.KefinTweaksConfig.userManager = normalizeConfig(next); if (window.KefinTweaksUtils?.saveConfigToJavaScriptInjector) { await window.KefinTweaksUtils.saveConfigToJavaScriptInjector(); LOG('Configuration saved'); return true; } if (window.KefinTweaksConfiguration?.findJavaScriptInjectorPlugin) { const pluginId = await window.KefinTweaksConfiguration.findJavaScriptInjectorPlugin(); if (pluginId) { await window.KefinTweaksConfiguration.saveConfigToJavaScriptInjector(window.KefinTweaksConfig); LOG('Configuration saved (fallback)'); return true; } } ERR('saveConfigToJavaScriptInjector not available'); return false; } catch (e) { ERR('Error saving config:', e); return false; } } async function fetchMediaFolders() { try { const url = ApiClient.getUrl('Library/MediaFolders'); const data = await ApiClient.getJSON(url); return Array.isArray(data?.Items) ? data.Items : (Array.isArray(data) ? data : []); } catch (e) { WARN('Failed to load media folders', e); return []; } } function buildCheckbox(id, checked, label, options) { return window.KefinTweaksUI?.buildCheckbox?.(id, checked, label, options) || ''; } function hourOptions(selected) { const opts = []; for (let h = 0; h <= 24; h += 0.5) { const hours = Math.floor(h) % 12 || 12; const mins = h % 1 ? '30' : '00'; const ampm = h < 12 || h === 24 ? 'AM' : 'PM'; const label = h === 24 ? '12:00 AM' : `${hours}:${mins} ${ampm}`; opts.push(``); } return opts.join(''); } function formatScheduleTime(h) { const n = Number(h); if (!Number.isFinite(n)) return ''; if (n === 24) return '12:00 AM'; const hours = Math.floor(n) % 12 || 12; const mins = n % 1 ? '30' : '00'; const ampm = n < 12 ? 'AM' : 'PM'; return `${hours}:${mins} ${ampm}`; } function bitrateToMbps(bps) { const n = Number(bps) || 0; if (n <= 0) return ''; return String(Math.round((n / 1000000) * 100) / 100); } function mbpsToBitrate(mbps) { const n = parseFloat(mbps); if (!Number.isFinite(n) || n <= 0) return 0; return Math.round(n * 1000000); } function buildTagListHTML(tags, listClass) { if (!tags?.length) { return `
No tags
`; } return tags.map((tag, i) => `

${escapeHtml(tag)}

`).join(''); } function buildScheduleListHTML(schedules) { if (!schedules?.length) { return `
No access schedules
`; } return schedules.map((s, i) => `

${escapeHtml(s.DayOfWeek)}

${formatScheduleTime(s.StartHour)} - ${formatScheduleTime(s.EndHour)}
`).join(''); } function buildEditorHTML(template, folders) { const policy = { ...getSeedPolicy(), ...(template?.policy || {}) }; const name = template?.name || ''; const enabledFolders = new Set(policy.EnabledFolders || []); const deleteFolders = new Set(policy.EnableContentDeletionFromFolders || []); const blockUnrated = new Set(policy.BlockUnratedItems || []); const allowedTags = policy.AllowedTags || []; const blockedTags = policy.BlockedTags || []; const schedules = policy.AccessSchedules || []; const folderChecks = folders.map((f) => { const id = f.Id || f.id; const label = f.Name || f.name || id; return buildCheckbox('', enabledFolders.has(id), label, { className: 'um-folder', wrapInSection: true, dataAttributes: { id } }); }).join(''); const deleteFolderChecks = folders.map((f) => { const id = f.Id || f.id; const label = f.Name || f.name || id; return buildCheckbox('', deleteFolders.has(id), label, { className: 'um-delete-folder', wrapInSection: true, dataAttributes: { id } }); }).join(''); const unratedChecks = UNRATED_TYPES.map((u) => buildCheckbox('', blockUnrated.has(u.type), u.label, { className: 'um-unrated', wrapInSection: true, dataAttributes: { itemtype: u.type } }) ).join(''); return `

Permissions

${buildCheckbox('', !!policy.EnableRemoteAccess, 'Allow remote connections to this server', { className: 'um-chk-remote', wrapInSection: true })} ${buildCheckbox('', !!policy.IsAdministrator, 'Allow this user to manage the server', { className: 'um-chk-admin', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableCollectionManagement, 'Allow this user to manage collections', { className: 'um-chk-collections', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableSubtitleManagement, 'Allow this user to edit subtitles', { className: 'um-chk-subtitles', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableLyricManagement, 'Allow this user to manage lyrics', { className: 'um-chk-lyrics', wrapInSection: true })}

Feature access

${buildCheckbox('', !!policy.EnableLiveTvAccess, 'Allow Live TV access', { className: 'um-chk-livetv', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableLiveTvManagement, 'Allow Live TV recording management', { className: 'um-chk-livetv-mgmt', wrapInSection: true })}

Media playback

${buildCheckbox('', !!policy.EnableMediaPlayback, 'Allow media playback', { className: 'um-chk-playback', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableAudioPlaybackTranscoding, 'Allow audio playback that requires transcoding', { className: 'um-chk-audio-tc', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableVideoPlaybackTranscoding, 'Allow video playback that requires transcoding', { className: 'um-chk-video-tc', wrapInSection: true })} ${buildCheckbox('', !!policy.EnablePlaybackRemuxing, 'Allow video playback that requires conversion without re-encoding', { className: 'um-chk-remux', wrapInSection: true })} ${buildCheckbox('', !!policy.ForceRemoteSourceTranscoding, 'Force transcoding of remote media sources such as Live TV', { className: 'um-chk-force-remote-tc', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableSyncTranscoding, 'Allow sync transcoding', { className: 'um-chk-sync-tc', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableMediaConversion, 'Allow media conversion', { className: 'um-chk-conversion', wrapInSection: true })}
Leave empty or 0 for no per-user limit.

Allow media deletion from

${buildCheckbox('', !!policy.EnableContentDeletion, 'All libraries', { className: 'um-chk-delete-all', wrapInSection: true })}
${deleteFolderChecks || '
No libraries found
'}

Remote Control

${buildCheckbox('', !!policy.EnableRemoteControlOfOtherUsers, 'Allow remote control of other users', { className: 'um-chk-remote-others', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableSharedDeviceControl, 'Allow remote control of shared devices', { className: 'um-chk-remote-shared', wrapInSection: true })}

Other

${buildCheckbox('', !!policy.EnableContentDownloading, 'Allow media downloads', { className: 'um-chk-download', wrapInSection: true })} ${buildCheckbox('', !!policy.EnableUserPreferenceAccess, 'Allow user preference access', { className: 'um-chk-prefs', wrapInSection: true })} ${buildCheckbox('', !!policy.IsDisabled, 'Disable this user', { className: 'um-chk-disabled', wrapInSection: true })} ${buildCheckbox('', !!policy.IsHidden, 'Hide this user from login screens', { className: 'um-chk-hidden', wrapInSection: true })}
-1 disables lockout. 0 inherits server default.
0 disables the limit.

Library Access

${buildCheckbox('', !!policy.EnableAllFolders, 'Enable access to all libraries', { className: 'um-chk-all-folders', wrapInSection: true })}

Libraries

${folderChecks || '
No libraries found
'}

Block items with no or unrecognized rating information

${unratedChecks}

Allow items with tags

Only show media with at least one of the specified tags.
${buildTagListHTML(allowedTags, 'allowed')}

Block items with tags

Hide media with at least one of the specified tags.
${buildTagListHTML(blockedTags, 'blocked')}

Access Schedule

Limit access to certain hours.

${buildScheduleListHTML(schedules)}
`; } function collectCheckedIds(root, selector) { return Array.from(root.querySelectorAll(`${selector}:checked`)) .map((el) => el.getAttribute('data-id')) .filter(Boolean); } function collectPolicyFromEditor(root, state) { const enableAllFolders = !!root.querySelector('.um-chk-all-folders')?.checked; const enableContentDeletion = !!root.querySelector('.um-chk-delete-all')?.checked; return { ...getSeedPolicy(), IsAdministrator: !!root.querySelector('.um-chk-admin')?.checked, IsHidden: !!root.querySelector('.um-chk-hidden')?.checked, EnableCollectionManagement: !!root.querySelector('.um-chk-collections')?.checked, EnableSubtitleManagement: !!root.querySelector('.um-chk-subtitles')?.checked, EnableLyricManagement: !!root.querySelector('.um-chk-lyrics')?.checked, IsDisabled: !!root.querySelector('.um-chk-disabled')?.checked, BlockedTags: state.blockedTags.slice(), AllowedTags: state.allowedTags.slice(), EnableUserPreferenceAccess: !!root.querySelector('.um-chk-prefs')?.checked, AccessSchedules: state.schedules.map((s) => ({ ...s })), BlockUnratedItems: Array.from(root.querySelectorAll('.um-unrated:checked')) .map((el) => el.getAttribute('data-itemtype')) .filter(Boolean), EnableRemoteControlOfOtherUsers: !!root.querySelector('.um-chk-remote-others')?.checked, EnableSharedDeviceControl: !!root.querySelector('.um-chk-remote-shared')?.checked, EnableRemoteAccess: !!root.querySelector('.um-chk-remote')?.checked, EnableLiveTvManagement: !!root.querySelector('.um-chk-livetv-mgmt')?.checked, EnableLiveTvAccess: !!root.querySelector('.um-chk-livetv')?.checked, EnableMediaPlayback: !!root.querySelector('.um-chk-playback')?.checked, EnableAudioPlaybackTranscoding: !!root.querySelector('.um-chk-audio-tc')?.checked, EnableVideoPlaybackTranscoding: !!root.querySelector('.um-chk-video-tc')?.checked, EnablePlaybackRemuxing: !!root.querySelector('.um-chk-remux')?.checked, ForceRemoteSourceTranscoding: !!root.querySelector('.um-chk-force-remote-tc')?.checked, EnableContentDeletion: enableContentDeletion, EnableContentDeletionFromFolders: enableContentDeletion ? [] : collectCheckedIds(root, '.um-delete-folder'), EnableContentDownloading: !!root.querySelector('.um-chk-download')?.checked, EnableSyncTranscoding: !!root.querySelector('.um-chk-sync-tc')?.checked, EnableMediaConversion: !!root.querySelector('.um-chk-conversion')?.checked, EnabledDevices: [], EnableAllDevices: true, EnabledChannels: [], EnableAllChannels: true, EnabledFolders: enableAllFolders ? [] : collectCheckedIds(root, '.um-folder'), EnableAllFolders: enableAllFolders, LoginAttemptsBeforeLockout: parseInt(root.querySelector('#um-lockout')?.value ?? '-1', 10), MaxActiveSessions: parseInt(root.querySelector('#um-max-sessions')?.value ?? '0', 10), RemoteClientBitrateLimit: mbpsToBitrate(root.querySelector('#um-bitrate')?.value), AuthenticationProviderId: DEFAULT_AUTH, PasswordResetProviderId: DEFAULT_PASSWORD_RESET, SyncPlayAccess: root.querySelector('#um-syncplay')?.value || 'CreateAndJoinGroups' }; } function promptText(title, placeholder) { return new Promise((resolve) => { const content = document.createElement('div'); content.innerHTML = `
`; const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:0.75em;justify-content:flex-end;'; footer.innerHTML = ` `; const modal = window.ModalSystem.create({ id: TAG_MODAL_ID, title, content, footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, dialogStyle: { maxWidth: '420px', width: '90vw' }, onOpen: (m) => { const input = m.dialogContent.querySelector('#um-tag-input'); const finish = (val) => { window.ModalSystem.close(TAG_MODAL_ID); resolve(val); }; m.dialogFooter.querySelector('#um-tag-cancel')?.addEventListener('click', () => finish(null)); m.dialogFooter.querySelector('#um-tag-ok')?.addEventListener('click', () => { finish(input?.value?.trim() || null); }); input?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); finish(input.value.trim() || null); } }); setTimeout(() => input?.focus(), 50); }, onClose: () => resolve(null) }); }); } function promptSchedule() { return new Promise((resolve) => { const dayOpts = DAYS.map((d) => ``).join(''); const content = document.createElement('div'); content.innerHTML = `
`; const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:0.75em;justify-content:flex-end;'; footer.innerHTML = ` `; let settled = false; const finish = (val) => { if (settled) return; settled = true; window.ModalSystem.close(SCHEDULE_MODAL_ID); resolve(val); }; const modal = window.ModalSystem.create({ id: SCHEDULE_MODAL_ID, title: 'Access Schedule', content, footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, dialogStyle: { maxWidth: '420px', width: '90vw' }, onOpen: (m) => { m.dialogFooter.querySelector('#um-sched-cancel')?.addEventListener('click', () => finish(null)); m.dialogFooter.querySelector('#um-sched-ok')?.addEventListener('click', () => { const DayOfWeek = m.dialogContent.querySelector('#um-sched-day')?.value; const StartHour = parseFloat(m.dialogContent.querySelector('#um-sched-start')?.value); const EndHour = parseFloat(m.dialogContent.querySelector('#um-sched-end')?.value); if (!DayOfWeek || !Number.isFinite(StartHour) || !Number.isFinite(EndHour) || EndHour <= StartHour) { window.KefinTweaksToaster?.toast?.('End time must be after start time'); return; } finish({ DayOfWeek, StartHour, EndHour }); }); }, onClose: () => finish(null) }); }); } function refreshTagLists(root, state) { const allowedEl = root.querySelector('.um-allowed-tags'); const blockedEl = root.querySelector('.um-blocked-tags'); if (allowedEl) allowedEl.innerHTML = buildTagListHTML(state.allowedTags, 'allowed'); if (blockedEl) blockedEl.innerHTML = buildTagListHTML(state.blockedTags, 'blocked'); } function refreshSchedules(root, state) { const el = root.querySelector('.um-schedules'); if (el) el.innerHTML = buildScheduleListHTML(state.schedules); } function setCheckboxesChecked(root, selector, checked) { root.querySelectorAll(selector).forEach((el) => { el.checked = checked; if (checked) el.setAttribute('checked', ''); else el.removeAttribute('checked'); }); } function bindEditorInteractions(root, state) { const bindAllLibrariesToggle = (chkSel, listSel, itemSel) => { const chk = root.querySelector(chkSel); const list = root.querySelector(listSel); const sync = () => { const allChecked = !!chk?.checked; if (list) list.style.display = allChecked ? 'none' : ''; // When leaving "all", select every library by default if (!allChecked) setCheckboxesChecked(root, itemSel, true); }; chk?.addEventListener('change', sync); // Initial visibility only — do not force-select on load (preserve saved template) if (list) list.style.display = chk?.checked ? 'none' : ''; }; bindAllLibrariesToggle('.um-chk-all-folders', '.um-folder-list', '.um-folder'); bindAllLibrariesToggle('.um-chk-delete-all', '.um-delete-folders', '.um-delete-folder'); root.addEventListener('click', async (e) => { if (e.target.closest('.um-add-allowed-tag')) { const tag = await promptText('Allow tag', 'Enter a tag'); if (tag && !state.allowedTags.includes(tag)) { state.allowedTags.push(tag); refreshTagLists(root, state); } return; } if (e.target.closest('.um-add-blocked-tag')) { const tag = await promptText('Block tag', 'Enter a tag'); if (tag && !state.blockedTags.includes(tag)) { state.blockedTags.push(tag); refreshTagLists(root, state); } return; } if (e.target.closest('.um-add-schedule')) { const schedule = await promptSchedule(); if (schedule) { state.schedules.push(schedule); refreshSchedules(root, state); } return; } const delTag = e.target.closest('.um-delete-tag'); if (delTag) { const list = delTag.dataset.list; const index = parseInt(delTag.dataset.index, 10); if (list === 'allowed') state.allowedTags.splice(index, 1); if (list === 'blocked') state.blockedTags.splice(index, 1); refreshTagLists(root, state); return; } const delSched = e.target.closest('.um-delete-schedule'); if (delSched) { const index = parseInt(delSched.dataset.index, 10); state.schedules.splice(index, 1); refreshSchedules(root, state); } }); } function buildMainConfigHTML(config) { const options = [``] .concat(config.templates.map((t) => `` )).join(''); const list = config.templates.length ? config.templates.map((t) => `
${escapeHtml(t.name)}
Modified ${escapeHtml(t.dateModified ? new Date(t.dateModified).toLocaleString() : '—')}
`).join('') : `
No templates yet. Create one to get started.
`; return `
Create reusable user policy templates and apply them to new or existing accounts. Import / Export user data will be added here later.
Used as the initial selection on the Add User page.
${list}
`; } async function persistTemplates(mutator) { const config = loadConfig(); mutator(config); const ok = await saveConfig(config); if (!ok) { alert('Error saving configuration. Ensure the JavaScript Injector plugin is installed and you have administrator permissions.'); } return ok; } async function openTemplateEditor(existingTemplate) { const isEdit = !!(existingTemplate && existingTemplate.name); const originalName = isEdit ? existingTemplate.name : null; const folders = await fetchMediaFolders(); const state = { allowedTags: (existingTemplate?.policy?.AllowedTags || []).slice(), blockedTags: (existingTemplate?.policy?.BlockedTags || []).slice(), schedules: clone(existingTemplate?.policy?.AccessSchedules || []) }; const content = document.createElement('div'); content.innerHTML = buildEditorHTML( existingTemplate || { name: '', policy: getSeedPolicy() }, folders ); bindEditorInteractions(content, state); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:0.75em;justify-content:flex-end;'; footer.innerHTML = ` `; window.ModalSystem.create({ id: EDITOR_MODAL_ID, title: isEdit ? `Edit Template: ${existingTemplate.name}` : 'Create Template', content, footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, dialogStyle: window.innerWidth >= 900 ? { maxWidth: '90vw', width: '1400px', height: '90vh' } : undefined, onOpen: (modal) => { modal.dialogFooter.querySelector('#um-editor-cancel')?.addEventListener('click', () => { window.ModalSystem.close(EDITOR_MODAL_ID); }); modal.dialogFooter.querySelector('#um-editor-save')?.addEventListener('click', async () => { const root = modal.dialogContent; const name = root.querySelector('#um-template-name')?.value?.trim(); if (!name) { window.KefinTweaksToaster?.toast?.('Template name is required'); return; } const policy = collectPolicyFromEditor(root, state); const config = loadConfig(); if (!isEdit && config.templates.some((t) => t.name === name)) { window.KefinTweaksToaster?.toast?.('A template with that name already exists'); return; } if (isEdit && name !== originalName && config.templates.some((t) => t.name === name)) { window.KefinTweaksToaster?.toast?.('A template with that name already exists'); return; } const next = { name, dateModified: new Date().toISOString(), policy }; const ok = await persistTemplates((cfg) => { if (isEdit) { const idx = cfg.templates.findIndex((t) => t.name === originalName); if (idx >= 0) cfg.templates[idx] = next; else cfg.templates.push(next); if (cfg.defaultTemplateName === originalName) cfg.defaultTemplateName = name; } else { cfg.templates.push(next); } }); if (ok) { window.KefinTweaksToaster?.toast?.('Template saved'); window.ModalSystem.close(EDITOR_MODAL_ID); refreshMainConfigListIfOpen(); } }); } }); } function refreshMainConfigListIfOpen() { const listRoot = document.querySelector('.um-config-root'); if (!listRoot) return; const wrap = listRoot.parentElement; if (!wrap) return; wrap.innerHTML = buildMainConfigHTML(loadConfig()); bindMainConfigActions(wrap); } function bindMainConfigActions(root) { if (!root) return; const scope = root; scope.querySelector('#um-create-template')?.addEventListener('click', () => { openTemplateEditor(null); }); scope.querySelector('#um-default-template')?.addEventListener('change', async (e) => { const value = e.target.value || ''; await persistTemplates((config) => { config.defaultTemplateName = value; }); }); scope.addEventListener('click', async (e) => { const del = e.target.closest('.um-delete-template'); if (del) { e.stopPropagation(); const name = del.dataset.name; if (!name || !confirm(`Delete template "${name}"?`)) return; const ok = await persistTemplates((config) => { config.templates = config.templates.filter((t) => t.name !== name); if (config.defaultTemplateName === name) config.defaultTemplateName = ''; }); if (ok) refreshMainConfigListIfOpen(); return; } const editBtn = e.target.closest('.um-edit-template'); const row = e.target.closest('.um-template-row'); const target = editBtn || row; if (target) { e.stopPropagation(); const name = target.dataset.name; const template = loadConfig().templates.find((t) => t.name === name); if (template) openTemplateEditor(clone(template)); } }); } async function openConfigModal() { try { const config = loadConfig(); const content = document.createElement('div'); content.innerHTML = buildMainConfigHTML(config); bindMainConfigActions(content); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:0.75em;justify-content:flex-end;'; footer.innerHTML = ``; window.ModalSystem.create({ id: CONFIG_MODAL_ID, title: 'User Manager', content, footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, dialogStyle: window.innerWidth >= 900 ? { maxWidth: '90vw', width: '1400px', height: '90vh' } : undefined }); LOG('User Manager config opened'); } catch (e) { ERR('Error opening User Manager config', e); alert('Error opening configuration: ' + e.message); } } window.KefinTweaksFeatureConfigs = window.KefinTweaksFeatureConfigs || {}; window.KefinTweaksFeatureConfigs.userManager = { openConfigModal, openTemplateEditor, loadConfig, getSeedPolicy, clonePolicy: clone }; // Shared helpers for runtime script window.KefinUserManagerConfig = { loadConfig, getSeedPolicy, clonePolicy: clone, openTemplateEditor, openConfigModal }; console.log('[KefinTweaks UserManager Config] Script loaded'); })();