Changed plugin to use js and css from jsdelivr rather than embedded directly. Added setting for user to specify which release should be used (or "latest", default)

This commit is contained in:
IAmParadox27 committed 2025-08-20 01:03:50 +01:00
1 parent 6f432ec95c
commit f4851d4d1c
7 files changed
+17 -5037

No files matched your search

@@ -12,6 +12,8 @@ namespace Jellyfin.Plugin.MediaBar.Configuration
{
public MediaBarState Enabled { get; set; } = MediaBarState.Enabled;
public string VersionString { get; set; } = "latest";
public bool UseAvatarsFile { get; set; } = true;
public string AvatarsPlaylist { get; set; } = string.Empty;
@@ -28,6 +28,12 @@
</select>
<span>Whether the media bar should appear or not.</span>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="txtVersionString">Version</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="txtVersionString"></label>
<input id="txtVersionString" type="text" is="emby-input" class="emby-input" data-id="txtVersionString" />
<div class="fieldDescription">Which version of the jsdelivr'd plugin should be used.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input type="checkbox" id="chkUseAvatarsFile" is="emby-checkbox" class="emby-checkbox" data-id="chkUseAvatarsFile" />
@@ -65,6 +71,7 @@
Enabled: document.querySelector("[data-id=enabledSelect]").value,
UseAvatarsFile: document.querySelector("[data-id=chkUseAvatarsFile]").checked,
AvatarsPlaylist: document.querySelector("[data-id=txtAvatarsPlaylist]").value,
VersionString: document.querySelector("[data-id=txtVersionString]").value,
};
window.ApiClient.updatePluginConfiguration(MediaBarConfig.pluginId, config)
@@ -83,6 +90,7 @@
document.querySelector("[data-id=enabledSelect]").value = config.Enabled;
document.querySelector("[data-id=chkUseAvatarsFile]").checked = config.UseAvatarsFile;
document.querySelector("[data-id=txtAvatarsPlaylist]").value = config.AvatarsPlaylist;
document.querySelector("[data-id=txtVersionString]").value = config.VersionString;
})
.catch(function (error) {
console.error(error);
@@ -88,7 +88,11 @@ namespace Jellyfin.Plugin.MediaBar.Helpers
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(MediaBarPlugin).Namespace}.Inject.index.html")!;
using TextReader reader = new StreamReader(stream);
string regex = Regex.Replace(payload.Contents!, "(</body>)", $"{reader.ReadToEnd()}$1");
string importedHtml = reader
.ReadToEnd()
.Replace("{{Config.VersionString}}", MediaBarPlugin.Instance.Configuration.VersionString);
string regex = Regex.Replace(payload.Contents!, "(</head>)", $"{importedHtml}$1");
return regex;
}
@@ -1,3 +1,2 @@
<link rel="preload" href="/MediaBar/slideshowpure.css" as="style" />
<link rel="stylesheet" href="/MediaBar/slideshowpure.css" />
<script defer src="/MediaBar/slideshowpure.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/MakD/Jellyfin-Media-Bar@{{Config.VersionString}}/slideshowpure.css" />
<script defer src="https://cdn.jsdelivr.net/gh/MakD/Jellyfin-Media-Bar@{{Config.VersionString}}/slideshowpure.js"></script>
File diff suppressed because it is too large. Load diff
@@ -1,599 +0,0 @@
const getJellyfinCredentials = () => {
const jellyfinCreds = localStorage.getItem("jellyfin_credentials");
const deviceID = localStorage.getItem("_deviceId2");
if (!jellyfinCreds) {
console.error("No Jellyfin credentials found in localStorage");
return;
}
try {
const serverCredentials = JSON.parse(jellyfinCreds);
const firstServer = serverCredentials?.Servers?.[0];
if (!firstServer || !firstServer.AccessToken || !firstServer.UserId) {
console.error("Invalid Jellyfin credentials structure");
return;
}
return {
token: firstServer.AccessToken,
userId: firstServer.UserId,
deviceID
};
} catch (e) {
console.error("Could not parse Jellyfin credentials", e);
}
};
const credentials = getJellyfinCredentials();
if (credentials) {
console.log("Token:", credentials.token, "UserId:", credentials.userId, "DeviceID:", credentials.deviceID);
}
const initLoadingScreen = () => {
const currentPath = window.location.href.toLowerCase();
if (
currentPath.includes("/web/#/home.html") ||
currentPath.includes("/web/index.html#/home.html") ||
currentPath.endsWith("/web/")
) {
const loadingHTML = `
<div class="bar-loading" id="page-loader">
<h1>
<img src="https://raw.githubusercontent.com/jellyfin/jellyfin-ux/refs/heads/master/branding/android/logo_clean.svg"
alt="Server Logo"
style="width: 250px; height: auto;">
</h1>
<div class="docspinner-loader">
<div class="loader-spinner-layer"></div>
</div>
</div>`;
document.body.insertAdjacentHTML("beforeend", loadingHTML);
const interval = setInterval(() => {
if (
document.querySelector(".manualLoginForm") ||
(
document.querySelector(".homeSectionsContainer") &&
document.querySelector(".slide.active")
)
) {
$(".bar-loading").fadeOut(700, () => $(".bar-loading").remove());
clearInterval(interval);
}
}, 100);
}
};
initLoadingScreen();
const observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (window.location.hash.includes("#/home.html")) {
let container = document.getElementById("slides-container");
if (!container) {
container = document.createElement("div");
container.id = "slides-container";
document.body.appendChild(container);
slidesInit();
}
container.style.display = "block";
} else {
const container = document.getElementById("slides-container");
if (container) container.style.display = "none";
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
const slidesInit = () => {
if (window.hasInitializedSlideshow) {
console.log("Slideshow already initialized. Skipping re-init.");
return;
}
window.hasInitializedSlideshow = !0;
const shuffleInterval = 8000;
let isTransitioning = !1;
const listFileName = `${window.location.origin}/web/avatars/list.txt`;
const { token, userId } = getJellyfinCredentials();
const shuffleArray = (array) => array.sort(() => Math.random() - 0.5);
const truncateText = (element, maxLength) => {
let text = element.innerText;
if (text.length > maxLength)
element.innerText = text.substring(0, maxLength) + "...";
};
const createSlideElement = (item, title) => {
const itemId = item.Id;
const plot = item.Overview || "No overview available";
const rating = item.CommunityRating;
const criticRating = item.CriticRating;
const runtime = item.RunTimeTicks;
const genresArray = item.Genres;
const age = item.OfficialRating;
const date = item.PremiereDate;
const season = item.ChildCount;
function createSeparator() {
const separatorHtml =
'<i class="material-icons radio_button_off separator-icon"></i>';
const tempDiv = document.createElement("div");
tempDiv.innerHTML = separatorHtml;
return tempDiv.firstChild;
}
const slide = document.createElement("a");
slide.className = "slide";
slide.target = "_top";
slide.rel = "noreferrer";
slide.tabIndex = 0;
slide.style.display = "none";
const backdrop = document.createElement("img");
backdrop.className = "backdrop";
backdrop.src = `${window.location.origin}/Items/${itemId}/Images/Backdrop/0`;
backdrop.alt = "Backdrop";
backdrop.loading = "eager";
const backdropOverlay = document.createElement("div");
backdropOverlay.className = "backdrop-overlay";
const backdropContainer = document.createElement("div");
backdropContainer.className = "backdrop-container";
backdropContainer.appendChild(backdrop);
backdropContainer.appendChild(backdropOverlay);
const logo = document.createElement("img");
logo.className = "logo";
logo.src = `${window.location.origin}/Items/${itemId}/Images/Logo`;
logo.alt = "Logo";
logo.loading = "eager";
const logoContainer = document.createElement("div");
logoContainer.className = "logo-container";
logoContainer.appendChild(logo);
const featuredContent = document.createElement("div");
featuredContent.className = "featured-content";
featuredContent.textContent = title;
const plotElement = document.createElement("div");
plotElement.className = "plot";
plotElement.textContent = plot;
truncateText(plotElement, 360);
const plotContainer = document.createElement("div");
plotContainer.className = "plot-container";
plotContainer.appendChild(plotElement);
const gradientOverlay = document.createElement("div");
gradientOverlay.className = "gradient-overlay";
const runTimeElement = document.createElement("div");
runTimeElement.className = "runTime";
if (season === undefined) {
const milliseconds = runtime / 10000;
const currentTime = new Date();
const endTime = new Date(currentTime.getTime() + milliseconds);
const options = { hour: "2-digit", minute: "2-digit", hour12: !0 };
const formattedEndTime = endTime.toLocaleTimeString([], options);
runTimeElement.textContent = `Ends at ${formattedEndTime}`;
} else {
runTimeElement.textContent = `${season} Season${season > 1 ? "s" : ""}`;
}
const ratingTest = document.createElement("div");
ratingTest.className = "rating-value";
const imdbLogo = document.createElement("img");
imdbLogo.className = "imdb-logo";
imdbLogo.src =
"https://upload.wikimedia.org/wikipedia/commons/6/69/IMDB_Logo_2016.svg";
imdbLogo.alt = "IMDb Logo";
imdbLogo.style.width = "30px";
imdbLogo.style.height = "30px";
ratingTest.appendChild(imdbLogo);
if (typeof rating === "number") {
const formattedRating = rating.toFixed(1);
ratingTest.innerHTML += `${formattedRating}`;
} else {
ratingTest.innerHTML += `<span style="color: #fff9;">N/A</span>`;
}
ratingTest.appendChild(createSeparator());
const tomatoRatingDiv = document.createElement("div");
tomatoRatingDiv.className = "tomato-rating";
const tomatoLogo = document.createElement("img");
tomatoLogo.className = "tomato-logo";
tomatoLogo.src =
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Rotten_Tomatoes_positive_audience.svg/1920px-Rotten_Tomatoes_positive_audience.svg.png";
const criticLogo = document.createElement("img");
criticLogo.className = "critic-logo";
let valueElement;
if (typeof criticRating === "number") {
valueElement = document.createTextNode(`${criticRating}% `);
} else {
const naSpan = document.createElement("span");
naSpan.textContent = "N/A ";
naSpan.style.color = "#fff9";
valueElement = naSpan;
}
if (criticRating === undefined || criticRating <= 59) {
criticLogo.src =
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Rotten_Tomatoes_rotten.svg/1024px-Rotten_Tomatoes_rotten.svg.png";
criticLogo.alt = "Rotten Tomato";
} else {
criticLogo.src = "https://i.imgur.com/iMfwDk7.png";
criticLogo.alt = "Fresh Tomato";
}
tomatoLogo.style.width = "15px";
tomatoLogo.style.height = "17px";
criticLogo.style.width = "15px";
criticLogo.style.height = "15px";
tomatoRatingDiv.appendChild(tomatoLogo);
tomatoRatingDiv.appendChild(valueElement);
tomatoRatingDiv.appendChild(criticLogo);
tomatoRatingDiv.appendChild(createSeparator());
const ageRatingDiv = document.createElement("div");
ageRatingDiv.className = "age-rating";
if (item.OfficialRating) {
ageRatingDiv.innerHTML = `${item.OfficialRating}`;
} else {
ageRatingDiv.innerHTML = `N/A`;
}
const premiereDate = document.createElement("div");
premiereDate.className = "date";
const year = date ? new Date(date).getFullYear() : NaN;
if (isNaN(year)) {
const naSpan = document.createElement("span");
naSpan.textContent = "N/A";
naSpan.style.color = "#fff9";
premiereDate.innerHTML = "";
premiereDate.appendChild(naSpan);
} else {
premiereDate.textContent = year;
}
ratingTest.appendChild(tomatoRatingDiv);
ratingTest.appendChild(premiereDate);
ratingTest.appendChild(createSeparator());
ratingTest.appendChild(ageRatingDiv);
ratingTest.appendChild(createSeparator());
ratingTest.appendChild(runTimeElement);
function parseGenres(genresArray) {
if (genresArray && genresArray.length > 0) {
return genresArray.slice(0, 3).join(" 🔹 ");
} else {
return "No Genre Available";
}
}
const genreElement = document.createElement("div");
genreElement.className = "genre";
genreElement.innerHTML = parseGenres(genresArray);
const infoContainer = document.createElement("div");
infoContainer.className = "info-container";
infoContainer.appendChild(ratingTest);
const buttonContainer = document.createElement("div");
buttonContainer.className = "button-container";
const playButton = document.createElement("button");
playButton.className = "play-button";
playButton.innerHTML = `
<span class="play-icon"><i class="material-icons">play_circle_fill</i></span>
<span class="play-text">Play</span>
`;
playButton.onclick = async () => {
const apiClient = window.ApiClient;
const userId = apiClient.getCurrentUserId();
const mediabrowser = apiClient.appName();
const appVersion = apiClient.appVersion();
const deviceName = apiClient.deviceName();
const deviceId = apiClient.deviceId();
const accessToken = apiClient.accessToken();
const baseUrl = apiClient.serverAddress();
if (!userId || !mediabrowser || !deviceName || !deviceId || !appVersion || !accessToken || !baseUrl) {
console.error("Details not found.");
return;
}
let sessionId = null;
try {
const response = await fetch(`${baseUrl}/Sessions?deviceId=${encodeURIComponent(deviceId)}`, {
headers: {
'X-Emby-Authorization': `MediaBrowser Client="${mediabrowser}", Device="${deviceName}", DeviceId="${deviceId}", Version="${appVersion}", Token="${accessToken}"`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch session data: ${response.statusText}`);
}
const sessions = await response.json();
if (sessions.length === 0) {
console.warn("No sessions found for deviceId:", deviceId);
return;
}
const session = sessions[0];
sessionId = session.Id;
console.log("Found sessionId:", sessionId);
} catch (error) {
console.error("Error fetching session data:", error);
return;
}
if (!sessionId) {
console.error("Session ID not found.");
return;
}
const playUrl = `${baseUrl}/Sessions/${sessionId}/Playing?playCommand=PlayNow&itemIds=${itemId}`;
try {
const playResponse = await fetch(playUrl, {
method: 'POST',
headers: {
'X-Emby-Authorization': `MediaBrowser Client="${mediabrowser}", Device="${deviceName}", DeviceId="${deviceId}", Version="${appVersion}", Token="${accessToken}"`
}
});
if (!playResponse.ok) {
throw new Error(`Failed to send play command: ${playResponse.statusText}`);
}
console.log("Play command sent successfully to session:", sessionId);
} catch (error) {
console.error("Error sending play command:", error);
}
};
const detailButton = document.createElement("button");
detailButton.className = "detail-button";
detailButton.innerHTML = `
<span class="detail-icon"><i class="material-icons info_outline"></i></span>
<span class="detail-text">Info</span>
`;
detailButton.onclick = () => {
Emby.Page.show(`/details?id=${itemId}`);
}
buttonContainer.appendChild(detailButton);
buttonContainer.appendChild(playButton);
slide.append(
logoContainer,
backdropContainer,
gradientOverlay,
featuredContent,
plotContainer,
infoContainer,
genreElement,
buttonContainer
);
return slide;
};
const createSlideForItem = async (item, title) => {
const container = document.getElementById("slides-container");
const itemId = item.Id;
const slideElement = createSlideElement(item, title);
container.appendChild(slideElement);
if (container.children.length === 1) {
showSlide(0);
}
};
const fetchItemDetails = async (itemId) => {
const response = await fetch(`${window.location.origin}/Items/${itemId}`, {
headers: {
Authorization: `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"`,
},
});
const item = await response.json();
return item;
};
const fetchItemIdsFromList = async () => {
try {
const response = await fetch(listFileName);
if (!response.ok) {
throw new Error("Failed to fetch list.txt");
}
const text = await response.text();
return text
.split("\n")
.map((id) => id.trim())
.filter((id) => id)
.slice(1);
} catch (error) {
console.error("Error fetching list.txt:", error);
return [];
}
};
const fetchItemsFromServer = async () => {
try {
const response = await fetch(
`${window.location.origin}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&SortBy=random&isPlayed=False&Limit=100`,
{
headers: {
Authorization: `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"`,
},
}
);
const data = await response.json();
const items = data.Items;
const movies = items.filter((item) => item.Type === "Movie");
const tvShows = items.filter((item) => item.Type === "Series");
const shuffledMovies = shuffleArray(movies);
const shuffledTvShows = shuffleArray(tvShows);
const selectedMovies = shuffledMovies.slice(0, 15);
const selectedTvShows = shuffledTvShows.slice(0, 15);
const allItems = [];
const maxLength = Math.max(selectedMovies.length, selectedTvShows.length);
for (let i = 0; i < maxLength; i++) {
if (i < selectedMovies.length) allItems.push(selectedMovies[i]);
if (i < selectedTvShows.length) allItems.push(selectedTvShows[i]);
}
return allItems;
} catch (error) {
console.error("Error fetching items:", error);
return [];
}
};
const createSlidesForItems = async (items) => {
await Promise.all(
items.map((item) =>
createSlideForItem(item, item.Type === "Movie" ? "Movie" : "TV Show")
)
);
};
const createPaginationDots = () => {
const dotsContainer = document.createElement("div");
dotsContainer.className = "dots-container";
for (let i = 0; i < 5; i++) {
const dot = document.createElement("span");
dot.className = "dot";
dotsContainer.appendChild(dot);
}
const container = document.getElementById("slides-container");
container.appendChild(dotsContainer);
};
const showSlide = (index) => {
const slides = document.querySelectorAll(".slide");
const dots = document.querySelectorAll(".dot");
slides.forEach((slide, i) => {
if (i === index) {
slide.style.removeProperty("display");
slide.style.display = "block";
slide.offsetHeight;
slide.style.opacity = "1";
slide.classList.add("active");
} else {
slide.style.opacity = "0";
slide.classList.remove("active");
setTimeout(() => {
slide.style.display = "none";
}, 500);
}
document.getElementById("slides-container").offsetHeight;
});
dots.forEach((dot, i) => {
dot.classList.toggle("active", i === index % 5);
});
};
const initializeSlideshow = () => {
var slideInterval = null;
class SlideTimer {
constructor(fnctn, time) {
var timerObject = setInterval(fnctn, time);
this.stop = function () { if (timerObject) { clearInterval(timerObject); timerObject = null; } return this; };
this.start = function () { if (!timerObject) { this.stop(); timerObject = setInterval(fnctn, time); } return this; };
this.restart = function () { return this.stop().start(); };
}
}
const slides = document.querySelectorAll(".slide");
createPaginationDots();
const container = document.getElementById("slides-container");
let currentSlideIndex = 0;
let focusedSlide = null;
let containerFocused = !1;
const updateCurrentSlide = (index) => {
if (isTransitioning) return;
isTransitioning = !0;
currentSlideIndex = (index + slides.length) % slides.length;
showSlide(currentSlideIndex);
slideInterval.restart();
setTimeout(() => {
isTransitioning = !1;
}, 500);
};
const openActiveSlide = () => {
if (focusedSlide) {
window.location.href = focusedSlide.href;
}
};
if (slides.length > 0) {
showSlide(currentSlideIndex);
container.style.display = "block";
slideInterval =
new SlideTimer(function () {
updateCurrentSlide(currentSlideIndex + 1);
}, shuffleInterval);
}
slides.forEach((slide) => {
slide.addEventListener(
"focus",
() => {
focusedSlide = slide;
container.classList.remove("disable-interaction");
},
!0
);
slide.addEventListener(
"blur",
() => {
if (focusedSlide === slide) {
focusedSlide = null;
}
},
!0
);
});
document.addEventListener("keydown", (event) => {
if (containerFocused) {
switch (event.keyCode) {
case 37:
updateCurrentSlide(currentSlideIndex - 1);
break;
case 39:
updateCurrentSlide(currentSlideIndex + 1);
break;
case 13:
case 32:
openActiveSlide();
break;
}
}
});
const leftArrow = document.createElement("div");
const rightArrow = document.createElement("div");
leftArrow.classList.add("arrow", "left-arrow");
rightArrow.classList.add("arrow", "right-arrow");
leftArrow.innerHTML = '<i class="material-icons arrow_back_ios_new"></i>';
rightArrow.innerHTML = '<i class="material-icons arrow_forward_ios"></i>';
container.appendChild(leftArrow);
container.appendChild(rightArrow);
let touchStartX = 0;
let touchEndX = 0;
const minSwipeDistance = 50;
let isTouchOnButton = false;
container.addEventListener(
"touchstart",
(event) => {
touchStartX = event.touches[0].clientX;
isTouchOnButton =
event.target.closest(".play-button, .detail-button") !== null;
},
{ passive: false }
);
container.addEventListener(
"touchmove",
(event) => {
touchEndX = event.touches[0].clientX;
},
{ passive: false }
);
container.addEventListener(
"touchend",
(event) => {
if (isTouchOnButton) return;
const swipeDistance = touchEndX - touchStartX;
if (Math.abs(swipeDistance) > minSwipeDistance) {
event.preventDefault();
event.stopPropagation();
if (swipeDistance < 0) {
updateCurrentSlide(currentSlideIndex + 1);
} else {
updateCurrentSlide(currentSlideIndex - 1);
}
}
},
{ passive: false }
);
leftArrow.addEventListener("click", () =>
updateCurrentSlide(currentSlideIndex - 1)
);
rightArrow.addEventListener("click", () =>
updateCurrentSlide(currentSlideIndex + 1)
);
container.addEventListener("focus", () => {
containerFocused = !0;
});
container.addEventListener("blur", () => {
containerFocused = !1;
});
};
const initializeSlides = async () => {
const itemIds = await fetchItemIdsFromList();
let items;
if (itemIds.length > 0) {
const itemPromises = itemIds.map((id) => fetchItemDetails(id));
items = await Promise.all(itemPromises);
} else {
const allItems = await fetchItemsFromServer();
const itemPromises = allItems.map((item) => fetchItemDetails(item.Id));
items = await Promise.all(itemPromises);
}
items = shuffleArray(items);
await createSlidesForItems(items);
initializeSlideshow();
};
initializeSlides();
};
@@ -21,10 +21,6 @@
<EmbeddedResource Include="Configuration\config.html" />
<None Remove="Inject\index.html" />
<EmbeddedResource Include="Inject\index.html" />
<None Remove="Inject\slideshowpure.css" />
<EmbeddedResource Include="Inject\slideshowpure.css" />
<None Remove="Inject\slideshowpure.js" />
<EmbeddedResource Include="Inject\slideshowpure.js" />
</ItemGroup>
<ItemGroup>