Some changes

This commit is contained in:
Daniel
2026-03-02 17:34:44 +01:00
parent 7bdf39f0fa
commit 68622363f5
4 changed files with 1302 additions and 49 deletions

View File

@@ -50,7 +50,10 @@ class HistoryController {
}
showSkin() {
this.optionsElement.innerHTML = this.currentSkin.generateHTML({ includePreview: false });
this.optionsElement.innerHTML = this.currentSkin.generateHTML({
includePreview: false,
includeLivePreview: false
});
this.currentSkin.attachEventListeners();
}
@@ -61,6 +64,40 @@ class HistoryController {
console.log(`Skin changed to: ${this.currentSkin.name}`);
}
async applyCurrentSkin() {
if (!this.currentSkin) {
return;
}
const css = this.currentSkin.generateCSS();
const appliedSkinName = this.currentSkin.name;
try {
const serverConfig = await ApiClient.getServerConfiguration();
await ApiClient.updateServerConfiguration(serverConfig);
const brandingConfig = await ApiClient.getNamedConfiguration("branding");
const existingCss = brandingConfig && typeof brandingConfig.CustomCss === "string"
? brandingConfig.CustomCss
: "";
if (existingCss && !this.configController.isManagedCss(existingCss)) {
await this.configController.saveUserCss(existingCss);
}
brandingConfig.CustomCss = css;
await ApiClient.updateNamedConfiguration("branding", brandingConfig);
Dashboard.processServerConfigurationUpdateResult();
await this.configController.saveSkin(this.currentSkin);
await this.configController.setSelectedSkin(appliedSkinName);
window.location.reload(true);
} catch (error) {
console.error("Error applying skin from history:", error);
}
}
initEventListeners() {
this.setSkinButton.addEventListener('click', () => {
if (this.currentSkin) {

File diff suppressed because it is too large Load Diff

View File

@@ -10,21 +10,23 @@ class Skin {
console.log(`Skin "${this.name}" initialized with ${this.categories.length} categories.`);
}
generateHTML({ includePreview = true } = {}) {
generateHTML({ includePreview = true, includeLivePreview = true } = {}) {
const categoriesHTML = this.categories
.map(category => category.generateHTML())
.join('');
const previewsHTML = includePreview ? this.generatePreviewHTML() : '';
const hasPreviews = includePreview && Array.isArray(this.previews) && this.previews.length > 0;
const previewsHTML = hasPreviews ? this.generatePreviewHTML() : '';
const livePreviewHTML = includeLivePreview ? this.generateLivePreviewHTML() : '';
return `
<div data-role="controlgroup" class="optionsContainer">
<div data-role="controlgroup" class="optionsContainer" data-has-previews="${hasPreviews}">
<div class="categoriesContainer">
${categoriesHTML}
</div>
<br/>
${previewsHTML}
${hasPreviews ? `<div class="previewsContainer">${previewsHTML}</div>` : ""}
</div>
${livePreviewHTML}
`;
}
@@ -53,19 +55,66 @@ class Skin {
}
generatePreviewHTML() {
if (!this.previews || this.previews.length === 0) return '';
const hasPreviews = Array.isArray(this.previews) && this.previews.length > 0;
const slides = hasPreviews
? this.previews.map((preview, index) => {
const label = preview.name || `Preview ${index + 1}`;
return `
<div class="previewSlide" data-index="${index}">
<div class="previewSlide-imgWrapper">
<img src="${preview.url}" alt="${label}" loading="lazy">
</div>
<div class="previewSlide-label">${label}</div>
</div>
`;
}).join("")
: `<p class="previewEmpty">Esta skin no tiene capturas todavia.</p>`;
const dots = hasPreviews
? `<div class="previewDots">
${this.previews.map((_, index) => `<button type="button" class="previewDot" data-index="${index}" aria-label="Vista ${index + 1}"></button>`).join("")}
</div>`
: "";
return `
<div class="verticalSection verticalSection-extrabottompadding previewSection">
<h2 class="sectionTitle">Previews</h2>
${this.previews
.map(p => `
<fieldset class="verticalSection verticalSection-extrabottompadding">
<img src="${p.url}" alt="${p.name || ''}">
<legend>${p.name}</legend>
</fieldset>`
)
.join("")}
<div class="verticalSection verticalSection-extrabottompadding previewSection" data-has-previews="${hasPreviews}">
<div class="previewSection-header">
<div>
<h2 class="sectionTitle">Previsualizacion</h2>
<p class="sectionSubtitle">Recorre capturas estaticas de la skin.</p>
</div>
</div>
<div class="previewCarousel" role="region" aria-label="Capturas de la skin">
${hasPreviews ? '<button type="button" class="previewNav previewNav-prev" data-action="prev" aria-label="Anterior">&#8592;</button>' : ''}
<div class="previewViewport">
<div class="previewTrack" id="skinPreviewTrack">
${slides}
</div>
</div>
${hasPreviews ? '<button type="button" class="previewNav previewNav-next" data-action="next" aria-label="Siguiente">&#8594;</button>' : ''}
</div>
${dots}
<div class="previewCaption" id="skinPreviewCaption"></div>
</div>
`;
}
generateLivePreviewHTML() {
return `
<div class="livePreviewShell" id="livePreviewShell">
<div class="livePreviewHeader">
<div>
<h3 class="livePreviewTitle">Vista previa en vivo</h3>
<p class="livePreviewHint">Se recarga automaticamente al cambiar opciones. Siempre abre /web/#/home.</p>
</div>
<button is="emby-button" type="button" class="previewAction previewAction-secondary" id="livePreviewExpand">
<span>Ampliar vista previa</span>
</button>
</div>
<div class="livePreviewFrameWrapper">
<iframe id="skinLivePreviewFrame" title="Vista previa del skin" sandbox="allow-same-origin allow-scripts" loading="lazy"></iframe>
</div>
</div>
`;
}
@@ -85,4 +134,3 @@ class Skin {
}
}

View File

@@ -170,21 +170,35 @@
border: none;
}
.optionsContainer {
display: flex;
flex-direction: row;
gap: 10px;
}
.optionsContainer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
.optionsContainer .categoriesContainer {
flex: 6;
}
.optionsContainer .categoriesContainer {
flex: 6;
}
.previewsContainer {
flex: 5;
min-width: 320px;
}
.optionsContainer[data-has-previews="false"] .previewsContainer {
display: none;
}
.optionsContainer[data-has-previews="false"] .categoriesContainer {
flex: 1 1 100%;
}
.optionsContainer:only-child .categoriesContainer,
.optionsContainer .categoriesContainer:only-child {
flex: 1 1 100%;
}
.optionsContainer:only-child .categoriesContainer,
.optionsContainer .categoriesContainer:only-child {
flex: 1 1 100%;
}
@media (max-width: 600px) {
.optionsContainer {
@@ -195,7 +209,11 @@
.optionsContainer div {
flex: 1 1 100%;
}
}
.optionsContainer .previewSection {
min-width: 100%;
}
}
.fontPickerInput {
width: 100%;
@@ -215,9 +233,216 @@
}
.previewSection {
max-width: fit-content;
}
.previewSection {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
background: rgba(255, 255, 255, 0.02);
}
.previewSection-header {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.previewActions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.previewAction {
min-width: 160px;
}
.previewAction-secondary {
background: rgba(255, 255, 255, 0.06) !important;
}
.sectionSubtitle {
margin: 4px 0 0;
color: rgba(255, 255, 255, 0.7);
font-size: 0.95rem;
}
.previewCarousel {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 10px;
align-items: center;
}
.previewCarousel:only-child {
grid-template-columns: 1fr;
}
.previewViewport {
position: relative;
overflow: hidden;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.35);
min-height: 280px;
}
.previewTrack {
display: flex;
width: 100%;
height: 100%;
transition: transform 0.35s ease;
}
.previewSection[data-has-previews="false"] .previewViewport {
min-height: 120px;
}
.previewSlide {
flex: 0 0 100%;
display: flex;
flex-direction: column;
}
.previewSlide-imgWrapper {
flex: 1;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
}
.previewSlide img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.previewSlide-label {
padding: 10px 12px;
font-weight: 600;
background: rgba(0, 0, 0, 0.25);
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.previewCaption {
margin: 0;
color: rgba(255, 255, 255, 0.75);
font-size: 0.95rem;
}
.previewDots {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
}
.previewDot {
width: 10px;
height: 10px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.15);
cursor: pointer;
transition: all 0.2s ease;
}
.previewDot.is-active {
background: #00a4dc;
border-color: #00a4dc;
width: 20px;
}
.previewNav {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
color: inherit;
width: 42px;
height: 42px;
border-radius: 50%;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
transition: background 0.2s ease, transform 0.2s ease;
}
.previewNav:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-1px);
}
.previewEmpty {
margin: 0;
padding: 16px;
color: rgba(255, 255, 255, 0.7);
}
.previewSection[data-has-previews="false"] .previewDots,
.previewSection[data-has-previews="false"] .previewNav {
display: none;
}
.livePreviewShell {
width: 100%;
max-width: 100%;
margin-top: 6px;
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
gap: 10px;
}
.livePreviewHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
}
.livePreviewTitle {
margin: 0 0 4px;
}
.livePreviewHint {
margin: 0;
color: rgba(255, 255, 255, 0.7);
font-size: 0.95rem;
}
.livePreviewSync {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.livePreviewFrameWrapper {
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #0a0a0a;
min-height: 340px;
}
#livePreviewShell[data-expanded="true"] .livePreviewFrameWrapper {
min-height: 70vh;
max-height: calc(100vh - 180px);
}
#skinLivePreviewFrame {
width: 100%;
height: 100%;
border: none;
}
.textareaLabel {
display: inline-block;