diff --git a/README.md b/README.md
index 1d15bba..52a3f10 100644
--- a/README.md
+++ b/README.md
@@ -117,9 +117,11 @@ The features in KefinTweaks which use local data caching are listed below, along
- ✅ [Watched History](https://features.jellyfin.org/posts/633/watched-history)
- ✅ [On-Demand Subtitle Search in Video OSD](https://features.jellyfin.org/posts/3385/on-demand-subtitle-search)
- ✅ [Add genres and recommendations to home screen](https://features.jellyfin.org/posts/3501/add-genres-and-recommendations-to-home-screen)
+- ✅ [New or Improved Home Screen Sections](https://features.jellyfin.org/posts/1986/new-or-improved-home-secreen-sections)
+- ✅ [Custom Carousel Options on Home Screen](https://features.jellyfin.org/posts/1439/custom-carousel-options-on-home-screen)
- ✅ [Improved Playlist UX](https://features.jellyfin.org/posts/2823/playlist-moviesummary)
- ✅ [Flattening TV Shows with 1 season](https://features.jellyfin.org/posts/8/flattening-tv-shows) [[2]](https://features.jellyfin.org/posts/3352/add-option-to-flatten-single-season-tv-shows-skip-season-level)
-- ✅ [List all collections that an item belongs to on the details page](https://features.jellyfin.org/posts/540/list-all-collections-that-a-movie-belong-to-in-movie-details) [[2]](https://features.jellyfin.org/posts/3540/collection-data-present-on-content-within-the-collection)
+- ✅ [List all collections that an item belongs to on the details page](https://features.jellyfin.org/posts/540/list-all-collections-that-a-movie-belong-to-in-movie-details) [[2]](https://features.jellyfin.org/posts/3540/collection-data-present-on-content-within-the-collection) [[3]](https://features.jellyfin.org/posts/3549/show-collections-on-movie-page)
- ✅ [Custom Skins/Themes](https://features.jellyfin.org/posts/2509/themes-skins-or-clone-plex-layout-to-convert-the-rest-of-the-plex-and-emby-user-base-to-jellyfin) [[2]](https://features.jellyfin.org/posts/2616/theme-skins)
- ✅ [TV Season Selection at the Series level](https://forum.jellyfin.org/t-tv-season-selection-when-browsing-seasons)
@@ -183,6 +185,9 @@ Creates sections for movies and episodes released in the last 7 days (trending s
- **Infinite Discovery Sections**:
Load discovery sections based on items you've watched and favorited, as well as from genres or top people in your library
+- **HSS Support**:
+Full compatibility with [Home Screen Sections](https://github.com/IAmParadox27/jellyfin-plugin-home-sections) plugin by [IAmParadox27](https://github.com/IAmParadox27/)
+
- **Seasonal Content**:
Seasonally-themed sections (Halloween, Christmas, etc.)
@@ -269,6 +274,9 @@ Adds a card overlay button to remove items from Continue Watching for all resuma
+#### **Collection Sorting**:
+Adds the ability to sort collection items on the Collection page. Supports sorting by Title, Release Date, Date Added, Community Rating, Critic Rating.
+
### Navigation Improvements
#### **Header Tab Enhancements**:
@@ -413,6 +421,7 @@ Allow users to also customize various configuration options through the UI
### Version History
+- **v0.2.3**: New Feature: Collection Sorting, general fixes
- **v0.2.2**: Add support for skin server versions, fixes for HeaderTabs
- **v0.2.1**: Caching improvements for Watchlist and Top People, Mobile UI fixes and Flatten TV Shows improvements
- **v0.2.0**: New Features: Skin Manager, Collections on Details Page, Flatten TV Shows
diff --git a/custom-menu.js b/custom-menu.js
deleted file mode 100644
index 2455e48..0000000
--- a/custom-menu.js
+++ /dev/null
@@ -1,99 +0,0 @@
-// Custom Menu Script
-// Removes target="_blank" from custom menu links to prevent opening in new tabs
-// Ensures all custom menu links open in the same tab for better user experience
-
-(function() {
- 'use strict';
-
- // Common logging function
- const LOG = (...args) => console.log('[CustomMenu]', ...args);
- const WARN = (...args) => console.warn('[CustomMenu]', ...args);
- const ERR = (...args) => console.error('[CustomMenu]', ...args);
-
- LOG('Script loaded - JS Injector mode');
-
- function initMenuWatcher() {
- const menuContainer = document.querySelector('.customMenuOptions');
- if (!menuContainer) return false;
-
- LOG('Custom menu container found, initializing watcher');
-
- const cleanLinks = (node) => {
- if (node.matches?.('a.navMenuOption[target="_blank"]')) {
- LOG('Removing target="_blank" from menu link');
- node.removeAttribute('target');
- }
- node.querySelectorAll?.('a.navMenuOption[target="_blank"]').forEach(link => {
- LOG('Removing target="_blank" from nested menu link');
- link.removeAttribute('target');
- });
- };
-
- // Clean existing links
- cleanLinks(menuContainer);
-
- // Watch for new/changed nodes
- const observer = new MutationObserver((mutations) => {
- for (const mutation of mutations) {
- mutation.addedNodes.forEach(node => cleanLinks(node));
- if (mutation.type === 'attributes' && mutation.target.matches('a.navMenuOption')) {
- cleanLinks(mutation.target);
- }
- }
- });
-
- observer.observe(menuContainer, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['target']
- });
-
- LOG('Menu watcher initialized successfully');
- return true;
- }
-
- // Watch for menu container to appear
- const waitForMenu = () => {
- const menuContainer = document.querySelector('.customMenuOptions');
- if (menuContainer) {
- initMenuWatcher();
- return;
- }
-
- LOG('Custom menu container not found, waiting for it to appear');
-
- // Use MutationObserver to watch for the menu container
- const observer = new MutationObserver((mutations) => {
- for (const mutation of mutations) {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // Check if the added node is the menu or contains it
- if (node.matches?.('.customMenuOptions') || node.querySelector?.('.customMenuOptions')) {
- LOG('Custom menu container detected, initializing watcher');
- observer.disconnect();
- initMenuWatcher();
- return;
- }
- }
- });
- }
- });
-
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
-
- // Fallback timeout after 10 seconds
- setTimeout(() => {
- observer.disconnect();
- WARN('Menu not found after 10 seconds');
- }, 10000);
- };
-
- // Start watching
- waitForMenu();
-
- LOG('Custom menu functionality initialized');
-})();
diff --git a/injector.js b/injector.js
index 65d66c2..96dbe8c 100644
--- a/injector.js
+++ b/injector.js
@@ -6,7 +6,7 @@
'use strict';
console.log('[KefinTweaks Injector] Initializing...');
- const versionNumber = "0.2.2";
+ const versionNumber = "0.2.3";
// Configuration: Start with defaults, then merge user config
// This allows new scripts to work out of the box without requiring config updates
@@ -310,8 +310,15 @@
}
// Get the root path for scripts from configuration
- function getScriptRoot() {
- return window.KefinTweaksConfig?.scriptRoot || 'https://ranaldsgift.github.io/KefinTweaks/';
+ function getScriptRoot() {
+ const configScriptRoot = window.KefinTweaksConfig?.scriptRoot;
+
+ // Handle legacy configuration pointing to the root directory
+ if (configScriptRoot === 'https://ranaldsgift.github.io/KefinTweaks/') {
+ return 'https://ranaldsgift.github.io/KefinTweaks/scripts/';
+ }
+
+ return window.KefinTweaksConfig?.scriptRoot || 'https://ranaldsgift.github.io/KefinTweaks/scripts/';
}
// Load a CSS file
diff --git a/kefinTweaks.js b/kefinTweaks.js
index 32eae30..eccc7da 100644
--- a/kefinTweaks.js
+++ b/kefinTweaks.js
@@ -33,10 +33,12 @@
// - Search: Enable Jellyseerr integration adds an option to search for Jellyseerr results only
//
// Considerations for the home screen sections:
-// 1. Custom sections (order 1-29) - Your playlist-based sections
-// 2. New and Trending sections (order 30-49) - Newest movies and episodes
-// 3. Seasonal sections (order 50+) - Holiday-themed content
-// 4. Discovery sections (order 200+) - Intelligent recommendations based on your library
+// - Default Jellyfin sections (order 1-10)
+// - New releases sections (order 30-49) - Newest movies and episodes
+// - Seasonal sections (order 50-59) - Holiday-themed content
+// - One-off sections: Watchlist, TV Networks, section (order 60-69) - Your watchlist
+// - Discovery sections (order 1000+) - Intelligent recommendations based on your library
+// - Custom sections can be placed anywhere in the order by using the order field in the configuration
(function() {
'use strict';
@@ -50,7 +52,8 @@
// SCRIPT ROOT CONFIGURATION
// ============================================================================
// Configure where scripts are loaded from
- scriptRoot: 'https://ranaldsgift.github.io/KefinTweaks/',
+ kefinTweaksRoot: 'https://ranaldsgift.github.io/KefinTweaks/',
+ scriptRoot: 'https://ranaldsgift.github.io/KefinTweaks/scripts/',
// ============================================================================
// SCRIPT ENABLE/DISABLE FLAGS
@@ -117,7 +120,7 @@
enableDiscovery: true, // Enables discovery sections on the home screen
// Discovery section behavior and filtering
- enableInfiniteScroll: false, // Use infinite scroll vs Load More button
+ enableInfiniteScroll: true, // Use infinite scroll vs Load More button
minPeopleAppearances: 10, // Minimum movie appearances for people to be featured
minGenreMovieCount: 50, // Minimum movie count for genres to be included
minimumShowsForNetwork: 5, // Minimum show count for TV networks to be featured
@@ -297,7 +300,7 @@
// ============================================================================
// Load the injector script which handles loading individual scripts based on configuration
const script = document.createElement("script");
- script.src = `${KEFIN_TWEAKS_CONFIG.scriptRoot}injector.js?v=${new Date().getTime()}`;
+ script.src = `${KEFIN_TWEAKS_CONFIG.kefinTweaksRoot}injector.js?v=${new Date().getTime()}`;
script.async = true;
document.head.appendChild(script);
diff --git a/kefinTweaks.minimal.js b/kefinTweaks.minimal.js
new file mode 100644
index 0000000..47c7fa5
--- /dev/null
+++ b/kefinTweaks.minimal.js
@@ -0,0 +1,63 @@
+(function() {
+ 'use strict';
+
+ const KEFIN_TWEAKS_CONFIG = {
+ kefinTweaksRoot: 'https://ranaldsgift.github.io/KefinTweaks/',
+ scriptRoot: 'https://ranaldsgift.github.io/KefinTweaks/scripts/',
+
+ scripts: {
+ collections: true,
+ homeScreen: true,
+ search: true,
+ watchlist: true,
+ headerTabs: true,
+ customMenuLinks: true,
+ breadcrumbs: true,
+ playlist: true,
+ itemDetailsCollections: true,
+ flattenSingleSeasonShows: true,
+ exclusiveElsewhere: false,
+ backdropLeakFix: true,
+ dashboardButtonFix: true,
+ infiniteScroll: true,
+ removeContinue: true,
+ subtitleSearch: true,
+ skinManager: true
+ },
+ homeScreen: {
+ enableNewAndTrending: true,
+ enableNewMovies: true,
+ enableNewEpisodes: true,
+ enableTrending: false,
+ enableDiscovery: true,
+ enableInfiniteScroll: true,
+ minPeopleAppearances: 10,
+ minGenreMovieCount: 50,
+ minimumShowsForNetwork: 5,
+ enableWatchlist: true,
+ enableSeasonal: true,
+ seasonalItemLimit: 16,
+ customSections: [],
+ },
+ exclusiveElsewhere: {
+ hideServerName: false
+ },
+ search: {
+ enableJellyseerr: false
+ },
+ skins: [],
+ defaultSkin: null,
+ themes: [],
+ customMenuLinks: [],
+ };
+
+ window.KefinTweaksConfig = KEFIN_TWEAKS_CONFIG;
+
+ const script = document.createElement("script");
+ script.src = `${KEFIN_TWEAKS_CONFIG.kefinTweaksRoot}injector.js?v=${new Date().getTime()}`;
+ script.async = true;
+ document.head.appendChild(script);
+
+ console.log('[KefinTweaks] Central configuration loaded. Available at window.KefinTweaksConfig');
+})();
+
diff --git a/backdropLeakFix.js b/scripts/backdropLeakFix.js
similarity index 100%
rename from backdropLeakFix.js
rename to scripts/backdropLeakFix.js
diff --git a/breadcrumbNav.css b/scripts/breadcrumbNav.css
similarity index 100%
rename from breadcrumbNav.css
rename to scripts/breadcrumbNav.css
diff --git a/breadcrumbs.js b/scripts/breadcrumbs.js
similarity index 100%
rename from breadcrumbs.js
rename to scripts/breadcrumbs.js
diff --git a/cardBuilder.js b/scripts/cardBuilder.js
similarity index 100%
rename from cardBuilder.js
rename to scripts/cardBuilder.js
diff --git a/collections.js b/scripts/collections.js
similarity index 99%
rename from collections.js
rename to scripts/collections.js
index 35b178a..5298718 100644
--- a/collections.js
+++ b/scripts/collections.js
@@ -547,7 +547,7 @@
// Poll for section title (up to 5 seconds, every 500ms)
const sectionTitle = await pollForElement(() => {
return collectionItemsContainer.querySelector('.sectionTitle');
- }, 5000, 500);
+ }, 5000, 100);
if (!sectionTitle) {
WARN('Section title not found after polling for 5 seconds');
diff --git a/customMenuLinks.js b/scripts/customMenuLinks.js
similarity index 100%
rename from customMenuLinks.js
rename to scripts/customMenuLinks.js
diff --git a/dashboardButtonFix.js b/scripts/dashboardButtonFix.js
similarity index 100%
rename from dashboardButtonFix.js
rename to scripts/dashboardButtonFix.js
diff --git a/defaultSkin.css b/scripts/defaultSkin.css
similarity index 92%
rename from defaultSkin.css
rename to scripts/defaultSkin.css
index 481d065..e8873e5 100644
--- a/defaultSkin.css
+++ b/scripts/defaultSkin.css
@@ -31,4 +31,8 @@ div#seriesScheduleSection {
.headerRight .headerAppearanceButton {
display: none;
}
+}
+
+.itemBackdrop {
+ pointer-events: none;
}
\ No newline at end of file
diff --git a/scripts/deviceManager.js b/scripts/deviceManager.js
new file mode 100644
index 0000000..e36a7e5
--- /dev/null
+++ b/scripts/deviceManager.js
@@ -0,0 +1,392 @@
+// Device Manager
+// Adds a "Remove Device" button to active device sessions on the dashboard page
+// Allows users to remove devices via a custom endpoint
+
+(function() {
+ 'use strict';
+
+ // Logging functions
+ const LOG = (...args) => console.log('[DeviceManager]', ...args);
+ const WARN = (...args) => console.warn('[DeviceManager]', ...args);
+ const ERR = (...args) => console.error('[DeviceManager]', ...args);
+
+ LOG('Initializing Device Manager');
+
+ // Polling interval references
+ let pollingInterval = null;
+ let continuousPollingInterval = null;
+
+ // Wait for KefinTweaksUtils to become available
+ async function waitForUtils(maxWaitTime = 30000, checkInterval = 100) {
+ const startTime = Date.now();
+
+ while (Date.now() - startTime < maxWaitTime) {
+ if (window.KefinTweaksUtils && window.KefinTweaksUtils.onViewPage) {
+ LOG('KefinTweaksUtils is now available');
+ return true;
+ }
+
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
+ }
+
+ ERR('KefinTweaksUtils not available after waiting. Device Manager requires utils.js');
+ return false;
+ }
+
+ // Initialize the device manager after utils are available
+ async function initializeDeviceManager() {
+ // Wait for utils to be available
+ const utilsAvailable = await waitForUtils();
+ if (!utilsAvailable) {
+ return;
+ }
+
+ // Register handler for dashboard page
+ window.KefinTweaksUtils.onViewPage((view, element) => {
+ // Check if we're on the dashboard page
+ const isDashboardPage = view === 'dashboard' ||
+ view === 'dashboardPage' ||
+ window.location.hash.includes('dashboard') ||
+ document.querySelector('#dashboardPage:not(.hide)');
+
+ if (!isDashboardPage) {
+ // Not on dashboard, clear polling intervals
+ if (pollingInterval) {
+ clearInterval(pollingInterval);
+ pollingInterval = null;
+ }
+ if (continuousPollingInterval) {
+ clearInterval(continuousPollingInterval);
+ continuousPollingInterval = null;
+ }
+ return;
+ }
+
+ LOG('Dashboard page detected, initializing device manager');
+
+ // Start polling for activeDevicesContainer
+ initializeDashboard();
+ }, {
+ pages: ['dashboard', 'dashboardPage']
+ });
+
+ LOG('Device Manager initialized');
+ }
+
+ // Check if ApiClient is available
+ function isApiClientAvailable() {
+ return window.ApiClient &&
+ typeof window.ApiClient.accessToken === 'function' &&
+ typeof window.ApiClient.serverAddress === 'function' &&
+ typeof window.ApiClient.getSessions === 'function';
+ }
+
+ // Get session data by session ID
+ async function getSessionData(sessionId, sessions) {
+ try {
+ if (!isApiClientAvailable()) {
+ throw new Error('ApiClient not available or getSessions method not found');
+ }
+
+ if (!sessions || !Array.isArray(sessions)) {
+ throw new Error('Failed to get sessions or sessions is not an array');
+ }
+
+ LOG(`Retrieved ${sessions.length} sessions, filtering for session ID: ${sessionId}`);
+
+ // Filter sessions to find the one matching our session ID
+ const sessionData = sessions.find(session => session.Id === sessionId);
+
+ if (!sessionData) {
+ WARN(`Session with ID ${sessionId} not found in sessions list`);
+ return null;
+ }
+
+ return sessionData;
+ } catch (error) {
+ ERR('Error fetching session data:', error);
+ return null;
+ }
+ }
+
+ // Remove device via custom endpoint
+ async function removeDevice(deviceId) {
+ try {
+ if (!isApiClientAvailable()) {
+ throw new Error('ApiClient not available');
+ }
+
+ const token = window.ApiClient.accessToken();
+ if (!token) {
+ throw new Error('Access token not available');
+ }
+
+ LOG(`Removing device: ${deviceId}`);
+
+ const serverAddress = window.ApiClient.serverAddress();
+ if (!serverAddress) {
+ throw new Error('Server address not available');
+ }
+
+ const response = await fetch(`${serverAddress}/Devices?Id=${encodeURIComponent(deviceId)}`, {
+ method: 'DELETE',
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `MediaBrowser Token="${token}"`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ LOG('Device removed successfully');
+ return true;
+ } catch (error) {
+ ERR('Error removing device:', error);
+ return false;
+ }
+ }
+
+ // Create remove device button
+ function createRemoveDeviceButton(deviceId) {
+ const button = document.createElement('button');
+ button.setAttribute('is', 'paper-icon-button-light');
+ button.className = 'sessionCardButton btnSessionRemoveDevice paper-icon-button-light';
+ button.setAttribute('title', 'Remove Device');
+
+ const iconSpan = document.createElement('span');
+ iconSpan.className = 'material-icons';
+ iconSpan.setAttribute('aria-hidden', 'true');
+ iconSpan.textContent = 'delete';
+
+ button.appendChild(iconSpan);
+
+ // Add click handler
+ button.addEventListener('click', async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (!confirm('Are you sure you want to remove this device?')) {
+ return;
+ }
+
+ button.disabled = true;
+ const originalTitle = button.getAttribute('title');
+ button.setAttribute('title', 'Removing...');
+
+ const success = await removeDevice(deviceId);
+
+ if (success) {
+ // Remove the session container from the DOM
+ const sessionContainer = button.closest('.activeSession');
+ if (sessionContainer) {
+ sessionContainer.style.transition = 'opacity 0.3s';
+ sessionContainer.style.opacity = '0';
+ setTimeout(() => {
+ sessionContainer.remove();
+ }, 300);
+ }
+
+ // Show success message if toaster is available
+ if (window.KefinTweaksToaster) {
+ window.KefinTweaksToaster.show('Device removed successfully', 'success');
+ }
+ } else {
+ button.disabled = false;
+ button.setAttribute('title', originalTitle);
+
+ // Show error message if toaster is available
+ if (window.KefinTweaksToaster) {
+ window.KefinTweaksToaster.show('Failed to remove device', 'error');
+ }
+ }
+ });
+
+ return button;
+ }
+
+ // Process a single session container
+ async function processSessionContainer(container, sessions) {
+ // Check if button already exists
+ const existingButton = container.querySelector('.btnSessionRemoveDevice');
+ if (existingButton) {
+ LOG('Remove device button already exists for this session');
+ return;
+ }
+
+ // Get session ID from container ID
+ const containerId = container.id;
+ if (!containerId || !containerId.startsWith('session')) {
+ WARN('Container ID does not match expected format:', containerId);
+ return;
+ }
+
+ const sessionId = containerId.substring(7); // Remove "session" prefix
+ if (!sessionId) {
+ WARN('Could not extract session ID from container ID:', containerId);
+ return;
+ }
+
+ LOG(`Processing session container with ID: ${sessionId}`);
+
+ // Find the sessionCardButtons container
+ const buttonsContainer = container.querySelector('.sessionCardButtons');
+ if (!buttonsContainer) {
+ WARN('sessionCardButtons container not found');
+ return;
+ }
+
+ // Fetch session data to get device ID
+ const sessionData = await getSessionData(sessionId, sessions);
+ if (!sessionData || !sessionData.DeviceId) {
+ WARN('Could not get device ID from session data');
+ return;
+ }
+
+ const deviceId = sessionData.DeviceId;
+ LOG(`Found device ID: ${deviceId}`);
+
+ // Create and add remove device button
+ const removeButton = createRemoveDeviceButton(deviceId);
+ buttonsContainer.appendChild(removeButton);
+
+ LOG('Remove device button added successfully');
+ }
+
+ // Process all active session containers on the dashboard
+ async function processActiveSessions() {
+ const dashboardPage = document.querySelector('#dashboardPage:not(.hide)');
+ if (!dashboardPage) {
+ return;
+ }
+
+ const activeDevicesContainer = dashboardPage.querySelector('.activeDevices');
+ if (!activeDevicesContainer) {
+ return;
+ }
+
+ const activeSessions = activeDevicesContainer.querySelectorAll('.activeSession');
+ LOG(`Found ${activeSessions.length} active session containers`);
+
+ if (activeSessions.length === 0) {
+ return;
+ }
+
+ let sessions = null;
+
+ if (window.ApiClient && window.ApiClient.getSessions) {
+ sessions = await window.ApiClient.getSessions();
+ }
+
+ if (!sessions) {
+ WARN('Failed to get sessions');
+ return;
+ }
+
+ // Process each session container
+ for (const sessionContainer of activeSessions) {
+ await processSessionContainer(sessionContainer, sessions);
+ }
+ }
+
+ // Poll for activeDevicesContainer and process sessions
+ function pollForActiveDevicesContainer(maxAttempts = 50, interval = 500) {
+ // Clear any existing intervals
+ if (pollingInterval) {
+ clearInterval(pollingInterval);
+ }
+ if (continuousPollingInterval) {
+ clearInterval(continuousPollingInterval);
+ }
+
+ let attempts = 0;
+
+ pollingInterval = setInterval(async () => {
+ attempts++;
+
+ const dashboardPage = document.querySelector('#dashboardPage:not(.hide)');
+ if (!dashboardPage) {
+ if (attempts >= maxAttempts) {
+ clearInterval(pollingInterval);
+ pollingInterval = null;
+ LOG('Dashboard page not found after max attempts');
+ }
+ return;
+ }
+
+ const activeDevicesContainer = dashboardPage.querySelector('.activeDevices');
+ if (!activeDevicesContainer) {
+ if (attempts >= maxAttempts) {
+ clearInterval(pollingInterval);
+ pollingInterval = null;
+ LOG('Active devices container not found after max attempts');
+ }
+ return;
+ }
+
+ // Found the container, check for active sessions
+ clearInterval(pollingInterval);
+ pollingInterval = null;
+ LOG('Active devices container found, checking for sessions');
+
+ // Check if there are any active session containers
+ const activeSessions = activeDevicesContainer.querySelectorAll('.activeSession');
+
+ if (activeSessions.length > 0) {
+ LOG(`Found ${activeSessions.length} active session containers, processing and stopping polling`);
+
+ // Process all existing sessions
+ await processActiveSessions();
+
+ // Stop polling once we've found and processed sessions
+ LOG('Sessions processed, polling stopped');
+ return;
+ }
+
+ // No sessions found yet, continue polling periodically
+ if (!continuousPollingInterval) {
+ continuousPollingInterval = setInterval(async () => {
+ const currentDashboardPage = document.querySelector('#dashboardPage:not(.hide)');
+ if (!currentDashboardPage) {
+ // Dashboard page no longer visible, stop continuous polling
+ clearInterval(continuousPollingInterval);
+ continuousPollingInterval = null;
+ // Restart initial polling
+ pollForActiveDevicesContainer();
+ return;
+ }
+
+ const currentActiveDevicesContainer = currentDashboardPage.querySelector('.activeDevices');
+ if (!currentActiveDevicesContainer) {
+ return;
+ }
+
+ const currentActiveSessions = currentActiveDevicesContainer.querySelectorAll('.activeSession');
+ if (currentActiveSessions.length > 0) {
+ LOG(`Found ${currentActiveSessions.length} active session containers, processing and stopping polling`);
+
+ // Stop continuous polling
+ clearInterval(continuousPollingInterval);
+ continuousPollingInterval = null;
+
+ // Process all sessions
+ await processActiveSessions();
+
+ LOG('Sessions processed, polling stopped');
+ }
+ }, interval * 2); // Poll less frequently after initial find
+ }
+ }, interval);
+ }
+
+ // Main initialization
+ function initializeDashboard() {
+ // Start polling for activeDevicesContainer
+ pollForActiveDevicesContainer();
+ }
+
+ // Start initialization
+ initializeDeviceManager();
+})();
+
diff --git a/exclusiveElsewhere.js b/scripts/exclusiveElsewhere.js
similarity index 100%
rename from exclusiveElsewhere.js
rename to scripts/exclusiveElsewhere.js
diff --git a/flattenSingleSeasonShows.js b/scripts/flattenSingleSeasonShows.js
similarity index 100%
rename from flattenSingleSeasonShows.js
rename to scripts/flattenSingleSeasonShows.js
diff --git a/headerTabs.js b/scripts/headerTabs.js
similarity index 100%
rename from headerTabs.js
rename to scripts/headerTabs.js
diff --git a/homeScreen.css b/scripts/homeScreen.css
similarity index 100%
rename from homeScreen.css
rename to scripts/homeScreen.css
diff --git a/homeScreen.js b/scripts/homeScreen.js
similarity index 100%
rename from homeScreen.js
rename to scripts/homeScreen.js
diff --git a/indexedDBCache.js b/scripts/indexedDBCache.js
similarity index 100%
rename from indexedDBCache.js
rename to scripts/indexedDBCache.js
diff --git a/infiniteScroll.js b/scripts/infiniteScroll.js
similarity index 100%
rename from infiniteScroll.js
rename to scripts/infiniteScroll.js
diff --git a/itemDetailsCollections.js b/scripts/itemDetailsCollections.js
similarity index 100%
rename from itemDetailsCollections.js
rename to scripts/itemDetailsCollections.js
diff --git a/localStorageCache.js b/scripts/localStorageCache.js
similarity index 100%
rename from localStorageCache.js
rename to scripts/localStorageCache.js
diff --git a/modal.css b/scripts/modal.css
similarity index 100%
rename from modal.css
rename to scripts/modal.css
diff --git a/modal.js b/scripts/modal.js
similarity index 100%
rename from modal.js
rename to scripts/modal.js
diff --git a/playlist.js b/scripts/playlist.js
similarity index 100%
rename from playlist.js
rename to scripts/playlist.js
diff --git a/removeContinue.js b/scripts/removeContinue.js
similarity index 100%
rename from removeContinue.js
rename to scripts/removeContinue.js
diff --git a/search.css b/scripts/search.css
similarity index 100%
rename from search.css
rename to scripts/search.css
diff --git a/search.js b/scripts/search.js
similarity index 100%
rename from search.js
rename to scripts/search.js
diff --git a/skinManager.js b/scripts/skinManager.js
similarity index 100%
rename from skinManager.js
rename to scripts/skinManager.js
diff --git a/subtitleSearch.css b/scripts/subtitleSearch.css
similarity index 100%
rename from subtitleSearch.css
rename to scripts/subtitleSearch.css
diff --git a/subtitleSearch.js b/scripts/subtitleSearch.js
similarity index 100%
rename from subtitleSearch.js
rename to scripts/subtitleSearch.js
diff --git a/toaster.js b/scripts/toaster.js
similarity index 100%
rename from toaster.js
rename to scripts/toaster.js
diff --git a/updoot.js b/scripts/updoot.js
similarity index 100%
rename from updoot.js
rename to scripts/updoot.js
diff --git a/userConfig.js b/scripts/userConfig.js
similarity index 100%
rename from userConfig.js
rename to scripts/userConfig.js
diff --git a/utils.js b/scripts/utils.js
similarity index 99%
rename from utils.js
rename to scripts/utils.js
index 27b70b4..b4d8760 100644
--- a/utils.js
+++ b/scripts/utils.js
@@ -195,7 +195,6 @@
// Get the item promise once - will be shared across all handlers
// The promise is cached/fetched by getCurrentItem(), ensuring only one API call
- LOG('Getting current item for ID:', currentItemId);
const itemPromise = getCurrentItem();
handlers.forEach((config) => {
diff --git a/watchlist.css b/scripts/watchlist.css
similarity index 100%
rename from watchlist.css
rename to scripts/watchlist.css
diff --git a/watchlist.js b/scripts/watchlist.js
similarity index 100%
rename from watchlist.js
rename to scripts/watchlist.js