mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Update script dependencies, add Create Backup option, add Prompt to Update
This commit is contained in:
1 parent
4fbece6369
commit
41734e0085
7 files changed
+486
-25
No files matched your search
+306
-3
@@ -426,7 +426,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)'
|
||||
},
|
||||
{
|
||||
@@ -513,11 +513,18 @@
|
||||
dependencies: [],
|
||||
description: 'Watchlist statistics charts and activity analytics'
|
||||
},
|
||||
{
|
||||
name: 'watchlist-configuration',
|
||||
script: '../configuration/watchlist-configuration.js',
|
||||
css: null,
|
||||
dependencies: ['modal', 'ui'],
|
||||
description: 'Configuration UI for Watchlist menu placements'
|
||||
},
|
||||
{
|
||||
name: 'watchlist',
|
||||
script: 'watchlist.js',
|
||||
css: 'watchlist.css',
|
||||
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics'],
|
||||
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics', 'watchlist-configuration', 'websocketHelper'],
|
||||
thirdParty: ['chartjs'],
|
||||
description: 'Adds watchlist functionality throughout Jellyfin interface'
|
||||
},
|
||||
@@ -1157,7 +1164,7 @@
|
||||
}
|
||||
|
||||
async function loadConfigurationJS() {
|
||||
const configDependencyNames = ['modal', 'toaster', 'utils', 'homeScreenConfig2', 'ui', 'homeScreen-migration', 'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration', 'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration', 'customMenuLinks-configuration', 'thumbnailScrubber-configuration', 'watchTogether-configuration', 'userManager-configuration', 'apiHelper', 'sectionHelper'];
|
||||
const configDependencyNames = ['modal', 'toaster', 'utils', 'homeScreenConfig2', 'ui', 'homeScreen-migration', 'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration', 'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration', 'customMenuLinks-configuration', 'thumbnailScrubber-configuration', 'watchTogether-configuration', 'watchlist-configuration', 'userManager-configuration', 'apiHelper', 'sectionHelper'];
|
||||
for (const depName of configDependencyNames) {
|
||||
const depScript = SCRIPT_DEFINITIONS.find(script => script.name === depName);
|
||||
if (depScript) {
|
||||
@@ -1507,6 +1514,300 @@
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeReleaseTag(tag) {
|
||||
const t = String(tag || '').trim();
|
||||
if (!t) return '';
|
||||
return t.startsWith('v') || t.startsWith('V') ? `v${t.slice(1)}` : `v${t}`;
|
||||
}
|
||||
|
||||
function normalizeUpdateState(config) {
|
||||
if (!config || typeof config !== 'object') return { dismissed: true, latestVersion: '' };
|
||||
if (!config.updateState || typeof config.updateState !== 'object') {
|
||||
config.updateState = { dismissed: true, latestVersion: '' };
|
||||
} else {
|
||||
if (typeof config.updateState.dismissed !== 'boolean') {
|
||||
config.updateState.dismissed = true;
|
||||
}
|
||||
if (typeof config.updateState.latestVersion !== 'string') {
|
||||
config.updateState.latestVersion = '';
|
||||
} else {
|
||||
config.updateState.latestVersion = normalizeReleaseTag(config.updateState.latestVersion)
|
||||
|| config.updateState.latestVersion;
|
||||
}
|
||||
}
|
||||
return config.updateState;
|
||||
}
|
||||
|
||||
function isGithubJsdelivrKefinRoot(root) {
|
||||
return /cdn\.jsdelivr\.net\/gh\/ranaldsgift\/KefinTweaks@/i.test(String(root || ''));
|
||||
}
|
||||
|
||||
function isSymbolicLatestRoot(root) {
|
||||
return /@latest(\/|$)/i.test(String(root || ''));
|
||||
}
|
||||
|
||||
function parseGithubRootRef(root) {
|
||||
const s = String(root || '');
|
||||
const floating = s.match(/@(latest|main|master|experimental)(\/|$)/i);
|
||||
if (floating) {
|
||||
const name = floating[1].toLowerCase();
|
||||
return { kind: name === 'master' ? 'main' : name, value: name === 'master' ? 'main' : name };
|
||||
}
|
||||
const semver = s.match(/@(v?\d+\.\d+\.\d+)(\/|$)/i);
|
||||
if (semver) {
|
||||
return { kind: 'semver', value: normalizeReleaseTag(semver[1]) };
|
||||
}
|
||||
const sha = s.match(/@([0-9a-f]{40})(\/|$)/i);
|
||||
if (sha) {
|
||||
return { kind: 'sha', value: sha[1].toLowerCase() };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseShaFromRoot(root) {
|
||||
const m = String(root || '').match(/@([0-9a-f]{40})(\/|$)/i);
|
||||
return m ? m[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
async function waitForGlobal(getter, timeoutMs = 15000, intervalMs = 250) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const value = getter();
|
||||
if (value) return value;
|
||||
} catch (_) { /* ignore */ }
|
||||
await new Promise((r) => setTimeout(r, intervalMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function persistConfigFromStartup(config) {
|
||||
const saveFn = await waitForGlobal(
|
||||
() => window.KefinTweaksUtils?.saveConfigToJavaScriptInjector,
|
||||
20000
|
||||
);
|
||||
if (!saveFn) {
|
||||
console.warn('[KefinTweaks Startup] saveConfigToJavaScriptInjector unavailable; updateState not persisted');
|
||||
return false;
|
||||
}
|
||||
window.KefinTweaksConfig = config;
|
||||
try {
|
||||
return !!(await saveFn(config));
|
||||
} catch (e) {
|
||||
console.warn('[KefinTweaks Startup] Failed to persist config:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGithubJson(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function fetchLatestGithubRelease() {
|
||||
const data = await fetchGithubJson('https://api.github.com/repos/ranaldsgift/KefinTweaks/releases/latest');
|
||||
if (!data?.tag_name) return null;
|
||||
const publishedAt = data.published_at ? new Date(data.published_at) : null;
|
||||
if (!publishedAt || Number.isNaN(publishedAt.getTime())) return null;
|
||||
return {
|
||||
tag: normalizeReleaseTag(data.tag_name),
|
||||
publishedAt
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchReleasePublishedAt(tag) {
|
||||
const normalized = normalizeReleaseTag(tag);
|
||||
if (!normalized) return null;
|
||||
const data = await fetchGithubJson(
|
||||
`https://api.github.com/repos/ranaldsgift/KefinTweaks/releases/tags/${encodeURIComponent(normalized)}`
|
||||
);
|
||||
if (!data?.published_at) return null;
|
||||
const d = new Date(data.published_at);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
async function fetchCommitDate(shaOrRef) {
|
||||
if (!shaOrRef) return null;
|
||||
const data = await fetchGithubJson(
|
||||
`https://api.github.com/repos/ranaldsgift/KefinTweaks/commits/${encodeURIComponent(shaOrRef)}`
|
||||
);
|
||||
const iso = data?.commit?.committer?.date || data?.commit?.author?.date;
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
async function resolveInstalledCommitDate(root, config, branchName) {
|
||||
const resolvedSha = parseShaFromRoot(config?.kefinTweaksRootResolved)
|
||||
|| parseShaFromRoot(resolvedRootCache);
|
||||
if (resolvedSha) {
|
||||
const fromResolved = await fetchCommitDate(resolvedSha);
|
||||
if (fromResolved) return fromResolved;
|
||||
}
|
||||
return fetchCommitDate(branchName || 'main');
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the install should treat GitHub Latest as a "new update" vs current pin/tip.
|
||||
*/
|
||||
async function isGithubInstallBehindLatest(root, config, latest) {
|
||||
const ref = parseGithubRootRef(root);
|
||||
if (!ref) return true;
|
||||
|
||||
if (ref.kind === 'semver') {
|
||||
if (ref.value === latest.tag) return false;
|
||||
const installedPublished = await fetchReleasePublishedAt(ref.value);
|
||||
if (!installedPublished) return true;
|
||||
return installedPublished.getTime() < latest.publishedAt.getTime();
|
||||
}
|
||||
|
||||
if (ref.kind === 'sha') {
|
||||
const commitDate = await fetchCommitDate(ref.value);
|
||||
if (!commitDate) return true;
|
||||
return commitDate.getTime() < latest.publishedAt.getTime();
|
||||
}
|
||||
|
||||
if (ref.kind === 'main' || ref.kind === 'experimental') {
|
||||
const tipDate = await resolveInstalledCommitDate(root, config, ref.kind);
|
||||
if (!tipDate) return true;
|
||||
// Tip newer than Latest ⇒ following branch tip, not a release-update prompt
|
||||
if (tipDate.getTime() >= latest.publishedAt.getTime()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildConcreteLatestRoot(latestTag) {
|
||||
const tag = normalizeReleaseTag(latestTag);
|
||||
return `https://cdn.jsdelivr.net/gh/ranaldsgift/KefinTweaks@${tag}/`;
|
||||
}
|
||||
|
||||
async function showPinnedVersionUpdateToast(config, latestTag) {
|
||||
const toastFn = await waitForGlobal(() => window.KefinTweaksToaster?.toast, 20000);
|
||||
if (!toastFn) {
|
||||
console.warn('[KefinTweaks Startup] Toaster unavailable; skipping update toast');
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = normalizeReleaseTag(latestTag);
|
||||
const githubInstall = isGithubJsdelivrKefinRoot(config?.kefinTweaksRoot);
|
||||
|
||||
const dismissAndSave = async () => {
|
||||
normalizeUpdateState(config);
|
||||
config.updateState.dismissed = true;
|
||||
if (tag) config.updateState.latestVersion = tag;
|
||||
await persistConfigFromStartup(config);
|
||||
};
|
||||
|
||||
const handle = toastFn(
|
||||
'An update for KefinTweaks is available.',
|
||||
'0',
|
||||
false,
|
||||
{
|
||||
buttons: [
|
||||
{
|
||||
label: 'Update',
|
||||
onClick: async () => {
|
||||
try {
|
||||
normalizeUpdateState(config);
|
||||
config.updateState.dismissed = true;
|
||||
if (tag) config.updateState.latestVersion = tag;
|
||||
|
||||
if (githubInstall && tag) {
|
||||
const nextRoot = buildConcreteLatestRoot(tag);
|
||||
config.kefinTweaksRoot = nextRoot;
|
||||
config.kefinTweaksRootResolved = nextRoot;
|
||||
await persistConfigFromStartup(config);
|
||||
handle?.dismiss?.();
|
||||
toastFn(
|
||||
'KefinTweaks updated. Please refresh the page for the changes to take effect.',
|
||||
'8'
|
||||
);
|
||||
} else {
|
||||
await persistConfigFromStartup(config);
|
||||
handle?.dismiss?.();
|
||||
const url = tag
|
||||
? `https://github.com/ranaldsgift/KefinTweaks/releases/tag/${encodeURIComponent(tag)}`
|
||||
: 'https://github.com/ranaldsgift/KefinTweaks/releases/latest';
|
||||
try {
|
||||
window.open(url, '_blank', 'noopener');
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[KefinTweaks Startup] Update action failed:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Dismiss',
|
||||
onClick: async () => {
|
||||
try {
|
||||
await dismissAndSave();
|
||||
handle?.dismiss?.();
|
||||
} catch (e) {
|
||||
console.error('[KefinTweaks Startup] Dismiss action failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function checkPinnedVersionUpdate() {
|
||||
const config = window.KefinTweaksConfig;
|
||||
const root = config?.kefinTweaksRoot;
|
||||
if (!root) return;
|
||||
|
||||
// Floating @latest already auto-resolves to GitHub Latest — skip this prompt flow
|
||||
if (isSymbolicLatestRoot(root)) {
|
||||
normalizeUpdateState(config);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = normalizeUpdateState(config);
|
||||
let latest;
|
||||
try {
|
||||
latest = await fetchLatestGithubRelease();
|
||||
} catch (e) {
|
||||
console.warn('[KefinTweaks Startup] Failed to fetch latest release:', e);
|
||||
return;
|
||||
}
|
||||
if (!latest?.tag) return;
|
||||
|
||||
const known = normalizeReleaseTag(state.latestVersion);
|
||||
if (known !== latest.tag) {
|
||||
let markNew = false;
|
||||
if (isGithubJsdelivrKefinRoot(root)) {
|
||||
try {
|
||||
markNew = await isGithubInstallBehindLatest(root, config, latest);
|
||||
} catch (e) {
|
||||
console.warn('[KefinTweaks Startup] Install-vs-latest compare failed; treating as new:', e);
|
||||
markNew = true;
|
||||
}
|
||||
} else {
|
||||
// Self-hosted / custom root
|
||||
markNew = true;
|
||||
}
|
||||
|
||||
if (markNew) {
|
||||
state.dismissed = false;
|
||||
state.latestVersion = latest.tag;
|
||||
await persistConfigFromStartup(config);
|
||||
} else {
|
||||
// Same or newer install relative to Latest — remember tag without prompting
|
||||
state.latestVersion = latest.tag;
|
||||
await persistConfigFromStartup(config);
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.dismissed) {
|
||||
await showPinnedVersionUpdateToast(config, latest.tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Startup task - runs only for admin users
|
||||
async function startupTask() {
|
||||
console.log('[KefinTweaks Startup] Starting startup task...');
|
||||
@@ -1528,6 +1829,8 @@
|
||||
try {
|
||||
// Fire-and-forget: remove leftover Watchlist Custom Tab (best-effort)
|
||||
//removeWatchlistFromCustomTabPlugin();
|
||||
|
||||
await checkPinnedVersionUpdate();
|
||||
|
||||
console.log('[KefinTweaks Startup] Startup task completed successfully');
|
||||
} catch (error) {
|
||||
|
||||
@@ -64,5 +64,9 @@ window.KefinTweaksDefaultConfig = {
|
||||
"defaultSkin": null,
|
||||
"themes": [],
|
||||
"customMenuLinks": [],
|
||||
"optionalIncludes": []
|
||||
"optionalIncludes": [],
|
||||
"updateState": {
|
||||
"dismissed": true,
|
||||
"latestVersion": ""
|
||||
}
|
||||
};
|
||||
@@ -1006,7 +1006,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
}
|
||||
|
||||
resolvedHashEl.style.display = 'block';
|
||||
resolvedHashEl.textContent = 'Resolved commit: …';
|
||||
resolvedHashEl.textContent = 'Version: …';
|
||||
|
||||
let sha = extractShaFromRoot(currentConfig?.kefinTweaksRootResolved);
|
||||
const symbolicMatchesSelection =
|
||||
@@ -1029,9 +1029,9 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
|
||||
if (sha) {
|
||||
const short = sha.length > 12 ? sha.slice(0, 12) : sha;
|
||||
resolvedHashEl.textContent = `Resolved commit: ${short}`;
|
||||
resolvedHashEl.textContent = `Version: ${short}`;
|
||||
} else {
|
||||
resolvedHashEl.textContent = 'Resolved commit: unavailable';
|
||||
resolvedHashEl.textContent = 'Version: unavailable';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user