Skip to content

Commit e643e34

Browse files
committed
Refactor injection logic and add File Transformation support
Replaces the scheduled task with a StartupService that registers a file transformation for index.html if the File Transformation plugin is present, or falls back to direct injection. Adds helpers for transformation, updates configuration UI, introduces PatchRequestPayload, and updates dependencies. Removes legacy InjectScriptTask and improves script cleanup and injection logic for better upgrade and uninstall handling.
1 parent 6f9ec24 commit e643e34

File tree

9 files changed

+386
-180
lines changed

9 files changed

+386
-180
lines changed

Jellyfin.Plugin.JavaScriptInjector/Configuration/configPage.html

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,44 @@
11
<!DOCTYPE html>
22
<html>
3+
34
<head>
45
<title>JavaScript Injector</title>
56
</head>
7+
68
<body>
79
<div id="JavaScriptInjectorConfigPage" data-role="page" class="page type-interior pluginConfigurationPage">
810
<div data-role="content">
911
<div class="content-primary">
1012
<form id="JavaScriptInjectorConfigForm">
1113
<div class="verticalSection verticalSection-extrabottompadding">
1214
<div class="sectionTitleContainer flex align-items-center">
13-
<h2 class="sectionTitle">JavaScript Injector Settings</h2>
15+
<h2 class="sectionTitle">JavaScript Injector</h2>
16+
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
17+
target="_blank" href="https://github.com/n00bcodr/Jellyfin-JavaScript-Injector">
18+
<i class="md-icon button-icon button-icon-left secondaryText"></i>
19+
<span>Help</span>
20+
</a>
1421
</div>
15-
22+
<hr><br>
1623
<div id="scriptsContainer" class="verticalSection-extrabottompadding">
17-
</div>
24+
<!-- Script entries will be dynamically inserted here -->
25+
</div>
1826

1927
<div class="verticalSection">
2028
<button is="emby-button" type="button" id="addScriptBtn" class="raised button-submit block">
2129
<span>Add Script</span>
2230
</button>
2331
</div>
24-
2532
<br/>
26-
<div class="infoBox">
27-
<p class="infoBoxText">
28-
<strong>ℹ️</strong> Changes require a browser refresh to take effect. Scripts marked with 'Requires Authentication' will only be loaded for authenticated users.
29-
</p>
33+
<div
34+
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
35+
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
36+
<div>
37+
Changes require a browser refresh to take effect. <br/>
38+
Scripts marked with 'Requires Authentication' will only be loaded for authenticated
39+
users.
40+
</div>
3041
</div>
31-
3242
<div>
3343
<button is="emby-button" type="submit" class="raised button-submit block">
3444
<span>Save</span>
@@ -40,18 +50,21 @@ <h2 class="sectionTitle">JavaScript Injector Settings</h2>
4050
</div>
4151

4252
<template id="script-entry-template">
43-
<details class="verticalSection" style="border-radius: 8px; margin-bottom: 1em; box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1), 0 2px 4px rgba(0, 0, 0, 0.5); transition: box-shadow 0.3s ease;">
44-
<summary style="cursor: pointer; list-style: none; display: flex; align-items: center; padding: 0.75em 1em;">
45-
<i class="material-icons arrow-closed" style="display: inline-block;">arrow_right</i>
46-
<i class="material-icons arrow-open" style="display: none;">arrow_drop_down</i>
47-
<h3 class="sectionTitle script-name-header" style="margin: 0px 0px 0px 0.5em;"></h3>
53+
<details class="verticalSection"
54+
style="background-color: rgba(255, 255, 255, 0.05); border-radius: 8px; margin-bottom: 1em; transition: box-shadow 0.3s ease;">
55+
<summary
56+
style="cursor: pointer; list-style: none; display: flex; align-items: center; padding: 0.75em 1em;">
57+
<i class="material-icons arrow-icon" style="transition: transform 0.2s ease-in-out;">arrow_right</i>
58+
<h3 class="sectionTitle script-name-header" style="margin: 0 0 0 0.5em;"></h3>
4859
</summary>
49-
<div class="script-container" style="padding: 0 1.5em 1.5em 1.5em;">
60+
<div class="script-container" style="padding: 1.5em; border-top: 1px solid rgba(255, 255, 255, 0.1);">
5061
<div class="inputContainer">
5162
<input is="emby-input" type="text" class="script-name-input" label="Script Name:" />
5263
</div>
5364
<div class="inputContainer">
54-
<textarea is="emby-textarea" class="script-code-textarea" label="JavaScript Code:" style="height: 300px; width: 100%;" placeholder="// Insert javascript code here"></textarea>
65+
<textarea class="script-code-textarea emby-textarea emby-input"
66+
style="height: 300px; width: 100%;"
67+
placeholder="// Insert your custom JavaScript code here..."></textarea>
5568
</div>
5669
<div style="display: flex; align-items: center; gap: 2em; margin-top: 1em;">
5770
<label class="emby-checkbox-label">
@@ -74,7 +87,7 @@ <h3 class="sectionTitle script-name-header" style="margin: 0px 0px 0px 0.5em;"><
7487
var JavaScriptInjectorConfig = {
7588
pluginId: 'f5a34f7b-2e8a-4e6a-a722-3a216a81b374',
7689

77-
loadConfiguration: function() {
90+
loadConfiguration: function () {
7891
Dashboard.showLoadingMsg();
7992
ApiClient.getPluginConfiguration(JavaScriptInjectorConfig.pluginId).then(function (config) {
8093
const scriptsContainer = document.querySelector('#scriptsContainer');
@@ -85,12 +98,12 @@ <h3 class="sectionTitle script-name-header" style="margin: 0px 0px 0px 0.5em;"><
8598
});
8699
}
87100
Dashboard.hideLoadingMsg();
88-
}).catch(function() {
101+
}).catch(function () {
89102
Dashboard.hideLoadingMsg();
90103
});
91104
},
92105

93-
createScriptEntry: function(script) {
106+
createScriptEntry: function (script) {
94107
const template = document.querySelector('#script-entry-template');
95108
const clone = template.content.cloneNode(true);
96109

@@ -101,8 +114,7 @@ <h3 class="sectionTitle script-name-header" style="margin: 0px 0px 0px 0.5em;"><
101114
const enabledCheckbox = clone.querySelector('.script-enabled-checkbox');
102115
const authCheckbox = clone.querySelector('.script-auth-checkbox');
103116
const removeBtn = clone.querySelector('.script-remove-btn');
104-
const arrowOpen = clone.querySelector('.arrow-open');
105-
const arrowClosed = clone.querySelector('.arrow-closed');
117+
const arrowIcon = clone.querySelector('.arrow-icon');
106118

107119
// Populate the cloned template with script data
108120
nameHeader.textContent = script.Name;
@@ -120,32 +132,22 @@ <h3 class="sectionTitle script-name-header" style="margin: 0px 0px 0px 0.5em;"><
120132
detailsElement.remove();
121133
});
122134

123-
// Handle hover effect for box-shadow
124-
const defaultShadow = 'inset 0 1px 1px rgba(255, 255, 255, 0.1), 0 2px 4px rgba(0, 0, 0, 0.5)';
125-
const hoverShadow = 'inset 0 1px 1px rgba(255, 255, 255, 0.1), 0 4px 8px rgba(0, 0, 0, 0.6)';
126135
detailsElement.addEventListener('mouseover', () => {
127-
detailsElement.style.boxShadow = hoverShadow;
136+
detailsElement.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';
128137
});
129138
detailsElement.addEventListener('mouseout', () => {
130-
detailsElement.style.boxShadow = defaultShadow;
139+
detailsElement.style.boxShadow = 'none';
131140
});
132141

133142
// Handle arrow icon switching on toggle
134143
detailsElement.addEventListener('toggle', () => {
135-
if (detailsElement.open) {
136-
arrowOpen.style.display = 'inline-block';
137-
arrowClosed.style.display = 'none';
138-
} else {
139-
arrowOpen.style.display = 'none';
140-
arrowClosed.style.display = 'inline-block';
141-
}
144+
arrowIcon.style.transform = detailsElement.open ? 'rotate(90deg)' : 'rotate(0deg)';
142145
});
143146

144-
145147
return clone;
146148
},
147149

148-
saveConfiguration: function() {
150+
saveConfiguration: function () {
149151
Dashboard.showLoadingMsg();
150152
const scripts = [];
151153
document.querySelectorAll('#scriptsContainer .script-container').forEach(container => {
@@ -164,27 +166,28 @@ <h3 class="sectionTitle script-name-header" style="margin: 0px 0px 0px 0.5em;"><
164166
});
165167
},
166168

167-
registerEvents: function() {
168-
document.querySelector('#JavaScriptInjectorConfigForm').addEventListener('submit', function(e) {
169+
registerEvents: function () {
170+
document.querySelector('#JavaScriptInjectorConfigForm').addEventListener('submit', function (e) {
169171
e.preventDefault();
170172
JavaScriptInjectorConfig.saveConfiguration();
171173
return false;
172174
});
173175

174-
document.querySelector('#addScriptBtn').addEventListener('click', function() {
176+
document.querySelector('#addScriptBtn').addEventListener('click', function () {
175177
const scriptsContainer = document.querySelector('#scriptsContainer');
176178
scriptsContainer.appendChild(JavaScriptInjectorConfig.createScriptEntry({ Name: 'New Script', Script: '', Enabled: true, RequiresAuthentication: false }));
177179
});
178180
}
179181
};
180182

181-
document.addEventListener('pageshow', function(e) {
183+
document.addEventListener('pageshow', function (e) {
182184
if (e.target.id === 'JavaScriptInjectorConfigPage') {
183-
JavaScriptInjectorConfig.loadConfiguration();
184-
JavaScriptInjectorConfig.registerEvents();
185+
JavaScriptInjectorConfig.loadConfiguration();
186+
JavaScriptInjectorConfig.registerEvents();
185187
}
186188
});
187189
</script>
188190
</div>
189191
</body>
192+
190193
</html>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using Jellyfin.Plugin.JavaScriptInjector.Model;
2+
3+
namespace Jellyfin.Plugin.JavaScriptInjector.Helpers
4+
{
5+
public static class TransformationPatches
6+
{
7+
public static string IndexHtml(PatchRequestPayload content)
8+
{
9+
if (string.IsNullOrEmpty(content.Contents))
10+
{
11+
return content.Contents ?? string.Empty;
12+
}
13+
14+
var startComment = "<!-- BEGIN JavaScript Injector Plugin -->";
15+
var endComment = "<!-- END JavaScript Injector Plugin -->";
16+
// Public scripts are loaded immediately for all users (including on the login page).
17+
var publicScriptTag = "<script defer src=\"/JavaScriptInjector/public.js\"></script>";
18+
// This inline script waits for the user to be authenticated and then fetches the private scripts.
19+
// It uses the official ApiClient.fetch method, which automatically includes authentication headers.
20+
var privateScriptLoader = @"
21+
<script>
22+
(function() {
23+
'use strict';
24+
const fetchPrivateScripts = () => {
25+
// Check if the API client is fully initialized and a user is logged in.
26+
if (window.ApiClient && typeof window.ApiClient.getCurrentUserId === 'function' && window.ApiClient.getCurrentUserId() && window.ApiClient.serverInfo) {
27+
// Once authenticated, stop checking.
28+
clearInterval(authInterval);
29+
// Use the built-in ApiClient.fetch to make an authenticated request for the private scripts.
30+
ApiClient.fetch({
31+
url: ApiClient.getUrl('JavaScriptInjector/private.js'),
32+
type: 'GET',
33+
dataType: 'text'
34+
}).then(scriptText => {
35+
if (scriptText && scriptText.trim().length > 0) {
36+
const scriptElement = document.createElement('script');
37+
scriptElement.textContent = scriptText;
38+
document.head.appendChild(scriptElement);
39+
console.log('JavaScript Injector: Private scripts loaded successfully.');
40+
}
41+
}).catch(err => {
42+
console.error('JavaScript Injector: Failed to load private scripts.', err);
43+
});
44+
}
45+
};
46+
// Set an interval to check for authentication status every 300 milliseconds.
47+
const authInterval = setInterval(fetchPrivateScripts, 300);
48+
})();
49+
</script>";
50+
// The full block to be injected, wrapped in comments.
51+
var injectionBlock = $@"{startComment}
52+
<!-- Injected using file-transformation -->
53+
{publicScriptTag}
54+
{privateScriptLoader}
55+
{endComment}";
56+
57+
if (content.Contents.Contains("</body>"))
58+
{
59+
return content.Contents.Replace("</body>", $"{injectionBlock}</body>");
60+
}
61+
62+
return content.Contents;
63+
}
64+
}
65+
}

Jellyfin.Plugin.JavaScriptInjector/Jellyfin.Plugin.JavaScriptInjector.csproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
55
<RootNamespace>Jellyfin.Plugin.JavaScriptInjector</RootNamespace>
6-
<AssemblyVersion>1.1.1.1</AssemblyVersion>
7-
<FileVersion>1.1.1.1</FileVersion>
8-
<Version>1.1.1.1</Version>
6+
<AssemblyVersion>2.0.0.0</AssemblyVersion>
7+
<FileVersion>2.0.0.0</FileVersion>
98
</PropertyGroup>
109

1110
<ItemGroup>
12-
<PackageReference Include="Jellyfin.Controller" Version="10.10.0" />
13-
<PackageReference Include="Jellyfin.Model" Version="10.10.0" />
11+
<PackageReference Include="Jellyfin.Controller" Version="10.9.4" />
12+
<PackageReference Include="Jellyfin.Model" Version="10.9.4" />
13+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
1414
</ItemGroup>
1515

1616
<ItemGroup>
1717
<None Remove="Configuration\configPage.html" />
1818
<EmbeddedResource Include="Configuration\configPage.html" />
1919
</ItemGroup>
2020

21-
</Project>
21+
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#nullable enable
2+
using System.Text.Json.Serialization;
3+
4+
namespace Jellyfin.Plugin.JavaScriptInjector.Model
5+
{
6+
public class PatchRequestPayload
7+
{
8+
[JsonPropertyName("contents")]
9+
public string? Contents { get; set; }
10+
}
11+
}

0 commit comments

Comments
 (0)