Update script dependencies, add Create Backup option, add Prompt to Update

This commit is contained in:
HighImKevin committed 2026-10-16 03:16:55 -04:00
1 parent 4fbece6369
commit 41734e0085
7 files changed
+486 -25

No files matched your search

+66
View File
@@ -4944,6 +4944,7 @@
`).join('');
return `
<div style="display:flex;flex-wrap:wrap;gap:0.5em;align-items:center;margin-bottom:0.75em;">
<button type="button" class="emby-button raised" id="manageBackupsCreateBtn" style="padding:0.4em 1em;">Create</button>
<button type="button" class="emby-button raised" id="manageBackupsSelectAllBtn" style="padding:0.4em 1em;">Select all</button>
<button type="button" class="emby-button raised" id="manageBackupsDeselectAllBtn" style="padding:0.4em 1em;">Deselect all</button>
<button type="button" class="emby-button raised" id="manageBackupsDeleteSelectedBtn" style="padding:0.4em 1em;" disabled>Delete selected</button>
@@ -4989,6 +4990,54 @@
return confirmDeleteBackups([name]);
}
function confirmCreateBackup() {
return new Promise((resolve) => {
if (!window.ModalSystem) {
const label = window.prompt('Optional backup name (leave blank for default):', '') || '';
resolve(window.confirm('Create a configuration backup now?')
? { create: true, label: String(label).trim() }
: { create: false });
return;
}
window.ModalSystem.create({
id: 'manageCreateBackupConfirm',
title: 'Create Backup',
content: `
<div class="listItemBodyText" style="margin-bottom:1em;">
Create a backup of your current KefinTweaks configuration?
</div>
<label class="listItemBodyText" for="manageCreateBackupName" style="display:block;margin-bottom:0.35em;">
Optional name
</label>
<input type="text" id="manageCreateBackupName" class="fld emby-input" placeholder="e.g. PreRelease"
style="width:100%;" autocomplete="off">
<div class="listItemBodyText secondary" style="margin-top:0.35em;">
When set, the backup is named KefinTweaks-Config-Backup-[Name]-Timestamp. Leave blank for the default name.
</div>
`,
footer: `
<button class="emby-button raised button-submit" id="manageCreateBackupConfirmBtn" style="padding:0.75em 2em;margin-right:1em;">Create</button>
<button class="emby-button raised" id="manageCreateBackupCancelBtn" style="padding:0.75em 2em;">Cancel</button>
`,
closeOnBackdrop: true,
closeOnEscape: true,
onOpen: (mi) => {
const input = mi.dialogContent?.querySelector('#manageCreateBackupName');
setTimeout(() => input?.focus(), 100);
mi.dialogFooter?.querySelector('#manageCreateBackupConfirmBtn')?.addEventListener('click', () => {
const label = (input?.value || '').trim();
mi.close();
resolve({ create: true, label });
});
mi.dialogFooter?.querySelector('#manageCreateBackupCancelBtn')?.addEventListener('click', () => {
mi.close();
resolve({ create: false });
});
}
});
});
}
function confirmDeleteBackups(names) {
const list = (Array.isArray(names) ? names : []).filter(Boolean);
return new Promise((resolve) => {
@@ -5147,6 +5196,7 @@
});
tableHost?.addEventListener('click', async (e) => {
const createBtn = e.target.closest?.('#manageBackupsCreateBtn');
const selectAllBtn = e.target.closest?.('#manageBackupsSelectAllBtn');
const deselectAllBtn = e.target.closest?.('#manageBackupsDeselectAllBtn');
const deleteSelectedBtn = e.target.closest?.('#manageBackupsDeleteSelectedBtn');
@@ -5175,6 +5225,22 @@
syncSelectionUi();
};
if (createBtn) {
const choice = await confirmCreateBackup();
if (!choice?.create) return;
const result = await window.KefinTweaksUtils?.createKefinConfigBackup?.(config, {
reason: 'manual',
kind: 'manual',
label: choice.label || ''
});
if (result?.ok) {
toastMsg(`Backup created: ${result.name}`);
await refreshManageBackupsTable(tableHost);
} else {
toastMsg('Failed to create backup', '5');
}
return;
}
if (selectAllBtn) {
setAllChecked(true);
return;
+3 -10
View File
@@ -257,13 +257,6 @@
dependencies: ['moviesCache', 'seriesCache'],
description: 'Library cache facade for KefinTweaks'
},
{
name: 'externalList',
script: 'externalList.js',
css: null,
dependencies: ['libraryCache', 'libraryCacheUtils', 'apiHelper'],
description: 'MDBList external list matching for home sections'
},
{
name: 'skinManager',
script: 'skinManager.js',
@@ -290,7 +283,7 @@
name: 'cardBuilder',
script: 'cardBuilder.js',
css: 'cardBuilder.css',
dependencies: ['apiHelper', 'userHelper'],
dependencies: ['apiHelper', 'userHelper', 'websocketHelper'],
description: 'Core card building functionality (required by other scripts)'
},
{
@@ -388,7 +381,7 @@
name: 'watchlist',
script: 'watchlist.js',
css: 'watchlist.css',
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics', 'watchlist-configuration'],
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics', 'watchlist-configuration', 'websocketHelper'],
thirdParty: ['chartjs'],
description: 'Adds watchlist functionality throughout Jellyfin interface'
},
@@ -396,7 +389,7 @@
name: 'homeScreen',
script: 'homeScreen3.js',
css: 'homeScreen.css',
dependencies: ['cardBuilder', 'localStorageCache', 'utils', 'userHelper', 'homeScreenConfig2', 'homeScreen-configuration', 'peopleCache', 'studiosCache', 'moviesCache', 'seriesCache', 'libraryCache', 'libraryCacheUtils', 'externalList', 'indexedDBCache', 'homeScreenConfigCommunity', 'dataHelper', 'apiHelper', 'sectionHelper', 'homeScreen-migration', 'homeScreen-user-configuration', 'homeScreenSectionConfigure', 'homeScreenPin'],
dependencies: ['cardBuilder', 'localStorageCache', 'utils', 'userHelper', 'homeScreenConfig2', 'homeScreen-configuration', 'peopleCache', 'studiosCache', 'moviesCache', 'seriesCache', 'libraryCache', 'libraryCacheUtils', 'indexedDBCache', 'homeScreenConfigCommunity', 'dataHelper', 'apiHelper', 'sectionHelper', 'homeScreen-migration', 'homeScreen-user-configuration', 'homeScreenSectionConfigure', 'homeScreenPin'],
priority: true, // Load immediately after dependencies to reduce UI disruption
description: 'Adds custom home screen sections'
},
+59 -2
View File
@@ -56,6 +56,35 @@
.toast.kefin-toast-rich .kefin-toast-action .material-icons {
font-size: 1.25rem;
}
.toast.kefin-toast-rich.kefin-toast-with-buttons {
flex-wrap: wrap;
align-items: flex-start;
}
.toast.kefin-toast-rich .kefin-toast-buttons {
flex: 1 1 100%;
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.15em;
}
.toast.kefin-toast-rich .kefin-toast-text-btn {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.35em 0.9em;
margin: 0;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 4px;
background: rgba(255, 255, 255, 0.08);
color: inherit;
font: inherit;
font-weight: 500;
cursor: pointer;
}
.toast.kefin-toast-rich .kefin-toast-text-btn:hover {
background: rgba(255, 255, 255, 0.16);
}
`;
document.head.appendChild(style);
}
@@ -119,7 +148,10 @@
function buildToastContent(toastElement, message, options) {
const link = options?.link;
const action = options?.action;
const rich = !!(link || action);
const buttons = Array.isArray(options?.buttons)
? options.buttons.filter((b) => b && typeof b.label === 'string' && typeof b.onClick === 'function')
: [];
const rich = !!(link || action || buttons.length);
if (!rich) {
toastElement.textContent = message;
return;
@@ -127,6 +159,9 @@
ensureToastStyles();
toastElement.classList.add('kefin-toast-rich');
if (buttons.length) {
toastElement.classList.add('kefin-toast-with-buttons');
}
const messageEl = document.createElement('span');
messageEl.className = 'kefin-toast-message';
@@ -202,6 +237,28 @@
});
toastElement.appendChild(btn);
}
if (buttons.length) {
const row = document.createElement('div');
row.className = 'kefin-toast-buttons';
buttons.forEach((spec) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'kefin-toast-text-btn';
btn.textContent = spec.label;
btn.addEventListener('click', (ev) => {
ev.preventDefault();
ev.stopPropagation();
try {
spec.onClick();
} catch (e) {
ERR('Toast button action failed:', e);
}
});
row.appendChild(btn);
});
toastElement.appendChild(row);
}
}
/**
@@ -209,7 +266,7 @@
* @param {string} message - The message to display
* @param {string|number} duration - Duration in seconds
* @param {boolean} autoHide - Whether to automatically hide the toast after duration (default: true)
* @param {{ link?: { label: string, href?: string, onClick?: Function }, action?: { icon?: string, label?: string, onClick: Function } }} [options]
* @param {{ link?: { label: string, href?: string, onClick?: Function }, action?: { icon?: string, label?: string, onClick: Function }, buttons?: Array<{ label: string, onClick: Function }> }} [options]
* @returns {{ dismiss: Function, element: HTMLElement }|undefined}
*/
function toast(message, duration = '3', autoHide = true, options = null) {
+44 -6
View File
@@ -2500,6 +2500,27 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`;
}
}
function padBackupTimePart(n) {
return String(n).padStart(2, '0');
}
/** Local wall-clock stamp for backup filenames (matches toLocaleString calendar day). */
function formatBackupTimestampLocal(date = new Date()) {
let d = date instanceof Date ? date : new Date(date);
if (Number.isNaN(d.getTime())) d = new Date();
return [
d.getFullYear(),
padBackupTimePart(d.getMonth() + 1),
padBackupTimePart(d.getDate())
].join('-')
+ 'T'
+ [
padBackupTimePart(d.getHours()),
padBackupTimePart(d.getMinutes()),
padBackupTimePart(d.getSeconds())
].join('-');
}
function parseBackupTimestamp(name, script) {
const createdMatch = String(script || '').match(/Created:\s*(\d{4}-\d{2}-\d{2}T[^\s\n]+)/i);
if (createdMatch) {
@@ -2508,8 +2529,9 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`;
}
const nameMatch = String(name || '').match(/(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/);
if (nameMatch) {
const iso = nameMatch[1].replace(/T(\d{2})-(\d{2})-(\d{2})/, 'T$1:$2:$3') + 'Z';
const d = new Date(iso);
// Filenames use local wall-clock; do not append Z (that caused UTC→local day skew).
const localIso = nameMatch[1].replace(/T(\d{2})-(\d{2})-(\d{2})/, 'T$1:$2:$3');
const d = new Date(localIso);
if (!Number.isNaN(d.getTime())) return d;
}
return null;
@@ -2524,11 +2546,27 @@ ${reasonLine}// Created: ${new Date().toISOString()}
window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
}
function makeBackupName(kind = 'manual') {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
function sanitizeBackupLabel(label) {
let s = String(label || '').trim();
if (!s) return '';
s = s
.replace(/[\\/<>:"|?*\u0000-\u001f;]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
if (s.length > 48) s = s.slice(0, 48).replace(/-+$/g, '');
return s;
}
function makeBackupName(kind = 'manual', options = {}) {
const timestamp = formatBackupTimestampLocal(new Date());
const label = sanitizeBackupLabel(options.label || options.name || '');
if (kind === 'scheduled') {
return `KefinTweaks-Config-ScheduledBackup-${timestamp}`;
}
if (label) {
return `KefinTweaks-Config-Backup-${label}-${timestamp}`;
}
return `KefinTweaks-Config-Backup-${timestamp}`;
}
@@ -2601,7 +2639,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
}
async function createKefinConfigBackup(config = null, options = {}) {
const { reason = 'manual', kind } = options;
const { reason = 'manual', kind, label, name } = options;
try {
const configToBackup = config || window.KefinTweaksConfig;
if (!configToBackup) throw new Error('No config available to backup');
@@ -2609,7 +2647,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
const backupKind = kind
|| (reason === 'scheduled-daily' ? 'scheduled' : 'manual');
const { pluginId, injectorConfig } = await fetchInjectorConfigForBackups();
const backupName = makeBackupName(backupKind);
const backupName = makeBackupName(backupKind, { label: label || name });
injectorConfig.CustomJavaScripts.push({
Name: backupName,
Script: buildConfigBackupScript(configToBackup, reason),