Skip to content

Commit

Permalink
Switch properties to a JavaScript API to support system themes (#171)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Aug 19, 2023
1 parent fa95d72 commit ada1152
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 7 deletions.
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
<artifactId>blueocean-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>jackson2-api</artifactId>
</dependency>
<dependency>
<groupId>io.jenkins</groupId>
<artifactId>configuration-as-code</artifactId>
Expand Down
41 changes: 40 additions & 1 deletion src/main/java/io/jenkins/plugins/thememanager/Theme.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -22,24 +23,41 @@
public class Theme {

private static final String JS_HTML = "<script type=\"text/javascript\" src=\"{0}\"></script>";
private static final String JSON_HTML = "<script id=\"theme-manager-{0}\" type=\"application/json\">{1}</script>";

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private static final String CSS_HTML = "<link type=\"text/css\" rel=\"stylesheet\" href=\"{0}\"/>";

private final List<String> cssUrls;
private final List<String> javascriptUrls;
private final boolean blueOceanCompatible;

private final boolean respectSystemAppearance;
private final Map<String, String> properties;

private Theme(
List<String> cssUrls,
List<String> javascriptUrls,
boolean blueOceanCompatible,
boolean respectSystemAppearance,
Map<String, String> properties) {
this.cssUrls = cssUrls;
this.javascriptUrls = javascriptUrls;
this.blueOceanCompatible = blueOceanCompatible;
this.respectSystemAppearance = respectSystemAppearance;
this.properties = properties;
}

@Restricted(NoExternalUse.class)
String generateProperties() {
try {
return MessageFormat.format(JSON_HTML, "properties", OBJECT_MAPPER.writeValueAsString(properties));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Restricted(NoExternalUse.class)
Set<String> generateHeaderElements(boolean injectCss) {
Set<String> headerElements = new HashSet<>();
Expand Down Expand Up @@ -84,6 +102,10 @@ public boolean isBlueOceanCompatible() {
return blueOceanCompatible;
}

public boolean isRespectSystemAppearance() {
return respectSystemAppearance;
}

/**
* Additional information that theme authors can provide to influence other plugins
*
Expand All @@ -101,15 +123,19 @@ public List<String> getProperties(String artifactId) {
}

/**
* Do not use this as it doesn't support system themes.
* Additional information that theme authors can provide to influence other plugins
*
* <p>e.g. the Prism API plugin can read properties and use a default theme based on this
* information.
*
* @param artifactId the plugin to retrieve the properties for
* @param propertyName the property to retrieve
*
* @deprecated use <code>getThemeManagerProperty</code> from JavaScript instead
* @return the properties associated with the plugin requested
*/
@Deprecated
public Optional<String> getProperty(String artifactId, String propertyName) {
return Optional.ofNullable(properties.get(artifactId + ":" + propertyName));
}
Expand All @@ -128,6 +154,8 @@ public static class Builder {
private List<String> cssUrls = emptyList();
private List<String> javascriptUrls = emptyList();
private boolean blueOceanCompatible = false;

private boolean respectSystemAppearance = false;
private final Map<String, String> properties = new HashMap<>();

Builder() {}
Expand Down Expand Up @@ -186,6 +214,17 @@ public Builder disableOnBlueOcean() {
return this;
}

/**
* Marks the theme as a 'system' respecting theme that will adapt to light and dark system
* configuration
*
* @return the current builder with respect system appearance enabled.
*/
public Builder respectSystemAppearance() {
this.respectSystemAppearance = true;
return this;
}

/**
* Properties are a way a theme author can provide extra information to plugins. e.g. the Prism
* API plugin can read properties and use a default theme based on this information.
Expand Down Expand Up @@ -240,7 +279,7 @@ public Builder withJavascriptUrls(List<String> javascriptUrls) {
* @return the theme.
*/
public Theme build() {
return new Theme(cssUrls, javascriptUrls, blueOceanCompatible, properties);
return new Theme(cssUrls, javascriptUrls, blueOceanCompatible, respectSystemAppearance, properties);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,15 @@ public String getThemeKey() {
}
return null;
}

@SuppressWarnings("unused") // called by jelly
public boolean isRespectSystemAppearance() {
ThemeManagerPageDecorator themeManagerPageDecorator = ThemeManagerPageDecorator.get();
Theme theme = themeManagerPageDecorator.findTheme();

if (theme.isBlueOceanCompatible()) {
return themeManagerPageDecorator.isRespectSystemAppearance();
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public String getHeaderHtml() {
.collect(Collectors.toSet());

ThemeManagerFactory themeManagerFactory = findThemeFactory();
namespacedThemes.add(themeManagerFactory.getTheme().generateProperties());

if (!themeManagerFactory.getDescriptor().isNamespaced()) {
Set<String> data =
new LinkedHashSet<>(themeManagerFactory.getTheme().generateHeaderElements(injectCss));
Expand All @@ -119,6 +121,13 @@ public String getThemeKey() {
return themeFactory.getDescriptor().getThemeKey();
}

@SuppressWarnings("unused") // called by jelly
public boolean isRespectSystemAppearance() {
ThemeManagerFactory themeFactory = findThemeFactory();

return themeFactory.getTheme().isRespectSystemAppearance();
}

/**
* Filter to only inject CSS into "normal" Jenkins pages. Some plugins replace the whole layout of
* Jenkins and we don't want to disturb them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ public String getHeaderHtml() {
public String getThemeKey() {
return ThemeManagerPageDecorator.get().getThemeKey();
}

@SuppressWarnings("unused") // called by jelly
public boolean isRespectSystemAppearance() {
return ThemeManagerPageDecorator.get().isRespectSystemAppearance();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?jelly escape-by-default='false'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<script id="theme-manager-theme" type="application/json">{ "id": "${it.themeKey}" }</script>
<st:adjunct includes="io.jenkins.plugins.thememanager.header.main" />
<script id="theme-manager-theme" type="application/json">{ "id": "${it.themeKey}", "respect_system_appearance": ${it.respectSystemAppearance} }</script>
${it.getHeaderHtml()}
<st:adjunct includes="io.jenkins.plugins.thememanager.header.main" />
</j:jelly>
25 changes: 21 additions & 4 deletions src/main/resources/io/jenkins/plugins/thememanager/header/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
const themeJson = document.getElementById('theme-manager-theme').text
const theme = JSON.parse(themeJson);
(function () {
const themeJson = document.getElementById('theme-manager-theme').text
const theme = JSON.parse(themeJson);

if (theme.id && theme.id !== '') {
if (theme.id && theme.id !== '') {
document.documentElement.dataset.theme = theme.id
}
}
window.isSystemRespectingTheme = theme.respect_system_appearance

const propertiesJson = document.getElementById('theme-manager-properties').text
const parsedProperties = JSON.parse(propertiesJson);


window.getThemeManagerProperty = function (plugin, propertyName) {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches

let propertyNameNormalised = propertyName
if (isSystemRespectingTheme) {
propertyNameNormalised = isDark ? `${propertyName}-dark` : `${propertyName}-light`
}
return parsedProperties[`${plugin}:${propertyNameNormalised}`]
}
})()

0 comments on commit ada1152

Please sign in to comment.