Allow loading specific KefinTweaks builds and most custom page styles to kefinTweaks-plugin.js only

This commit is contained in:
HighImKevin
2026-10-05 00:39:00 -04:00
parent 809aea7f30
commit 1b2de91d2d
2 changed files with 89 additions and 77 deletions
+82 -14
View File
@@ -431,8 +431,13 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
const LATEST_RELEASE_NAME = 'Latest';
const DEVELOPMENT_NAME = 'Development';
const EXPERIMENTAL_NAME = 'Experimental';
const SPECIFIC_BUILD_NAME = 'Specific Build';
const SELF_HOSTED_NAME = 'Self Hosted';
function isFullCommitSha(value) {
return typeof value === 'string' && /^[0-9a-f]{40}$/i.test(value.trim());
}
// Parse source URL to determine source type and version
function parseKefinTweaksSource(url) {
if (!url || url === '') {
@@ -452,9 +457,8 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
return { sourceType: 'github', version: DEVELOPMENT_NAME };
} else if (version === 'experimental') {
return { sourceType: 'github', version: EXPERIMENTAL_NAME };
} else if (version && version.length === 40 && /^[0-9a-f]+$/.test(version)) {
// Commit hash URL (e.g. from a previous Experimental selection)
return { sourceType: 'github', version: EXPERIMENTAL_NAME };
} else if (isFullCommitSha(version)) {
return { sourceType: 'github', version: SPECIFIC_BUILD_NAME, commit: version.toLowerCase() };
} else {
return { sourceType: 'github', version: version.replace(/^v/, '') };
}
@@ -477,16 +481,16 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
.map(release => release.tag_name.replace(/^v/, ''))
.filter(tag => tag.match(/^\d+\.\d+\.\d+/)); // Only semantic versions
versionsCache = [LATEST_RELEASE_NAME, DEVELOPMENT_NAME, EXPERIMENTAL_NAME, ...versions];
versionsCache = [LATEST_RELEASE_NAME, DEVELOPMENT_NAME, EXPERIMENTAL_NAME, SPECIFIC_BUILD_NAME, ...versions];
return versionsCache;
} catch (error) {
console.warn('[KefinTweaks Installer] Error fetching versions:', error);
return [LATEST_RELEASE_NAME, DEVELOPMENT_NAME, EXPERIMENTAL_NAME];
return [LATEST_RELEASE_NAME, DEVELOPMENT_NAME, EXPERIMENTAL_NAME, SPECIFIC_BUILD_NAME];
}
}
// Build kefinTweaksRoot URL from selections
function buildKefinTweaksRootUrl(sourceType, source) {
function buildKefinTweaksRootUrl(sourceType, source, commitHash) {
if (sourceType === 'custom') {
// Ensure URL ends with /
return source.endsWith('/') ? source : source + '/';
@@ -498,6 +502,12 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
versionTag = 'main';
} else if (source === EXPERIMENTAL_NAME) {
versionTag = 'experimental';
} else if (source === SPECIFIC_BUILD_NAME) {
const sha = String(commitHash || '').trim().toLowerCase();
if (!isFullCommitSha(sha)) {
throw new Error('Specific Build requires a full 40-character commit hash');
}
versionTag = sha;
} else if (source !== LATEST_RELEASE_NAME) {
versionTag = source.startsWith('v') ? source : 'v' + source;
}
@@ -505,6 +515,13 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
return `${JSDELIVR_BASE}@${versionTag}/`;
}
function formatSpecificBuildDisplay(sourceInfo) {
const sha = sourceInfo?.commit || '';
return sha
? `${SPECIFIC_BUILD_NAME} (#${sha.slice(0, 7)})`
: SPECIFIC_BUILD_NAME;
}
// Simple modal creation (doesn't depend on modal.js)
function createSimpleModal(id, title, content, footer) {
// Remove existing modal
@@ -860,6 +877,29 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
customUrlInput.value = sourceInfo.sourceType === 'custom' ? currentRoot : '';
customUrlInput.style.display = sourceInfo.sourceType === 'custom' ? 'block' : 'none';
// Specific Build commit hash input
const commitHashLabel = document.createElement('label');
commitHashLabel.textContent = 'Commit Hash:';
commitHashLabel.style.display = 'block';
commitHashLabel.style.marginBottom = '0.5em';
commitHashLabel.style.marginTop = '1em';
const commitHashInput = document.createElement('input');
commitHashInput.type = 'text';
commitHashInput.className = 'fld emby-input';
commitHashInput.id = 'kefinTweaksCommitHash';
commitHashInput.placeholder = '40-character git commit SHA';
commitHashInput.spellcheck = false;
commitHashInput.autocomplete = 'off';
commitHashInput.value = sourceInfo.commit || '';
const updateCommitHashVisibility = () => {
const showCommit = sourceTypeSelect.value === 'github'
&& sourceSelect.value === SPECIFIC_BUILD_NAME;
commitHashLabel.style.display = showCommit ? 'block' : 'none';
commitHashInput.style.display = showCommit ? 'block' : 'none';
};
// Set initial visibility based on source type
if (sourceInfo.sourceType === 'custom') {
sourceSelect.style.display = 'none';
@@ -870,6 +910,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
sourceLabel.style.display = 'block';
customUrlInput.style.display = 'none';
}
updateCommitHashVisibility();
// Toggle visibility based on source type
sourceTypeSelect.onchange = () => {
@@ -882,12 +923,19 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
sourceLabel.style.display = 'block';
customUrlInput.style.display = 'none';
}
updateCommitHashVisibility();
};
sourceSelect.onchange = () => {
updateCommitHashVisibility();
};
content.appendChild(sourceTypeLabel);
content.appendChild(sourceTypeSelect);
content.appendChild(sourceLabel);
content.appendChild(sourceSelect);
content.appendChild(commitHashLabel);
content.appendChild(commitHashInput);
content.appendChild(customUrlInput);
// Footer with buttons
@@ -914,7 +962,19 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
return;
}
const kefinTweaksRoot = buildKefinTweaksRootUrl(sourceType, source);
let commitHash = '';
if (sourceType === 'github' && source === SPECIFIC_BUILD_NAME) {
commitHash = (commitHashInput.value || '').trim().toLowerCase();
if (!isFullCommitSha(commitHash)) {
showAlertModal(
'Invalid Commit Hash',
'<p>Please enter a full 40-character git commit SHA for Specific Build.</p>'
);
return;
}
}
const kefinTweaksRoot = buildKefinTweaksRootUrl(sourceType, source, commitHash);
const kefinTweaksRootResolvedRaw = await resolveRootVersion(kefinTweaksRoot);
const kefinTweaksRootResolved = kefinTweaksRootResolvedRaw.endsWith('/')
? kefinTweaksRootResolvedRaw
@@ -979,6 +1039,8 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
let versionDisplay = '';
if (sourceInfo.sourceType === 'custom') {
versionDisplay = SELF_HOSTED_NAME;
} else if (sourceInfo.version === SPECIFIC_BUILD_NAME) {
versionDisplay = formatSpecificBuildDisplay(sourceInfo);
} else if (sourceInfo.version === DEVELOPMENT_NAME) {
versionDisplay = DEVELOPMENT_NAME;
} else if (sourceInfo.version === LATEST_RELEASE_NAME) {
@@ -1171,6 +1233,8 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
if (root) {
if (sourceInfo.sourceType === 'custom') {
versionDisplay = SELF_HOSTED_NAME;
} else if (sourceInfo.version === SPECIFIC_BUILD_NAME) {
versionDisplay = formatSpecificBuildDisplay(sourceInfo);
} else if (sourceInfo.version === DEVELOPMENT_NAME) {
versionDisplay = DEVELOPMENT_NAME;
} else if (sourceInfo.version === LATEST_RELEASE_NAME) {
@@ -1367,6 +1431,8 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
if (root) {
if (sourceInfo.sourceType === 'custom') {
versionDisplay = SELF_HOSTED_NAME;
} else if (sourceInfo.version === SPECIFIC_BUILD_NAME) {
versionDisplay = formatSpecificBuildDisplay(sourceInfo);
} else if (sourceInfo.version === DEVELOPMENT_NAME) {
versionDisplay = DEVELOPMENT_NAME;
} else if (sourceInfo.version === LATEST_RELEASE_NAME) {
@@ -1693,15 +1759,11 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
display: none;
}
#reactRoot:not(.kefin-custom-page-active) .pageTitle {
display: none !important;
}
#reactRoot:not(.kefin-custom-page-active) .skinBody #fallbackPage > * {
#reactRoot:not(.kefin-custom-page-active) #fallbackPage:not([data-kefin-fallback-ready]) > * {
display: none;
}
#reactRoot:not(.kefin-custom-page-active) #fallbackPage::after {
#reactRoot:not(.kefin-custom-page-active) #fallbackPage:not([data-kefin-fallback-ready])::after {
content: '';
display: inline-block;
width: 20px;
@@ -1716,6 +1778,11 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
top: 1em;
}
#fallbackPage[data-kefin-fallback-ready]::after {
display: none !important;
content: none !important;
}
#reactRoot.kefin-custom-page-active .backdropImage {
background: none !important;
}
@@ -1739,10 +1806,11 @@ header.MuiPaper-root + main.MuiBox-root .customPage {
main.MuiBox-root .customPage.libraryPage:not(.noSecondaryNavPage)[data-kefin-custom-page] {
padding-top: 1rem !important;
}
.layout-mobile :not(main.MuiBox-root) .customPage.libraryPage:not(.noSecondaryNavPage)[data-kefin-custom-page] {
.layout-mobile :not(main.MuiBox-root) > .skinBody > .customPage.libraryPage:not(.noSecondaryNavPage)[data-kefin-custom-page] {
padding-top: 5rem !important;
}
`;
window.KefinTweaksCustomPageStyles = css;
let style = document.getElementById('kefin-custom-page-styles');
if (!style) {
style = document.createElement('style');
+7 -63
View File
@@ -2492,7 +2492,7 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`;
let watchlistUrl = '#/watchlist';
if (watchlistTabIndex !== null && watchlistTabIndex !== undefined && !isModernUI) {
if (watchlistTabIndex !== null && watchlistTabIndex !== undefined) {
watchlistUrl = `#/home?tab=${watchlistTabIndex}`;
}
window.KefinTweaksUtils._watchlistUrl = watchlistUrl;
@@ -2509,8 +2509,8 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`;
}
_watchlistTabIndex = await fetchWatchlistTabIndex();
return _watchlistTabIndex;
}
return _watchlistTabIndex;
}
// ----- Custom pages (hash routes → .customPage in skinBody) -----
const DISALLOWED_CUSTOM_PAGE_SEGMENTS = new Set([
@@ -2571,73 +2571,17 @@ window.KefinTweaksConfig = ${JSON.stringify(configToSave, null, 2)};`;
return slug || 'page';
}
const CUSTOM_PAGE_STYLES_CSS = `
#reactRoot.kefin-custom-page-active .skinBody #fallbackPage {
display: none;
}
#reactRoot:not(.kefin-custom-page-active) #fallbackPage:not([data-kefin-fallback-ready]) > * {
display: none;
}
#reactRoot:not(.kefin-custom-page-active) #fallbackPage:not([data-kefin-fallback-ready])::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #4ecdc4;
border-radius: 50%;
animation: spin 1s linear infinite;
position: relative;
left: 50%;
transform: translateX(-50%);
top: 1em;
}
#fallbackPage[data-kefin-fallback-ready]::after {
display: none !important;
content: none !important;
}
#reactRoot.kefin-custom-page-active .backdropImage {
background: none !important;
}
header.MuiPaper-root + main.MuiBox-root .customPage {
margin-top: 3rem;
}
.layout-mobile .kefin-custom-page-active .MuiToolbar-root > .MuiStack-root > :nth-child(n+2),
.layout-mobile .kefin-custom-page-active .MuiToolbar-root > .MuiStack-root > :first-child .MuiButton-icon img {
display: none;
}
.layout-mobile .kefin-custom-page-active .MuiToolbar-root > .MuiStack-root > :first-child .MuiButton-icon::after {
content: '\\e88a';
font-family: 'Material Icons';
font-size: 1.2rem;
}
.layout-mobile .kefin-custom-page-active .MuiToolbar-root > .MuiStack-root > :first-child {
font-size: 0px;
}
main.MuiBox-root .customPage.libraryPage:not(.noSecondaryNavPage)[data-kefin-custom-page] {
padding-top: 1rem !important;
}
.layout-mobile :not(main.MuiBox-root) > .skinBody > .customPage.libraryPage:not(.noSecondaryNavPage)[data-kefin-custom-page] {
padding-top: 5rem !important;
}
`;
/** Always refresh textContent so early-inject :has() rules get replaced on JMP. */
/** Re-apply plugin SoT custom-page CSS (window.KefinTweaksCustomPageStyles). No local CSS blob. */
function ensureCustomPageStyles() {
const css = window.KefinTweaksCustomPageStyles;
if (typeof css !== 'string' || !css) return;
let style = document.getElementById('kefin-custom-page-styles');
if (!style) {
style = document.createElement('style');
style.id = 'kefin-custom-page-styles';
(document.head || document.documentElement).appendChild(style);
}
style.textContent = CUSTOM_PAGE_STYLES_CSS;
style.textContent = css;
}
function syncCustomPageActiveClass(active) {