init
194
.editorconfig
Normal file
@@ -0,0 +1,194 @@
|
||||
# With more recent updates Visual Studio 2017 supports EditorConfig files out of the box
|
||||
# Visual Studio Code needs an extension: https://github.com/editorconfig/editorconfig-vscode
|
||||
# For emacs, vim, np++ and other editors, see here: https://github.com/editorconfig
|
||||
###############################
|
||||
# Core EditorConfig Options #
|
||||
###############################
|
||||
root = true
|
||||
# All files
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
max_line_length = off
|
||||
|
||||
# YAML indentation
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
# XML indentation
|
||||
[*.{csproj,xml}]
|
||||
indent_size = 2
|
||||
|
||||
###############################
|
||||
# .NET Coding Conventions #
|
||||
###############################
|
||||
[*.{cs,vb}]
|
||||
# Organize usings
|
||||
dotnet_sort_system_directives_first = true
|
||||
# this. preferences
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
# Parentheses preferences
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
# Expression-level preferences
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
|
||||
###############################
|
||||
# Naming Conventions #
|
||||
###############################
|
||||
# Style Definitions (From Roslyn)
|
||||
|
||||
# Non-private static fields are PascalCase
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
|
||||
|
||||
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
|
||||
|
||||
# Constants are PascalCase
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
|
||||
|
||||
dotnet_naming_symbols.constants.applicable_kinds = field, local
|
||||
dotnet_naming_symbols.constants.required_modifiers = const
|
||||
|
||||
dotnet_naming_style.constant_style.capitalization = pascal_case
|
||||
|
||||
# Static fields are camelCase and start with s_
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
|
||||
|
||||
dotnet_naming_symbols.static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_style.static_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.static_field_style.required_prefix = _
|
||||
|
||||
# Instance fields are camelCase and start with _
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
|
||||
|
||||
dotnet_naming_symbols.instance_fields.applicable_kinds = field
|
||||
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
# Locals and parameters are camelCase
|
||||
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
|
||||
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
|
||||
|
||||
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
|
||||
|
||||
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||
|
||||
# Local functions are PascalCase
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
|
||||
|
||||
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
|
||||
|
||||
dotnet_naming_style.local_function_style.capitalization = pascal_case
|
||||
|
||||
# By default, name items with PascalCase
|
||||
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
|
||||
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
|
||||
|
||||
dotnet_naming_symbols.all_members.applicable_kinds = *
|
||||
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
|
||||
###############################
|
||||
# C# Coding Conventions #
|
||||
###############################
|
||||
[*.cs]
|
||||
# var preferences
|
||||
csharp_style_var_for_built_in_types = true:silent
|
||||
csharp_style_var_when_type_is_apparent = true:silent
|
||||
csharp_style_var_elsewhere = true:silent
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
# Null-checking preferences
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
# Modifier preferences
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
|
||||
# Expression-level preferences
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
|
||||
###############################
|
||||
# C# Formatting Rules #
|
||||
###############################
|
||||
# New line preferences
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
# Indentation preferences
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_statements = true
|
||||
csharp_preserve_single_line_blocks = true
|
||||
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
.idea/
|
||||
artifacts
|
||||
7
Directory.Build.props
Normal file
@@ -0,0 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>0.0.0.0</Version>
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
28
Jellyfin.Plugin.Seasonals.sln
Normal file
@@ -0,0 +1,28 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
#
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Seasonals", "Jellyfin.Plugin.Seasonals\Jellyfin.Plugin.Seasonals.csproj", "{D921B930-CF91-406F-ACBC-08914DCD0D34}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
59
Jellyfin.Plugin.Seasonals/Api/SeasonalsController.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals.Api;
|
||||
|
||||
[ApiController]
|
||||
[Route("Seasonals")]
|
||||
public class SeasonalsController : ControllerBase
|
||||
{
|
||||
[HttpGet("Config")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<object> GetConfig()
|
||||
{
|
||||
var config = Plugin.Instance?.Configuration;
|
||||
return new
|
||||
{
|
||||
selectedSeason = config?.SelectedSeason ?? "none",
|
||||
automateSeasonSelection = config?.AutomateSeasonSelection ?? true
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("Resources/{*path}")]
|
||||
public ActionResult GetResource(string path)
|
||||
{
|
||||
// Sanitize path
|
||||
if (string.IsNullOrWhiteSpace(path) || path.Contains(".."))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
// Convert path to resource name
|
||||
// path: "autumn_images/acorn1.png" -> "Jellyfin.Plugin.Seasonals.Web.autumn_images.acorn1.png"
|
||||
var resourcePath = path.Replace('/', '.').Replace('\\', '.');
|
||||
var resourceName = $"Jellyfin.Plugin.Seasonals.Web.{resourcePath}";
|
||||
|
||||
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
return NotFound($"Resource not found: {resourceName}");
|
||||
}
|
||||
|
||||
string contentType = GetContentType(path);
|
||||
return File(stream, contentType);
|
||||
}
|
||||
|
||||
private string GetContentType(string path)
|
||||
{
|
||||
if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) return "application/javascript";
|
||||
if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) return "text/css";
|
||||
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
|
||||
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
|
||||
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin configuration.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
SelectedSeason = "none";
|
||||
AutomateSeasonSelection = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected season.
|
||||
/// </summary>
|
||||
public string SelectedSeason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to automate season selection.
|
||||
/// </summary>
|
||||
public bool AutomateSeasonSelection { get; set; }
|
||||
}
|
||||
76
Jellyfin.Plugin.Seasonals/Configuration/configPage.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Template</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<form id="SeasonalsConfigForm">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="AutomateSeasonSelection" name="AutomateSeasonSelection" type="checkbox" is="emby-checkbox" />
|
||||
<span>Automate Season Selection</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Automatically select the season based on the date.</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="SelectedSeason">Selected Season</label>
|
||||
<select is="emby-select" id="SelectedSeason" name="SelectedSeason" class="emby-select-withcolor emby-select">
|
||||
<option value="none">None</option>
|
||||
<option value="snowflakes">Snowflakes</option>
|
||||
<option value="snowfall">Snowfall</option>
|
||||
<option value="snowstorm">Snowstorm</option>
|
||||
<option value="fireworks">Fireworks</option>
|
||||
<option value="halloween">Halloween</option>
|
||||
<option value="hearts">Hearts</option>
|
||||
<option value="christmas">Christmas</option>
|
||||
<option value="santa">Santa</option>
|
||||
<option value="autumn">Autumn</option>
|
||||
<option value="easter">Easter</option>
|
||||
</select>
|
||||
<div class="fieldDescription">The season to display if automation is disabled.</div>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var SeasonalsConfig = {
|
||||
pluginUniqueId: 'eb5d7894-8eef-4b36-aa6f-5d124e828ce1'
|
||||
};
|
||||
|
||||
document.querySelector('#SeasonalsConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
|
||||
document.querySelector('#SelectedSeason').value = config.SelectedSeason;
|
||||
document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#SeasonalsConfigForm')
|
||||
.addEventListener('submit', function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
|
||||
config.SelectedSeason = document.querySelector('#SelectedSeason').value;
|
||||
config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked;
|
||||
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
35
Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj
Normal file
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.Seasonals</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.7" >
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.10.7">
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Configuration\configPage.html" />
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
<None Remove="Web\**" />
|
||||
<EmbeddedResource Include="Web\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
57
Jellyfin.Plugin.Seasonals/Plugin.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Jellyfin.Plugin.Seasonals.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals;
|
||||
|
||||
/// <summary>
|
||||
/// The main plugin.
|
||||
/// </summary>
|
||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
private readonly ScriptInjector _scriptInjector;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILoggerFactory loggerFactory)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
_scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>());
|
||||
_scriptInjector.Inject();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Seasonals";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current plugin instance.
|
||||
/// </summary>
|
||||
public static Plugin? Instance { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
return
|
||||
[
|
||||
new PluginPageInfo
|
||||
{
|
||||
Name = Name,
|
||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
119
Jellyfin.Plugin.Seasonals/ScriptInjector.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals;
|
||||
|
||||
public class ScriptInjector
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<ScriptInjector> _logger;
|
||||
private const string ScriptTag = "<script src=\"Seasonals/Resources/seasonals.js\"></script>";
|
||||
private const string Marker = "</body>";
|
||||
|
||||
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Inject()
|
||||
{
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath))
|
||||
{
|
||||
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
var indexPath = Path.Combine(webPath, "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
_logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (content.Contains(ScriptTag))
|
||||
{
|
||||
_logger.LogInformation("Seasonals script already injected.");
|
||||
return;
|
||||
}
|
||||
|
||||
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}");
|
||||
if (newContent == content)
|
||||
{
|
||||
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllText(indexPath, newContent);
|
||||
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error injecting Seasonals script.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath)) return;
|
||||
|
||||
var indexPath = Path.Combine(webPath, "index.html");
|
||||
if (!File.Exists(indexPath)) return;
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (!content.Contains(ScriptTag)) return;
|
||||
|
||||
var newContent = content.Replace(ScriptTag, "").Replace($"{ScriptTag}\n", "");
|
||||
File.WriteAllText(indexPath, newContent);
|
||||
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error removing Seasonals script.");
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetWebPath()
|
||||
{
|
||||
// Try to find the web path using IApplicationPaths
|
||||
// Note: The property name might vary depending on the Jellyfin version,
|
||||
// but usually it's accessible via the configuration or standard paths.
|
||||
// For this plugin context, we'll try to rely on the standard path structure if IApplicationPaths doesn't expose it directly
|
||||
// or if we need to infer it.
|
||||
|
||||
// In many Jellyfin versions, appPaths.WebPath is the property.
|
||||
// However, since we are in a plugin, we might need to use reflection if the interface doesn't expose it in the reference assembly,
|
||||
// or just assume it's there.
|
||||
|
||||
// Let's try to use the property if it exists.
|
||||
// If the compilation fails, we will adjust.
|
||||
// For now, we assume 'WebPath' is available on IApplicationPaths implementation
|
||||
// but it might not be on the interface in the nuget package.
|
||||
|
||||
// Workaround: Check known locations if property is missing or use reflection.
|
||||
|
||||
// Attempt 1: Reflection to get WebPath property (safest if interface varies)
|
||||
var prop = _appPaths.GetType().GetProperty("WebPath");
|
||||
if (prop != null)
|
||||
{
|
||||
return prop.GetValue(_appPaths) as string;
|
||||
}
|
||||
|
||||
// Attempt 2: Guess based on ProgramDataPath (common in Windows)
|
||||
// Usually <ProgramData>/jellyfin/web or <InstallDir>/jellyfin-web
|
||||
|
||||
// Let's try to look relative to the application executable if possible, but that's hard from a plugin.
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
155
Jellyfin.Plugin.Seasonals/Web/autumn.css
Normal file
@@ -0,0 +1,155 @@
|
||||
.autumn-container {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: leaf-fall, leaf-shake;
|
||||
-webkit-animation-duration: 7s, 4s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
-webkit-user-select: none;
|
||||
animation-name: leaf-fall, leaf-shake;
|
||||
animation-duration: 7s, 4s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
/* Class to disable rotation */
|
||||
.no-rotation {
|
||||
--rotate-start: 0deg !important;
|
||||
--rotate-end: 0deg !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes leaf-shake {
|
||||
0%, 100% {
|
||||
-webkit-transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaf-shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
||||
}
|
||||
50% {
|
||||
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
||||
}
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
--rotate-start: -25deg;
|
||||
--rotate-end: 22deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 0.5s;
|
||||
--rotate-start: -32deg;
|
||||
--rotate-end: 35deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 1s;
|
||||
--rotate-start: -28deg;
|
||||
--rotate-end: 28deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 1.5s;
|
||||
--rotate-start: -38deg;
|
||||
--rotate-end: 32deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 0.8s;
|
||||
--rotate-start: -22deg;
|
||||
--rotate-end: 38deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 2s;
|
||||
--rotate-start: -35deg;
|
||||
--rotate-end: 25deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 1.2s;
|
||||
--rotate-start: -40deg;
|
||||
--rotate-end: 40deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 0.3s;
|
||||
--rotate-start: -30deg;
|
||||
--rotate-end: 30deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 1.8s;
|
||||
--rotate-start: -26deg;
|
||||
--rotate-end: 36deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 0.7s;
|
||||
--rotate-start: -34deg;
|
||||
--rotate-end: 24deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 2.3s;
|
||||
--rotate-start: -29deg;
|
||||
--rotate-end: 33deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 1.4s;
|
||||
--rotate-start: -37deg;
|
||||
--rotate-end: 27deg;
|
||||
}
|
||||
175
Jellyfin.Plugin.Seasonals/Web/autumn.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const leaves = true; // enable/disable leaves
|
||||
const randomLeaves = true; // enable random leaves
|
||||
const randomLeavesMobile = false; // enable random leaves on mobile devices
|
||||
const enableDiffrentDuration = true; // enable different duration for the random leaves
|
||||
const enableRotation = false; // enable/disable leaf rotation
|
||||
const leafCount = 25; // count of random extra leaves
|
||||
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
// function to check and control the leaves
|
||||
function toggleAutumn() {
|
||||
const autumnContainer = document.querySelector('.autumn-container');
|
||||
if (!autumnContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide leaves if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
autumnContainer.style.display = 'none'; // hide leaves
|
||||
if (!msgPrinted) {
|
||||
console.log('Autumn hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
autumnContainer.style.display = 'block'; // show leaves
|
||||
if (msgPrinted) {
|
||||
console.log('Autumn visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleAutumn);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"Seasonals/Resources/autumn_images/acorn1.png",
|
||||
"Seasonals/Resources/autumn_images/acorn2.png",
|
||||
"Seasonals/Resources/autumn_images/leaf1.png",
|
||||
"Seasonals/Resources/autumn_images/leaf2.png",
|
||||
"Seasonals/Resources/autumn_images/leaf3.png",
|
||||
"Seasonals/Resources/autumn_images/leaf4.png",
|
||||
"Seasonals/Resources/autumn_images/leaf5.png",
|
||||
"Seasonals/Resources/autumn_images/leaf6.png",
|
||||
"Seasonals/Resources/autumn_images/leaf7.png",
|
||||
"Seasonals/Resources/autumn_images/leaf8.png",
|
||||
"Seasonals/Resources/autumn_images/leaf9.png",
|
||||
"Seasonals/Resources/autumn_images/leaf10.png",
|
||||
"Seasonals/Resources/autumn_images/leaf11.png",
|
||||
"Seasonals/Resources/autumn_images/leaf12.png",
|
||||
"Seasonals/Resources/autumn_images/leaf13.png",
|
||||
"Seasonals/Resources/autumn_images/leaf14.png",
|
||||
"Seasonals/Resources/autumn_images/leaf15.png",
|
||||
];
|
||||
|
||||
function addRandomLeaves(count) {
|
||||
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||
if (!autumnContainer) return; // exit if leave container is not found
|
||||
|
||||
console.log('Adding random leaves');
|
||||
|
||||
// Array of leave characters
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new leave element
|
||||
const leaveDiv = document.createElement('div');
|
||||
leaveDiv.className = enableRotation ? "leaf" : "leaf no-rotation";
|
||||
|
||||
// pick a random leaf symbol
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
leaveDiv.appendChild(img);
|
||||
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 4; // delay for shake+rotate (0s to 4s)
|
||||
|
||||
// apply styles
|
||||
leaveDiv.style.left = `${randomLeft}%`;
|
||||
leaveDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 16s)
|
||||
const randomAnimationDuration2 = Math.random() * 3 + 2; // shake+rotate duration (2s to 5s)
|
||||
leaveDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// set random rotation angles (only if rotation is enabled)
|
||||
if (enableRotation) {
|
||||
const randomRotateStart = -(Math.random() * 40 + 20); // -20deg to -60deg
|
||||
const randomRotateEnd = Math.random() * 40 + 20; // 20deg to 60deg
|
||||
leaveDiv.style.setProperty('--rotate-start', `${randomRotateStart}deg`);
|
||||
leaveDiv.style.setProperty('--rotate-end', `${randomRotateEnd}deg`);
|
||||
} else {
|
||||
// No rotation - set to 0 degrees
|
||||
leaveDiv.style.setProperty('--rotate-start', '0deg');
|
||||
leaveDiv.style.setProperty('--rotate-end', '0deg');
|
||||
}
|
||||
|
||||
// add the leave to the container
|
||||
autumnContainer.appendChild(leaveDiv);
|
||||
}
|
||||
console.log('Random leaves added');
|
||||
}
|
||||
|
||||
// initialize standard leaves
|
||||
function initLeaves() {
|
||||
const container = document.querySelector('.autumn-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.autumn-container')) {
|
||||
container.className = "autumn-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const leafDiv = document.createElement("div");
|
||||
leafDiv.className = enableRotation ? "leaf" : "leaf no-rotation";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = images[Math.floor(Math.random() * images.length)];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 16s)
|
||||
const randomAnimationDuration2 = Math.random() * 3 + 2; // shake+rotate duration (2s to 5s)
|
||||
leafDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// set random rotation angles for standard leaves too (only if rotation is enabled)
|
||||
if (enableRotation) {
|
||||
const randomRotateStart = -(Math.random() * 40 + 20); // -20deg to -60deg
|
||||
const randomRotateEnd = Math.random() * 40 + 20; // 20deg to 60deg
|
||||
leafDiv.style.setProperty('--rotate-start', `${randomRotateStart}deg`);
|
||||
leafDiv.style.setProperty('--rotate-end', `${randomRotateEnd}deg`);
|
||||
} else {
|
||||
// No rotation - set to 0 degrees
|
||||
leafDiv.style.setProperty('--rotate-start', '0deg');
|
||||
leafDiv.style.setProperty('--rotate-end', '0deg');
|
||||
}
|
||||
|
||||
leafDiv.appendChild(img);
|
||||
container.appendChild(leafDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize leaves and add random leaves
|
||||
function initializeLeaves() {
|
||||
if (!leaves) return; // exit if leaves are disabled
|
||||
initLeaves();
|
||||
toggleAutumn();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomLeaves && (screenWidth > 768 || randomLeavesMobile)) { // add random leaves only on larger screens, unless enabled for mobile devices
|
||||
addRandomLeaves(leafCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeLeaves();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/acorn1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/acorn2.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf1.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf10.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf11.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf12.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf13.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf14.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf15.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf2.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf3.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf4.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf5.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf6.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf7.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf8.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/autumn_images/leaf9.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
132
Jellyfin.Plugin.Seasonals/Web/christmas.css
Normal file
@@ -0,0 +1,132 @@
|
||||
.christmas-container {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.christmas {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
-webkit-animation-name: christmas-fall, christmas-shake;
|
||||
-webkit-animation-duration: 10s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: christmas-fall, christmas-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes christmas-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes christmas-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 0s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 2.5s;
|
||||
}
|
||||
124
Jellyfin.Plugin.Seasonals/Web/christmas.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const christmas = true; // enable/disable christmas
|
||||
const randomChristmas = true; // enable random Christmas
|
||||
const randomChristmasMobile = false; // enable random Christmas on mobile devices
|
||||
const enableDiffrentDuration = true; // enable different duration for the random Christmas symbols
|
||||
const christmasCount = 25; // count of random extra christmas
|
||||
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
// function to check and control the christmas
|
||||
function toggleChristmas() {
|
||||
const christmasContainer = document.querySelector('.christmas-container');
|
||||
if (!christmasContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide christmas if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
christmasContainer.style.display = 'none'; // hide christmas
|
||||
if (!msgPrinted) {
|
||||
console.log('Christmas hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
christmasContainer.style.display = 'block'; // show christmas
|
||||
if (msgPrinted) {
|
||||
console.log('Christmas visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleChristmas);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
// Array of christmas characters
|
||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||
|
||||
function addRandomChristmas(count) {
|
||||
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
||||
if (!christmasContainer) return; // exit if christmas container is not found
|
||||
|
||||
console.log('Adding random christmas');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new christmas element
|
||||
const christmasDiv = document.createElement('div');
|
||||
christmasDiv.classList.add('christmas');
|
||||
|
||||
// pick a random christmas symbol
|
||||
christmasDiv.textContent = christmasSymbols[Math.floor(Math.random() * christmasSymbols.length)];
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 12 + 8; // delay (8s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 5 + 3; // delay (0s to 5s)
|
||||
|
||||
// apply styles
|
||||
christmasDiv.style.left = `${randomLeft}%`;
|
||||
christmasDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
christmasDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// add the christmas to the container
|
||||
christmasContainer.appendChild(christmasDiv);
|
||||
}
|
||||
console.log('Random christmas added');
|
||||
}
|
||||
|
||||
// initialize standard christmas
|
||||
function initChristmas() {
|
||||
const christmasContainer = document.querySelector('.christmas-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.christmas-container')) {
|
||||
christmasContainer.className = "christmas-container";
|
||||
christmasContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(christmasContainer);
|
||||
}
|
||||
|
||||
// create the 12 standard christmas
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const christmasDiv = document.createElement('div');
|
||||
christmasDiv.className = 'christmas';
|
||||
christmasDiv.textContent = christmasSymbols[Math.floor(Math.random() * christmasSymbols.length)];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
christmasDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
christmasContainer.appendChild(christmasDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize christmas and add random christmas symbols
|
||||
function initializeChristmas() {
|
||||
if (!christmas) return; // exit if christmas is disabled
|
||||
initChristmas();
|
||||
toggleChristmas();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomChristmas && (screenWidth > 768 || randomChristmasMobile)) { // add random christmas only on larger screens, unless enabled for mobile devices
|
||||
addRandomChristmas(christmasCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeChristmas();
|
||||
152
Jellyfin.Plugin.Seasonals/Web/easter.css
Normal file
@@ -0,0 +1,152 @@
|
||||
.easter-container {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hopping-rabbit {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
width: 70px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hopping-rabbit {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.easter {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: easter-fall, easter-shake;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: easter-fall, easter-shake;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
.easter img {
|
||||
height: auto;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes easter-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes easter-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes easter-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes easter-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.easter:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 0s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 2.5s;
|
||||
}
|
||||
241
Jellyfin.Plugin.Seasonals/Web/easter.js
Normal file
@@ -0,0 +1,241 @@
|
||||
const easter = true; // enable/disable easter
|
||||
const randomEaster = true; // enable random easter
|
||||
const randomEasterMobile = false; // enable random easter on mobile devices
|
||||
const enableDiffrentDuration = true; // enable different duration for the random easter
|
||||
const easterEggCount = 20; // count of random extra easter
|
||||
|
||||
const bunny = true; // enable/disable hopping bunny
|
||||
const bunnyDuration = 12000; // duration of the bunny animation in ms
|
||||
const hopHeight = 12; // height of the bunny hops in px
|
||||
const minBunnyRestTime = 2000; // minimum time the bunny rests in ms
|
||||
const maxBunnyRestTime = 5000; // maximum time the bunny rests in ms
|
||||
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
let animationFrameId;
|
||||
|
||||
// function to check and control the easter
|
||||
function toggleEaster() {
|
||||
const easterContainer = document.querySelector('.easter-container');
|
||||
if (!easterContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide easter if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
easterContainer.style.display = 'none'; // hide easter
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
if (!msgPrinted) {
|
||||
console.log('Easter hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
easterContainer.style.display = 'block'; // show easter
|
||||
if (!animationFrameId) {
|
||||
animateRabbit(); // start animation
|
||||
}
|
||||
if (msgPrinted) {
|
||||
console.log('Easter visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleEaster);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"Seasonals/Resources/easter_images/egg_1.png",
|
||||
"Seasonals/Resources/easter_images/egg_2.png",
|
||||
"Seasonals/Resources/easter_images/egg_3.png",
|
||||
"Seasonals/Resources/easter_images/egg_4.png",
|
||||
"Seasonals/Resources/easter_images/egg_5.png",
|
||||
"Seasonals/Resources/easter_images/egg_6.png",
|
||||
"Seasonals/Resources/easter_images/egg_7.png",
|
||||
"Seasonals/Resources/easter_images/egg_8.png",
|
||||
"Seasonals/Resources/easter_images/egg_9.png",
|
||||
"Seasonals/Resources/easter_images/egg_10.png",
|
||||
"Seasonals/Resources/easter_images/egg_11.png",
|
||||
"Seasonals/Resources/easter_images/egg_12.png",
|
||||
];
|
||||
const rabbit = "Seasonals/Resources/easter_images/easter-bunny.png";
|
||||
|
||||
function addRandomEaster(count) {
|
||||
const easterContainer = document.querySelector('.easter-container'); // get the leave container
|
||||
if (!easterContainer) return; // exit if leave container is not found
|
||||
|
||||
console.log('Adding random easter eggs');
|
||||
|
||||
// Array of leave characters
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new leave element
|
||||
const eggDiv = document.createElement('div');
|
||||
eggDiv.className = "easter";
|
||||
|
||||
// pick a random easter symbol
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
eggDiv.appendChild(img);
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 12; // delay (0s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
|
||||
// apply styles
|
||||
eggDiv.style.left = `${randomLeft}%`;
|
||||
eggDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
|
||||
// add the leave to the container
|
||||
easterContainer.appendChild(eggDiv);
|
||||
}
|
||||
console.log('Random easter added');
|
||||
}
|
||||
|
||||
function addHoppingRabbit() {
|
||||
if (!bunny) return; // Nur ausführen, wenn Easter aktiviert ist
|
||||
|
||||
const easterContainer = document.querySelector('.easter-container');
|
||||
if (!easterContainer) return;
|
||||
|
||||
// Hase erstellen
|
||||
const rabbitImg = document.createElement("img");
|
||||
rabbitImg.id = "rabbit";
|
||||
rabbitImg.src = rabbit; // Bildpfad aus der bestehenden Definition
|
||||
rabbitImg.alt = "Hoppelnder Osterhase";
|
||||
|
||||
// CSS-Klassen hinzufügen
|
||||
rabbitImg.classList.add("hopping-rabbit");
|
||||
|
||||
easterContainer.appendChild(rabbitImg);
|
||||
|
||||
rabbitImg.style.bottom = (hopHeight / 2 + 6) + "px";
|
||||
|
||||
animateRabbit(rabbitImg);
|
||||
}
|
||||
|
||||
function animateRabbit(rabbitElement) {
|
||||
const rabbit = rabbitElement || document.querySelector('#rabbit');
|
||||
if (!rabbit) return;
|
||||
|
||||
let startTime = null;
|
||||
|
||||
function animationStep(timestamp) {
|
||||
if (!startTime) {
|
||||
startTime = timestamp;
|
||||
|
||||
// random start position and direction
|
||||
const startFromLeft = Math.random() >= 0.5;
|
||||
rabbit.startX = startFromLeft ? -10 : 110;
|
||||
rabbit.endX = startFromLeft ? 110 : -10;
|
||||
rabbit.direction = startFromLeft ? 1 : -1;
|
||||
|
||||
// mirror the rabbit image if it starts from the right
|
||||
rabbit.style.transform = startFromLeft ? '' : 'scaleX(-1)';
|
||||
}
|
||||
const progress = timestamp - startTime;
|
||||
|
||||
// calculate the horizontal position (linear interpolation)
|
||||
const x = rabbit.startX + (progress / bunnyDuration) * (rabbit.endX - rabbit.startX);
|
||||
|
||||
// calculate the vertical position (sinus curve)
|
||||
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
|
||||
|
||||
// set the new position
|
||||
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`;
|
||||
|
||||
if (progress < bunnyDuration) {
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
} else {
|
||||
// let the bunny rest for a while before hiding easter eggs again
|
||||
const pauseDuration = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
|
||||
setTimeout(() => {
|
||||
startTime = null;
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
}, pauseDuration);
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
}
|
||||
|
||||
|
||||
// initialize standard easter
|
||||
function initEaster() {
|
||||
const container = document.querySelector('.easter-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.easter-container')) {
|
||||
container.className = "easter-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// shuffle the easter images
|
||||
let currentIndex = images.length;
|
||||
let randomIndex;
|
||||
while (currentIndex != 0) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[images[currentIndex], images[randomIndex]] = [images[randomIndex], images[currentIndex]];
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const eggDiv = document.createElement("div");
|
||||
eggDiv.className = "easter";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = images[i];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
eggDiv.appendChild(img);
|
||||
container.appendChild(eggDiv);
|
||||
}
|
||||
|
||||
addHoppingRabbit();
|
||||
}
|
||||
|
||||
|
||||
// initialize easter and add random easter after the DOM is loaded
|
||||
function initializeEaster() {
|
||||
if (!easter) return; // exit if easter are disabled
|
||||
initEaster();
|
||||
toggleEaster();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomEaster && (screenWidth > 768 || randomEasterMobile)) { // add random easter only on larger screens, unless enabled for mobile devices
|
||||
addRandomEaster(easterEggCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
initializeEaster();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/easter-bunny.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_1.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_10.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_11.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_12.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_2.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_3.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_4.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_5.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_6.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_7.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_8.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/egg_9.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/eggs.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/rabbit_1.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/rabbit_2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
63
Jellyfin.Plugin.Seasonals/Web/fireworks.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.fireworks {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.rocket-trail {
|
||||
position: absolute;
|
||||
left: var(--trailX);
|
||||
top: var(--trailStartY);
|
||||
width: 4px;
|
||||
|
||||
/* activate the following for rocket trail */
|
||||
height: 60px;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
|
||||
filter: blur(2px);
|
||||
|
||||
/* activate the following for rocket trail as a point */
|
||||
/*height: 4px;*/
|
||||
/*background: white;*/
|
||||
/*border-radius: 50%;
|
||||
box-shadow: 0 0 8px 2px white;*/
|
||||
|
||||
animation: rocket-trail-animation 1s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes rocket-trail-animation {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(calc(var(--trailEndY) - var(--trailStartY)));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for the particles */
|
||||
@keyframes fireworkParticle {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(var(--x), var(--y));
|
||||
}
|
||||
}
|
||||
|
||||
.firework {
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
animation: fireworkParticle 1.5s ease-out forwards;
|
||||
filter: blur(1px);
|
||||
}
|
||||
161
Jellyfin.Plugin.Seasonals/Web/fireworks.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const fireworks = true; // enable/disable fireworks
|
||||
const scrollFireworks = true; // enable fireworks to scroll with page content
|
||||
const particlesPerFirework = 50; // count of particles per firework
|
||||
const minFireworks = 3; // minimum number of simultaneous fireworks
|
||||
const maxFireworks = 6; // maximum number of simultaneous fireworks
|
||||
const intervalOfFireworks = 3200; // interval for the fireworks in milliseconds
|
||||
|
||||
// array of color palettes for the fireworks
|
||||
const colorPalettes = [
|
||||
['#ff0000', '#ff7300', '#ff4500'], // red's
|
||||
['#0040ff', '#5a9bff', '#b0d9ff'], // blue's
|
||||
['#47ff00', '#8eff47', '#00ff7f'], // green's
|
||||
['#ffd700', '#c0c0c0', '#ff6347'], // gold, silver, red
|
||||
['#ff00ff', '#ff99ff', '#800080'], // magenta's
|
||||
['#ffef00', '#ffff99', '#ffd700'], // yellow's
|
||||
['#ff4500', '#ff6347', '#ff7f50'], // orange's
|
||||
['#e3e3e3', '#c0c0c0', '#7d7c7c'], // silver's
|
||||
];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
let spacing = 0; // spacing between fireworks
|
||||
|
||||
// function to check and control fireworks
|
||||
function toggleFirework() {
|
||||
const fireworksContainer = document.querySelector('.fireworks');
|
||||
if (!fireworksContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide fireworks if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
fireworksContainer.style.display = 'none'; // hide fireworks
|
||||
if (!msgPrinted) {
|
||||
console.log('Fireworks hidden');
|
||||
clearInterval(fireworksInterval);
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
fireworksContainer.style.display = 'block'; // show fireworks
|
||||
if (msgPrinted) {
|
||||
console.log('Fireworks visible');
|
||||
startFireworks();
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleFirework);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
// Function to create a rocket trail
|
||||
function createRocketTrail(x, startY, endY) {
|
||||
const fireworkContainer = document.querySelector('.fireworks');
|
||||
const rocketTrail = document.createElement('div');
|
||||
rocketTrail.classList.add('rocket-trail');
|
||||
fireworkContainer.appendChild(rocketTrail);
|
||||
|
||||
// Set position and animation
|
||||
rocketTrail.style.setProperty('--trailX', `${x}px`);
|
||||
rocketTrail.style.setProperty('--trailStartY', `${startY}px`);
|
||||
rocketTrail.style.setProperty('--trailEndY', `${endY}px`);
|
||||
|
||||
// Remove the element after the animation
|
||||
setTimeout(() => {
|
||||
fireworkContainer.removeChild(rocketTrail);
|
||||
}, 2000); // Duration of the animation
|
||||
}
|
||||
|
||||
// Function for particle explosion
|
||||
function createExplosion(x, y) {
|
||||
const fireworkContainer = document.querySelector('.fireworks');
|
||||
|
||||
// Choose a random color palette
|
||||
const chosenPalette = colorPalettes[Math.floor(Math.random() * colorPalettes.length)];
|
||||
|
||||
for (let i = 0; i < particlesPerFirework; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.classList.add('firework');
|
||||
|
||||
const angle = Math.random() * 2 * Math.PI; // Random direction
|
||||
const distance = Math.random() * 180 + 100; // Random distance
|
||||
const xOffset = Math.cos(angle) * distance;
|
||||
const yOffset = Math.sin(angle) * distance;
|
||||
|
||||
particle.style.left = `${x}px`;
|
||||
particle.style.top = `${y}px`;
|
||||
particle.style.setProperty('--x', `${xOffset}px`);
|
||||
particle.style.setProperty('--y', `${yOffset}px`);
|
||||
particle.style.background = chosenPalette[Math.floor(Math.random() * chosenPalette.length)];
|
||||
|
||||
fireworkContainer.appendChild(particle);
|
||||
|
||||
// Remove particle after the animation
|
||||
setTimeout(() => particle.remove(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Function for the firework with trail
|
||||
function launchFirework() {
|
||||
// Random horizontal position
|
||||
const x = Math.random() * window.innerWidth; // Any value across the entire width
|
||||
|
||||
// Trail starts at the bottom and ends at a random height around the middle
|
||||
let startY, endY;
|
||||
if (scrollFireworks) {
|
||||
// Y-position considers scrolling
|
||||
startY = window.scrollY + window.innerHeight; // Bottom edge of the window plus the scroll offset
|
||||
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2 + window.scrollY; // Area around the middle, but also with scrolling
|
||||
} else {
|
||||
startY = window.innerHeight; // Bottom edge of the window
|
||||
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2; // Area around the middle
|
||||
}
|
||||
|
||||
// Create trail
|
||||
createRocketTrail(x, startY, endY);
|
||||
|
||||
// Create explosion
|
||||
setTimeout(() => {
|
||||
createExplosion(x, endY); // Explosion at the end height
|
||||
}, 1000); // or 1200
|
||||
}
|
||||
|
||||
// Start the firework routine
|
||||
function startFireworks() {
|
||||
const fireworkContainer = document.querySelector('.fireworks') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.fireworks')) {
|
||||
fireworkContainer.className = "fireworks";
|
||||
fireworkContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(fireworkContainer);
|
||||
}
|
||||
|
||||
fireworksInterval = setInterval(() => {
|
||||
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
|
||||
for (let i = 0; i < randomCount; i++) {
|
||||
setTimeout(() => {
|
||||
launchFirework();
|
||||
}, i * 200); // 200ms delay between fireworks
|
||||
}
|
||||
}, intervalOfFireworks); // Interval between fireworks
|
||||
}
|
||||
|
||||
// Initialize fireworks and add random fireworks
|
||||
function initializeFireworks() {
|
||||
if (!fireworks) return; // exit if fireworks are disabled
|
||||
startFireworks();
|
||||
toggleFirework();
|
||||
}
|
||||
|
||||
initializeFireworks();
|
||||
146
Jellyfin.Plugin.Seasonals/Web/halloween.css
Normal file
@@ -0,0 +1,146 @@
|
||||
.halloween-container {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.halloween {
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
z-index: 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: halloween-fall, halloween-shake;
|
||||
-webkit-animation-duration: 10s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
-webkit-animation-play-state: running, running;
|
||||
animation-name: halloween-fall, halloween-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-play-state: running, running
|
||||
}
|
||||
|
||||
@-webkit-keyframes halloween-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes halloween-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes halloween-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes halloween-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-webkit-animation-delay: 4s, 2s;
|
||||
animation-delay: 4s, 2s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(4) {
|
||||
left: 40%;
|
||||
-webkit-animation-delay: 2s, 2s;
|
||||
animation-delay: 2s, 2s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(5) {
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 2s, 0s;
|
||||
animation-delay: 2s, 0s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(11) {
|
||||
left: 65%;
|
||||
-webkit-animation-delay: 4s, 2.5s;
|
||||
animation-delay: 4s, 2.5s
|
||||
}
|
||||
137
Jellyfin.Plugin.Seasonals/Web/halloween.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const halloween = true; // enable/disable halloween
|
||||
const randomSymbols = true; // enable more random symbols
|
||||
const randomSymbolsMobile = false; // enable random symbols on mobile devices
|
||||
const enableDiffrentDuration = true; // enable different duration for the random halloween symbols
|
||||
const halloweenCount = 25; // count of random extra symbols
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
// function to check and control the halloween
|
||||
function toggleHalloween() {
|
||||
const halloweenContainer = document.querySelector('.halloween-container');
|
||||
if (!halloweenContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide halloween if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
halloweenContainer.style.display = 'none'; // hide halloween
|
||||
if (!msgPrinted) {
|
||||
console.log('Halloween hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
halloweenContainer.style.display = 'block'; // show halloween
|
||||
if (msgPrinted) {
|
||||
console.log('Halloween visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleHalloween);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
];
|
||||
|
||||
function addRandomSymbols(count) {
|
||||
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
|
||||
if (!halloweenContainer) return; // exit if halloween container is not found
|
||||
|
||||
console.log('Adding random halloween symbols');
|
||||
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new halloween elements
|
||||
const halloweenDiv = document.createElement("div");
|
||||
halloweenDiv.className = "halloween";
|
||||
|
||||
// pick a random halloween symbol
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
halloweenDiv.appendChild(img);
|
||||
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s)
|
||||
const randomAnimationDelay2 = Math.random() * 3; // delay (0s to 3s)
|
||||
|
||||
// apply styles
|
||||
halloweenDiv.style.left = `${randomLeft}%`;
|
||||
halloweenDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// add the halloween to the container
|
||||
halloweenContainer.appendChild(halloweenDiv);
|
||||
}
|
||||
console.log('Random halloween symbols added');
|
||||
}
|
||||
|
||||
// create halloween objects
|
||||
function createHalloween() {
|
||||
const container = document.querySelector('.halloween-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.halloween-container')) {
|
||||
container.className = "halloween-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
images.forEach(imageSrc => {
|
||||
const halloweenDiv = document.createElement("div");
|
||||
halloweenDiv.className = "halloween";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
halloweenDiv.appendChild(img);
|
||||
container.appendChild(halloweenDiv);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// initialize halloween
|
||||
function initializeHalloween() {
|
||||
if (!halloween) return; // exit if halloween is disabled
|
||||
createHalloween();
|
||||
toggleHalloween();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
||||
addRandomSymbols(halloweenCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeHalloween();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/halloween_images/bat_20x20.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/halloween_images/ghost_20x20.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/halloween_images/lep_30x30.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/halloween_images/pumpkin_20x20.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
144
Jellyfin.Plugin.Seasonals/Web/hearts.css
Normal file
@@ -0,0 +1,144 @@
|
||||
.hearts-container {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heart {
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
z-index: 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: heart-fall, heart-shake;
|
||||
-webkit-animation-duration: 14s, 5s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: heart-fall, heart-shake;
|
||||
animation-duration: 14s, 5s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes heart-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes heart-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heart-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heart-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
.heart:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-webkit-animation-delay: 4s, 2s;
|
||||
animation-delay: 4s, 2s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(4) {
|
||||
left: 40%;
|
||||
-webkit-animation-delay: 2s, 2s;
|
||||
animation-delay: 2s, 2s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(5) {
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 2s, 0s;
|
||||
animation-delay: 2s, 0s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(11) {
|
||||
left: 65%;
|
||||
-webkit-animation-delay: 4s, 2.5s;
|
||||
animation-delay: 4s, 2.5s
|
||||
}
|
||||
126
Jellyfin.Plugin.Seasonals/Web/hearts.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const hearts = true; // enable/disable hearts
|
||||
const randomSymbols = true; // enable more random symbols
|
||||
const randomSymbolsMobile = false; // enable random symbols on mobile devices
|
||||
const enableDiffrentDuration = true; // enable different animation duration for random symbols
|
||||
const heartsCount = 25; // count of random extra symbols
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
// function to check and control the hearts
|
||||
function toggleHearts() {
|
||||
const heartsContainer = document.querySelector('.hearts-container');
|
||||
if (!heartsContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide hearts if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
heartsContainer.style.display = 'none'; // hide hearts
|
||||
if (!msgPrinted) {
|
||||
console.log('Hearts hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
heartsContainer.style.display = 'block'; // show hearts
|
||||
if (msgPrinted) {
|
||||
console.log('Hearts visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleHearts);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
// Array of hearts characters
|
||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||
|
||||
|
||||
function addRandomSymbols(count) {
|
||||
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
||||
if (!heartsContainer) return; // exit if hearts container is not found
|
||||
|
||||
console.log('Adding random heart symbols');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new hearts elements
|
||||
const heartsDiv = document.createElement("div");
|
||||
heartsDiv.className = "heart";
|
||||
|
||||
// pick a random hearts symbol
|
||||
heartsDiv.textContent = heartSymbols[Math.floor(Math.random() * heartSymbols.length)];
|
||||
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 14; // delay (0s to 14s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
|
||||
// apply styles
|
||||
heartsDiv.style.left = `${randomLeft}%`;
|
||||
heartsDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 16 + 12; // delay (12s to 16s)
|
||||
const randomAnimationDuration2 = Math.random() * 7 + 3; // delay (3s to 7s)
|
||||
heartsDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// add the hearts to the container
|
||||
heartsContainer.appendChild(heartsDiv);
|
||||
}
|
||||
console.log('Random hearts symbols added');
|
||||
}
|
||||
|
||||
// create hearts objects
|
||||
function createHearts() {
|
||||
const container = document.querySelector('.hearts-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.hearts-container')) {
|
||||
container.className = "hearts-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const heartsDiv = document.createElement("div");
|
||||
heartsDiv.className = "heart";
|
||||
heartsDiv.textContent = heartSymbols[i % heartSymbols.length];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 16 + 12; // delay (12s to 16s)
|
||||
const randomAnimationDuration2 = Math.random() * 7 + 3; // delay (3s to 7s)
|
||||
heartsDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
container.appendChild(heartsDiv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// initialize hearts
|
||||
function initializeHearts() {
|
||||
if (!hearts) return; // exit if hearts is disabled
|
||||
createHearts();
|
||||
toggleHearts();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random heartss only on larger screens, unless enabled for mobile devices
|
||||
addRandomSymbols(heartsCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeHearts();
|
||||
33
Jellyfin.Plugin.Seasonals/Web/santa.css
Normal file
@@ -0,0 +1,33 @@
|
||||
.santa-container {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.santa {
|
||||
position: fixed;
|
||||
width: 220px;
|
||||
height: auto;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.present {
|
||||
position: fixed;
|
||||
width: 15px;
|
||||
height: auto;
|
||||
z-index: 999;
|
||||
pointer-events: none;
|
||||
}
|
||||
303
Jellyfin.Plugin.Seasonals/Web/santa.js
Normal file
@@ -0,0 +1,303 @@
|
||||
const santaIsFlying = true; // enable/disable santa
|
||||
let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = 250; // count of snowflakes on mobile devices
|
||||
const snowFallSpeed = 3; // speed of snowfall (recommended values: 0-5)
|
||||
const santaSpeed = 10; // speed of santa in seconds (recommended values: 5000-15000)
|
||||
const santaSpeedMobile = 8; // speed of santa on mobile devices in seconds
|
||||
const maxSantaRestTime = 8; // maximum time santa rests in seconds
|
||||
const minSantaRestTime = 3; // minimum time santa rests in seconds
|
||||
const maxPresentFallSpeed = 5; // maximum speed of falling presents in seconds
|
||||
const minPresentFallSpeed = 2; // minimum speed of falling presents in seconds
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
let isMobile = false; // flag to detect mobile devices
|
||||
let canvas, ctx; // canvas and context for drawing snowflakes
|
||||
let animationFrameId; // ID of the animation frame
|
||||
let animationFrameIdSanta; // ID of the animation frame for santa
|
||||
|
||||
// function to check and control the santa
|
||||
function toggleSnowfall() {
|
||||
const santaContainer = document.querySelector('.santa-container');
|
||||
if (!santaContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide santa if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
santaContainer.style.display = 'none'; // hide santa
|
||||
removeCanvas();
|
||||
if (!msgPrinted) {
|
||||
console.log('Snowfall hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
santaContainer.style.display = 'block'; // show santa
|
||||
if (!animationFrameId && !animationFrameIdSanta) {
|
||||
initializeCanvas();
|
||||
snowflakes = createSnowflakes(santaContainer);
|
||||
animateAll();
|
||||
}
|
||||
|
||||
if (msgPrinted) {
|
||||
console.log('Snowfall visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowfall);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
function initializeCanvas() {
|
||||
if (document.getElementById('snowfallCanvas')) {
|
||||
console.warn('Canvas already exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.querySelector('.santa-container');
|
||||
if (!container) {
|
||||
console.error('Error: No element with class "santa-container" found.');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = 'snowfallCanvas';
|
||||
container.appendChild(canvas);
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
resizeCanvas(container);
|
||||
window.addEventListener('resize', () => resizeCanvas(container));
|
||||
}
|
||||
|
||||
function removeCanvas() {
|
||||
const canvas = document.getElementById('snowfallCanvas');
|
||||
if (canvas) {
|
||||
canvas.remove();
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
console.log('Animation frame canceled');
|
||||
}
|
||||
if (animationFrameIdSanta) {
|
||||
cancelAnimationFrame(animationFrameIdSanta);
|
||||
animationFrameIdSanta = null;
|
||||
console.log('Santa animation frame canceled');
|
||||
}
|
||||
console.log('Canvas removed');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(container) {
|
||||
if (!canvas) return;
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
}
|
||||
|
||||
function createSnowflakes(container) {
|
||||
return Array.from({ length: snowflakesCount }, () => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
radius: Math.random() * 0.6 + 1,
|
||||
speed: Math.random() * snowFallSpeed + 1,
|
||||
swing: Math.random() * 2 - 1,
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize snowflakes
|
||||
let snowflakes = [];
|
||||
|
||||
function drawSnowflakes() {
|
||||
if (!ctx || !canvas) {
|
||||
console.error('Error: Canvas or context not found.');
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // empty canvas
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'white'; // color of snowflakes
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSnowflakes() {
|
||||
snowflakes.forEach(flake => {
|
||||
flake.y += flake.speed; // downwards movement
|
||||
flake.x += flake.swing; // sideways movement
|
||||
|
||||
// reset snowflake if it reaches the bottom
|
||||
if (flake.y > canvas.height) {
|
||||
flake.y = 0;
|
||||
flake.x = Math.random() * canvas.width; // with new random X position
|
||||
}
|
||||
|
||||
// wrap snowflakes around the screen edges
|
||||
if (flake.x > canvas.width) flake.x = 0;
|
||||
if (flake.x < 0) flake.x = canvas.width;
|
||||
});
|
||||
}
|
||||
|
||||
// credits: flaticon.com
|
||||
const presentImages = [
|
||||
'Seasonals/Resources/santa_images/gift1.png',
|
||||
'Seasonals/Resources/santa_images/gift2.png',
|
||||
'Seasonals/Resources/santa_images/gift3.png',
|
||||
'Seasonals/Resources/santa_images/gift4.png',
|
||||
'Seasonals/Resources/santa_images/gift5.png',
|
||||
'Seasonals/Resources/santa_images/gift6.png',
|
||||
'Seasonals/Resources/santa_images/gift7.png',
|
||||
'Seasonals/Resources/santa_images/gift8.png',
|
||||
];
|
||||
|
||||
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
|
||||
const santaImage = 'Seasonals/Resources/santa_images/santa.gif';
|
||||
|
||||
|
||||
function createSantaElement() {
|
||||
const santa = document.createElement('img');
|
||||
santa.src = santaImage;
|
||||
santa.classList.add('santa');
|
||||
const santaContainer = document.querySelector('.santa-container');
|
||||
santaContainer.appendChild(santa);
|
||||
}
|
||||
|
||||
function dropPresent(santa, fromLeft) {
|
||||
const presentSrc = presentImages[Math.floor(Math.random() * presentImages.length)];
|
||||
const present = document.createElement('img');
|
||||
present.src = presentSrc;
|
||||
present.classList.add('present');
|
||||
santa.parentElement.appendChild(present);
|
||||
|
||||
// Get Santa's position
|
||||
const santaRect = santa.getBoundingClientRect();
|
||||
present.style.left = fromLeft ? `${santaRect.left}px` : `${santaRect.left + santaRect.width - 15}px`;
|
||||
present.style.top = `${santaRect.bottom - 50}px`;
|
||||
|
||||
// Start falling
|
||||
const duration = Math.random() * (maxPresentFallSpeed - minPresentFallSpeed) + minPresentFallSpeed;
|
||||
present.style.transition = `top ${duration}s linear`;
|
||||
requestAnimationFrame(() => {
|
||||
present.style.top = `${window.innerHeight}px`;
|
||||
});
|
||||
|
||||
// Remove from DOM after animation
|
||||
present.addEventListener('transitionend', () => {
|
||||
present.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function reloadSantaGif() {
|
||||
const santa = document.querySelector('.santa');
|
||||
const src = santa.src;
|
||||
santa.src = '';
|
||||
santa.src = src;
|
||||
}
|
||||
|
||||
function animateSanta() {
|
||||
const santa = document.querySelector('.santa');
|
||||
|
||||
function startAnimation() {
|
||||
const santaHeight = santa.offsetHeight;
|
||||
if (santaHeight === 0) {
|
||||
setTimeout(startAnimation, 100);
|
||||
return;
|
||||
}
|
||||
// console.log('Santa height: ', santaHeight);
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight;
|
||||
const fromLeft = Math.random() < 0.5;
|
||||
const startX = fromLeft ? -220 : screenWidth + 220;
|
||||
const endX = fromLeft ? screenWidth + 220 : -220;
|
||||
const startY = Math.random() * (screenHeight / 5) + santaHeight; // Restrict to upper screen
|
||||
const endY = Math.random() * (screenHeight / 5) + santaHeight; // Restrict to upper screen
|
||||
const angle = Math.random() * 16 - 8; // -8 to 8 degrees
|
||||
|
||||
santa.style.left = `${startX}px`;
|
||||
santa.style.top = `${startY}px`;
|
||||
santa.style.transform = `rotate(${angle}deg) ${fromLeft ? 'scaleX(-1)' : 'scaleX(1)'}`; // Mirror if not from left
|
||||
|
||||
let duration;
|
||||
if (isMobile) {
|
||||
duration = santaSpeedMobile * 1000;
|
||||
} else {
|
||||
duration = santaSpeed * 1000;
|
||||
}
|
||||
const deltaX = endX - startX;
|
||||
const deltaY = endY - startY;
|
||||
const startTime = performance.now();
|
||||
|
||||
function move() {
|
||||
const currentTime = performance.now();
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const currentY = startY + deltaY * progress - 50 * Math.sin(progress * Math.PI);
|
||||
santa.style.left = `${startX + deltaX * progress}px`;
|
||||
santa.style.top = `${currentY}px`;
|
||||
|
||||
if (Math.random() < 0.05) { // 5% chance to drop a present
|
||||
dropPresent(santa, fromLeft);
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameIdSanta = requestAnimationFrame(move);
|
||||
} else {
|
||||
const pause = Math.random() * ((maxSantaRestTime - minSantaRestTime) * 1000) + minSantaRestTime * 1000;
|
||||
setTimeout(animateSanta, pause);
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameIdSanta = requestAnimationFrame(move);
|
||||
}
|
||||
|
||||
reloadSantaGif();
|
||||
|
||||
startAnimation();
|
||||
}
|
||||
|
||||
|
||||
function animateAll() {
|
||||
drawSnowflakes();
|
||||
updateSnowflakes();
|
||||
animationFrameId = requestAnimationFrame(animateAll);
|
||||
}
|
||||
|
||||
// initialize santa
|
||||
function initializeSanta() {
|
||||
if (!santaIsFlying) {
|
||||
console.warn('Sante is disabled.');
|
||||
return; // exit if santa is disabled
|
||||
}
|
||||
const container = document.querySelector('.santa-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
isMobile = true;
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
}
|
||||
|
||||
console.log('Santa enabled.');
|
||||
initializeCanvas();
|
||||
snowflakes = createSnowflakes(container);
|
||||
createSantaElement();
|
||||
animateAll();
|
||||
animateSanta();
|
||||
}
|
||||
}
|
||||
|
||||
initializeSanta();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift1.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift2.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift3.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift4.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift5.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift6.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift7.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/gift8.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/santa_images/santa.gif
Normal file
|
After Width: | Height: | Size: 80 KiB |
206
Jellyfin.Plugin.Seasonals/Web/seasonals.js
Normal file
@@ -0,0 +1,206 @@
|
||||
// theme-configs.js
|
||||
|
||||
// theme configurations
|
||||
const themeConfigs = {
|
||||
snowflakes: {
|
||||
css: 'Seasonals/Resources/snowflakes.css',
|
||||
js: 'Seasonals/Resources/snowflakes.js',
|
||||
containerClass: 'snowflakes'
|
||||
},
|
||||
snowfall: {
|
||||
css: 'Seasonals/Resources/snowfall.css',
|
||||
js: 'Seasonals/Resources/snowfall.js',
|
||||
containerClass: 'snowfall-container'
|
||||
},
|
||||
snowstorm: {
|
||||
css: 'Seasonals/Resources/snowstorm.css',
|
||||
js: 'Seasonals/Resources/snowstorm.js',
|
||||
containerClass: 'snowstorm-container'
|
||||
},
|
||||
fireworks: {
|
||||
css: 'Seasonals/Resources/fireworks.css',
|
||||
js: 'Seasonals/Resources/fireworks.js',
|
||||
containerClass: 'fireworks'
|
||||
},
|
||||
halloween: {
|
||||
css: 'Seasonals/Resources/halloween.css',
|
||||
js: 'Seasonals/Resources/halloween.js',
|
||||
containerClass: 'halloween-container'
|
||||
},
|
||||
hearts: {
|
||||
css: 'Seasonals/Resources/hearts.css',
|
||||
js: 'Seasonals/Resources/hearts.js',
|
||||
containerClass: 'hearts-container'
|
||||
},
|
||||
christmas: {
|
||||
css: 'Seasonals/Resources/christmas.css',
|
||||
js: 'Seasonals/Resources/christmas.js',
|
||||
containerClass: 'christmas-container'
|
||||
},
|
||||
santa: {
|
||||
css: 'Seasonals/Resources/santa.css',
|
||||
js: 'Seasonals/Resources/santa.js',
|
||||
containerClass: 'santa-container'
|
||||
},
|
||||
autumn: {
|
||||
css: 'Seasonals/Resources/autumn.css',
|
||||
js: 'Seasonals/Resources/autumn.js',
|
||||
containerClass: 'autumn-container'
|
||||
},
|
||||
easter: {
|
||||
css: 'Seasonals/Resources/easter.css',
|
||||
js: 'Seasonals/Resources/easter.js',
|
||||
containerClass: 'easter-container'
|
||||
},
|
||||
summer: {
|
||||
css: 'Seasonals/Resources/summer.css',
|
||||
js: 'Seasonals/Resources/summer.js',
|
||||
containerClass: 'summer-container'
|
||||
},
|
||||
spring: {
|
||||
css: 'Seasonals/Resources/spring.css',
|
||||
js: 'Seasonals/Resources/spring.js',
|
||||
containerClass: 'spring-container'
|
||||
},
|
||||
none: {
|
||||
containerClass: 'none'
|
||||
},
|
||||
};
|
||||
|
||||
// determine current theme based on the current month
|
||||
function determineCurrentTheme() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth(); // 0-11
|
||||
const day = date.getDate(); // 1-31
|
||||
|
||||
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5
|
||||
|
||||
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18
|
||||
|
||||
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27
|
||||
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27
|
||||
|
||||
if (month === 11) return 'snowflakes'; // snowflakes december
|
||||
if (month === 0 || month === 1) return 'snowfall'; // snow january, february
|
||||
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february
|
||||
|
||||
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25
|
||||
|
||||
//NOT IMPLEMENTED YET
|
||||
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
|
||||
|
||||
//NOT IMPLEMENTED YET
|
||||
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
|
||||
|
||||
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
|
||||
|
||||
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
|
||||
|
||||
return 'none'; // Fallback (nothing)
|
||||
}
|
||||
|
||||
// load theme csss
|
||||
function loadThemeCSS(cssPath) {
|
||||
if (!cssPath) return;
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = cssPath;
|
||||
|
||||
link.onerror = () => {
|
||||
console.error(`Failed to load CSS: ${cssPath}`);
|
||||
};
|
||||
|
||||
document.body.appendChild(link);
|
||||
console.log(`CSS file "${cssPath}" loaded.`);
|
||||
}
|
||||
|
||||
// load theme js
|
||||
function loadThemeJS(jsPath) {
|
||||
if (!jsPath) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = jsPath;
|
||||
|
||||
script.onerror = () => {
|
||||
console.error(`Failed to load JS: ${jsPath}`);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
console.log(`JS file "${jsPath}" loaded.`);
|
||||
}
|
||||
|
||||
// update theme container class name
|
||||
function updateThemeContainer(containerClass) {
|
||||
// Create container if it doesn't exist
|
||||
let container = document.querySelector('.seasonals-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'seasonals-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
container.className = `seasonals-container ${containerClass}`;
|
||||
console.log(`Seasonals-Container class updated to "${containerClass}".`);
|
||||
}
|
||||
|
||||
function removeSelf() {
|
||||
const script = document.currentScript;
|
||||
if (script) script.parentNode.removeChild(script);
|
||||
console.log('External script removed:', script);
|
||||
}
|
||||
|
||||
// initialize theme
|
||||
async function initializeTheme() {
|
||||
let automateThemeSelection = true;
|
||||
let defaultTheme = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/Seasonals/Config');
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
automateThemeSelection = config.automateSeasonSelection;
|
||||
defaultTheme = config.selectedSeason;
|
||||
} else {
|
||||
console.error('Failed to fetch Seasonals config');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching Seasonals config:', error);
|
||||
}
|
||||
|
||||
let currentTheme;
|
||||
if (!automateThemeSelection) {
|
||||
currentTheme = defaultTheme;
|
||||
} else {
|
||||
currentTheme = determineCurrentTheme();
|
||||
}
|
||||
|
||||
console.log(`Selected theme: ${currentTheme}`);
|
||||
|
||||
if (currentTheme === 'none') {
|
||||
console.log('No theme selected.');
|
||||
removeSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
const theme = themeConfigs[currentTheme];
|
||||
|
||||
if (!theme) {
|
||||
console.error(`Theme "${currentTheme}" not found.`);
|
||||
return;
|
||||
}
|
||||
updateThemeContainer(theme.containerClass);
|
||||
|
||||
if (theme.css) loadThemeCSS(theme.css);
|
||||
if (theme.js) loadThemeJS(theme.js);
|
||||
|
||||
console.log(`Theme "${currentTheme}" loaded.`);
|
||||
|
||||
removeSelf();
|
||||
}
|
||||
|
||||
|
||||
//document.addEventListener('DOMContentLoaded', initializeTheme);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeTheme();
|
||||
});
|
||||
17
Jellyfin.Plugin.Seasonals/Web/snowfall.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.snowfall-container {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
170
Jellyfin.Plugin.Seasonals/Web/snowfall.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const snowfall = true; // enable/disable snowfall
|
||||
let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = 250; // count of snowflakes on mobile devices
|
||||
const snowFallSpeed = 3; // speed of snowfall (recommended values: 0-5)
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
let canvas, ctx; // canvas and context for drawing snowflakes
|
||||
let animationFrameId; // ID of the animation frame
|
||||
|
||||
// function to check and control the snowfall
|
||||
function toggleSnowfall() {
|
||||
const snowfallContainer = document.querySelector('.snowfall-container');
|
||||
if (!snowfallContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide snowfall if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
snowfallContainer.style.display = 'none'; // hide snowfall
|
||||
removeCanvas();
|
||||
if (!msgPrinted) {
|
||||
console.log('Snowfall hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
snowfallContainer.style.display = 'block'; // show snowfall
|
||||
if (!animationFrameId) {
|
||||
initializeCanvas();
|
||||
snowflakes = createSnowflakes(snowfallContainer);
|
||||
animateSnowfall();
|
||||
} else {
|
||||
console.warn('could not initialize snowfall: animation frame is already running');
|
||||
}
|
||||
|
||||
if (msgPrinted) {
|
||||
console.log('Snowfall visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowfall);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
function initializeCanvas() {
|
||||
if (document.getElementById('snowfallCanvas')) {
|
||||
console.warn('Canvas already exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.querySelector('.snowfall-container');
|
||||
if (!container) {
|
||||
console.error('Error: No element with class "snowfall-container" found.');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = 'snowfallCanvas';
|
||||
container.appendChild(canvas);
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
resizeCanvas(container);
|
||||
window.addEventListener('resize', () => resizeCanvas(container));
|
||||
}
|
||||
|
||||
function removeCanvas() {
|
||||
const canvas = document.getElementById('snowfallCanvas');
|
||||
if (canvas) {
|
||||
canvas.remove();
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
console.log('Animation frame canceled');
|
||||
}
|
||||
console.log('Canvas removed');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(container) {
|
||||
if (!canvas) return;
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
}
|
||||
|
||||
function createSnowflakes(container) {
|
||||
return Array.from({ length: snowflakesCount }, () => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
radius: Math.random() * 1.2 + 1,
|
||||
speed: Math.random() * snowFallSpeed + 1,
|
||||
swing: Math.random() * 2 - 1,
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize snowflakes
|
||||
let snowflakes = [];
|
||||
|
||||
function drawSnowflakes() {
|
||||
if (!ctx || !canvas) {
|
||||
console.error('Error: Canvas or context not found.');
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // empty canvas
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'white'; // color of snowflakes
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSnowflakes() {
|
||||
snowflakes.forEach(flake => {
|
||||
flake.y += flake.speed; // downwards movement
|
||||
flake.x += flake.swing; // sideways movement
|
||||
|
||||
// reset snowflake if it reaches the bottom
|
||||
if (flake.y > canvas.height) {
|
||||
flake.y = 0;
|
||||
flake.x = Math.random() * canvas.width; // with new random X position
|
||||
}
|
||||
|
||||
// wrap snowflakes around the screen edges
|
||||
if (flake.x > canvas.width) flake.x = 0;
|
||||
if (flake.x < 0) flake.x = canvas.width;
|
||||
});
|
||||
}
|
||||
|
||||
function animateSnowfall() {
|
||||
drawSnowflakes();
|
||||
updateSnowflakes();
|
||||
animationFrameId = requestAnimationFrame(animateSnowfall);
|
||||
}
|
||||
|
||||
// initialize snowfall
|
||||
function initializeSnowfall() {
|
||||
if (!snowfall) {
|
||||
console.warn('Snowfall is disabled.');
|
||||
return; // exit if snowfall is disabled
|
||||
}
|
||||
const container = document.querySelector('.snowfall-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
}
|
||||
|
||||
console.log('Snowfall enabled.');
|
||||
initializeCanvas();
|
||||
snowflakes = createSnowflakes(container);
|
||||
animateSnowfall();
|
||||
}
|
||||
}
|
||||
|
||||
initializeSnowfall();
|
||||
132
Jellyfin.Plugin.Seasonals/Web/snowflakes.css
Normal file
@@ -0,0 +1,132 @@
|
||||
.snowflakes {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: heart-fall, heart-shake;
|
||||
-webkit-animation-duration: 12s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: snowflakes-fall, snowflakes-shake;
|
||||
animation-duration: 12s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes snowflakes-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes snowflakes-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 0s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 2.5s;
|
||||
}
|
||||
132
Jellyfin.Plugin.Seasonals/Web/snowflakes.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const snowflakes = true; // enable/disable snowflakes
|
||||
const randomSnowflakes = true; // enable random Snowflakes
|
||||
const randomSnowflakesMobile = false; // enable random Snowflakes on mobile devices
|
||||
const enableColoredSnowflakes = true; // enable colored snowflakes on mobile devices
|
||||
const enableDiffrentDuration = true; // enable different animation duration for random symbols
|
||||
const snowflakeCount = 25; // count of random extra snowflakes
|
||||
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
// function to check and control the snowflakes
|
||||
function toggleSnowflakes() {
|
||||
const snowflakeContainer = document.querySelector('.snowflakes');
|
||||
if (!snowflakeContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide snowflakes if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
snowflakeContainer.style.display = 'none'; // hide snowflakes
|
||||
if (!msgPrinted) {
|
||||
console.log('Snowflakes hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
snowflakeContainer.style.display = 'block'; // show snowflakes
|
||||
if (msgPrinted) {
|
||||
console.log('Snowflakes visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowflakes);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
function addRandomSnowflakes(count) {
|
||||
const snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
||||
if (!snowflakeContainer) return; // exit if snowflake container is not found
|
||||
|
||||
console.log('Adding random snowflakes');
|
||||
|
||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new snowflake element
|
||||
const snowflake = document.createElement('div');
|
||||
snowflake.classList.add('snowflake');
|
||||
|
||||
// pick a random snowflake symbol
|
||||
if (enableColoredSnowflakes) {
|
||||
snowflake.textContent = snowflakeSymbolsMobile[Math.floor(Math.random() * snowflakeSymbolsMobile.length)];
|
||||
} else {
|
||||
snowflake.textContent = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)];
|
||||
}
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 8; // delay (0s to 8s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
|
||||
// apply styles
|
||||
snowflake.style.left = `${randomLeft}%`;
|
||||
snowflake.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 14 + 10; // delay (10s to 14s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 3; // delay (3s to 5s)
|
||||
snowflake.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// add the snowflake to the container
|
||||
snowflakeContainer.appendChild(snowflake);
|
||||
}
|
||||
console.log('Random snowflakes added');
|
||||
}
|
||||
|
||||
// initialize standard snowflakes
|
||||
function initSnowflakes() {
|
||||
const snowflakesContainer = document.querySelector('.snowflakes') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.snowflakes')) {
|
||||
snowflakesContainer.className = "snowflakes";
|
||||
snowflakesContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(snowflakesContainer);
|
||||
}
|
||||
|
||||
// Array of snowflake characters
|
||||
const snowflakeSymbols = ['❅', '❆'];
|
||||
|
||||
// create the 12 standard snowflakes
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const snowflake = document.createElement('div');
|
||||
snowflake.className = 'snowflake';
|
||||
snowflake.textContent = snowflakeSymbols[i % 2]; // change between ❅ and ❆
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 14 + 10; // delay (10s to 14s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 3; // delay (3s to 5s)
|
||||
snowflake.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
snowflakesContainer.appendChild(snowflake);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize snowflakes and add random snowflakes
|
||||
function initializeSnowflakes() {
|
||||
if (!snowflakes) return; // exit if snowflakes are disabled
|
||||
initSnowflakes();
|
||||
toggleSnowflakes();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSnowflakes && (screenWidth > 768 || randomSnowflakesMobile)) { // add random snowflakes only on larger screens, unless enabled for mobile devices
|
||||
addRandomSnowflakes(snowflakeCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeSnowflakes();
|
||||
7
Jellyfin.Plugin.Seasonals/Web/snowstorm.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
173
Jellyfin.Plugin.Seasonals/Web/snowstorm.js
Normal file
@@ -0,0 +1,173 @@
|
||||
const snowstorm = true; // enable/disable snowstorm
|
||||
let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = 250; // count of snowflakes on mobile devices
|
||||
const snowFallSpeed = 6; // speed of snowfall (recommended values: 4-8)
|
||||
const horizontalWind = 4; // horizontal wind speed (recommended value: 4)
|
||||
const verticalVariation = 2; // vertical variation (recommended value: 2)
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
let canvas, ctx; // canvas and context for drawing snowflakes
|
||||
let animationFrameId; // ID of the animation frame
|
||||
|
||||
// function to check and control the snowstorm
|
||||
function toggleSnowstorm() {
|
||||
const snowstormContainer = document.querySelector('.snowstorm-container');
|
||||
if (!snowstormContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide snowstorm if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
snowstormContainer.style.display = 'none'; // hide snowstorm
|
||||
removeCanvas();
|
||||
if (!msgPrinted) {
|
||||
console.log('Snowstorm hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
snowstormContainer.style.display = 'block'; // show snowstorm
|
||||
if (!animationFrameId) {
|
||||
initializeCanvas();
|
||||
snowflakes = createSnowflakes(snowstormContainer);
|
||||
animateSnowstorm();
|
||||
} else {
|
||||
console.warn('could not initialize snowfall: animation frame is already running');
|
||||
}
|
||||
|
||||
if (msgPrinted) {
|
||||
console.log('Snowstorm visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowstorm);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
function initializeCanvas() {
|
||||
if (document.getElementById('snowfallCanvas')) {
|
||||
console.warn('Canvas already exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.querySelector('.snowstorm-container');
|
||||
if (!container) {
|
||||
console.error('Error: No element with class "snowfall-container" found.');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = 'snowstormCanvas';
|
||||
container.appendChild(canvas);
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
resizeCanvas(container);
|
||||
window.addEventListener('resize', () => resizeCanvas(container));
|
||||
}
|
||||
|
||||
function removeCanvas() {
|
||||
const canvas = document.getElementById('snowstormCanvas');
|
||||
if (canvas) {
|
||||
canvas.remove();
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
console.log('Animation frame canceled');
|
||||
}
|
||||
console.log('Canvas removed');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(container) {
|
||||
if (!canvas) return;
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
}
|
||||
|
||||
function createSnowflakes(container) {
|
||||
return Array.from({ length: snowflakesCount }, () => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
radius: Math.random() * 1.2 + 1,
|
||||
fallspeed: Math.random() * snowFallSpeed + 2,
|
||||
horizontal: Math.random() * horizontalWind * 2 - horizontalWind,
|
||||
vertical: Math.random() * verticalVariation * 2 - verticalVariation,
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize snowflakes
|
||||
let snowflakes = [];
|
||||
|
||||
function drawSnowflakes() {
|
||||
if (!ctx || !canvas) {
|
||||
console.error('Error: Canvas or context not found.');
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // empty canvas
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'white'; // color of snowflakes
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSnowflakes() {
|
||||
snowflakes.forEach(flake => {
|
||||
flake.y += flake.fallspeed + flake.vertical; // downwards movement
|
||||
flake.x += flake.horizontal; // sideways movement
|
||||
|
||||
// reset snowflake if it reaches the bottom
|
||||
if (flake.y > canvas.height) {
|
||||
flake.y = 0;
|
||||
flake.x = Math.random() * canvas.width; // with new random X position
|
||||
}
|
||||
|
||||
// wrap snowflakes around the screen edges
|
||||
if (flake.x > canvas.width) flake.x = 0;
|
||||
if (flake.x < 0) flake.x = canvas.width;
|
||||
});
|
||||
}
|
||||
|
||||
function animateSnowstorm() {
|
||||
drawSnowflakes();
|
||||
updateSnowflakes();
|
||||
animationFrameId = requestAnimationFrame(animateSnowstorm);
|
||||
}
|
||||
|
||||
// initialize snowfall
|
||||
function initializeSnowstorm() {
|
||||
if (!snowstorm) {
|
||||
console.warn('Snowstorm is disabled.');
|
||||
return; // exit if snowfall is disabled
|
||||
}
|
||||
const container = document.querySelector('.snowstorm-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
}
|
||||
|
||||
console.log('Snowstorm enabled.');
|
||||
initializeCanvas();
|
||||
snowflakes = createSnowflakes(container);
|
||||
animateSnowstorm();
|
||||
}
|
||||
}
|
||||
|
||||
initializeSnowstorm();
|
||||
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Jellyfin Seasonals Plugin
|
||||
|
||||
This plugin adds seasonal effects (snow, leaves, etc.) to the Jellyfin web interface.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Build the plugin and install it in Jellyfin.
|
||||
2. Restart Jellyfin.
|
||||
3. The plugin will automatically attempt to inject the necessary script into your `index.html`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Go to the Jellyfin Dashboard > Plugins > Seasonals to configure the active season or enable automatic selection.
|
||||
15
build.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: "Seasonals"
|
||||
guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"
|
||||
version: "1.0.0.0"
|
||||
targetAbi: "10.10.7.0"
|
||||
framework: "net9.0"
|
||||
overview: "Seasonal effects for Jellyfin"
|
||||
description: >
|
||||
Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.
|
||||
category: "General"
|
||||
owner: "CodeDevMLH"
|
||||
artifacts:
|
||||
- "Jellyfin.Plugin.Seasonals.dll"
|
||||
changelog: >
|
||||
Initial release
|
||||
108
jellyfin.ruleset
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
||||
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
|
||||
<Rule Id="SA1009" Action="None" />
|
||||
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
|
||||
<Rule Id="SA1011" Action="None" />
|
||||
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
|
||||
<Rule Id="SA1101" Action="None" />
|
||||
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
|
||||
<Rule Id="SA1108" Action="None" />
|
||||
<!-- disable warning SA1118: Parameter must not span multiple lines. -->
|
||||
<Rule Id="SA1118" Action="None" />
|
||||
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
|
||||
<Rule Id="SA1128" Action="None" />
|
||||
<!-- disable warning SA1130: Use lambda syntax -->
|
||||
<Rule Id="SA1130" Action="None" />
|
||||
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
|
||||
<Rule Id="SA1200" Action="None" />
|
||||
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
|
||||
<Rule Id="SA1202" Action="None" />
|
||||
<!-- disable warning SA1204: Static members must appear before non-static members -->
|
||||
<Rule Id="SA1204" Action="None" />
|
||||
<!-- disable warning SA1309: Fields must not begin with an underscore -->
|
||||
<Rule Id="SA1309" Action="None" />
|
||||
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
|
||||
<Rule Id="SA1413" Action="None" />
|
||||
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
|
||||
<Rule Id="SA1512" Action="None" />
|
||||
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
|
||||
<Rule Id="SA1515" Action="None" />
|
||||
<!-- disable warning SA1600: Elements should be documented -->
|
||||
<Rule Id="SA1600" Action="None" />
|
||||
<!-- disable warning SA1602: Enumeration items should be documented -->
|
||||
<Rule Id="SA1602" Action="None" />
|
||||
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
|
||||
<Rule Id="SA1633" Action="None" />
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
|
||||
<!-- error on CA1305: Specify IFormatProvider -->
|
||||
<Rule Id="CA1305" Action="Error" />
|
||||
<!-- error on CA1725: Parameter names should match base declaration -->
|
||||
<Rule Id="CA1725" Action="Error" />
|
||||
<!-- error on CA1725: Call async methods when in an async method -->
|
||||
<Rule Id="CA1727" Action="Error" />
|
||||
<!-- error on CA1843: Do not use 'WaitAll' with a single task -->
|
||||
<Rule Id="CA1843" Action="Error" />
|
||||
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
|
||||
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
|
||||
<Rule Id="CA2016" Action="Error" />
|
||||
<!-- error on CA2254: Template should be a static expression -->
|
||||
<Rule Id="CA2254" Action="Error" />
|
||||
|
||||
<!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
|
||||
<Rule Id="CA1014" Action="Info" />
|
||||
<!-- disable warning CA1024: Use properties where appropriate -->
|
||||
<Rule Id="CA1024" Action="Info" />
|
||||
<!-- disable warning CA1031: Do not catch general exception types -->
|
||||
<Rule Id="CA1031" Action="Info" />
|
||||
<!-- disable warning CA1032: Implement standard exception constructors -->
|
||||
<Rule Id="CA1032" Action="Info" />
|
||||
<!-- disable warning CA1040: Avoid empty interfaces -->
|
||||
<Rule Id="CA1040" Action="Info" />
|
||||
<!-- disable warning CA1062: Validate arguments of public methods -->
|
||||
<Rule Id="CA1062" Action="Info" />
|
||||
<!-- TODO: enable when false positives are fixed -->
|
||||
<!-- disable warning CA1508: Avoid dead conditional code -->
|
||||
<Rule Id="CA1508" Action="Info" />
|
||||
<!-- disable warning CA1716: Identifiers should not match keywords -->
|
||||
<Rule Id="CA1716" Action="Info" />
|
||||
<!-- disable warning CA1720: Identifiers should not contain type names -->
|
||||
<Rule Id="CA1720" Action="Info" />
|
||||
<!-- disable warning CA1724: Type names should not match namespaces -->
|
||||
<Rule Id="CA1724" Action="Info" />
|
||||
<!-- disable warning CA1805: Do not initialize unnecessarily -->
|
||||
<Rule Id="CA1805" Action="Info" />
|
||||
<!-- disable warning CA1812: internal class that is apparently never instantiated.
|
||||
If so, remove the code from the assembly.
|
||||
If this class is intended to contain only static members, make it static -->
|
||||
<Rule Id="CA1812" Action="Info" />
|
||||
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
|
||||
<Rule Id="CA1822" Action="Info" />
|
||||
<!-- disable warning CA2000: Dispose objects before losing scope -->
|
||||
<Rule Id="CA2000" Action="Info" />
|
||||
<!-- disable warning CA2253: Named placeholders should not be numeric values -->
|
||||
<Rule Id="CA2253" Action="Info" />
|
||||
<!-- disable warning CA5394: Do not use insecure randomness -->
|
||||
<Rule Id="CA5394" Action="Info" />
|
||||
|
||||
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
|
||||
<Rule Id="CA1054" Action="None" />
|
||||
<!-- disable warning CA1055: URI return values should not be strings -->
|
||||
<Rule Id="CA1055" Action="None" />
|
||||
<!-- disable warning CA1056: URI properties should not be strings -->
|
||||
<Rule Id="CA1056" Action="None" />
|
||||
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
|
||||
<Rule Id="CA1303" Action="None" />
|
||||
<!-- disable warning CA1308: Normalize strings to uppercase -->
|
||||
<Rule Id="CA1308" Action="None" />
|
||||
<!-- disable warning CA1848: Use the LoggerMessage delegates -->
|
||||
<Rule Id="CA1848" Action="None" />
|
||||
<!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments -->
|
||||
<Rule Id="CA2101" Action="None" />
|
||||
<!-- disable warning CA2234: Pass System.Uri objects instead of strings -->
|
||||
<Rule Id="CA2234" Action="None" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
21
manifest.json
Normal file
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4",
|
||||
"name": "Seasonals",
|
||||
"description": "Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.",
|
||||
"overview": "Seasonal effects for Jellyfin",
|
||||
"owner": "CodeDevMLH",
|
||||
"category": "General",
|
||||
"imageUrl": "https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-seasonals/main/icon.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0.0",
|
||||
"changelog": "Initial release",
|
||||
"targetAbi": "10.10.7.0",
|
||||
"sourceUrl": "https://github.com/CodeDevMLH/jellyfin-plugin-seasonals",
|
||||
"checksum": "46e786c6e26b69748775a77474025e18",
|
||||
"timestamp": "2025-12-14T18:18:50Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||