diff --git a/Shoko.Server/API/WebUI/WebUIThemeProvider.cs b/Shoko.Server/API/WebUI/WebUIThemeProvider.cs
index 7f494c621..12b38ca91 100644
--- a/Shoko.Server/API/WebUI/WebUIThemeProvider.cs
+++ b/Shoko.Server/API/WebUI/WebUIThemeProvider.cs
@@ -82,57 +82,54 @@ public static bool RemoveTheme(ThemeDefinition theme)
/// The updated theme metadata.
public static async Task UpdateTheme(ThemeDefinition theme, bool preview = false)
{
+ // Return the local theme if we don't have an update url.
if (string.IsNullOrEmpty(theme.URL))
- {
- throw new ValidationException("Update URL is empty.");
- }
+ if (preview)
+ throw new ValidationException("No update URL in existing theme definition.");
+ else
+ return theme;
if (!(Uri.TryCreate(theme.URL, UriKind.Absolute, out var updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps)))
- {
- throw new ValidationException("Invalid update URL.");
- }
+ throw new ValidationException("Invalid update URL in existing theme definition.");
- using (var httpClient = new HttpClient())
- {
- httpClient.Timeout = TimeSpan.FromMinutes(1);
- var response = await httpClient.GetAsync(updateUrl.AbsoluteUri);
-
- // Check if the response was a success.
- if (response.StatusCode != HttpStatusCode.OK)
- throw new HttpRequestException("Failed to retrieve theme file.");
-
- // Check if the response is using the correct content-type.
- var contentType = response.Content.Headers.ContentType?.MediaType;
- if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType))
- throw new HttpRequestException("Invalid content-type. Expected JSON.");
-
- // Simple sanity check before parsing the response content.
- var content = await response.Content.ReadAsStringAsync();
- content = content?.Trim();
- if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[content.Length - 1] != '}')
- throw new HttpRequestException("Invalid theme file format.");
-
- // Try to parse the updated theme.
- var updatedTheme = ThemeDefinition.FromJson(content, theme.ID, theme.FileName, preview);
- if (updatedTheme == null)
- throw new HttpRequestException("Failed to parse the updated theme.");
-
- // Save the updated theme file if we're not pre-viewing.
- if (!preview)
- {
- var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
- if (!Directory.Exists(dirPath))
- Directory.CreateDirectory(dirPath);
+ using var httpClient = new HttpClient();
+ httpClient.Timeout = TimeSpan.FromMinutes(1);
+ var response = await httpClient.GetAsync(updateUrl.AbsoluteUri);
- var filePath = Path.Combine(dirPath, theme.FileName);
- await File.WriteAllTextAsync(filePath, content);
+ // Check if the response was a success.
+ if (response.StatusCode != HttpStatusCode.OK)
+ throw new HttpRequestException("Failed to retrieve theme file.");
- if (ThemeDict != null && !ThemeDict.TryAdd(theme.ID, updatedTheme))
- ThemeDict[theme.ID] = updatedTheme;
- }
+ // Check if the response is using the correct content-type.
+ var contentType = response.Content.Headers.ContentType?.MediaType;
+ if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType))
+ throw new HttpRequestException("Invalid content-type. Expected JSON.");
- return updatedTheme;
+ // Simple sanity check before parsing the response content.
+ var content = await response.Content.ReadAsStringAsync();
+ content = content?.Trim();
+ if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[^1] != '}')
+ throw new HttpRequestException("Invalid theme file format.");
+
+ // Try to parse the updated theme.
+ var updatedTheme = ThemeDefinition.FromJson(content, theme.ID, theme.FileName, preview) ??
+ throw new HttpRequestException("Failed to parse the updated theme.");
+
+ // Save the updated theme file if we're not pre-viewing.
+ if (!preview)
+ {
+ var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
+ if (!Directory.Exists(dirPath))
+ Directory.CreateDirectory(dirPath);
+
+ var filePath = Path.Combine(dirPath, theme.FileName);
+ await File.WriteAllTextAsync(filePath, content);
+
+ if (ThemeDict != null && !ThemeDict.TryAdd(theme.ID, updatedTheme))
+ ThemeDict[theme.ID] = updatedTheme;
}
+
+ return updatedTheme;
}
///
@@ -165,48 +162,45 @@ public static async Task InstallTheme(string url, bool preview
if (string.IsNullOrEmpty(fileName) || !FileNameRegex.IsMatch(fileName))
throw new ValidationException("Invalid theme file name.");
- using (var httpClient = new HttpClient())
- {
- httpClient.Timeout = TimeSpan.FromMinutes(1);
- var response = await httpClient.GetAsync(updateUrl.AbsoluteUri);
+ using var httpClient = new HttpClient();
+ httpClient.Timeout = TimeSpan.FromMinutes(1);
+ var response = await httpClient.GetAsync(updateUrl.AbsoluteUri);
- // Check if the response was a success.
- if (response.StatusCode != HttpStatusCode.OK)
- throw new HttpRequestException("Failed to retrieve theme file.");
+ // Check if the response was a success.
+ if (response.StatusCode != HttpStatusCode.OK)
+ throw new HttpRequestException("Failed to retrieve theme file.");
- // Check if the response is using the correct content-type.
- var contentType = response.Content.Headers.ContentType?.MediaType;
- if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType))
- throw new HttpRequestException("Invalid content-type. Expected JSON.");
+ // Check if the response is using the correct content-type.
+ var contentType = response.Content.Headers.ContentType?.MediaType;
+ if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType))
+ throw new HttpRequestException("Invalid content-type. Expected JSON.");
- // Simple sanity check before parsing the response content.
- var content = await response.Content.ReadAsStringAsync();
- content = content?.Trim();
- if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[content.Length - 1] != '}')
- throw new HttpRequestException("Invalid theme file format.");
+ // Simple sanity check before parsing the response content.
+ var content = await response.Content.ReadAsStringAsync();
+ content = content?.Trim();
+ if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[^1] != '}')
+ throw new HttpRequestException("Invalid theme file format.");
- // Try to parse the new theme.
- var id = FileNameToID(fileName);
- var theme = ThemeDefinition.FromJson(content, id, fileName + extName, preview);
- if (theme == null)
- throw new HttpRequestException("Failed to parse the new theme.");
-
- // Save the new theme file if we're not pre-viewing.
- if (!preview)
- {
- var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
- if (!Directory.Exists(dirPath))
- Directory.CreateDirectory(dirPath);
+ // Try to parse the new theme.
+ var id = FileNameToID(fileName);
+ var theme = ThemeDefinition.FromJson(content, id, fileName + extName, preview) ??
+ throw new HttpRequestException("Failed to parse the new theme.");
- var filePath = Path.Combine(dirPath, fileName + extName);
- await File.WriteAllTextAsync(filePath, content);
+ // Save the new theme file if we're not pre-viewing.
+ if (!preview)
+ {
+ var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
+ if (!Directory.Exists(dirPath))
+ Directory.CreateDirectory(dirPath);
- if (ThemeDict != null && !ThemeDict.TryAdd(id, theme))
- ThemeDict[id] = theme;
- }
+ var filePath = Path.Combine(dirPath, fileName + extName);
+ await File.WriteAllTextAsync(filePath, content);
- return theme;
+ if (ThemeDict != null && !ThemeDict.TryAdd(id, theme))
+ ThemeDict[id] = theme;
}
+
+ return theme;
}
public class ThemeDefinitionInput
@@ -344,9 +338,7 @@ public ThemeDefinition(ThemeDefinitionInput input, string id, string fileName, b
}
public string ToCSS()
- {
- return $".theme-{ID} {{{string.Join(" ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))} }}";
- }
+ => $".theme-{ID} {{{string.Join(" ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))} }}";
internal static IReadOnlyList FromDirectory(string dirPath)
{
@@ -379,7 +371,7 @@ internal static IReadOnlyList FromDirectory(string dirPath)
return null;
// Safely try to read
- string? fileContents = null;
+ string? fileContents;
try
{
fileContents = File.ReadAllText(filePath)?.Trim();
@@ -389,7 +381,7 @@ internal static IReadOnlyList FromDirectory(string dirPath)
return null;
}
// Simple sanity check before parsing the file contents.
- if (string.IsNullOrWhiteSpace(fileContents) || fileContents[0] != '{' || fileContents[fileContents.Length - 1] != '}')
+ if (string.IsNullOrWhiteSpace(fileContents) || fileContents[0] != '{' || fileContents[^1] != '}')
return null;
var id = FileNameToID(fileName);
@@ -415,17 +407,17 @@ internal static IReadOnlyList FromDirectory(string dirPath)
}
private static string FileNameToID(string fileName)
- {
- return fileName.ToLowerInvariant().Replace('_', '-');
- }
+ => fileName.ToLowerInvariant().Replace('_', '-');
private static string NameFromID(string id)
- {
- return string.Join(' ', id.Replace('_', '-').Replace('-', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(segment => segment[0..1].ToUpperInvariant() + segment[1..].ToLowerInvariant()));
- }
+ => string.Join(
+ ' ',
+ id.Replace('_', '-')
+ .Replace('-', ' ')
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(segment => segment[0..1].ToUpperInvariant() + segment[1..].ToLowerInvariant())
+ );
public static string ToCSS(this IEnumerable list)
- {
- return string.Join(" ", list.Select(theme => theme.ToCSS()));
- }
+ => string.Join(" ", list.Select(theme => theme.ToCSS()));
}
diff --git a/Shoko.Server/API/v3/Controllers/WebUIController.cs b/Shoko.Server/API/v3/Controllers/WebUIController.cs
index 7457e0141..5cb42ce2b 100644
--- a/Shoko.Server/API/v3/Controllers/WebUIController.cs
+++ b/Shoko.Server/API/v3/Controllers/WebUIController.cs
@@ -160,16 +160,42 @@ public ActionResult RemoveTheme([FromRoute] string themeID)
return NoContent();
}
+ ///
+ /// Preview the update to a theme by its ID.
+ ///
+ /// The ID of the theme to update.
+ /// The preview of the updated theme.
+ [ResponseCache(Duration = 60 /* 1 minute in seconds */)]
+ [HttpGet("Theme/{themeID}/Update")]
+ public async Task> PreviewUpdatedTheme([FromRoute] string themeID)
+ {
+ var theme = WebUIThemeProvider.GetTheme(themeID, true);
+ if (theme == null)
+ return NotFound("A theme with the given id was not found.");
+
+ try
+ {
+ theme = await WebUIThemeProvider.UpdateTheme(theme, true);
+ return new WebUITheme(theme);
+ }
+ catch (ValidationException valEx)
+ {
+ return BadRequest(valEx.Message);
+ }
+ catch (HttpRequestException httpEx)
+ {
+ return InternalError(httpEx.Message);
+ }
+ }
///
/// Updates a theme by its ID.
///
/// The ID of the theme to update.
- /// Flag indicating whether to enable preview mode.
/// The updated theme.
[Authorize("admin")]
[HttpPost("Theme/{themeID}/Update")]
- public async Task> UpdateTheme([FromRoute] string themeID, [FromQuery] bool preview = false)
+ public async Task> UpdateTheme([FromRoute] string themeID)
{
var theme = WebUIThemeProvider.GetTheme(themeID, true);
if (theme == null)
@@ -177,7 +203,7 @@ public async Task> UpdateTheme([FromRoute] string theme
try
{
- theme = await WebUIThemeProvider.UpdateTheme(theme, preview);
+ theme = await WebUIThemeProvider.UpdateTheme(theme, false);
return new WebUITheme(theme);
}
catch (ValidationException valEx)