Skip to content

Commit

Permalink
fix: use nested css for themes + allow css only themes
Browse files Browse the repository at this point in the history
  • Loading branch information
revam committed Sep 6, 2024
1 parent e4f7161 commit f0e3681
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 45 deletions.
92 changes: 49 additions & 43 deletions Shoko.Server/API/WebUI/WebUIThemeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Shoko.Commons.Extensions;
using Shoko.Server.Extensions;
using Shoko.Server.Utilities;

#nullable enable
Expand Down Expand Up @@ -420,10 +422,24 @@ public class ThemeDefinition
/// </summary>
public readonly bool IsPreview;

private bool? _isInstalled;

/// <summary>
/// Indicates the theme is installed locally.
/// </summary>
public readonly bool IsInstalled;
public bool IsInstalled => _isInstalled ??= File.Exists(Path.Combine(Utils.ApplicationPath, "themes", FileName)) || File.Exists(Path.Combine(Utils.ApplicationPath, "themes", CssFileName));

public ThemeDefinition(string id)
{
ID = id;
FileName = $"{id}.json";
Name = NameFromID(ID);
Tags = [];
Author = "<unknown>";
Version = new Version(1, 0, 0, 0);
Values = new Dictionary<string, string>();
IsPreview = false;
}

public ThemeDefinition(ThemeDefinitionInput input, string id, string fileName, bool preview = false)
{
Expand All @@ -441,27 +457,34 @@ public ThemeDefinition(ThemeDefinitionInput input, string id, string fileName, b
FileName = fileName;
Name = string.IsNullOrEmpty(input.Name) ? NameFromID(ID) : input.Name;
Description = string.IsNullOrWhiteSpace(input.Description) ? null : input.Description;
Tags = input.Tags ?? new List<string>();
Tags = input.Tags ?? [];
Author = input.Author;
Version = new Version(major, minor, build, revision);
Values = input.Values ?? new Dictionary<string, string>();
CssUrl = input.CssUrl;
UpdateUrl = input.UpdateUrl;
IsPreview = preview;
IsInstalled = File.Exists(Path.Combine(Utils.ApplicationPath, "themes", fileName));
}

public string ToCSS(bool all = false)
public string ToCSS()
{
if (!all)
return $".theme-{ID} {{{string.Join(" ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))} }}";
var cssFile = Path.Combine(Utils.ApplicationPath, "themes", CssFileName);
var css = $".theme-{ID} {{\n {string.Join("\n ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))}\n}}";
if (File.Exists(cssFile))
{
css += "\n\n" + File.ReadAllText(cssFile);
}
return css;
var css = new StringBuilder()
.Append('\n')
.Append($".theme-{ID} {{\n");
if (Values.Count > 0)
css.Append(" " + Values.Select(pair => $" --{pair.Key}: {pair.Value};").Join("\n ") + "\n");

if (Values.Count == 0 && !string.IsNullOrWhiteSpace(CssContent))
css.Append('\n');

if (!string.IsNullOrWhiteSpace(CssContent))
css
.Append(" " + CssContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None).Select(line => string.IsNullOrWhiteSpace(line) ? string.Empty : $" {line.TrimEnd()}").Join("\n ") + "\n");

return css
.AppendLine("}\n")
.ToString();
}

internal static IReadOnlyList<ThemeDefinition> FromDirectory(string dirPath)
Expand All @@ -472,58 +495,41 @@ internal static IReadOnlyList<ThemeDefinition> FromDirectory(string dirPath)
if (!Directory.Exists(dirPath))
return new List<ThemeDefinition>();

var allowedExtensions = new HashSet<string>() { ".json", ".css" };
return Directory.GetFiles(dirPath)
.GroupBy(a => Path.GetFileNameWithoutExtension(a))
.Where(a => !string.IsNullOrEmpty(a.Key) && FileNameRegex.IsMatch(a.Key) && a.Any(b => allowedExtensions.Contains(Path.GetExtension(b))))
.Select(FromPath)
.WhereNotNull()
.DistinctBy(theme => theme.ID)
.OrderBy(theme => theme.ID)
.ToList();
}

internal static ThemeDefinition? FromPath(string filePath)
internal static ThemeDefinition? FromPath(IGrouping<string, string> fileDetails)
{
// Check file extension.
var extName = Path.GetExtension(filePath);
if (string.IsNullOrEmpty(extName) || !string.Equals(extName, ".json", StringComparison.InvariantCultureIgnoreCase))
return null;

var fileName = Path.GetFileNameWithoutExtension(filePath);
if (string.IsNullOrEmpty(fileName) || !FileNameRegex.IsMatch(fileName))
return null;

if (!File.Exists(filePath))
return null;

// Safely try to read
string? fileContents;
try
{
fileContents = File.ReadAllText(filePath)?.Trim();
}
catch
{
return null;
}
// Simple sanity check before parsing the file contents.
if (string.IsNullOrWhiteSpace(fileContents) || fileContents[0] != '{' || fileContents[^1] != '}')
return null;

var id = FileNameToID(fileName);
var theme = FromJson(fileContents, id, Path.GetFileName(filePath));
var id = FileNameToID(fileDetails.Key);
var jsonFile = fileDetails.FirstOrDefault(a => string.Equals(Path.GetExtension(a), ".json", StringComparison.InvariantCultureIgnoreCase));
var theme = string.IsNullOrEmpty(jsonFile) ? new(id) : FromJson(File.ReadAllText(jsonFile)?.Trim(), id, Path.GetFileName(jsonFile));
if (theme is not null)
{
var cssFileName = Path.Combine(Path.GetDirectoryName(filePath)!, theme.CssFileName);
var cssFileName = fileDetails.FirstOrDefault(a => string.Equals(Path.GetExtension(a), ".css", StringComparison.InvariantCultureIgnoreCase)) ??
Path.Combine(Path.GetDirectoryName(jsonFile)!, theme.CssFileName);
if (File.Exists(cssFileName))
theme.CssContent = File.ReadAllText(cssFileName);
}

return theme;
}

internal static ThemeDefinition? FromJson(string json, string id, string fileName, bool preview = false)
internal static ThemeDefinition? FromJson(string? json, string id, string fileName, bool preview = false)
{
try
{
// Simple sanity check before parsing the file contents.
if (string.IsNullOrWhiteSpace(json) || json[0] != '{' || json[^1] != '}')
return null;
var input = JsonConvert.DeserializeObject<ThemeDefinitionInput>(json);
if (input == null)
return null;
Expand Down Expand Up @@ -551,5 +557,5 @@ private static string NameFromID(string id)
);

public static string ToCSS(this IEnumerable<ThemeDefinition> list)
=> string.Join(" ", list.Select(theme => theme.ToCSS()));
=> list.Select(theme => theme.ToCSS()).Join("");
}
2 changes: 1 addition & 1 deletion Shoko.Server/API/v3/Controllers/WebUIController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public ActionResult<string> GetThemeCSS([FromRoute] string themeID, [FromQuery]
if (theme is null)
return NotFound("A theme with the given id was not found.");

return Content(theme.ToCSS(true), "text/css");
return Content(theme.ToCSS(), "text/css");
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Shoko.Server/API/v3/Models/Shoko/WebUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public class WebUITheme(WebUIThemeProvider.ThemeDefinition definition, bool with
/// The CSS representation of the theme.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? CSS { get; init; } = withCSS ? definition.ToCSS(true) : null;
public string? CSS { get; init; } = withCSS ? definition.ToCSS() : null;
}

public class WebUIGroupExtra
Expand Down

0 comments on commit f0e3681

Please sign in to comment.