mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 07:42:47 +01:00
Add support for admins to configure KefinTweaks from Jellyfin UI
This commit is contained in:
@@ -13,6 +13,7 @@ I think it's also worth mentioning that before the 1.0 release, you can expect t
|
||||
- [Installation](#-installation)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup Instructions](#setup-instructions)
|
||||
- [Configuration](#configuration)
|
||||
- [Features Overview](#features-overview)
|
||||
- [Data Caching](#data-caching)
|
||||
- [Feature Requests Completed](#-feature-requests-completed)
|
||||
@@ -62,27 +63,70 @@ I think it's also worth mentioning that before the 1.0 release, you can expect t
|
||||
0a. **Install Prerequisites listed above**
|
||||
0b. **If you are using JellyfinEnhanced, you MUST disable the "Watchlist" feature from the plugin settings**
|
||||
1. **Add a new script to your JS Injector Plugin**
|
||||
2. **Copy the entire contents of kefinTweaks.js into the new script**
|
||||
2. **Copy the the code below and paste it into the new script:**
|
||||
|
||||
**Optionally update the "scriptRoot" in the KefinTweaks script if you wish to customize and host these scripts yourself.**
|
||||
```javascript
|
||||
const script = document.createElement("script");
|
||||
script.src = `https://ranaldsgift.github.io/KefinTweaks/injector.js?v=${new Date().getTime()}`;
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
```
|
||||
|
||||
3. **Ensure the script is enabled**
|
||||
4. **Save your changes**
|
||||
3. **Save your changes**
|
||||
|
||||
<div align="center">
|
||||
<img src="pages/images/injector.png" alt="injector" style="max-width: 100%; height: auto;"/>
|
||||
</div>
|
||||
<br/>
|
||||
<hr>
|
||||
|
||||
## Configuration
|
||||
|
||||
KefinTweaks includes a comprehensive configuration interface accessible to administrators. The configuration UI allows you to customize all aspects of KefinTweaks without editing code files.
|
||||
|
||||
### Accessing Configuration
|
||||
|
||||
The configuration interface can be accessed in multiple ways:
|
||||
|
||||
1. **From User Preferences**: Navigate to your User Preferences page and find the "Administration" section. Click the "Configure KefinTweaks" button.
|
||||
2. **From Dashboard**: In the Dashboard side menu, find the "KefinTweaks" button under the Plugins section.
|
||||
3. **From Left Navigation Menu**: A "Configure" link will appear in the left navigation menu beneath a KefinTweaks heading.
|
||||
|
||||
### Configuration Features
|
||||
|
||||
All configuration options for KefinTweaks can be managed directly from the UI.
|
||||
|
||||
- **Feature Toggle**: Toggle individual KefinTweaks features on or off
|
||||
- **Home Screen Configuration**: Configure custom sections, discovery features, and seasonal content
|
||||
- **Search Configuration**: Enable/disable Jellyseerr integration
|
||||
- **Skin Configuration**: Manage available skins, set default skins, and enable/disable specific skins
|
||||
- **Theme Configuration**: Add custom themes available to all users
|
||||
- **Exclusive Elsewhere Configuration**: Configure branding behavior
|
||||
- **Custom Menu Links**: Add custom links to the navigation menu
|
||||
- **Script Root URLs**: Configure custom script hosting locations (advanced)
|
||||
|
||||
### Export and Import
|
||||
|
||||
- **Export**: Export your entire configuration to clipboard as JSON for backup or sharing
|
||||
- **Import**: Import a previously exported configuration to restore settings or apply configurations from other servers
|
||||
|
||||
5. **Required for Watchlist:**
|
||||
Add a new tab to your Custom Tabs plugin with the following content:
|
||||
```html
|
||||
<div class="sections watchlist"></div>
|
||||
```
|
||||
<div align="center">
|
||||
<img src="pages/images/customtab.png" alt="customtab" style="max-width: 100%; height: auto;"/>
|
||||
</div>
|
||||
<br/>
|
||||
<img src="pages/images/config-1.png" alt="Configuration Interface" style="max-width: 100%; height: auto;"/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
<img src="pages/images/config-2.png" alt="Configuration Interface" style="max-width: 100%; height: auto;"/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
### Automatic Setup
|
||||
|
||||
On first load, KefinTweaks automatically:
|
||||
- Creates the `KefinTweaks-Config` script in the JavaScript Injector plugin with default settings
|
||||
- Ensures the Watchlist tab exists in the CustomTabs plugin with the correct HTML structure
|
||||
|
||||
These tasks only run for administrator users and happen automatically in the background.
|
||||
<hr>
|
||||
|
||||
## Features Overview
|
||||
@@ -373,6 +417,7 @@ Coming soon...requires the [Updoot](https://github.com/BobHasNoSoul/jellyfin-upd
|
||||
| `modal.js` | Generic modal system for dialogs |
|
||||
| `toaster.js` | Toast notification system |
|
||||
| `userConfig.js` | User-specific configuration for skins and settings |
|
||||
| `apiHelper.js` | API helper functions for common operations |
|
||||
|
||||
### Feature Scripts
|
||||
|
||||
@@ -382,7 +427,7 @@ Coming soon...requires the [Updoot](https://github.com/BobHasNoSoul/jellyfin-upd
|
||||
| `homeScreen.js` | Custom home screen sections | `cardBuilder`, `localStorageCache`, `utils` |
|
||||
| `search.js` | Enhanced search functionality | `cardBuilder`, `utils` |
|
||||
| `headerTabs.js` | Header tab improvements | None |
|
||||
| `customMenu.js` | Custom menu link handling | `utils` |
|
||||
| `customMenuLinks.js` | Custom menu link handling | `utils` |
|
||||
| `exclusiveElsewhere.js` | Custom branding for unavailable content | None |
|
||||
| `updoot.js` | Upvote functionality integration | None |
|
||||
| `backdropLeakFix.js` | Memory leak fixes | None |
|
||||
@@ -395,6 +440,9 @@ Coming soon...requires the [Updoot](https://github.com/BobHasNoSoul/jellyfin-upd
|
||||
| `itemDetailsCollections.js` | Collections display on item details pages | `indexedDBCache`, `utils`, `cardBuilder` |
|
||||
| `flattenSingleSeasonShows.js` | Flatten single-season shows to show episodes directly | `cardBuilder`, `utils` |
|
||||
| `skinManager.js` | Skin selection and management | `utils`, `userConfig` |
|
||||
| `collections.js` | Collection sorting functionality | `utils`, `modal` |
|
||||
| `sonarrRequests.js` | Sonarr request functionality for missing episodes and series | None |
|
||||
| `deviceManager.js` | Device management with remove device functionality | `utils` |
|
||||
<br>
|
||||
|
||||
## Auto-Inject Dependencies
|
||||
@@ -432,12 +480,10 @@ This README and the inline comments in kefinTweaks.js and the other scripts is t
|
||||
### Planned Features
|
||||
- **Jellyfin 10.11 Support**:
|
||||
|
||||
- **UI Configuration**:
|
||||
Allow admins to customize the configuration through the UI instead of the JS script
|
||||
Allow users to also customize various configuration options through the UI
|
||||
<hr>
|
||||
|
||||
### Version History
|
||||
- **v0.3.0**: KefinTweaks configuration for admins in the Jellyfin UI!
|
||||
- **v0.2.4**: New Feature: Watchlist Export/Import
|
||||
- **v0.2.3**: New Feature: Collection Sorting, general fixes
|
||||
- **v0.2.2**: Add support for skin server versions, fixes for HeaderTabs
|
||||
|
||||
+331
-1
@@ -6,7 +6,7 @@
|
||||
'use strict';
|
||||
|
||||
console.log('[KefinTweaks Injector] Initializing...');
|
||||
const versionNumber = "0.2.4";
|
||||
const versionNumber = "0.3.0";
|
||||
|
||||
// Configuration: Start with defaults, then merge user config
|
||||
// This allows new scripts to work out of the box without requiring config updates
|
||||
@@ -497,6 +497,16 @@
|
||||
|
||||
console.log('[KefinTweaks Injector] All scripts loaded successfully!');
|
||||
|
||||
// Always load configuration.js for admin UI (loads after other scripts)
|
||||
console.log('[KefinTweaks Injector] Loading configuration script...');
|
||||
try {
|
||||
await loadScript('configuration.js');
|
||||
await loadCSS('configuration.css');
|
||||
console.log('[KefinTweaks Injector] Configuration script loaded successfully');
|
||||
} catch (error) {
|
||||
console.warn('[KefinTweaks Injector] Failed to load configuration script:', error);
|
||||
}
|
||||
|
||||
// Dispatch custom event when all scripts are loaded
|
||||
const event = new CustomEvent('kefinTweaksLoaded', {
|
||||
detail: {
|
||||
@@ -549,15 +559,335 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Check if user is admin (with timeout for login)
|
||||
async function checkAdminWithTimeout(maxWaitMs = 5000) {
|
||||
const startTime = Date.now();
|
||||
const checkInterval = 100; // Check every 100ms
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const checkAdmin = async () => {
|
||||
try {
|
||||
if (window.ApiClient && window.ApiClient.getCurrentUser) {
|
||||
const user = await window.ApiClient.getCurrentUser();
|
||||
if (user && user.Policy && user.Policy.IsAdministrator === true) {
|
||||
console.log('[KefinTweaks Startup] User is admin');
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
// User is logged in but not admin
|
||||
if (user) {
|
||||
console.log('[KefinTweaks Startup] User is not admin');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// User might not be logged in yet, continue waiting
|
||||
}
|
||||
|
||||
// Check if we've exceeded max wait time
|
||||
if (Date.now() - startTime >= maxWaitMs) {
|
||||
console.log('[KefinTweaks Startup] Timeout waiting for user login');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue checking
|
||||
setTimeout(checkAdmin, checkInterval);
|
||||
};
|
||||
|
||||
checkAdmin();
|
||||
});
|
||||
}
|
||||
|
||||
// Find plugin by name
|
||||
async function findPlugin(pluginName) {
|
||||
try {
|
||||
if (!window.ApiClient || !window.ApiClient._serverAddress || !window.ApiClient.accessToken) {
|
||||
throw new Error('ApiClient not available');
|
||||
}
|
||||
|
||||
const server = ApiClient._serverAddress;
|
||||
const token = ApiClient.accessToken();
|
||||
|
||||
const response = await fetch(`${server}/Plugins`, {
|
||||
headers: {
|
||||
'X-Emby-Token': token
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const plugins = await response.json();
|
||||
const pluginsList = Array.isArray(plugins) ? plugins : (plugins.Items || []);
|
||||
|
||||
const plugin = pluginsList.find(p => p.Name === pluginName);
|
||||
return plugin ? plugin.Id : null;
|
||||
} catch (error) {
|
||||
console.error(`[KefinTweaks Startup] Error finding plugin ${pluginName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get plugin configuration
|
||||
async function getPluginConfig(pluginId) {
|
||||
try {
|
||||
if (!window.ApiClient || !window.ApiClient._serverAddress || !window.ApiClient.accessToken) {
|
||||
throw new Error('ApiClient not available');
|
||||
}
|
||||
|
||||
const server = ApiClient._serverAddress;
|
||||
const token = ApiClient.accessToken();
|
||||
|
||||
const response = await fetch(`${server}/Plugins/${pluginId}/Configuration`, {
|
||||
headers: {
|
||||
'X-Emby-Token': token
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('[KefinTweaks Startup] Error getting plugin config:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Save plugin configuration
|
||||
async function savePluginConfig(pluginId, config) {
|
||||
try {
|
||||
if (!window.ApiClient || !window.ApiClient._serverAddress || !window.ApiClient.accessToken) {
|
||||
throw new Error('ApiClient not available');
|
||||
}
|
||||
|
||||
const server = ApiClient._serverAddress;
|
||||
const token = ApiClient.accessToken();
|
||||
|
||||
const response = await fetch(`${server}/Plugins/${pluginId}/Configuration`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Emby-Token': token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[KefinTweaks Startup] Error saving plugin config:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure KefinTweaks-Config exists in JS Injector
|
||||
async function ensureKefinTweaksConfig() {
|
||||
try {
|
||||
console.log('[KefinTweaks Startup] Ensuring KefinTweaks-Config exists in JS Injector...');
|
||||
|
||||
const pluginId = await findPlugin('JavaScript Injector') || await findPlugin('JS Injector');
|
||||
if (!pluginId) {
|
||||
console.warn('[KefinTweaks Startup] JavaScript Injector plugin not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const injectorConfig = await getPluginConfig(pluginId);
|
||||
|
||||
// Ensure CustomJavaScripts array exists
|
||||
if (!injectorConfig.CustomJavaScripts) {
|
||||
injectorConfig.CustomJavaScripts = [];
|
||||
}
|
||||
|
||||
// Check if KefinTweaks-Config already exists
|
||||
const existingScript = injectorConfig.CustomJavaScripts.find(
|
||||
script => script.Name === 'KefinTweaks-Config'
|
||||
);
|
||||
|
||||
if (existingScript) {
|
||||
console.log('[KefinTweaks Startup] KefinTweaks-Config already exists in JS Injector');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load default config
|
||||
let defaultConfig;
|
||||
if (window.KefinTweaksDefaultConfig) {
|
||||
defaultConfig = window.KefinTweaksDefaultConfig;
|
||||
} else {
|
||||
// Try to load it
|
||||
try {
|
||||
const defaultConfigUrl = window.KefinTweaksConfig?.kefinTweaksRoot
|
||||
? `${window.KefinTweaksConfig.kefinTweaksRoot}kefinTweaks-default-config.js`
|
||||
: 'https://ranaldsgift.github.io/KefinTweaks/kefinTweaks-default-config.js';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = defaultConfigUrl;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error('Failed to load default config'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
defaultConfig = window.KefinTweaksDefaultConfig;
|
||||
} catch (error) {
|
||||
console.error('[KefinTweaks Startup] Error loading default config:', error);
|
||||
// Create minimal config
|
||||
defaultConfig = {
|
||||
kefinTweaksRoot: 'https://ranaldsgift.github.io/KefinTweaks/',
|
||||
scriptRoot: 'https://ranaldsgift.github.io/KefinTweaks/scripts/',
|
||||
scripts: {},
|
||||
homeScreen: {},
|
||||
exclusiveElsewhere: {},
|
||||
search: {},
|
||||
skins: [],
|
||||
themes: [],
|
||||
customMenuLinks: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create the script content
|
||||
const scriptContent = `// KefinTweaks Configuration
|
||||
// This file is automatically generated by KefinTweaks Configuration UI
|
||||
// Do not edit manually unless you know what you're doing
|
||||
|
||||
window.KefinTweaksConfig = ${JSON.stringify(defaultConfig, null, 2)};`;
|
||||
|
||||
// Add new script
|
||||
injectorConfig.CustomJavaScripts.push({
|
||||
Name: 'KefinTweaks-Config',
|
||||
Script: scriptContent,
|
||||
Enabled: true,
|
||||
RequiresAuthentication: false
|
||||
});
|
||||
|
||||
// Save the configuration
|
||||
await savePluginConfig(pluginId, injectorConfig);
|
||||
console.log('[KefinTweaks Startup] KefinTweaks-Config added to JS Injector');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[KefinTweaks Startup] Error ensuring KefinTweaks-Config:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Watchlist tab exists in CustomTabs
|
||||
async function ensureWatchlistTab() {
|
||||
try {
|
||||
// Check if Watchlist is enabled in the configuration
|
||||
if (!window.KefinTweaksConfig.scripts.watchlist) {
|
||||
console.log('[KefinTweaks Startup] Watchlist is disabled in configuration, skipping Watchlist tab check');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[KefinTweaks Startup] Ensuring Watchlist tab exists in CustomTabs...');
|
||||
|
||||
const pluginId = await findPlugin('Custom Tabs');
|
||||
if (!pluginId) {
|
||||
console.warn('[KefinTweaks Startup] CustomTabs plugin not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current CustomTabs config
|
||||
const server = ApiClient._serverAddress;
|
||||
const token = ApiClient.accessToken();
|
||||
|
||||
const response = await fetch(`${server}/CustomTabs/Config`, {
|
||||
headers: {
|
||||
'X-Emby-Token': token
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
let tabs = await response.json();
|
||||
if (!Array.isArray(tabs)) {
|
||||
tabs = [];
|
||||
}
|
||||
|
||||
// Check if Watchlist tab exists
|
||||
const watchlistTab = tabs.find(tab => tab.Title === 'Watchlist');
|
||||
|
||||
if (watchlistTab) {
|
||||
// Ensure ContentHtml is correct
|
||||
if (watchlistTab.ContentHtml !== '<div class="sections watchlist"></div>') {
|
||||
watchlistTab.ContentHtml = '<div class="sections watchlist"></div>';
|
||||
// Update the tab in the array
|
||||
const index = tabs.findIndex(tab => tab.Title === 'Watchlist');
|
||||
tabs[index] = watchlistTab;
|
||||
|
||||
// Save updated config
|
||||
await savePluginConfig(pluginId, { Tabs: tabs });
|
||||
console.log('[KefinTweaks Startup] Updated Watchlist tab ContentHtml');
|
||||
} else {
|
||||
console.log('[KefinTweaks Startup] Watchlist tab already exists with correct ContentHtml');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add Watchlist tab
|
||||
tabs.push({
|
||||
Title: 'Watchlist',
|
||||
ContentHtml: '<div class="sections watchlist"></div>'
|
||||
});
|
||||
|
||||
// Save updated config
|
||||
await savePluginConfig(pluginId, { Tabs: tabs });
|
||||
console.log('[KefinTweaks Startup] Added Watchlist tab to CustomTabs');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[KefinTweaks Startup] Error ensuring Watchlist tab:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Startup task - runs only for admin users
|
||||
async function startupTask() {
|
||||
console.log('[KefinTweaks Startup] Starting startup task...');
|
||||
|
||||
// Check if user is admin (wait up to 5s for login)
|
||||
const isAdmin = await checkAdminWithTimeout(5000);
|
||||
|
||||
if (!isAdmin) {
|
||||
console.log('[KefinTweaks Startup] User is not admin or not logged in, skipping startup task');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Task 1: Ensure KefinTweaks-Config exists in JS Injector
|
||||
await ensureKefinTweaksConfig();
|
||||
|
||||
// Task 2: Ensure Watchlist tab exists in CustomTabs
|
||||
await ensureWatchlistTab();
|
||||
|
||||
console.log('[KefinTweaks Startup] Startup task completed successfully');
|
||||
} catch (error) {
|
||||
console.error('[KefinTweaks Startup] Error in startup task:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start initialization when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
injectVersionBadgeCSS();
|
||||
initialize();
|
||||
// Run startup task after a short delay to ensure ApiClient is ready
|
||||
setTimeout(startupTask, 1000);
|
||||
});
|
||||
} else {
|
||||
injectVersionBadgeCSS();
|
||||
initialize();
|
||||
// Run startup task after a short delay to ensure ApiClient is ready
|
||||
setTimeout(startupTask, 1000);
|
||||
}
|
||||
|
||||
console.log('[KefinTweaks Injector] Injector script loaded. Available at window.KefinTweaks');
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
window.KefinTweaksDefaultConfig = {
|
||||
"kefinTweaksRoot": "https://ranaldsgift.github.io/KefinTweaks/",
|
||||
"scriptRoot": "https://ranaldsgift.github.io/KefinTweaks/scripts/",
|
||||
"scripts": {
|
||||
"homeScreen": true,
|
||||
"search": true,
|
||||
"watchlist": true,
|
||||
"headerTabs": true,
|
||||
"customMenuLinks": true,
|
||||
"breadcrumbs": true,
|
||||
"playlist": true,
|
||||
"itemDetailsCollections": true,
|
||||
"flattenSingleSeasonShows": true,
|
||||
"collections": 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": [
|
||||
{
|
||||
"name": "Default",
|
||||
"author": "Jellyfin",
|
||||
"url": null,
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "ElegantFin",
|
||||
"author": "lscambo13",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@main/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css",
|
||||
"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@main/Theme/assets/add-ons/media-bar-plugin-support-nightly.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/elegantKefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "NeutralFin",
|
||||
"author": "KartoffelChipss",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/KartoffelChipss/NeutralFin@latest/theme/neutralfin-minified.css",
|
||||
"https://cdn.jsdelivr.net/gh/KartoffelChipss/Jellyfin-Lucide@main/theme/jellyfin-lucide.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/neutralfin-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Jellypane",
|
||||
"author": "tedhinklater",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10],
|
||||
"urls": ["https://cdn.jsdelivr.net/gh/tedhinklater/Jellypane@main/Jellypane.css"]
|
||||
},
|
||||
{
|
||||
"majorServerVersions": [11],
|
||||
"urls": ["https://cdn.jsdelivr.net/gh/tedhinklater/Jellypane@main/Jellypane10.11.css"]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Jellyfish",
|
||||
"author": "n00bcodr",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10],
|
||||
"urls": ["https://cdn.jsdelivr.net/gh/n00bcodr/jellyfish@main/theme.css"]
|
||||
},
|
||||
{
|
||||
"majorServerVersions": [11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/n00bcodr/jellyfish@main/theme.css",
|
||||
"https://cdn.jsdelivr.net/gh/n00bcodr/jellyfish@main/10.11_fixes.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [
|
||||
{
|
||||
"name": "Default",
|
||||
"url": ""
|
||||
},
|
||||
{
|
||||
"name": "Banana",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/banana.css"
|
||||
},
|
||||
{
|
||||
"name": "Coal",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/coal.css"
|
||||
},
|
||||
{
|
||||
"name": "Coral",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/coral.css"
|
||||
},
|
||||
{
|
||||
"name": "Grass",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/grass.css"
|
||||
},
|
||||
{
|
||||
"name": "Jellyblue",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/jellyblue.css"
|
||||
},
|
||||
{
|
||||
"name": "Jellyflix",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/jellyflix.css"
|
||||
},
|
||||
{
|
||||
"name": "Lavender",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/lavender.css"
|
||||
},
|
||||
{
|
||||
"name": "Mint",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/mint.css"
|
||||
},
|
||||
{
|
||||
"name": "Ocean",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/ocean.css"
|
||||
},
|
||||
{
|
||||
"name": "Peach",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/peach.css"
|
||||
},
|
||||
{
|
||||
"name": "Watermelon",
|
||||
"url": "https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/watermelon.css"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Glassmorphism",
|
||||
"author": "alexyle",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": ["https://cdn.jsdelivr.net/gh/alexyle/jellyfin-theme@main/glassmorphism/theme.css"]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Zombie",
|
||||
"author": "MakD",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10],
|
||||
"urls": ["https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/zombie_revived.css"]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [
|
||||
{
|
||||
"name": "Default",
|
||||
"url": ""
|
||||
},
|
||||
{
|
||||
"name": "Amazon Prime",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/amazon-prime.css"
|
||||
},
|
||||
{
|
||||
"name": "Apple TV",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/apple-tv.css"
|
||||
},
|
||||
{
|
||||
"name": "Disney",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/disney.css"
|
||||
},
|
||||
{
|
||||
"name": "HBO Max",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/hbo-max.css"
|
||||
},
|
||||
{
|
||||
"name": "Hulu",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/hulu.css"
|
||||
},
|
||||
{
|
||||
"name": "Netflix",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/netflix.css"
|
||||
},
|
||||
{
|
||||
"name": "YouTube",
|
||||
"url": "https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/youtube.css"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Jamfin",
|
||||
"author": "JamsRepos",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/JamsRepos/Jamfin@latest/theme/complete.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/jamfin-kefin.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/jamfin-kefin-10.css"
|
||||
]
|
||||
},
|
||||
{
|
||||
"majorServerVersions": [11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/JamsRepos/Jamfin@latest/theme/complete.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/jamfin-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Scyfin",
|
||||
"author": "loof2736",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/loof2736/[email protected]/CSS/scyfin-theme.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/scyfin-kefin.css"
|
||||
]
|
||||
},
|
||||
{
|
||||
"majorServerVersions": [11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/scyfin-theme.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/scyfin-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [
|
||||
{
|
||||
"name": "Default",
|
||||
"url": ""
|
||||
},
|
||||
{
|
||||
"name": "Seafoam",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-seafoam.css"
|
||||
},
|
||||
{
|
||||
"name": "Coral",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-coral.css"
|
||||
},
|
||||
{
|
||||
"name": "Snow",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-snow.css"
|
||||
},
|
||||
{
|
||||
"name": "OLED",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-oled.css"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Catppuccin",
|
||||
"author": "Catppuccin",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": ["https://jellyfin.catppuccin.com/theme.css"]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [
|
||||
{
|
||||
"name": "Latte",
|
||||
"url": "https://jellyfin.catppuccin.com/catppuccin-latte.css"
|
||||
},
|
||||
{
|
||||
"name": "Frappé",
|
||||
"url": "https://jellyfin.catppuccin.com/catppuccin-frappe.css"
|
||||
},
|
||||
{
|
||||
"name": "Macchiato",
|
||||
"url": "https://jellyfin.catppuccin.com/catppuccin-macchiato.css"
|
||||
},
|
||||
{
|
||||
"name": "Mocha",
|
||||
"url": "https://jellyfin.catppuccin.com/catppuccin-mocha.css"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Flow",
|
||||
"author": "LitCastVlog",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/ScyFlow-oneliner.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/flow-kefin.css"
|
||||
]
|
||||
},
|
||||
{
|
||||
"majorServerVersions": [11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/ScyFlow-oneliner.css",
|
||||
"https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/ScyFlow-Compatibility.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/flow-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [
|
||||
{
|
||||
"name": "Default",
|
||||
"url": ""
|
||||
},
|
||||
{
|
||||
"name": "Orange",
|
||||
"url": "https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Orange.css"
|
||||
},
|
||||
{
|
||||
"name": "Pink",
|
||||
"url": "https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Pink.css"
|
||||
},
|
||||
{
|
||||
"name": "Rainbow",
|
||||
"url": "https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Rainbow.css"
|
||||
},
|
||||
{
|
||||
"name": "Red",
|
||||
"url": "https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Red.css"
|
||||
},
|
||||
{
|
||||
"name": "White",
|
||||
"url": "https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-White.css"
|
||||
},
|
||||
{
|
||||
"name": "Blue",
|
||||
"url": "https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Blue.css"
|
||||
},
|
||||
{
|
||||
"name": "Seafoam",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-seafoam.css"
|
||||
},
|
||||
{
|
||||
"name": "Coral",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-coral.css"
|
||||
},
|
||||
{
|
||||
"name": "Snow",
|
||||
"url": "https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-snow.css"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Monochromic",
|
||||
"author": "CTalvio",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/presets/monochromic_preset.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/chromic-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Kaleidochromic",
|
||||
"author": "CTalvio",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/presets/kaleidochromic_preset.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/chromic-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "Novachromic",
|
||||
"author": "CTalvio",
|
||||
"url": [
|
||||
{
|
||||
"majorServerVersions": [10, 11],
|
||||
"urls": [
|
||||
"https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/presets/novachromic_preset.css",
|
||||
"https://ranaldsgift.github.io/KefinTweaks/skins/chromic-kefin.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colorSchemes": [],
|
||||
"enabled": true,
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"defaultSkin": null,
|
||||
"themes": [],
|
||||
"customMenuLinks": []
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,64 @@
|
||||
.kefin-config-button {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
appearance: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
flex-grow: 1;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
min-width: 0px;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
outline: 0px;
|
||||
border-width: 0px;
|
||||
border-style: initial;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
margin: 0px;
|
||||
border-radius: 0px;
|
||||
text-decoration: none;
|
||||
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.customMenuOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.customMenuOptions > * {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
a[href="#"].navMenuOption:has(.build) {
|
||||
order: 9999;
|
||||
margin-top: 2em !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a[href="#"].navMenuOption:has(.build)::before {
|
||||
content: 'KefinTweaks';
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.listItemContent:has([data-skin-name]) {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#kefinTweaksConfigModal .listItem {
|
||||
align-items: start;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+72
-10
@@ -14,11 +14,13 @@ window.ModalSystem = (function() {
|
||||
* @param {Object} options - Modal configuration
|
||||
* @param {string} options.id - Unique modal ID
|
||||
* @param {string} options.title - Modal title
|
||||
* @param {string} options.content - HTML content for the modal body
|
||||
* @param {string|HTMLElement} options.content - HTML content for the modal body
|
||||
* @param {string|HTMLElement} options.footer - Optional footer HTML content
|
||||
* @param {Function} options.onClose - Callback when modal closes
|
||||
* @param {Function} options.onOpen - Callback when modal opens
|
||||
* @param {boolean} options.closeOnBackdrop - Whether to close when clicking backdrop (default: true)
|
||||
* @param {boolean} options.closeOnEscape - Whether to close on Escape key (default: true)
|
||||
* @param {boolean} options.showCloseButton - Whether to show close button in header (default: true if title exists)
|
||||
* @returns {Object} Modal instance
|
||||
*/
|
||||
function createModal(options = {}) {
|
||||
@@ -26,10 +28,12 @@ window.ModalSystem = (function() {
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
footer,
|
||||
onClose,
|
||||
onOpen,
|
||||
closeOnBackdrop = true,
|
||||
closeOnEscape = true
|
||||
closeOnEscape = true,
|
||||
showCloseButton = true
|
||||
} = options;
|
||||
|
||||
if (!id) {
|
||||
@@ -54,30 +58,86 @@ window.ModalSystem = (function() {
|
||||
dialog.setAttribute('data-autofocus', 'true');
|
||||
dialog.setAttribute('data-removeonclose', 'true');
|
||||
dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup';
|
||||
dialog.style.display = 'flex';
|
||||
dialog.style.flexDirection = 'column';
|
||||
dialog.style.maxHeight = '90vh';
|
||||
|
||||
const dialogContent = document.createElement('div');
|
||||
dialogContent.style.margin = '0';
|
||||
dialogContent.style.padding = '1.25em 1.5em 1.5em';
|
||||
|
||||
// Add title if provided
|
||||
// Create header if title is provided
|
||||
let dialogHeader = null;
|
||||
if (title) {
|
||||
dialogHeader = document.createElement('div');
|
||||
dialogHeader.className = 'formDialogHeader';
|
||||
dialogHeader.style.display = 'flex';
|
||||
dialogHeader.style.justifyContent = 'space-between';
|
||||
dialogHeader.style.alignItems = 'center';
|
||||
dialogHeader.style.padding = '1.25em 1.5em';
|
||||
dialogHeader.style.borderBottom = '1px solid rgba(255,255,255,0.1)';
|
||||
dialogHeader.style.flexShrink = '0';
|
||||
|
||||
const titleElement = document.createElement('h2');
|
||||
titleElement.style.margin = '0 0 .5em';
|
||||
titleElement.style.margin = '0';
|
||||
titleElement.style.textAlign = 'left';
|
||||
titleElement.textContent = title;
|
||||
dialogContent.appendChild(titleElement);
|
||||
dialogHeader.appendChild(titleElement);
|
||||
|
||||
// Add close button if enabled
|
||||
if (showCloseButton) {
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.setAttribute('is', 'paper-icon-button-light');
|
||||
closeButton.className = 'btnCancel btnClose autoSize paper-icon-button-light';
|
||||
closeButton.setAttribute('tabindex', '-1');
|
||||
closeButton.title = 'Close';
|
||||
closeButton.onclick = () => closeModal(id);
|
||||
|
||||
const closeIcon = document.createElement('span');
|
||||
closeIcon.className = 'material-icons close';
|
||||
closeIcon.setAttribute('aria-hidden', 'true');
|
||||
closeButton.appendChild(closeIcon);
|
||||
|
||||
dialogHeader.appendChild(closeButton);
|
||||
}
|
||||
|
||||
dialog.appendChild(dialogHeader);
|
||||
}
|
||||
|
||||
// Create scrollable content area
|
||||
const dialogContent = document.createElement('div');
|
||||
dialogContent.style.marginBottom = '5em';
|
||||
dialogContent.style.padding = title || footer ? '1.25em 1.5em' : '1.25em 1.5em 1.5em';
|
||||
dialogContent.style.overflowY = 'auto';
|
||||
dialogContent.style.flex = '1';
|
||||
dialogContent.style.minHeight = '0';
|
||||
|
||||
// Add content
|
||||
if (content) {
|
||||
if (typeof content === 'string') {
|
||||
dialogContent.innerHTML += content;
|
||||
dialogContent.innerHTML = content;
|
||||
} else if (content instanceof HTMLElement) {
|
||||
dialogContent.appendChild(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Create footer if provided
|
||||
let dialogFooter = null;
|
||||
if (footer) {
|
||||
dialogFooter = document.createElement('div');
|
||||
dialogFooter.className = 'formDialogFooter';
|
||||
dialogFooter.style.padding = '1.25em 1.5em';
|
||||
dialogFooter.style.borderTop = '1px solid rgba(255,255,255,0.1)';
|
||||
dialogFooter.style.flexShrink = '0';
|
||||
|
||||
if (typeof footer === 'string') {
|
||||
dialogFooter.innerHTML = footer;
|
||||
} else if (footer instanceof HTMLElement) {
|
||||
dialogFooter.appendChild(footer);
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble modal
|
||||
dialog.appendChild(dialogContent);
|
||||
if (dialogFooter) {
|
||||
dialog.appendChild(dialogFooter);
|
||||
}
|
||||
dialogContainer.appendChild(dialog);
|
||||
|
||||
// Add to DOM
|
||||
@@ -91,6 +151,8 @@ window.ModalSystem = (function() {
|
||||
dialogContainer,
|
||||
dialog,
|
||||
dialogContent,
|
||||
dialogHeader,
|
||||
dialogFooter,
|
||||
isOpen: true,
|
||||
close: () => closeModal(id),
|
||||
updateContent: (newContent) => updateModalContent(id, newContent),
|
||||
|
||||
@@ -59,4 +59,11 @@ label[for="searchTextInput"] {
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span.searchfields-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
+24
-4
@@ -36,12 +36,30 @@
|
||||
xhr.send = function() {
|
||||
// Simulate a successful response
|
||||
setTimeout(() => {
|
||||
xhr.readyState = 4;
|
||||
xhr.status = 200;
|
||||
xhr.responseText = '{"Items":[],"TotalRecordCount":0}';
|
||||
// Use Object.defineProperty to override read-only properties
|
||||
Object.defineProperty(xhr, 'readyState', {
|
||||
value: 4,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(xhr, 'status', {
|
||||
value: 200,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(xhr, 'responseText', {
|
||||
value: '{"Items":[],"TotalRecordCount":0}',
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Trigger the event handlers
|
||||
if (xhr.onreadystatechange) {
|
||||
xhr.onreadystatechange();
|
||||
}
|
||||
if (xhr.onload) {
|
||||
xhr.onload();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
} else {
|
||||
@@ -581,7 +599,9 @@
|
||||
smartMusicBtn.addEventListener('click', ()=>setSearchType('music'));
|
||||
smartBooksBtn.addEventListener('click', ()=>setSearchType('books'));
|
||||
smartAllBtn.addEventListener('click', ()=>setSearchType('all'));
|
||||
smartRequestBtn.addEventListener('click', ()=>setSearchType('request'));
|
||||
if (smartRequestBtn) {
|
||||
smartRequestBtn.addEventListener('click', ()=>setSearchType('request'));
|
||||
}
|
||||
|
||||
// Parse type parameter from URL
|
||||
let typeFromUrl = 'videos'; // default
|
||||
|
||||
+18
-14
@@ -208,18 +208,15 @@
|
||||
|
||||
// Load and merge skin configurations
|
||||
function loadSkinConfig() {
|
||||
const userSkins = window.KefinTweaksUserConfig?.skins || [];
|
||||
const additionalSkins = window.KefinTweaksConfig?.skins || [];
|
||||
// Only load from window.KefinTweaksConfig (from JS Injector)
|
||||
const configSkins = window.KefinTweaksConfig?.skins || [];
|
||||
|
||||
// Start with user skins (these take priority)
|
||||
SKINS_CONFIG = [...userSkins];
|
||||
|
||||
// Add additional skins from main config, avoiding duplicates by name
|
||||
additionalSkins.forEach(skin => {
|
||||
const exists = SKINS_CONFIG.some(existingSkin => existingSkin.name === skin.name);
|
||||
if (!exists) {
|
||||
SKINS_CONFIG.push(skin);
|
||||
}
|
||||
// Filter to only include enabled skins (enabled !== false)
|
||||
// Default to enabled if the property is not set
|
||||
SKINS_CONFIG = configSkins.filter(skin => {
|
||||
// Include skin if enabled is not explicitly false
|
||||
// If enabled is undefined/null, treat as enabled (true)
|
||||
return skin.enabled !== false;
|
||||
});
|
||||
|
||||
// Ensure we always have at least a default skin
|
||||
@@ -241,7 +238,9 @@
|
||||
LOG(`Filtered skins by server version: ${skinsBeforeFilter} -> ${skinsAfterFilter} (removed ${skinsBeforeFilter - skinsAfterFilter} incompatible skins)`);
|
||||
}
|
||||
|
||||
LOG(`Loaded ${SKINS_CONFIG.length} skins (${userSkins.length} user + ${additionalSkins.length} additional)`);
|
||||
const enabledCount = configSkins.filter(s => s.enabled !== false).length;
|
||||
const disabledCount = configSkins.length - enabledCount;
|
||||
LOG(`Loaded ${SKINS_CONFIG.length} enabled skins from KefinTweaksConfig (${enabledCount} enabled, ${disabledCount} disabled)`);
|
||||
}
|
||||
|
||||
// Load and merge theme configurations
|
||||
@@ -374,8 +373,13 @@
|
||||
const selectedSkin = SKINS_CONFIG.find(skin => skin.name === selectedSkinName);
|
||||
|
||||
if (!selectedSkin) {
|
||||
ERR('Selected skin not found during verification');
|
||||
return false;
|
||||
ERR('Selected skin not found, reverting to default skin');
|
||||
const defaultSkinName = getDefaultSkinName();
|
||||
const defaultSkin = SKINS_CONFIG.find(skin => skin.name === defaultSkinName);
|
||||
if (defaultSkin) {
|
||||
loadSkin(defaultSkin);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if skin CSS is properly loaded
|
||||
|
||||
+95
-45
@@ -16,7 +16,8 @@
|
||||
// State management
|
||||
let isInitialized = false;
|
||||
let isAddingCCButton = false;
|
||||
let subtitleDialogObserver = null;
|
||||
let onViewPageUnregister = null;
|
||||
let dialogCheckInterval = null;
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
@@ -1020,6 +1021,66 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start checking for subtitle dialogs periodically (only when on video page)
|
||||
* This is much more efficient than a MutationObserver watching the entire document
|
||||
*/
|
||||
function startDialogCheck() {
|
||||
// Clear any existing interval
|
||||
if (dialogCheckInterval) {
|
||||
clearInterval(dialogCheckInterval);
|
||||
}
|
||||
|
||||
// Check for dialogs periodically while on video page
|
||||
// The interval is automatically stopped when navigating away via onViewPage
|
||||
dialogCheckInterval = setInterval(() => {
|
||||
addSearchButtonToExistingDialogs();
|
||||
}, 500); // Check every 500ms
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop checking for subtitle dialogs
|
||||
*/
|
||||
function stopDialogCheck() {
|
||||
if (dialogCheckInterval) {
|
||||
clearInterval(dialogCheckInterval);
|
||||
dialogCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize subtitle search functionality for video page
|
||||
*/
|
||||
async function initializeForVideoPage() {
|
||||
try {
|
||||
// Check if ApiClient is available first
|
||||
if (!isApiClientAvailable()) {
|
||||
LOG('ApiClient not available, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions first
|
||||
const hasPermission = await checkSubtitlePermissions();
|
||||
if (!hasPermission) {
|
||||
LOG('User does not have subtitle management permission, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
LOG('User has subtitle management permission, initializing for video page...');
|
||||
|
||||
// Add CC button if it doesn't exist
|
||||
addCCButton();
|
||||
|
||||
// Start checking for subtitle dialogs
|
||||
startDialogCheck();
|
||||
|
||||
LOG('Subtitle search functionality initialized for video page');
|
||||
|
||||
} catch (error) {
|
||||
ERR('Error initializing subtitle search:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize subtitle search functionality
|
||||
@@ -1046,37 +1107,36 @@
|
||||
|
||||
LOG('User has subtitle management permission, initializing...');
|
||||
|
||||
// Add CC button if it doesn't exist
|
||||
// Wait for KefinTweaksUtils to be available
|
||||
if (!window.KefinTweaksUtils || !window.KefinTweaksUtils.onViewPage) {
|
||||
LOG('KefinTweaksUtils.onViewPage not available, retrying in 1 second');
|
||||
setTimeout(initialize, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register onViewPage handler for video page
|
||||
onViewPageUnregister = window.KefinTweaksUtils.onViewPage(async (view, element, hash) => {
|
||||
if (hash && hash.includes('#/video')) {
|
||||
LOG('Video page detected via onViewPage, initializing...');
|
||||
// Small delay to ensure OSD is ready
|
||||
setTimeout(() => {
|
||||
initializeForVideoPage();
|
||||
}, 500);
|
||||
} else {
|
||||
// Stop dialog checking when not on video page
|
||||
stopDialogCheck();
|
||||
}
|
||||
}, {
|
||||
pages: ['video']
|
||||
});
|
||||
|
||||
// Also check current page on initial load
|
||||
if (window.location.hash.includes('#/video')) {
|
||||
LOG('Video page detected, adding CC button');
|
||||
addCCButton();
|
||||
setTimeout(() => {
|
||||
initializeForVideoPage();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Set up observer for subtitle dialogs
|
||||
if (subtitleDialogObserver) {
|
||||
subtitleDialogObserver.disconnect();
|
||||
}
|
||||
|
||||
subtitleDialogObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if a subtitle dialog was added
|
||||
if (node.classList && node.classList.contains('dialogContainer')) {
|
||||
setTimeout(() => addSearchButtonToExistingDialogs(), 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
subtitleDialogObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
LOG('Subtitle search functionality initialized successfully');
|
||||
|
||||
@@ -1089,10 +1149,13 @@
|
||||
* Clean up resources
|
||||
*/
|
||||
function cleanup() {
|
||||
if (subtitleDialogObserver) {
|
||||
subtitleDialogObserver.disconnect();
|
||||
subtitleDialogObserver = null;
|
||||
stopDialogCheck();
|
||||
|
||||
if (onViewPageUnregister) {
|
||||
onViewPageUnregister();
|
||||
onViewPageUnregister = null;
|
||||
}
|
||||
|
||||
isInitialized = false;
|
||||
LOG('Subtitle search functionality cleaned up');
|
||||
}
|
||||
@@ -1104,19 +1167,6 @@
|
||||
initialize();
|
||||
}
|
||||
|
||||
// Re-initialize when navigating (for SPA behavior)
|
||||
let lastUrl = location.href;
|
||||
new MutationObserver(() => {
|
||||
const url = location.href;
|
||||
if (url !== lastUrl) {
|
||||
lastUrl = url;
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
initialize();
|
||||
}, 1000);
|
||||
}
|
||||
}).observe(document, { subtree: true, childList: true });
|
||||
|
||||
// Expose functions globally for debugging
|
||||
window.subtitleSearchCleanup = cleanup;
|
||||
window.getSession = getSession;
|
||||
|
||||
@@ -4123,6 +4123,8 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 10;
|
||||
let retries = 0;
|
||||
// Use WebSocket to listen for UserDataChanged messages instead of intercepting fetch
|
||||
// This is more reliable and doesn't interfere with fetch requests
|
||||
function setupWebSocketMonitoring() {
|
||||
@@ -4131,6 +4133,11 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
const socket = (window.ApiClient && (window.ApiClient.webSocket || window.ApiClient._webSocket)) || null;
|
||||
|
||||
if (!socket) {
|
||||
if (retries >= maxRetries) {
|
||||
ERR('Max retries reached, giving up on WebSocket monitoring');
|
||||
return false;
|
||||
}
|
||||
retries++;
|
||||
setTimeout(() => {
|
||||
if (!playbackMonitorInitialized) {
|
||||
setupWebSocketMonitoring();
|
||||
@@ -4190,7 +4197,7 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
//setupWebSocketMonitoring();
|
||||
|
||||
// Try to set up immediately
|
||||
/* if (!setupWebSocketMonitoring()) {
|
||||
if (!setupWebSocketMonitoring()) {
|
||||
// If WebSocket isn't available yet, retry after a short delay
|
||||
// This can happen if the page loads before the WebSocket connection is established
|
||||
setTimeout(() => {
|
||||
@@ -4198,7 +4205,7 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
setupWebSocketMonitoring();
|
||||
}
|
||||
}, 1000);
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
let watchedItems = [];
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
-o-backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
-ms-backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
height: 100%;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kefinTweaks-breadcrumbs-wrapper[style="display: block;"] {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.watchlist-icon .detailButton-content:before {
|
||||
content: 'Watchlist';
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
.material-icons.favorite_border:before {
|
||||
content: '\e0f6';
|
||||
}
|
||||
.material-icons.build:before {
|
||||
content: '\e1b1';
|
||||
}
|
||||
|
||||
/* Lazy workaround for the pause button on the media bar plugin */
|
||||
.pause-button .material-icons {
|
||||
|
||||
Reference in New Issue
Block a user