mirror of
https://github.com/danieladov/jellyfin-plugin-skin-manager.git
synced 2026-01-18 16:37:31 +01:00
V.3
This commit is contained in:
152
Jellyfin.Plugin.SkinManager/Configuration/ConfigController.js
Normal file
152
Jellyfin.Plugin.SkinManager/Configuration/ConfigController.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
class ConfigController {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
console.log("ConfigController initialized");
|
||||||
|
this.pluginId = "e9ca8b8e-ca6d-40e7-85dc-58e536df8eb3";
|
||||||
|
this.MANAGED_CSS_MARKER = "/* Skin Manager CSS */";
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSkin(skin) {
|
||||||
|
skin.name = new Date().toLocaleString() + " - " + skin.name;
|
||||||
|
console.log("Saving skin:", skin);
|
||||||
|
const config = await this.getPluginConfiguration();
|
||||||
|
const serialized = this.serializeSkin(skin);
|
||||||
|
if (serialized) {
|
||||||
|
config.skinHistory.push(serialized);
|
||||||
|
}
|
||||||
|
const result = await ApiClient.updatePluginConfiguration(this.pluginId, config);
|
||||||
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadHistorySkins() {
|
||||||
|
const config = await this.getPluginConfiguration();
|
||||||
|
return [...config.skinHistory]
|
||||||
|
.reverse()
|
||||||
|
.map(s => this.deserializeSkin(s))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeSkin(skin) {
|
||||||
|
try {
|
||||||
|
// Convert the skin object into a JSON string
|
||||||
|
return JSON.stringify(skin);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error serializing the skin:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
deserializeSkin(serializedSkin) {
|
||||||
|
try {
|
||||||
|
// Parse the JSON string back into a JavaScript object
|
||||||
|
const skinData = JSON.parse(serializedSkin);
|
||||||
|
|
||||||
|
return new Skin(skinData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing the skin data:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveUserCss(css) {
|
||||||
|
if (!css || !css.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await this.getPluginConfiguration();
|
||||||
|
|
||||||
|
const existingHistory = config.userCssHistory
|
||||||
|
.map(entry => this.deserializeUserCssEntry(entry))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const lastEntry = existingHistory[existingHistory.length - 1];
|
||||||
|
if (lastEntry && lastEntry.css === css) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssEntry = this.createUserCssEntry(css);
|
||||||
|
const serialized = this.serializeUserCssEntry(cssEntry);
|
||||||
|
if (serialized) {
|
||||||
|
config.userCssHistory.push(serialized);
|
||||||
|
}
|
||||||
|
await ApiClient.updatePluginConfiguration(this.pluginId, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUserCssHistory() {
|
||||||
|
const config = await this.getPluginConfiguration();
|
||||||
|
return config.userCssHistory
|
||||||
|
.map(entry => this.deserializeUserCssEntry(entry))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeUserCssEntry(entry) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(entry);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error serializing user CSS entry:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializeUserCssEntry(serializedEntry) {
|
||||||
|
if (!serializedEntry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(serializedEntry);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing user CSS entry:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createUserCssEntry(css) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const id = this.generateId();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
css,
|
||||||
|
savedAt: now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
generateId() {
|
||||||
|
if (typeof window !== "undefined" && window.crypto && window.crypto.randomUUID) {
|
||||||
|
return window.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `user-css-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPluginConfiguration() {
|
||||||
|
const config = await ApiClient.getPluginConfiguration(this.pluginId);
|
||||||
|
if (!Array.isArray(config.skinHistory)) {
|
||||||
|
config.skinHistory = [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(config.userCssHistory)) {
|
||||||
|
config.userCssHistory = [];
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSelectedSkin(skinName) {
|
||||||
|
const config = await this.getPluginConfiguration();
|
||||||
|
config.selectedSkin = skinName || "";
|
||||||
|
|
||||||
|
const result = await ApiClient.updatePluginConfiguration(this.pluginId, config);
|
||||||
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
isManagedCss(css) {
|
||||||
|
if (!css) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = css.trimStart();
|
||||||
|
return trimmed.startsWith(this.MANAGED_CSS_MARKER)
|
||||||
|
|| trimmed.startsWith("#Skin Manager CSS");
|
||||||
|
}
|
||||||
|
}
|
||||||
325
Jellyfin.Plugin.SkinManager/Configuration/HistoryController.js
Normal file
325
Jellyfin.Plugin.SkinManager/Configuration/HistoryController.js
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
class HistoryController {
|
||||||
|
constructor() {
|
||||||
|
// Initialize ConfigController to handle skin serialization
|
||||||
|
this.configController = new ConfigController();
|
||||||
|
|
||||||
|
// Array to keep track of the history of saved skins
|
||||||
|
this.history = [];
|
||||||
|
this.currentSkin = null;
|
||||||
|
this.userCssHistory = [];
|
||||||
|
this.currentCss = null;
|
||||||
|
|
||||||
|
this.selectElement = document.getElementById("cssOptions-history");
|
||||||
|
this.optionsElement = document.getElementById("options-history");
|
||||||
|
this.setSkinButton = document.getElementById("setSkin-history");
|
||||||
|
|
||||||
|
this.cssContainerElement = document.getElementById("cssHistoryContainer");
|
||||||
|
this.cssSelectElement = document.getElementById("cssHistorySelect");
|
||||||
|
this.cssMetaElement = document.getElementById("cssHistoryMeta");
|
||||||
|
this.cssCodeElement = document.getElementById("cssHistoryCode");
|
||||||
|
this.cssEmptyElement = document.getElementById("cssHistoryEmpty");
|
||||||
|
this.restoreCssButton = document.getElementById("restoreCssButton");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize history controller logic
|
||||||
|
async init() {
|
||||||
|
console.log("HistoryController initialized");
|
||||||
|
this.history = await this.configController.loadHistorySkins();
|
||||||
|
this.populateSelect();
|
||||||
|
this.initEventListeners();
|
||||||
|
|
||||||
|
await this.loadUserCssHistory();
|
||||||
|
this.renderCssHistory();
|
||||||
|
this.initCssEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateSelect() {
|
||||||
|
this.selectElement.innerHTML = "";
|
||||||
|
this.history.forEach((skin, index) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = index;
|
||||||
|
option.textContent = skin.name;
|
||||||
|
this.selectElement.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.history.length > 0) {
|
||||||
|
this.selectElement.value = 0;
|
||||||
|
this.currentSkin = this.history[0];
|
||||||
|
this.showSkin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSkin() {
|
||||||
|
this.optionsElement.innerHTML = this.currentSkin.generateHTML({ includePreview: false });
|
||||||
|
this.currentSkin.attachEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeSkin() {
|
||||||
|
const selectedIndex = this.selectElement.value;
|
||||||
|
this.currentSkin = this.history[selectedIndex];
|
||||||
|
this.showSkin();
|
||||||
|
console.log(`Skin changed to: ${this.currentSkin.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
initEventListeners() {
|
||||||
|
this.setSkinButton.addEventListener('click', () => {
|
||||||
|
if (this.currentSkin) {
|
||||||
|
this.applyCurrentSkin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectElement.addEventListener('change', () => {
|
||||||
|
this.changeSkin();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUserCssHistory() {
|
||||||
|
try {
|
||||||
|
const history = await this.configController.loadUserCssHistory();
|
||||||
|
this.userCssHistory = (history || [])
|
||||||
|
.filter(entry => entry && typeof entry.css === "string")
|
||||||
|
.map(entry => ({
|
||||||
|
...entry,
|
||||||
|
id: entry.id ? String(entry.id) : this.generateEntryId(entry)
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.savedAt || 0).getTime();
|
||||||
|
const dateB = new Date(b.savedAt || 0).getTime();
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading user CSS history:", error);
|
||||||
|
this.userCssHistory = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCssHistory() {
|
||||||
|
if (!this.cssSelectElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cssSelectElement.innerHTML = "";
|
||||||
|
|
||||||
|
if (!this.userCssHistory.length) {
|
||||||
|
this.cssSelectElement.disabled = true;
|
||||||
|
this.updateCssContainerState(true);
|
||||||
|
if (this.cssEmptyElement) {
|
||||||
|
this.cssEmptyElement.textContent = "No saved CSS yet.";
|
||||||
|
}
|
||||||
|
this.clearCssDetail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cssSelectElement.disabled = false;
|
||||||
|
this.userCssHistory.forEach(entry => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = String(entry.id);
|
||||||
|
option.textContent = this.buildCssListLabel(entry);
|
||||||
|
this.cssSelectElement.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstEntryId = String(this.userCssHistory[0].id);
|
||||||
|
this.cssSelectElement.value = firstEntryId;
|
||||||
|
this.selectCssEntry(firstEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateEntryId(entry) {
|
||||||
|
if (entry.savedAt) {
|
||||||
|
return `user-css-${entry.savedAt}`;
|
||||||
|
}
|
||||||
|
return `user-css-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCssListLabel(entry) {
|
||||||
|
const formattedDate = this.formatDate(entry.savedAt);
|
||||||
|
return formattedDate ? `Saved on ${formattedDate}` : "Saved CSS";
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCssDetailMeta(entry) {
|
||||||
|
if (!entry.savedAt) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `Saved on ${this.formatDate(entry.savedAt)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(isoString) {
|
||||||
|
if (!isoString) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(isoString);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return isoString;
|
||||||
|
}
|
||||||
|
return date.toLocaleString();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unable to format date", error);
|
||||||
|
return isoString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCssEntry(id) {
|
||||||
|
const normalizedId = String(id);
|
||||||
|
const entry = this.userCssHistory.find(item => String(item.id) === normalizedId);
|
||||||
|
if (!entry) {
|
||||||
|
this.clearCssDetail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentCss = entry;
|
||||||
|
this.updateCssContainerState(false);
|
||||||
|
|
||||||
|
if (this.cssSelectElement && this.cssSelectElement.value !== normalizedId) {
|
||||||
|
this.cssSelectElement.value = normalizedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaText = this.buildCssDetailMeta(entry);
|
||||||
|
if (this.cssMetaElement) {
|
||||||
|
this.cssMetaElement.textContent = metaText;
|
||||||
|
this.cssMetaElement.style.display = metaText ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cssCodeElement) {
|
||||||
|
this.cssCodeElement.textContent = entry.css || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cssEmptyElement) {
|
||||||
|
this.cssEmptyElement.textContent = "Select a revision to inspect its content.";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleRestoreButton(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCssDetail() {
|
||||||
|
this.currentCss = null;
|
||||||
|
const hasEntries = this.userCssHistory.length > 0;
|
||||||
|
this.updateCssContainerState(!hasEntries);
|
||||||
|
|
||||||
|
if (this.cssMetaElement) {
|
||||||
|
this.cssMetaElement.textContent = "";
|
||||||
|
this.cssMetaElement.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cssCodeElement) {
|
||||||
|
this.cssCodeElement.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cssSelectElement) {
|
||||||
|
if (hasEntries) {
|
||||||
|
this.cssSelectElement.disabled = false;
|
||||||
|
} else {
|
||||||
|
this.cssSelectElement.disabled = true;
|
||||||
|
this.cssSelectElement.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cssEmptyElement) {
|
||||||
|
this.cssEmptyElement.textContent = hasEntries
|
||||||
|
? "Select a revision to inspect its content."
|
||||||
|
: "No saved CSS yet.";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleRestoreButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCssContainerState(isEmpty) {
|
||||||
|
if (this.cssContainerElement) {
|
||||||
|
this.cssContainerElement.dataset.empty = isEmpty ? "true" : "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRestoreButton(enabled) {
|
||||||
|
if (!this.restoreCssButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.restoreCssButton.dataset.restoreState === "working") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restoreCssButton.disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
initCssEventListeners() {
|
||||||
|
if (this.cssSelectElement) {
|
||||||
|
this.cssSelectElement.addEventListener("change", event => {
|
||||||
|
const selectedId = event.target.value;
|
||||||
|
this.selectCssEntry(selectedId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.restoreCssButton) {
|
||||||
|
this.restoreCssButton.addEventListener("click", () => {
|
||||||
|
this.restoreCurrentCss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreCurrentCss() {
|
||||||
|
if (!this.currentCss) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showRestoreProgress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.applyCssToServer(this.currentCss.css);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to restore CSS", error);
|
||||||
|
this.showRestoreIdle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyCssToServer(css) {
|
||||||
|
const serverConfig = await ApiClient.getServerConfiguration();
|
||||||
|
await ApiClient.updateServerConfiguration(serverConfig);
|
||||||
|
|
||||||
|
const brandingConfig = await ApiClient.getNamedConfiguration("branding");
|
||||||
|
brandingConfig.CustomCss = css;
|
||||||
|
await ApiClient.updateNamedConfiguration("branding", brandingConfig);
|
||||||
|
|
||||||
|
Dashboard.processServerConfigurationUpdateResult();
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
showRestoreProgress() {
|
||||||
|
if (!this.restoreCssButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = this.restoreCssButton.querySelector("span");
|
||||||
|
const originalText = label ? label.textContent : this.restoreCssButton.textContent;
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
label.dataset.originalText = originalText;
|
||||||
|
label.textContent = "Restoring...";
|
||||||
|
} else {
|
||||||
|
this.restoreCssButton.dataset.originalText = originalText;
|
||||||
|
this.restoreCssButton.textContent = "Restoring...";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restoreCssButton.dataset.restoreState = "working";
|
||||||
|
this.restoreCssButton.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showRestoreIdle() {
|
||||||
|
if (!this.restoreCssButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = this.restoreCssButton.querySelector("span");
|
||||||
|
const originalText = label ? label.dataset.originalText : this.restoreCssButton.dataset.originalText;
|
||||||
|
|
||||||
|
if (label && originalText) {
|
||||||
|
label.textContent = originalText;
|
||||||
|
delete label.dataset.originalText;
|
||||||
|
} else if (originalText) {
|
||||||
|
this.restoreCssButton.textContent = originalText;
|
||||||
|
delete this.restoreCssButton.dataset.originalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.restoreCssButton.dataset.restoreState;
|
||||||
|
this.restoreCssButton.disabled = !this.currentCss;
|
||||||
|
}
|
||||||
|
}
|
||||||
388
Jellyfin.Plugin.SkinManager/Configuration/MainController.js
Normal file
388
Jellyfin.Plugin.SkinManager/Configuration/MainController.js
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
class MainController {
|
||||||
|
constructor(jsonUrl) {
|
||||||
|
this.jsonUrl = jsonUrl;
|
||||||
|
this.skins = [];
|
||||||
|
this.currentSkin = null;
|
||||||
|
this.selectElement = document.getElementById("cssOptions");
|
||||||
|
this.descriptionElement = document.getElementById("description");
|
||||||
|
this.optionsElement = document.getElementById("options");
|
||||||
|
this.setSkinButton = document.getElementById("setSkin");
|
||||||
|
this.configController = new ConfigController();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const [json, currentSkin] = await Promise.all([
|
||||||
|
this.fetchJson(),
|
||||||
|
this.loadCurrentSkinFromHistory()
|
||||||
|
]);
|
||||||
|
this.loadSkins(json);
|
||||||
|
this.injectCurrentSkin(currentSkin);
|
||||||
|
this.populateSelect();
|
||||||
|
this.initEventListeners();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cargando las skins:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchJson() {
|
||||||
|
const response = await fetch(this.jsonUrl);
|
||||||
|
if (!response.ok) throw new Error(`Error HTTP: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCurrentSkinFromHistory() {
|
||||||
|
try {
|
||||||
|
const history = await this.configController.loadHistorySkins();
|
||||||
|
if (!Array.isArray(history) || history.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const latest = history[0];
|
||||||
|
return this.cloneAsCurrentSkin(latest);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("No se pudo cargar la skin actual:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSkins(json) {
|
||||||
|
this.skins = json.skins.map(s => new Skin(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
injectCurrentSkin(currentSkin) {
|
||||||
|
if (!currentSkin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.skins.unshift(currentSkin);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneAsCurrentSkin(skin) {
|
||||||
|
if (!skin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainSkin = JSON.parse(JSON.stringify(skin));
|
||||||
|
const baseName = this.extractSkinBaseName(plainSkin.name);
|
||||||
|
plainSkin.name = `Current Skin - ${baseName}`;
|
||||||
|
plainSkin.description = plainSkin.description || "Skin currently applied with your saved settings.";
|
||||||
|
|
||||||
|
if (Array.isArray(plainSkin.categories)) {
|
||||||
|
plainSkin.categories = plainSkin.categories.filter(cat => cat && cat.name !== "Custom CSS");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Skin(plainSkin);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractSkinBaseName(name) {
|
||||||
|
if (!name) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
const match = String(name).match(/-\s*(.+)$/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
return String(name).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateSelect() {
|
||||||
|
this.selectElement.innerHTML = ""; // Limpiar por si acaso
|
||||||
|
this.skins.forEach((skin, index) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = index;
|
||||||
|
option.textContent = skin.name;
|
||||||
|
this.selectElement.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mostrar la primera skin por defecto si existe
|
||||||
|
if (this.skins.length > 0) {
|
||||||
|
this.selectElement.value = 0;
|
||||||
|
this.currentSkin = this.skins[0];
|
||||||
|
this.showSkin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSkin() {
|
||||||
|
this.optionsElement.innerHTML = this.currentSkin.generateHTML();
|
||||||
|
this.currentSkin.attachEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSkinInfo(index) {
|
||||||
|
const skin = this.skins[index];
|
||||||
|
if (!skin) return;
|
||||||
|
|
||||||
|
// Actualizar descripción
|
||||||
|
this.descriptionElement.textContent = `Skin: ${skin.name}`;
|
||||||
|
|
||||||
|
// Actualizar categorías
|
||||||
|
this.optionsElement.innerHTML = "";
|
||||||
|
skin.categories.forEach(cat => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = cat;
|
||||||
|
this.optionsElement.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aplicar el CSS
|
||||||
|
this.applySkin(skin.css);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
changeSkin() {
|
||||||
|
const selectedIndex = parseInt(this.selectElement.value, 10);
|
||||||
|
if (Number.isNaN(selectedIndex) || !this.skins[selectedIndex]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentSkin = this.skins[selectedIndex];
|
||||||
|
this.showSkin();
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Save the skin to history after a successful update
|
||||||
|
await this.configController.saveSkin(this.currentSkin);
|
||||||
|
await this.configController.setSelectedSkin(appliedSkinName);
|
||||||
|
|
||||||
|
window.location.reload(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error applying skin:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
initEventListeners() {
|
||||||
|
this.setSkinButton.addEventListener('click', () => {
|
||||||
|
if (this.currentSkin) {
|
||||||
|
this.applyCurrentSkin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectElement.addEventListener('change', () => {
|
||||||
|
this.changeSkin();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async initDebug() {
|
||||||
|
try {
|
||||||
|
const json = `{
|
||||||
|
"skins": [
|
||||||
|
{
|
||||||
|
"name": "Finimalism",
|
||||||
|
"description": "A modern, customizable skin for Jellyfin.",
|
||||||
|
"css":"",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Default",
|
||||||
|
"controls": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Mode",
|
||||||
|
"description": "Select light or dark mode",
|
||||||
|
"id": "mode",
|
||||||
|
"default": "@import url('https://cdn.jsdelivr.net/gh/tedhinklater/finimalism@latest/finimalism10.11.css');",
|
||||||
|
"css": "%value%",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Colour",
|
||||||
|
"value": "@import url('https://cdn.jsdelivr.net/gh/tedhinklater/finimalism@latest/finimalism10.11.css');"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Black",
|
||||||
|
"value": "@import url('https://cdn.jsdelivr.net/gh/tedhinklater/finimalism@latest/finimalism10.11-black.css');"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JellySkin",
|
||||||
|
"description": "A modern, customizable skin for Jellyfin.",
|
||||||
|
"css": "@import url('https://cdn.jsdelivr.net/npm/jellyskin@latest/dist/main.css');",
|
||||||
|
"previews": [
|
||||||
|
{
|
||||||
|
"name": "Login Page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Home/Index Page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Library Page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/3.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Title page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/4.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Default",
|
||||||
|
"controls": [
|
||||||
|
{
|
||||||
|
"type": "fontPicker",
|
||||||
|
"label": "Base Font",
|
||||||
|
"description": "Select the base font for the skin",
|
||||||
|
"id": "baseFont",
|
||||||
|
"default": "Arial, sans-serif",
|
||||||
|
"css": ":root { --base-font: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "Background Color",
|
||||||
|
"description": "Set the background color of the skin",
|
||||||
|
"id": "bgColor",
|
||||||
|
"default": "#ffffff",
|
||||||
|
"css": ":root { --bg-color: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "Text Color",
|
||||||
|
"id": "textColor",
|
||||||
|
"default": "#000000",
|
||||||
|
"css": ":root { --text-color: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"label": "Font Size",
|
||||||
|
"id": "fontSize",
|
||||||
|
"min": 10,
|
||||||
|
"max": 30,
|
||||||
|
"default": 16,
|
||||||
|
"css": ":root { --font-size: %value%px; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkbox",
|
||||||
|
"label": "Dark Mode",
|
||||||
|
"description": "Enable dark mode",
|
||||||
|
"id": "checkbox",
|
||||||
|
"default": true,
|
||||||
|
"css": ":root { --dark-mode: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Border Radius",
|
||||||
|
"description": "Set the border radius",
|
||||||
|
"id": "borderRadius",
|
||||||
|
"min": 0,
|
||||||
|
"max": 50,
|
||||||
|
"default": 0,
|
||||||
|
"css": ":root { --border-radius: %value%px; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Font Family",
|
||||||
|
"description": "Select the font family",
|
||||||
|
"id": "fontFamily",
|
||||||
|
"default": "Arial",
|
||||||
|
"css": ":root { --font-family: %value%; }",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Arial",
|
||||||
|
"value": "Arial"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Verdana",
|
||||||
|
"value": "Verdana"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Georgia",
|
||||||
|
"value": "Georgia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Times New Roman",
|
||||||
|
"value": "Times New Roman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Trebuchet MS",
|
||||||
|
"value": "Trebuchet MS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Arial Black",
|
||||||
|
"value": "Arial Black"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Impact",
|
||||||
|
"value": "Impact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Comic Sans MS",
|
||||||
|
"value": "Comic Sans MS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Courier New",
|
||||||
|
"value": "Courier New"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Lucida Console",
|
||||||
|
"value": "Lucida Console"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "name": "DarkSkin",
|
||||||
|
"description": "A sleek dark theme for Jellyfin.",
|
||||||
|
"css": "@import url('https://cdn.jsdelivr.net/npm/jellyskin@latest/dist/dark.css');",
|
||||||
|
"previews": [],
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Dark Mode",
|
||||||
|
"controls": [
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "Background Color",
|
||||||
|
"description": "Set the background color of the skin",
|
||||||
|
"id": "bgColor",
|
||||||
|
"default": "#121212",
|
||||||
|
"css": ":root { --bg-color: %value%; }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
const jsonObj = JSON.parse(json);
|
||||||
|
this.loadSkins(jsonObj);
|
||||||
|
const currentSkin = await this.loadCurrentSkinFromHistory();
|
||||||
|
this.injectCurrentSkin(currentSkin);
|
||||||
|
this.populateSelect();
|
||||||
|
this.initEventListeners();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cargando las skins:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,14 @@ namespace Jellyfin.Plugin.SkinManager.Configuration
|
|||||||
public class PluginConfiguration : BasePluginConfiguration
|
public class PluginConfiguration : BasePluginConfiguration
|
||||||
{
|
{
|
||||||
public string selectedSkin { get; set; }
|
public string selectedSkin { get; set; }
|
||||||
public string[] options { get; set; }
|
public string[] skinHistory { get; set; }
|
||||||
|
public string[] userCssHistory { get; set; }
|
||||||
|
|
||||||
public PluginConfiguration()
|
public PluginConfiguration()
|
||||||
{
|
{
|
||||||
selectedSkin = "";
|
selectedSkin = "";
|
||||||
options = Array.Empty<String>();
|
skinHistory = Array.Empty<String>();
|
||||||
|
userCssHistory = Array.Empty<String>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2341
Jellyfin.Plugin.SkinManager/Configuration/colorpicker.js
Normal file
2341
Jellyfin.Plugin.SkinManager/Configuration/colorpicker.js
Normal file
File diff suppressed because it is too large
Load Diff
16
Jellyfin.Plugin.SkinManager/Configuration/common.js
Normal file
16
Jellyfin.Plugin.SkinManager/Configuration/common.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const getConfigurationPageUrl = (name) => {
|
||||||
|
return 'configurationpage?name=' + encodeURIComponent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabs() {
|
||||||
|
var tabs = [
|
||||||
|
{
|
||||||
|
href: getConfigurationPageUrl('SkinManager'),
|
||||||
|
name: 'Skin Manager'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: getConfigurationPageUrl('history'),
|
||||||
|
name: 'History'
|
||||||
|
}];
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
class Category {
|
||||||
|
|
||||||
|
constructor(name, controls) {
|
||||||
|
console.log('Initializing Category with controls:', controls);
|
||||||
|
this.name = name;
|
||||||
|
this.controls = controls.map(controlConfig => {
|
||||||
|
switch (controlConfig.type) {
|
||||||
|
case 'color':
|
||||||
|
return new ColorControl(controlConfig);
|
||||||
|
case 'slider':
|
||||||
|
return new SliderControl(controlConfig);
|
||||||
|
case 'checkbox':
|
||||||
|
return new CheckBoxControl(controlConfig);
|
||||||
|
case 'number':
|
||||||
|
return new NumberControl(controlConfig);
|
||||||
|
case 'select':
|
||||||
|
return new SelectControl(controlConfig);
|
||||||
|
case 'fontPicker':
|
||||||
|
return new FontPickerControl(controlConfig);
|
||||||
|
case 'textarea':
|
||||||
|
return new TextAreaControl(controlConfig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHeader() {
|
||||||
|
return `<fieldset class="verticalSection verticalSection-extrabottompadding"><legend>${this.name}</legend>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFooter() {
|
||||||
|
return "</fieldset>";
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
return this.generateHeader() + this.controls.map(control => control.generateHTML()).join('</br>') + this.generateFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCSS() {
|
||||||
|
return this.controls.map(control => control.generateCSS()).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
this.controls.forEach(control => control.attachEventListeners());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Category = Category;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
class CheckBoxControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.type = 'checkbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
var checkValue = this.value ? "checked" : "";
|
||||||
|
return `<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input class = "checkbox" type="checkbox" is="emby-checkbox" id= ${this.id} ${checkValue}/>
|
||||||
|
<span>${this.label}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${this.label}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
const checkbox = document.getElementById(this.id);
|
||||||
|
checkbox.addEventListener('change', (event) => {
|
||||||
|
this.value = event.target.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CheckBoxControl = CheckBoxControl;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
class ColorControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.type = 'color';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
return `
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label for="${this.id}">${this.label}</label>
|
||||||
|
<input
|
||||||
|
class="color"
|
||||||
|
type="text"
|
||||||
|
id="${this.id}"
|
||||||
|
name="${this.id}"
|
||||||
|
data-css="${this.css}"
|
||||||
|
/>
|
||||||
|
<div class="fieldDescription">${this.description}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
this.initColorpicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
initColorpicker() {
|
||||||
|
const $el = $("#" + this.id);
|
||||||
|
|
||||||
|
$el.spectrum({
|
||||||
|
color: this.value,
|
||||||
|
showInput: true,
|
||||||
|
className: "full-spectrum",
|
||||||
|
showInitial: true,
|
||||||
|
showPalette: true,
|
||||||
|
showAlpha: true,
|
||||||
|
showSelectionPalette: true,
|
||||||
|
maxSelectionSize: 10,
|
||||||
|
preferredFormat: "hex",
|
||||||
|
localStorageKey: "spectrum.demo",
|
||||||
|
move: (color) => {
|
||||||
|
this.value = this._formatColor(color);
|
||||||
|
},
|
||||||
|
show: function () {
|
||||||
|
|
||||||
|
},
|
||||||
|
beforeShow: function () {
|
||||||
|
|
||||||
|
},
|
||||||
|
hide: function () {
|
||||||
|
|
||||||
|
},
|
||||||
|
change: (color) => {
|
||||||
|
this.value = this._formatColor(color);
|
||||||
|
$el.val(this.value).trigger("input");
|
||||||
|
},
|
||||||
|
palette: [
|
||||||
|
["rgb(0, 0, 0)", "rgb(67, 67, 67)", "rgb(102, 102, 102)",
|
||||||
|
"rgb(204, 204, 204)", "rgb(217, 217, 217)", "rgb(255, 255, 255)"
|
||||||
|
],
|
||||||
|
["rgb(152, 0, 0)", "rgb(255, 0, 0)", "rgb(255, 153, 0)", "rgb(255, 255, 0)", "rgb(0, 255, 0)",
|
||||||
|
"rgb(0, 255, 255)", "rgb(74, 134, 232)", "rgb(0, 0, 255)", "rgb(153, 0, 255)", "rgb(255, 0, 255)"
|
||||||
|
],
|
||||||
|
["rgb(230, 184, 175)", "rgb(244, 204, 204)", "rgb(252, 229, 205)", "rgb(255, 242, 204)", "rgb(217, 234, 211)",
|
||||||
|
"rgb(208, 224, 227)", "rgb(201, 218, 248)", "rgb(207, 226, 243)", "rgb(217, 210, 233)", "rgb(234, 209, 220)",
|
||||||
|
"rgb(221, 126, 107)", "rgb(234, 153, 153)", "rgb(249, 203, 156)", "rgb(255, 229, 153)", "rgb(182, 215, 168)",
|
||||||
|
"rgb(162, 196, 201)", "rgb(164, 194, 244)", "rgb(159, 197, 232)", "rgb(180, 167, 214)", "rgb(213, 166, 189)",
|
||||||
|
"rgb(204, 65, 37)", "rgb(224, 102, 102)", "rgb(246, 178, 107)", "rgb(255, 217, 102)", "rgb(147, 196, 125)",
|
||||||
|
"rgb(118, 165, 175)", "rgb(109, 158, 235)", "rgb(111, 168, 220)", "rgb(142, 124, 195)", "rgb(194, 123, 160)",
|
||||||
|
"rgb(166, 28, 0)", "rgb(204, 0, 0)", "rgb(230, 145, 56)", "rgb(241, 194, 50)", "rgb(106, 168, 79)",
|
||||||
|
"rgb(69, 129, 142)", "rgb(60, 120, 216)", "rgb(61, 133, 198)", "rgb(103, 78, 167)", "rgb(166, 77, 121)",
|
||||||
|
"rgb(91, 15, 0)", "rgb(102, 0, 0)", "rgb(120, 63, 4)", "rgb(127, 96, 0)", "rgb(39, 78, 19)",
|
||||||
|
"rgb(12, 52, 61)", "rgb(28, 69, 135)", "rgb(7, 55, 99)", "rgb(32, 18, 77)", "rgb(76, 17, 48)"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatColor(color) {
|
||||||
|
return color.getAlpha() < 1 ? color.toHex8String() : color.toHexString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ColorControl = ColorControl;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class Control {
|
||||||
|
constructor({ label, description, value, default: defaultValue, css }) {
|
||||||
|
this.id = this.constructor.name + Math.random().toString(36).substring(7);
|
||||||
|
this.label = label;
|
||||||
|
this.value = value ?? defaultValue;
|
||||||
|
this.css = css;
|
||||||
|
this.description = description || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCSS() {
|
||||||
|
return this.css.replace("%value%", this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Control = Control;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
class FontPickerControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.type = 'fontPicker';
|
||||||
|
this.previewText = config.previewText || "Hello, This is your selected font-family.";
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
const fontValue = this.value ?? "";
|
||||||
|
const safeValue = fontValue.replace(/"/g, """);
|
||||||
|
const parsed = this._parseFontSpec(fontValue);
|
||||||
|
const previewStyle = parsed.family
|
||||||
|
? ` style="${this._composePreviewStyle(parsed)}"`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `<div class="inputContainer">
|
||||||
|
<label for="${this.id}">${this.label}</label>
|
||||||
|
<input
|
||||||
|
is="emby-input"
|
||||||
|
type="text"
|
||||||
|
class="fontPickerInput"
|
||||||
|
id="${this.id}"
|
||||||
|
name="${this.id}"
|
||||||
|
value="${safeValue}"
|
||||||
|
/>
|
||||||
|
<div class="fontCont fontPreviewContainer">
|
||||||
|
<p class="fontPreviewText" id="${this.id}-preview"${previewStyle}>${this.previewText}</p>
|
||||||
|
</div>
|
||||||
|
<div class="fieldDescription">${this.description}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
const input = document.getElementById(this.id);
|
||||||
|
const preview = document.getElementById(`${this.id}-preview`);
|
||||||
|
|
||||||
|
const updatePreview = (fontValue) => {
|
||||||
|
const sanitizedValue = fontValue || "";
|
||||||
|
this.value = sanitizedValue;
|
||||||
|
const { family, weight, italic } = this._parseFontSpec(sanitizedValue);
|
||||||
|
if (preview) {
|
||||||
|
preview.style.fontFamily = family || sanitizedValue;
|
||||||
|
preview.style.fontWeight = weight ? String(weight) : "";
|
||||||
|
preview.style.fontStyle = italic ? "italic" : "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
const $input = window.jQuery ? window.jQuery(input) : null;
|
||||||
|
if ($input && $input.fontpicker) {
|
||||||
|
$input.fontpicker({ variants: true });
|
||||||
|
$input.on('change', (event) => updatePreview(event.target.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', (event) => updatePreview(event.target.value));
|
||||||
|
input.addEventListener('change', (event) => updatePreview(event.target.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePreview(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_escapeFontFamily(fontFamily) {
|
||||||
|
return (fontFamily || "").replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseFontSpec(fontSpec) {
|
||||||
|
if (!fontSpec) {
|
||||||
|
return { family: "", weight: null, italic: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [familyPart, variantRaw = ""] = fontSpec.split(":");
|
||||||
|
const family = (familyPart || "").trim();
|
||||||
|
const variant = (variantRaw || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
let weight = null;
|
||||||
|
let italic = false;
|
||||||
|
|
||||||
|
if (!variant) {
|
||||||
|
return { family, weight, italic };
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = variant.match(/^(\d+)(i)?$/i);
|
||||||
|
if (match) {
|
||||||
|
weight = parseInt(match[1], 10);
|
||||||
|
italic = !!match[2];
|
||||||
|
return { family, weight, italic };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variant === "italic") {
|
||||||
|
italic = true;
|
||||||
|
return { family, weight, italic };
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeric = parseInt(variant, 10);
|
||||||
|
if (!isNaN(numeric)) {
|
||||||
|
weight = numeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { family, weight, italic };
|
||||||
|
}
|
||||||
|
|
||||||
|
_composePreviewStyle({ family, weight, italic }) {
|
||||||
|
const parts = [];
|
||||||
|
if (family) {
|
||||||
|
parts.push(`font-family: ${this._escapeFontFamily(family)};`);
|
||||||
|
}
|
||||||
|
if (weight) {
|
||||||
|
parts.push(`font-weight: ${weight};`);
|
||||||
|
}
|
||||||
|
if (italic) {
|
||||||
|
parts.push("font-style: italic;");
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.FontPickerControl = FontPickerControl;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
class NumberControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.min = config.min;
|
||||||
|
this.max = config.max;
|
||||||
|
this.type = 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
return `<div class="inputContainer">
|
||||||
|
<input is="emby-input" type="number" class="number"
|
||||||
|
value='${this.value}' id='${this.id}' min='${this.min}' max='${this.max}' label='${this.label}'>
|
||||||
|
<div class="fieldDescription">${this.description}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
const numberInput = document.getElementById(this.id);
|
||||||
|
numberInput.addEventListener('input', (event) => {
|
||||||
|
this.value = event.target.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.NumberControl = NumberControl;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
class SelectControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.options = config.options;
|
||||||
|
this.type = 'select';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
|
||||||
|
|
||||||
|
return `<div class="selectContainer">
|
||||||
|
<label for= "${this.id}" > ${this.label}</label >
|
||||||
|
<select is="emby-select" id="${this.id}">
|
||||||
|
${this.options.map(option => `<option value="${option.value}"
|
||||||
|
${option.value == this.value ? "selected" : ""} >${option.label}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
const select = document.getElementById(this.id);
|
||||||
|
select.addEventListener('change', (event) => {
|
||||||
|
console.log("selected ", event.target.value);
|
||||||
|
this.value = event.target.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SelectControl = SelectControl;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
class SliderControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.min = config.min;
|
||||||
|
this.max = config.max;
|
||||||
|
this.type = 'slider';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
return `<div class="inputContainer" >
|
||||||
|
<input type="range" class="slider" value="${this.value}" data-css="${this.css}" id="${this.id}" min="${this.min}" max="${this.max}" label="${this.name}">
|
||||||
|
<div class="fieldDescription">${this.label}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
const slider = document.getElementById(this.id);
|
||||||
|
slider.addEventListener('input', (event) => {
|
||||||
|
console.log(event.target.value);
|
||||||
|
this.value = event.target.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SliderControl = SliderControl;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
class TextAreaControl extends Control {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.type = 'textarea';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML() {
|
||||||
|
return `<div class="inputContainer customCssContainer">
|
||||||
|
<label class= "textareaLabel" for="${this.id}">${this.label}</label>
|
||||||
|
<textarea is="emby-textarea" class= "textarea-mono emby-textarea" id="${this.id}" rows="1">${this.value}</textarea>
|
||||||
|
<div class="fieldDescription">${this.description}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
const textArea = document.getElementById(this.id);
|
||||||
|
textArea.addEventListener('input', (event) => {
|
||||||
|
this.value = event.target.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TextAreaControl = TextAreaControl;
|
||||||
File diff suppressed because it is too large
Load Diff
102
Jellyfin.Plugin.SkinManager/Configuration/history.html
Normal file
102
Jellyfin.Plugin.SkinManager/Configuration/history.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Skin Manager</title>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="historyPage" data-role="page"
|
||||||
|
class="page type-interior pluginConfigurationPage tbsConfigurationPage withTabs"
|
||||||
|
data-require="emby-input,emby-button" data-controller="__plugin/fontpicker.js">
|
||||||
|
<script src="/web/configurationpage?name=controls.Control.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.ColorControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.SliderControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.CheckBoxControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.NumberControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.SelectControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.FontPickerControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.TextAreaControl.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=controls.Category.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=skin.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=fontpicker.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=colorpicker.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=MainController.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=common.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=ConfigController.js"></script>
|
||||||
|
<script src="/web/configurationpage?name=HistoryController.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/web/configurationpage?name=style.css" />
|
||||||
|
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<div class="tbsConfigurationPage">
|
||||||
|
<div class="sectionTitleContainer flex align-items-center">
|
||||||
|
<h2 class="sectionTitle">Skin Manager</h2>
|
||||||
|
<a is="emby-linkbutton" class="raised button-alt headerHelpButton emby-button" target="_blank"
|
||||||
|
href="https://prayag17.github.io/jellyfin-plugin-skin-manager/src/html/help.html">Help</a>
|
||||||
|
</div>
|
||||||
|
<div class="history-split">
|
||||||
|
<section class="history-column history-column-skins">
|
||||||
|
<header class="history-column-header">
|
||||||
|
<h3>Skin History</h3>
|
||||||
|
<p>Reapply any skin you used before.</p>
|
||||||
|
</header>
|
||||||
|
<div class="history-card">
|
||||||
|
<label for="cssOptions-history" class="history-label">Select a skin</label>
|
||||||
|
<select is="emby-select" id="cssOptions-history" class="history-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="history-panel" id="skinHistoryPanel">
|
||||||
|
<div class="history-panel-inner checkboxList checkboxList-verticalwrap"
|
||||||
|
id="options-history"></div>
|
||||||
|
</div>
|
||||||
|
<button is="emby-button" type="button" class="raised history-action"
|
||||||
|
id="setSkin-history"><span>Apply skin</span></button>
|
||||||
|
</section>
|
||||||
|
<section class="history-column history-column-css">
|
||||||
|
<header class="history-column-header">
|
||||||
|
<h3>User CSS</h3>
|
||||||
|
<p>Keep your personal tweaks safe when switching skins.</p>
|
||||||
|
</header>
|
||||||
|
<div class="css-history" id="cssHistoryContainer" data-empty="true">
|
||||||
|
<div class="history-card">
|
||||||
|
<label for="cssHistorySelect" class="history-label">Select a revision</label>
|
||||||
|
<select is="emby-select" id="cssHistorySelect" class="history-select"></select>
|
||||||
|
</div>
|
||||||
|
<button is="emby-button" type="button" class="raised history-action-secondary"
|
||||||
|
id="restoreCssButton">
|
||||||
|
<span>Restore CSS</span>
|
||||||
|
</button>
|
||||||
|
<p class="css-history-meta" id="cssHistoryMeta"></p>
|
||||||
|
<pre class="css-history-code" id="cssHistoryCode"></pre>
|
||||||
|
<div class="css-history-empty" id="cssHistoryEmpty">
|
||||||
|
No saved CSS yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
console.log("history page loaded");
|
||||||
|
const setHistoryTabs = function () {
|
||||||
|
LibraryMenu.setTabs('history', 1, getTabs);
|
||||||
|
};
|
||||||
|
if (!window.SkinManagerHistoryViewListener) {
|
||||||
|
window.SkinManagerHistoryViewListener = function (e) {
|
||||||
|
if (e.target && e.target.id === 'historyPage') {
|
||||||
|
setHistoryTabs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('viewshow', window.SkinManagerHistoryViewListener);
|
||||||
|
}
|
||||||
|
setHistoryTabs();
|
||||||
|
let historyController = new HistoryController();
|
||||||
|
historyController.init();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
88
Jellyfin.Plugin.SkinManager/Configuration/skin.js
Normal file
88
Jellyfin.Plugin.SkinManager/Configuration/skin.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
class Skin {
|
||||||
|
constructor({ name, description, css, previews, categories }) {
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.css = css;
|
||||||
|
this.previews = previews;
|
||||||
|
this.categories = categories.map(category => new Category(category.name, category.controls));
|
||||||
|
this.addCustomCssInput();
|
||||||
|
|
||||||
|
console.log(`Skin "${this.name}" initialized with ${this.categories.length} categories.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHTML({ includePreview = true } = {}) {
|
||||||
|
const categoriesHTML = this.categories
|
||||||
|
.map(category => category.generateHTML())
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const previewsHTML = includePreview ? this.generatePreviewHTML() : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div data-role="controlgroup" class="optionsContainer">
|
||||||
|
<div class="categoriesContainer">
|
||||||
|
${categoriesHTML}
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
${previewsHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
generateCSS() {
|
||||||
|
const marker = "/* Skin Manager CSS */";
|
||||||
|
const sections = [marker];
|
||||||
|
|
||||||
|
if (typeof this.css === "string" && this.css.trim()) {
|
||||||
|
sections.push(this.css.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryCss = this.categories
|
||||||
|
.map(c => c.generateCSS())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(section => typeof section === "string" ? section.trim() : section)
|
||||||
|
.filter(section => !!section && (typeof section !== "string" || section.length));
|
||||||
|
|
||||||
|
sections.push(...categoryCss);
|
||||||
|
|
||||||
|
return sections.join("\n\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListeners() {
|
||||||
|
this.categories.forEach(category => category.attachEventListeners());
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePreviewHTML() {
|
||||||
|
if (!this.previews || this.previews.length === 0) return '';
|
||||||
|
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addCustomCssInput() {
|
||||||
|
const CustomCssControl = new TextAreaControl({
|
||||||
|
label: "Custom CSS",
|
||||||
|
description: "Add your own CSS rules here. These will be applied last, so they can override other settings.",
|
||||||
|
value: "",
|
||||||
|
default: "",
|
||||||
|
css: "%value%"
|
||||||
|
});
|
||||||
|
|
||||||
|
const CustomCategory = new Category("Custom CSS", [CustomCssControl]);
|
||||||
|
this.categories.push(CustomCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
1250
Jellyfin.Plugin.SkinManager/Configuration/style.css
Normal file
1250
Jellyfin.Plugin.SkinManager/Configuration/style.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>NET8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<AssemblyVersion>2.0.2</AssemblyVersion>
|
<AssemblyVersion>2.0.2</AssemblyVersion>
|
||||||
<FileVersion>2.0.2</FileVersion>
|
<FileVersion>2.0.2</FileVersion>
|
||||||
@@ -10,15 +10,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.0" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.11.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Configuration\configurationpage.html" />
|
|
||||||
<EmbeddedResource Include="Configuration\configurationpage.html" />
|
|
||||||
|
|
||||||
<None Remove="Configuration\fontpicker.js" />
|
|
||||||
<EmbeddedResource Include="Configuration\fontpicker.js" />
|
<None Remove="Configuration\*" />
|
||||||
|
<EmbeddedResource Include="Configuration\*" />
|
||||||
|
|
||||||
|
<None Remove="Configuration\controls\*" />
|
||||||
|
<EmbeddedResource Include="Configuration\controls\*" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Jellyfin.Plugin.SkinManager.Configuration;
|
using Jellyfin.Plugin.SkinManager.Configuration;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
@@ -20,32 +21,53 @@ namespace Jellyfin.Plugin.SkinManager
|
|||||||
|
|
||||||
public static Plugin Instance { get; private set; }
|
public static Plugin Instance { get; private set; }
|
||||||
|
|
||||||
public override string Description
|
public override string Description => "Skin Manager";
|
||||||
=> "Skin Manager";
|
|
||||||
|
|
||||||
private readonly Guid _id = new Guid("e9ca8b8e-ca6d-40e7-85dc-58e536df8eb3");
|
private readonly Guid _id = new Guid("e9ca8b8e-ca6d-40e7-85dc-58e536df8eb3");
|
||||||
public override Guid Id => _id;
|
public override Guid Id => _id;
|
||||||
|
|
||||||
|
public PluginConfiguration PluginConfiguration => Configuration;
|
||||||
|
|
||||||
public IEnumerable<PluginPageInfo> GetPages()
|
public IEnumerable<PluginPageInfo> GetPages()
|
||||||
{
|
{
|
||||||
return new[]
|
var pageNames = new[]
|
||||||
{
|
{
|
||||||
new PluginPageInfo
|
"SkinManager",
|
||||||
{
|
"fontpicker.js",
|
||||||
Name = "SkinManager",
|
"fontpicker.css",
|
||||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configurationpage.html"
|
"skin.js",
|
||||||
},
|
"controls.Control.js",
|
||||||
new PluginPageInfo
|
"controls.ColorControl.js",
|
||||||
{
|
"controls.SliderControl.js",
|
||||||
Name = "fontpicker.js",
|
"controls.CheckBoxControl.js",
|
||||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.fontpicker.js"
|
"controls.NumberControl.js",
|
||||||
},
|
"controls.SelectControl.js",
|
||||||
new PluginPageInfo
|
"controls.FontPickerControl.js",
|
||||||
{
|
"controls.TextAreaControl.js",
|
||||||
Name = "fontpicker.css",
|
"controls.Category.js",
|
||||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.jquery.fontpicker.min.css"
|
"colorpicker.js",
|
||||||
}
|
"MainController.js",
|
||||||
|
"style.css",
|
||||||
|
"common.js",
|
||||||
|
"history",
|
||||||
|
"ConfigController.js",
|
||||||
|
"HistoryController.js",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var prefix = GetType().Namespace + ".Configuration.";
|
||||||
|
|
||||||
|
return pageNames.Select(name => new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
EmbeddedResourcePath = name switch
|
||||||
|
{
|
||||||
|
"SkinManager" => prefix + "configurationpage.html",
|
||||||
|
"history" => prefix + "history.html",
|
||||||
|
"fontpicker.css" => prefix + "jquery.fontpicker.min.css",
|
||||||
|
_ => prefix + name
|
||||||
|
},
|
||||||
|
EnableInMainMenu = name.Equals("SkinManager", StringComparison.OrdinalIgnoreCase)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
skins-4.0.json
Normal file
126
skins-4.0.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"skins": [
|
||||||
|
{
|
||||||
|
"name": "JellySkin",
|
||||||
|
"description": "A modern, customizable skin for Jellyfin.",
|
||||||
|
"css": "@import url('https://cdn.jsdelivr.net/npm/jellyskin@latest/dist/main.css');",
|
||||||
|
"previews": [
|
||||||
|
{
|
||||||
|
"name": "Login Page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Home/Index Page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Library Page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/3.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Title page",
|
||||||
|
"url": "https://raw.githubusercontent.com/danieladov/jellyfin-plugin-skin-manager/master/src/img/Default/4.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Default",
|
||||||
|
"controls": [
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "Background Color",
|
||||||
|
"description": "Set the background color of the skin",
|
||||||
|
"id": "bgColor",
|
||||||
|
"default": "#ffffff",
|
||||||
|
"css": ":root { --bg-color: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "Text Color",
|
||||||
|
"id": "textColor",
|
||||||
|
"default": "#000000",
|
||||||
|
"css": ":root { --text-color: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"label": "Font Size",
|
||||||
|
"id": "fontSize",
|
||||||
|
"min": 10,
|
||||||
|
"max": 30,
|
||||||
|
"default": 16,
|
||||||
|
"css": ":root { --font-size: %value%px; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkbox",
|
||||||
|
"label": "Dark Mode",
|
||||||
|
"description": "Enable dark mode",
|
||||||
|
"id": "checkbox",
|
||||||
|
"default": true,
|
||||||
|
"css": ":root { --dark-mode: %value%; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Border Radius",
|
||||||
|
"description": "Set the border radius",
|
||||||
|
"id": "borderRadius",
|
||||||
|
"min": 0,
|
||||||
|
"max": 50,
|
||||||
|
"default": 0,
|
||||||
|
"css": ":root { --"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Font Family",
|
||||||
|
"description": "Select the font family",
|
||||||
|
"id": "fontFamily",
|
||||||
|
"default": "Arial",
|
||||||
|
"css": ":root { --font-family: %value%; }",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Arial",
|
||||||
|
"value": "Arial"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Verdana",
|
||||||
|
"value": "Verdana"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Georgia",
|
||||||
|
"value": "Georgia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Times New Roman",
|
||||||
|
"value": "Times New Roman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Trebuchet MS",
|
||||||
|
"value": "Trebuchet MS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Arial Black",
|
||||||
|
"value": "Arial Black"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Impact",
|
||||||
|
"value": "Impact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Comic Sans MS",
|
||||||
|
"value": "Comic Sans MS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Courier New",
|
||||||
|
"value": "Courier New"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Lucida Console",
|
||||||
|
"value": "Lucida Console"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user