🔀 Merge pull request #93 from Lissy93/FEATURE/custom-theme-configurator

[FEATURE] Adds custom theme configurator
This commit is contained in:
Alicia Sykes 2021-07-18 20:59:16 +01:00 committed by GitHub
commit bd119ba1e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 622 additions and 51 deletions

View File

@ -96,6 +96,7 @@ Dashy supports 1-Click deployments on several popular cloud platforms. To spin u
- [<img src="https://i.ibb.co/d2P1WZ7/heroku.png" width="18"/> Deploy to Heroku](https://heroku.com/deploy?template=https://github.com/Lissy93/dashy)
- [<img src="https://i.ibb.co/Ld2FZzb/vercel.png" width="18"/> Deploy to Vercel](https://vercel.com/new/project?template=https://github.com/lissy93/dashy)
- [<img src="https://i.ibb.co/xCHtzgh/render.png" width="18"/> Deploy to Render](https://render.com/deploy?repo=https://github.com/lissy93/dashy/tree/deploy_render)
- [<img src="https://i.ibb.co/J7MGymY/googlecloud.png" width="18"/> Deploy to GCP](https://deploy.cloud.run/?git_repo=https://github.com/lissy93/dashy.git)
- [<img src="https://i.ibb.co/HVWVYF7/docker.png" width="18"/> Deploy to PWD](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml)
#### Basic Commands
@ -149,9 +150,15 @@ You may find these [example config](https://gist.github.com/Lissy93/000f712a5ce9
</a>
</p>
The app comes with a number of built-in themes, but it's also easy to write you're own. All colors, and most other CSS properties make use of CSS variables, which makes customizing the look and feel of Dashy very easy.
Dashy comes with a number of built-in themes, but it's also easy to make you're own. All colors, and most other CSS properties are specified using CSS variables, which are [documented here](./docs/theming.md#css-variables). This make modifying styles and customizing the look and feel of Dashy very easy.
You can also apply custom CSS overrides directly through the UI (Under Config menu --> Custom CSS), or specify it in your config file under `appConfig.customCss`. If you have a lot of custom styles, you can pass in the path to a stylesheet, in `appConfig.externalStyleSheet`.
You can select a theme, and customize it's colors directly through the UI. But it's also possible tp pass in external stylesheets and styles either in the config file (under `appConfig.externalStyleSheet`), or by mounting it to `/app/src/styles/user-defined-themes.scss` with Docker.
<p align="center">
<a href="https://i.ibb.co/cLDXj1R/dashy-theme-configurator.gif">
<img alt="Example Themes" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/theme-config-demo.gif" width="400" />
</a>
</p>
**[⬆️ Back to Top](#dashy)**

View File

@ -12,6 +12,5 @@
"home-lab",
"lissy93"
],
"image": "heroku/nodejs",
"stack": "heroku-20"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

@ -65,6 +65,7 @@ To disallow any changes from being written to disk via the UI config editor, set
**`iconSize`** | `string` | _Optional_ | The size of link items / icons. Can be either `small`, `medium,` or `large`. Defaults to `medium`. This can also be set directly from the UI.
**`theme`** | `string` | _Optional_ | The default theme for first load (you can change this later from the UI)
**`cssThemes`** | `string[]` | _Optional_ | An array of custom theme names which can be used in the theme switcher dropdown
**`customColors`** | `object`| _Optional_ | Enables you to apply a custom color palette to any given theme. Use the theme name (lowercase) as the key, for an object including key-value-pairs, with the color variable name as keys, and 6-digit hex code as value. See [Theming](/docs/theming.md#modifying-theme-colors) for more info
**`externalStyleSheet`** | `string` or `string[]` | _Optional_ | Either a URL to an external stylesheet or an array or URLs, which can be applied as themes within the UI
**`customCss`** | `string` | _Optional_ | Raw CSS that will be applied to the page. This can also be set from the UI. Please minify it first.
**`hideComponents`** | `object` | _Optional_ | A list of key page components (header, footer, search, settings, etc) that are present by default, but can be removed using this option. See [`appConfig.hideComponents`](#appconfighideComponents-optional)

View File

@ -123,6 +123,16 @@ https://vercel.com/new/project?template=https://github.com/lissy93/dashy
https://cloud.digitalocean.com/apps/new?repo=https://github.com/lissy93/dashy/tree/deploy_digital-ocean
```
#### Google Cloud Platform <img src="https://i.ibb.co/J7MGymY/googlecloud.png" width="24"/>
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run/?git_repo=https://github.com/lissy93/dashy.git)
[Cloud Run](https://cloud.google.com/run/) is a service offered by [Google Cloud](https://cloud.google.com/). It's a fully managed serverless platform, for developing and deploying highly scalable containerized applications. Similar to AWS and Azure, GCP offers a wide range of cloud services, which are billed on a payperuse basis, but Cloud Run has a [free tier](https://cloud.google.com/run/pricing) offering 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million requests per month.
To deploy Dashy to GCP, use the following link
```
https://deploy.cloud.run/?git_repo=https://github.com/lissy93/dashy.git
```
#### Platform.sh <img src="https://i.ibb.co/GdfvH3Z/platformsh.png" width="24"/>
[![Deploy to Platform.sh](https://platform.sh/images/deploy/deploy-button-lg-blue.svg)](https://console.platform.sh/projects/create-project/?template=https://github.com/lissy93/dashy&utm_campaign=deploy_on_platform?utm_medium=button&utm_source=affiliate_links&utm_content=https://github.com/lissy93/dashy)

View File

@ -33,6 +33,32 @@ Finally, from the UI use the theme dropdown menu to select your new theme, and y
You can also set `appConfig.theme` to pre-select a default theme, which will be applied immediately after deployment.
### Modifying Theme Colors
Themes can be modified either through the UI, using the color picker menu (to the right of the theme dropdown), or directly in the config file, under `appConfig.customColors`. Here you can specify the value for any of the [available CSS variables](#css-variables).
<p align="center">
<a href="https://i.ibb.co/cLDXj1R/dashy-theme-configurator.gif">
<img alt="Example Themes" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/theme-config-demo.gif" width="400" />
</a>
</p>
By default, any color modifications made to the current theme through the UI will only be applied locally. If you need these settings to be set globally, then click the 'Export' button, to get the color codes and variable names, which can then be backed up, or saved in your config file.
Custom colors are saved relative the the base theme selected. So if you switch themes after setting custom colors, then you're settings will no longer be applied. You're changes are not lost though, and switching back to the original theme will see your styles reapplied.
If these values are specified in your `conf.yml` file, then it will look something like the below example. Note that in YAML, values or keys which contain special characters, must be wrapped in quotes.
```yaml
appConfig:
customColors:
oblivion:
primary: '#75efff'
background: '#2a3647'
dracula:
primary: '#8be9fd'
```
### Adding your own Theme
User-defined styles and custom themes should be defined in `./src/styles/user-defined-themes.scss`. If you're using Docker, you can pass your own stylesheet in using the `--volume` flag. E.g. `v ./my-themes.scss:/app/src/styles/user-defined-themes.scss`. Don't forget to pass your theme name into `appConfig.cssThemes` so that it shows up on the theme-switcher dropdown.
@ -83,7 +109,7 @@ CSS variables are simple to use. You define them like: `--background: #fff;` and
You can determine the variable used by any given element, and visualize changes using the browser developer tools (Usually opened with `F12`, or Options --> More --> Developer Tools). Under the elements tab, click the Element Selector icon (usually top-left corner), you will then be able to select any DOM element on the page by hovering and clicking it. In the CSS panel you will see all styles assigned to that given element, including CSS variables. Click a variable to see it's parent value, and for color attributes, click the color square to modify the color. For more information, see this [getting started guide](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools), and these articles on [selecting elements](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Select_an_element) and [inspecting and modifying colors](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Inspect_and_select_colors).
#### Top-Level Variables
These are all that are required to create a theme. All other variables inherit their values from these variables.
These are all that are required to create a theme. All other variables inherit their values from these variables, and can optionally be overridden.
- `--primary` - Application primary color. Used for title, text, accents, and other features
- `--background` - Application background color

View File

@ -36,6 +36,7 @@
"vue-prism-editor": "^1.2.2",
"vue-router": "^3.0.3",
"vue-select": "^3.11.2",
"vue-swatches": "^2.1.1",
"vue-toasted": "^1.1.28"
},
"devDependencies": {
@ -86,4 +87,4 @@
"> 1%",
"last 2 versions"
]
}
}

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="window-close" class="svg-inline--fa fa-window-close fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm0 394c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V86c0-3.3 2.7-6 6-6h404c3.3 0 6 2.7 6 6v340zM356.5 194.6L295.1 256l61.4 61.4c4.6 4.6 4.6 12.1 0 16.8l-22.3 22.3c-4.6 4.6-12.1 4.6-16.8 0L256 295.1l-61.4 61.4c-4.6 4.6-12.1 4.6-16.8 0l-22.3-22.3c-4.6-4.6-4.6-12.1 0-16.8l61.4-61.4-61.4-61.4c-4.6-4.6-4.6-12.1 0-16.8l22.3-22.3c4.6-4.6 12.1-4.6 16.8 0l61.4 61.4 61.4-61.4c4.6-4.6 12.1-4.6 16.8 0l22.3 22.3c4.7 4.6 4.7 12.1 0 16.8z"></path></svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="palette" class="svg-inline--fa fa-palette fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M128 224c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.4-32-32-32zM418.6 58.1C359.2 9.3 281.3-10 204.6 5 104.9 24.4 24.7 104.2 5.1 203.7c-16.7 84.2 8.1 168.3 67.8 230.6 47.3 49.4 109.7 77.8 167.9 77.8 8.8 0 17.5-.6 26.1-2 24.2-3.7 44.6-18.7 56.1-41.1 12.3-24 12.3-52.7.2-76.6-6.1-12-5.5-26.2 1.8-38 7-11.8 18.7-18.4 32-18.4h72.2c46.4 0 82.8-35.7 82.8-81.3-.2-76.4-34.3-148.1-93.4-196.6zM429.2 288H357c-29.9 0-57.2 15.4-73 41.3-16 26.1-17.3 57.8-3.6 84.9 5.1 10.1 5.1 22.7-.2 32.9-2.6 5-8.7 13.7-20.6 15.6-49.3 7.7-108.9-16.6-152-61.6-48.8-50.9-69-119.4-55.4-188 15.9-80.6 80.8-145.3 161.6-161 62.6-12.3 126.1 3.5 174.3 43.1 48.1 39.5 75.7 97.6 75.9 159.6 0 18.6-15.3 33.2-34.8 33.2zM160 128c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.4-32-32-32zm96-32.1c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32c0-17.6-14.3-32-32-32zm96 32.1c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -331,6 +331,7 @@ p.small-screen-note {
height: -webkit-fill-available;
height: -moz-available;
height: stretch;
height: 100%; // Firefox
}
.tab-item {

View File

@ -1,5 +1,5 @@
<template>
<div class="json-editor-outer">
<div class="css-editor-outer">
<prism-editor class="my-editor" v-model="customCss" :highlight="highlighter" line-numbers />
<button class="save-button" @click="save()">Save Changes</button>
<p class="quick-note">
@ -7,10 +7,13 @@
Styles overides are only stored locally, so it is reccomended to make a copy of your CSS.
To remove all custom styles, delete the contents and hit Save Changes
</p>
<CustomThemeMaker :themeToEdit="currentTheme" class="color-config" />
</div>
</template>
<script>
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
import { getTheme } from '@/utils/ConfigHelpers';
import { PrismEditor } from 'vue-prism-editor';
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-css';
@ -25,11 +28,13 @@ export default {
config: Object,
},
components: {
CustomThemeMaker,
PrismEditor,
},
data() {
return {
customCss: this.config.appConfig.customCss || '\n\n\n\n\n',
currentTheme: getTheme(),
};
},
methods: {
@ -65,6 +70,11 @@ export default {
<style lang="scss">
div.css-editor-outer {
text-align: center;
padding-bottom: 1rem;
}
button.save-button {
padding: 0.5rem 1rem;
margin: 0.25rem auto;
@ -98,4 +108,22 @@ p.quick-note {
border-radius: var(--curve-factor);
}
.color-config.theme-configurator-wrapper {
border: 1px solid var(--config-settings-color);
background: var(--config-settings-background);
color: var(--config-settings-color);
position: relative;
width: 80%;
max-width: 24rem;
margin: 1rem auto;
box-shadow: none;
right: 0;
top: 0;
max-height: unset;
.color-row-container {
text-align: left;
max-height: unset;
}
}
</style>

View File

@ -155,12 +155,12 @@ export default {
i.fas, i.fab, i.far, i.fal, i.fad {
font-size: 2rem;
color: currentColor;
margin: 1px auto 4px;
margin: 1px 4px;
&.small {
font-size: 1.5rem;
}
&.large {
font-size: 3rem;
font-size: 2.5rem;
}
}
object.tile-icon {

View File

@ -0,0 +1,291 @@
<template>
<div :class="`theme-configurator-wrapper ${showingAllVars ? 'showing-all' : ''}`">
<h3 class="configurator-title">Theme Configurator</h3>
<div class="color-row-container">
<div class="color-row" v-for="colorName in Object.keys(customColors)" :key="colorName">
<label :for="`color-input-${colorName}`" class="color-name">
{{colorName.replaceAll('-', ' ')}}
</label>
<v-swatches
v-if="isColor(colorName, customColors[colorName])"
v-model="customColors[colorName]"
show-fallback
fallback-input-type="color"
popover-x="left"
:swatches="swatches"
@input="setVariable(colorName, customColors[colorName])"
>
<input
:id="`color-input-${colorName}`"
slot="trigger"
:value="customColors[colorName]"
class="swatch-input form__input__element"
readonly
:style="makeSwatchStyles(colorName)"
/>
</v-swatches>
<input v-else
:id="`color-input-${colorName}`"
:value="customColors[colorName]"
class="misc-input"
@input="setVariable(colorName, customColors[colorName])"
/>
</div> <!-- End of color list -->
</div>
<p @click="exportToClipboard" class="action-text-btn">
Export Custom Variables
</p>
<p @click="resetAndSave" class="action-text-btn show-all-vars-btn">
Reset Styles for '{{ themeToEdit }}'
</p>
<p @click="findAllVariableNames" class="action-text-btn">
Show All Variables
</p>
<div class="action-buttons">
<Button :click="saveChanges"><SaveIcon />Save</Button>
<Button :click="resetUnsavedColors"><CancelIcon />Cancel</Button>
</div>
</div>
</template>
<script>
import VSwatches from 'vue-swatches';
import 'vue-swatches/dist/vue-swatches.css';
import { localStorageKeys, mainCssVars, swatches } from '@/utils/defaults';
import Button from '@/components/FormElements/Button';
import SaveIcon from '@/assets/interface-icons/save-config.svg';
import CancelIcon from '@/assets/interface-icons/config-cancel.svg';
export default {
name: 'ThemeMaker',
components: {
VSwatches,
Button,
SaveIcon,
CancelIcon,
},
data() {
return {
customColors: this.makeInitialData(mainCssVars),
showingAllVars: false,
swatches,
};
},
props: {
themeToEdit: String,
},
methods: {
/* Finds the current dominent value for a given CSS variable */
getCssVariableValue(cssVar) {
return getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim() || 'inherit';
},
/* Sets the value to a given variable in the DOM */
setVariable(variable, value) {
document.documentElement.style.setProperty(`--${variable}`, value);
},
/* Saves the users omdified variables in local storage */
saveChanges() {
const priorSettings = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
priorSettings[this.themeToEdit] = this.customColors;
localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings));
this.$toasted.show('Theme Updates Succesfully');
this.$emit('closeThemeConfigurator');
},
/* Itterates over available variables, removing them from the DOM */
resetUnsavedColors() {
const variables = Object.keys(this.customColors);
variables.forEach((variable) => {
document.documentElement.style.removeProperty(`--${variable}`);
});
this.customColors = this.makeInitialData(mainCssVars);
this.$emit('closeThemeConfigurator');
},
/* Resets styles, and removes data for current theme from local storage */
resetAndSave() {
const priorSettings = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
delete priorSettings[this.themeToEdit];
localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings));
this.resetUnsavedColors();
this.$toasted.show(`Custom Colors for ${this.themeToEdit} Removed`);
},
/* Generates CSS for the currently applied variables, and copys to users clipboard */
exportToClipboard() {
const themeName = this.themeToEdit.replace(/^\w/, c => c.toUpperCase());
let clipboardText = `// Custom Colors for ${themeName}\n`;
Object.keys(this.customColors).forEach((customVar) => {
clipboardText += (`--${customVar}: ${this.customColors[customVar]};\n`);
});
navigator.clipboard.writeText(clipboardText);
this.$toasted.show(`Theme data for ${themeName} copied to clipboard`);
},
/* Returns a JSON object, with the variable name as key, and color as value */
makeInitialData(variableArray) {
const data = {};
const hasDash = (colorVar) => (/^--/.exec(colorVar));
const addDash = (colorVar) => (hasDash(colorVar) ? colorVar : `--${colorVar}`);
const removeDash = (colorVar) => (hasDash(colorVar) ? colorVar.replace('--', '') : colorVar);
variableArray.forEach((colorName) => {
data[removeDash(colorName)] = this.getCssVariableValue(addDash(colorName));
});
return data;
},
/* Find all available CSS variables for the current applied theme */
findAllVariableNames() {
const availableVariables = Array.from(document.styleSheets)
.filter(sheet => sheet.href === null || sheet.href.startsWith(window.location.origin))
.reduce(
((acc, sheet) => ([
...acc,
...Array.from(sheet.cssRules).reduce(
(def, rule) => (rule.selectorText === ':root'
? [...def, ...Array.from(rule.style).filter(name => name.startsWith('--'))] : def),
[],
),
])),
[],
);
this.customColors = this.makeInitialData(availableVariables);
this.showingAllVars = true;
},
/* Returns a complmenting text color for the palete input foreground */
/* White if the color is dark, otherwise black */
getForegroundColor(colorHex) {
const hexToRgb = (hex) => {
const colorParts = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!colorParts || colorParts.length < 3) return 'black';
const parse = (index) => parseInt(colorParts[index], 16);
return colorParts ? { r: parse(1), g: parse(2), b: parse(3) } : null;
};
const getLightness = (rgb) => (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
return getLightness(hexToRgb(colorHex.trim())) < 100 ? 'white' : 'black';
},
/* The contents of the style attribute, to set background and text color of swatch */
makeSwatchStyles(colorName) {
const contrastingColor = this.getForegroundColor(this.customColors[colorName]);
return `background:${this.customColors[colorName]};`
+ `color:${contrastingColor}; border: 1px solid ${contrastingColor}`;
},
/* Determines if a given variable should NOT use the color picker component */
isColor(variableName, variableValue) {
// If value is a dimension, then it aint a color
if ((/rem|px|%/.exec(variableValue))) return false;
const nonColorVariables = [ // Known non-color variables
'--curve-factor', '--curve-factor-navbar', '--curve-factor-small',
'--dimming-factor', '--scroll-bar-width', '--header-height', '--footer-height',
'--item-group-padding', '--item-shadow', '--item-hover-shadow:', '--item-icon-transform',
'--item-icon-transform-hover', '--item-group-shadow', '--context-menu-shadow',
'--settings-container-shadow', '--side-bar-width',
];
// If the variable name is known to not be a color (in above list)
if (nonColorVariables.includes(`--${variableName}`)) return false;
return true; // It must be a color, we'll use the color picker
},
},
};
</script>
<style lang="scss">
@import '@/styles/style-helpers.scss';
div.theme-configurator-wrapper {
position: absolute;
top: 4rem;
right: 1rem;
width: 16rem;
min-height: 12rem;
max-height: 28rem;
padding: 0.5rem;
z-index: 5;
overflow-y: visible;
background: var(--config-settings-background);
color: var(--config-settings-color);
border-radius: var(--curve-factor);
box-shadow: 0 8px 10px -2px rgba(0, 0, 0, 0.6), 1px 1px 6px var(--primary);
h3.configurator-title {
text-align: center;
font-weight: normal;
margin: 0.4rem;
}
div.color-row-container {
max-height: 16rem;
overflow-y: visible;
@extend .scroll-bar;
div.color-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0;
border-bottom: 1px dashed var(--primary);
label.color-name {
text-transform: capitalize;
}
}
}
input.swatch-input, input.misc-input {
border: none;
margin: 0.2rem;
padding: 0.5rem;
width: 4rem;
border-radius: var(--curve-factor);
font-family: var(--font-monospace);
color: var(--black);
font-weight: bold;
cursor: pointer;
&:hover {
box-shadow: inset 0 0 4px 4px #00000033;
}
&:active {
box-shadow: inset 0 0 4px 4px #00000080;
outline: none;
}
}
}
p.action-text-btn {
cursor: pointer;
margin: 0.5rem auto 0;
padding: 0.2rem 0.4rem;
width: fit-content;
text-align: center;
text-decoration: underline;
border-radius: var(--curve-factor);
border: 1px solid var(--background-darker);
&:hover {
background: var(--background);
border-color: var(--primary);
text-decoration: none;
}
&:active {
background: var(--primary);
color: var(--background);
}
&.hide {
display: none;
}
}
div.action-buttons {
display: flex;
justify-content: center;
button {
min-width: 6rem;
padding: 0.25rem 0.5rem;
margin: 1rem 0.5rem 0.5rem;
}
}
div.theme-configurator-wrapper.showing-all {
overflow: auto;
div.color-row-container {
overflow: auto;
}
p.show-all-vars-btn {
display: none;
}
}
</style>

View File

@ -7,7 +7,7 @@
/>
<div class="options-outer">
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
<ThemeSelector :themes="availableThemes"
<ThemeSelector :externalThemes="externalThemes" @modalChanged="modalChanged"
:confTheme="getInitialTheme()" :userThemes="getUserThemes()" />
<LayoutSelector :displayLayout="displayLayout" @layoutUpdated="updateDisplayLayout"/>
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
@ -50,7 +50,7 @@ export default {
props: {
displayLayout: String,
iconSize: String,
availableThemes: Object,
externalThemes: Object,
appConfig: Object,
pageInfo: Object,
sections: Array,

View File

@ -1,5 +1,6 @@
<template>
<div class="theme-selector-section" v-if="themes" >
<div class="theme-selector-section" v-click-outside="closeThemeConfigurator">
<div>
<span class="theme-label">Theme</span>
<v-select
:options="themeNames"
@ -7,41 +8,69 @@
class="theme-dropdown"
:tabindex="-2"
/>
</div>
<IconPalette
class="color-button"
@click="openThemeConfigurator"
/>
<CustomThemeMaker
v-if="themeConfiguratorOpen"
:themeToEdit="selectedTheme"
@closeThemeConfigurator="closeThemeConfigurator()"
/>
</div>
</template>
<script>
import ThemeHelper from '@/utils/ThemeHelper';
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
import {
LoadExternalTheme,
ApplyLocalTheme,
ApplyCustomVariables,
} from '@/utils/ThemeHelper';
import Defaults, { localStorageKeys } from '@/utils/defaults';
import IconPalette from '@/assets/interface-icons/config-color-palette.svg';
export default {
name: 'ThemeSelector',
props: {
themes: Object,
externalThemes: Object,
confTheme: String,
userThemes: Array,
},
components: {
CustomThemeMaker,
IconPalette,
},
watch: {
selectedTheme(newTheme) { this.updateTheme(newTheme); },
/* When the theme changes, then call the update method */
selectedTheme(newTheme) {
this.updateTheme(newTheme);
},
},
data() {
return {
selectedTheme: this.getInitialTheme(),
themeHelper: new ThemeHelper(),
loading: true,
builtInThemes: this.userThemes.concat(Defaults.builtInThemes),
builtInThemes: [...Defaults.builtInThemes, ...this.userThemes],
themeHelper: new LoadExternalTheme(),
themeConfiguratorOpen: false, // Control the opening of theme config popup
ApplyLocalTheme,
ApplyCustomVariables,
};
},
computed: {
/* Combines all theme names (builtin and user defined) together */
themeNames: function themeNames() {
const externalThemeNames = Object.keys(this.themes);
return externalThemeNames.concat(this.builtInThemes);
const externalThemeNames = Object.keys(this.externalThemes);
const specialThemes = ['custom'];
return [...externalThemeNames, ...this.builtInThemes, ...specialThemes];
},
},
created() {
const added = Object.keys(this.themes).map(
name => this.themeHelper.add(name, this.themes[name]),
// Pass all user custom stylesheets to the themehelper
const added = Object.keys(this.externalThemes).map(
name => this.themeHelper.add(name, this.externalThemes[name]),
);
// Quicker loading, if the theme is local we can apply it immidiatley
if (this.isThemeLocal(this.selectedTheme)) {
@ -54,19 +83,24 @@ export default {
}
},
methods: {
/* Sets the theme, by updating data-theme attribute on the html tag */
setLocalTheme(newTheme) {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', newTheme);
},
/* Get default theme */
getInitialTheme() {
return localStorage[localStorageKeys.THEME] || this.confTheme || Defaults.theme;
},
/* Determines if a given theme is local / not a custom user stylesheet */
isThemeLocal(themeToCheck) {
return this.builtInThemes.includes(themeToCheck);
},
/* Opens the theme color configurator popup */
openThemeConfigurator() {
this.$emit('modalChanged', true);
this.themeConfiguratorOpen = true;
},
/* Closes the theme color configurator popup */
closeThemeConfigurator() {
this.$emit('modalChanged', false);
this.themeConfiguratorOpen = false;
},
/* Updates theme. Checks if the new theme is local or external,
and calls appropirate updating function. Updates local storage */
updateTheme(newTheme) {
@ -74,12 +108,14 @@ export default {
this.resetToDefault();
this.themeHelper.theme = 'Deafault';
} else if (this.isThemeLocal(newTheme)) {
this.setLocalTheme(newTheme);
this.ApplyLocalTheme(newTheme);
} else {
this.themeHelper.theme = newTheme;
}
this.ApplyCustomVariables(newTheme);
localStorage.setItem(localStorageKeys.THEME, newTheme);
},
/* Removes any applied themes */
resetToDefault() {
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
},
@ -95,7 +131,7 @@ export default {
div.vs__dropdown-toggle {
border-color: var(--settings-text-color);
border-radius: var(--curve-factor);
min-width: 10rem;
width: 8rem;
height: 1.8rem;
font-size: 0.85rem;
cursor: pointer;
@ -111,6 +147,8 @@ export default {
width: auto;
background: var(--background);
z-index: 5;
max-width: 13rem;
overflow-x: hidden;
}
li.vs__dropdown-option--highlight {
background: var(--settings-text-color);
@ -123,7 +161,7 @@ export default {
.theme-selector-section {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: flex-start;
height: 100%;
span.theme-label {
@ -133,4 +171,24 @@ export default {
}
}
svg.color-button {
path {
fill: var(--settings-text-color);
}
width: 1rem;
height: 1rem;
padding: 0.2rem;
margin: 0.5rem;
align-self: flex-end;
text-align: center;
background: var(--background);
border: 1px solid var(--settings-text-color);;
border-radius: var(--curve-factor);
cursor: pointer;
&:hover, &.selected {
background: var(--settings-text-color);
path { fill: var(--background); }
}
}
</style>

View File

@ -482,7 +482,7 @@ html[data-theme='material'], html[data-theme='material-dark'] {
html[data-theme='material'] {
--primary: #363636;
--background: #f5f5f5;
--background-darker: #4285f4;
--background-darker: #5c90eb;
--item-group-outer-background: none;
--item-group-shadow: none;
--item-group-background: none;
@ -744,7 +744,9 @@ html[data-theme='vaporware'] {
// body {
// background: url('https://i.ibb.co/JqcJcGK/vaporwave-sunset-wallpaper.jpg');
// background-size: cover;
// div.home { background: none; }
// div.home, div.options-outer, div.options-container, section.filter-container {
// background: none;
// }
// }
}
@ -774,4 +776,48 @@ html[data-theme='cyberpunk'] {
--welcome-popup-background: var(--pink);
--welcome-popup-text-color: var(--blue);
--font-headings: 'Audiowide', cursive;
}
}
html[data-theme="oblivion"],
html[data-theme="oblivion-blue"],
html[data-theme="oblivion-mint"],
html[data-theme="oblivion-lemon"],
html[data-theme="oblivion-scotch"] {
--primary: #f35151;
--background: #1b2431;
--background-darker: #121a25;
--item-group-outer-background: none;
--item-group-shadow: none;
--item-group-background: none;
--item-background: var(--background-darker);
--item-background-hover: var(--background-darker);
--item-shadow: 0 1px 5px #18191a;
--item-hover-shadow: 2px 2px 3px #040505;
--item-group-heading-text-color-hover: var(--primary);
--nav-link-background-color: var(--background);
--curve-factor: 3px;
--curve-factor-navbar: 6px;
--item-group-heading-text-color: var(--primary);
--about-page-background: var(--background);
--about-page-color: var(--primary);
div.item-wrapper a.item {
border: 1px solid #313d4f;
}
section.filter-container form input#filter-tiles {
border: 1px solid #313d4f;
box-shadow: 0 1px 5px #0c0d0e;
}
}
html[data-theme="oblivion-blue"] {
--primary: #82a5f3;
}
html[data-theme="oblivion-mint"] {
--primary: #4acfd4;
}
html[data-theme="oblivion-lemon"] {
--primary: #d0ed87;
}
html[data-theme="oblivion-scotch"] {
--primary: #d69e3a;
}

View File

@ -1,6 +1,5 @@
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { visibleComponents } from '@/utils/defaults';
import { visibleComponents, localStorageKeys, theme as defaultTheme } from '@/utils/defaults';
/**
* Initiates the Accumulator class and generates a complete config object
@ -40,3 +39,24 @@ export const componentVisibility = (appConfig) => {
? !usersChoice.hideSplashScreen : visibleComponents.splashScreen,
};
};
/**
* Gets the users saved theme, first looks for local storage theme,
* then looks at user's appConfig, and finally checks the defaults
* @returns {string} Name of theme to apply
*/
export const getTheme = () => {
const localTheme = localStorage[localStorageKeys.THEME];
const appConfigTheme = config.appConfig.theme;
return localTheme || appConfigTheme || defaultTheme;
};
/**
* Gets any custom styles the user has applied, wither from local storage, or from the config
* @returns {object} An array of objects, one for each theme, containing kvps for variables
*/
export const getCustomColors = () => {
const localColors = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
const configColors = config.appConfig.customColors || {};
return Object.assign(configColors, localColors);
};

View File

@ -166,6 +166,10 @@
"type": "string"
}
},
"customColors": {
"type": "object",
"description": "Set a custom color palette for any theme"
},
"externalStyleSheet": {
"description": "URL or URLs of external stylesheets to add to dropdown/ load",
"type": [

View File

@ -1,8 +1,34 @@
import ErrorHandler from '@/utils/ErrorHandler';
import { getTheme, getCustomColors } from '@/utils/ConfigHelpers';
import { mainCssVars } from '@/utils/defaults';
/* Returns users current theme */
export const GetTheme = () => getTheme();
/* Gets user custom color preferences for current theme, and applies to DOM */
export const ApplyCustomVariables = (theme) => {
mainCssVars.forEach((vName) => { document.documentElement.style.removeProperty(`--${vName}`); });
const themeColors = getCustomColors()[theme];
if (themeColors) {
Object.keys(themeColors).forEach((customVar) => {
document.documentElement.style.setProperty(`--${customVar}`, themeColors[customVar]);
});
}
};
/* Sets the theme, by updating data-theme attribute on the html tag */
export const ApplyLocalTheme = (newTheme) => {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', newTheme);
};
/**
* A function for pre-loading, and easy switching of external stylesheets
* External CSS is preloaded to avoid FOUC
*/
const ThemeHelper = function th() {
export const LoadExternalTheme = function th() {
/* Preload selected external theme */
const preloadTheme = (href) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
@ -18,10 +44,21 @@ const ThemeHelper = function th() {
});
};
/* Check theme is selected, and it exists */
const checkTheme = (themes, name) => {
if ((!name) || (name !== 'custom' && !themes[name])) {
ErrorHandler(`Theme: '${name || '[not selected]'}' does not exist.`);
return false;
}
return true;
};
/* Disable all but selected theme */
const selectTheme = (themes, name) => {
const t = themes; // To avoid ESLint complaining about mutating a param
if (name && !themes[name]) throw new Error(`Theme: '${name}' does not exist.`);
Object.keys(themes).forEach(n => { t[n].disabled = (n !== name); });
if (checkTheme(themes, name)) {
const t = themes; // To avoid ESLint complaining about mutating a param
Object.keys(themes).forEach(n => { t[n].disabled = (n !== name); });
}
};
const themes = {};
@ -32,5 +69,3 @@ const ThemeHelper = function th() {
get theme() { return Object.keys(themes).find(n => !themes[n].disabled); },
};
};
export default ThemeHelper;

View File

@ -1,4 +1,5 @@
module.exports = {
/* Default pageInfo object, used if user does not specify their own */
pageInfo: {
title: 'Dashy',
description: '',
@ -8,12 +9,19 @@ module.exports = {
],
footerText: '',
},
/* Default appConfig to be used, if user does not specify their own */
appConfig: {},
/* Default icon size to be applied on initial load */
iconSize: 'medium',
/* Default layout to be applied on initial load */
layout: 'auto',
/* Default theme to be applied on initial load */
theme: 'default',
/* Default Font-Awesome API key, for FA icons (if used) */
fontAwesomeKey: '0821c65656',
/* Default API to use for fetching of user service favicon icons (if enabled) */
faviconApi: 'faviconkit',
/* List of built-in themes, to be displayed within the theme-switcher dropdown */
builtInThemes: [
'callisto',
'thebe',
@ -25,6 +33,7 @@ module.exports = {
'nord-frost',
'minimal-dark',
'minimal-light',
'oblivion',
'matrix',
'matrix-red',
'hacker-girl',
@ -39,6 +48,7 @@ module.exports = {
'high-contrast-dark',
'high-contrast-light',
],
/* Which structural components should be visible by default */
visibleComponents: {
pageTitle: true,
navigation: true,
@ -46,12 +56,14 @@ module.exports = {
settings: true,
footer: true,
},
/* Key names for local storage identifiers */
localStorageKeys: {
HIDE_WELCOME_BANNER: 'hideWelcomeHelpers',
LAYOUT_ORIENTATION: 'layoutOrientation',
COLLAPSE_STATE: 'collapseState',
ICON_SIZE: 'iconSize',
THEME: 'theme',
CUSTOM_COLORS: 'customColors',
CONF_SECTIONS: 'confSections',
PAGE_INFO: 'pageInfo',
APP_CONFIG: 'appConfig',
@ -60,23 +72,37 @@ module.exports = {
HIDE_SETTINGS: 'hideSettings',
USERNAME: 'username',
},
/* Key names for cookie identifiers */
cookieKeys: {
AUTH_TOKEN: 'authenticationToken',
},
/* Key names for session storage identifiers */
sessionStorageKeys: {
SW_STATUS: 'serviceWorkerStatus',
},
/* Unique IDs of modals within the app */
modalNames: {
CONF_EDITOR: 'CONF_EDITOR',
CLOUD_BACKUP: 'CLOUD_BACKUP',
REBUILD_APP: 'REBUILD_APP',
THEME_MAKER: 'THEME_MAKER',
ABOUT_APP: 'ABOUT_APP',
},
/* Key names for the top-level objects in conf.yml */
topLevelConfKeys: {
PAGE_INFO: 'pageInfo',
APP_CONFIG: 'appConfig',
SECTIONS: 'sections',
},
/* Which CSS variables to show in the first view of theme configurator */
mainCssVars: ['primary', 'background', 'background-darker'],
/* Amount of time to show splash screen, when enabled, in milliseconds */
splashScreenTime: 1900,
/* Page meta-data, rendered in the header of each view */
metaTagData: [
{ name: 'description', content: 'A simple static homepage for you\'re server' },
],
/* Default option for Toast messages */
toastedOptions: {
position: 'bottom-center',
duration: 2500,
@ -84,11 +110,9 @@ module.exports = {
className: 'toast-message',
iconPack: 'fontawesome',
},
/* Server location of the Backup & Sync cloud function */
backupEndpoint: 'https://dashy-sync-service.as93.net',
splashScreenTime: 1900,
metaTagData: [
{ name: 'description', content: 'A simple static homepage for you\'re server' },
],
/* Available services for fetching favicon icon for user apps */
faviconApiEndpoints: {
mcapi: 'https://eu.mc-api.net/v3/server/favicon/$URL',
clearbit: 'https://logo.clearbit.com/$URL',
@ -98,4 +122,14 @@ module.exports = {
allesedv: 'https://f1.allesedv.com/128/$URL',
webmasterapi: 'https://api.webmasterapi.com/v1/favicon/yEwx0ZFs0CSPshHq/$URL',
},
/* Available built-in colors for the theme builder */
swatches: [
['#eb5cad', '#985ceb', '#5346f3', '#5c90eb'],
['#5cdfeb', '#00CCB4', '#5ceb8d', '#afeb5c'],
['#eff961', '#ebb75c', '#eb615c', '#eb2d6c'],
['#060913', '#141b33', '#1c2645', '#263256'],
['#2b2d42', '#1a535c', '#372424', '#312437'],
['#f5f5f5', '#d9d9d9', '#bfbfbf', '#9a9a9a'],
['#636363', '#363636', '#313941', '#0d0d0d'],
],
};

View File

@ -8,7 +8,7 @@
@change-modal-visibility="updateModalVisibility"
:displayLayout="layout"
:iconSize="itemSizeBound"
:availableThemes="getExternalCSSLinks()"
:externalThemes="getExternalCSSLinks()"
:sections="getSections(sections)"
:appConfig="appConfig"
:pageInfo="pageInfo"

View File

@ -9,7 +9,8 @@
import SideBar from '@/components/Workspace/SideBar';
import WebContent from '@/components/Workspace/WebContent';
import Defaults, { localStorageKeys } from '@/utils/defaults';
import Defaults from '@/utils/defaults';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
export default {
name: 'Workspace',
@ -19,6 +20,9 @@ export default {
},
data: () => ({
url: '', // this.$route.query.url || '',
GetTheme,
ApplyLocalTheme,
ApplyCustomVariables,
}),
components: {
SideBar,
@ -29,10 +33,9 @@ export default {
this.url = url;
},
setTheme() {
const theme = localStorage[localStorageKeys.THEME] || this.confTheme || Defaults.theme;
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', theme);
const theme = this.GetTheme();
this.ApplyLocalTheme(theme);
this.ApplyCustomVariables(theme);
},
initiateFontAwesome() {
const fontAwesomeScript = document.createElement('script');

View File

@ -8936,6 +8936,11 @@ vue-svg-loader@^0.16.0:
loader-utils "^1.2.3"
svg-to-vue "^0.7.0"
vue-swatches@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/vue-swatches/-/vue-swatches-2.1.1.tgz#26c467fb7648ff4ee0887aea36d1e03b15032b83"
integrity sha512-YugkNbByxMz1dnx1nZyHSL3VSf/TnBH3/NQD+t8JKxPSqUmX87sVGBxjEaqH5IMraOLfVmU0pHCHl2BfXNypQg==
vue-template-compiler@^2.6.10:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz#a2f0e7d985670d42c9c9ee0d044fed7690f4f763"