Merge pull request #39 from zackbcom/dev

Added System Dark Mode, Updated theme settings, Persistent Vuex
This commit is contained in:
Hayden 2021-01-07 20:29:23 -09:00 committed by GitHub
commit b0bcb04539
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 161 deletions

View file

@ -46,3 +46,5 @@ General
- Improved documentation
- Fixed hot-reloading development environment - [grssmnn](https://github.com/grssmnn)
- Added Confirmation component to deleting recipes - [zackbcom](https://github.com/zackbcom)
- Added Persistent storage to vuex - [zackbcom](https://github.com/zackbcom)
- Updated Theme backend - [zackbcom](https://github.com/zackbcom)

View file

@ -4,12 +4,12 @@
## Theme Settings
Color themes can be created and set from the UI in the settings page. You can select an existing color theme or create a new one. On creation of a new color theme random colors will first be generated, then you can select and save as you'd like. By default the "default" theme will be loaded for all new users visiting the site. All created color themes are available to all users of the site. Separate color themes can be set for both Light and Dark modes.
Color themes can be created and set from the UI in the settings page. You can select an existing color theme or create a new one. On creation of a new color theme, the default colors will be used, then you can select and save as you'd like. By default the "default" theme will be loaded for all new users visiting the site. All created color themes are available to all users of the site. Theme Colors will be set for both light and dark modes.
![](../gifs/theme-demo.gif)
!!! note
Theme data is stored in cookies in the browser. Calling "Save Theme" will refresh the cookie with the selected theme as well save the theme to the database.
Theme data is stored in localstorage in the browser. Calling "Save colors and apply theme will refresh the localstorage with the selected theme as well save the theme to the database.

View file

@ -13,11 +13,11 @@
"qs": "^6.9.4",
"v-jsoneditor": "^1.4.2",
"vue": "^2.6.11",
"vue-cookies": "^1.7.4",
"vue-html-to-paper": "^1.3.1",
"vue-router": "^3.4.9",
"vuetify": "^2.4.1",
"vuex": "^3.6.0"
"vuex": "^3.6.0",
"vuex-persistedstate": "^4.0.0-beta.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",

View file

@ -2,7 +2,7 @@
<v-app>
<v-app-bar dense app color="primary" dark class="d-print-none">
<v-btn @click="$router.push('/')" icon class="d-flex align-center">
<v-icon size="40" >
<v-icon size="40">
mdi-silverware-variant
</v-icon>
</v-btn>
@ -37,6 +37,7 @@ import Menu from "./components/UI/Menu";
import SearchHeader from "./components/UI/SearchHeader";
import AddRecipe from "./components/AddRecipe";
import SnackBar from "./components/UI/SnackBar";
import Vuetify from "./plugins/vuetify";
export default {
name: "App",
@ -44,32 +45,53 @@ export default {
Menu,
AddRecipe,
SearchHeader,
SnackBar,
SnackBar
},
watch: {
$route() {
this.search = false;
},
}
},
mounted() {
this.$store.dispatch("initCookies");
this.$store.dispatch("initTheme");
this.$store.dispatch("requestRecentRecipes");
this.darkModeSystemCheck();
this.darkModeAddEventListener();
},
data: () => ({
search: false,
search: false
}),
methods: {
/**
* Checks if 'system' is set for dark mode and then sets the corrisponding value for vuetify
*/
darkModeSystemCheck() {
if (this.$store.getters.getDarkMode === "system")
Vuetify.framework.theme.dark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
},
/**
* This will monitor the OS level darkmode and call to update dark mode.
*/
darkModeAddEventListener() {
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
darkMediaQuery.addEventListener("change", () => {
this.darkModeSystemCheck();
});
},
toggleSearch() {
if (this.search === true) {
this.search = false;
} else {
this.search = true;
}
},
},
}
}
};
</script>

View file

@ -19,7 +19,6 @@ export default {
async requestByName(name) {
let response = await apiReq.get(settingsURLs.specificTheme(name));
console.log(response);
return response.data;
},

View file

@ -1,69 +1,117 @@
<template>
<v-card>
<v-card-title class="secondary white--text"> Theme Settings </v-card-title>
<v-card-text>
<h2 class="mt-4 mb-1">Dark Mode</h2>
<p>
Choose how Mealie looks to you. Set your theme preference to follow your
system settings, or choose to use the light or dark theme.
</p>
<v-row dense align="center">
<v-col cols="12">
<v-btn-toggle
v-model="selectedDarkMode"
color="primary "
mandatory
@change="setStoresDarkMode"
>
<v-btn value="system">
Default to system
</v-btn>
<v-btn value="light">
Light
</v-btn>
<v-btn value="dark">
Dark
</v-btn>
</v-btn-toggle>
</v-col>
</v-row></v-card-text
>
<v-divider class=""></v-divider>
<v-card-text>
<h2 class="mt-1 mb-1">Theme</h2>
<p>
Select a theme from the dropdown or create a new theme. Note that the
default theme will be served to all users who have not set a theme
preference.
</p>
<v-row dense align="center">
<v-col cols="12" md="2" sm="5">
<v-switch
v-model="darkMode"
inset
label="Dark Mode"
class="my-n3"
@change="toggleDarkMode"
></v-switch>
</v-col>
<v-col cols="12" md="4" sm="3">
<v-form ref="form" lazy-validation>
<v-form ref="form" lazy-validation>
<v-row dense align="center">
<v-col cols="12" md="4" sm="3">
<v-select
label="Saved Color Schemes"
label="Saved Color Theme"
:items="availableThemes"
item-text="name"
item-value="colors"
return-object
v-model="selectedScheme"
v-model="selectedTheme"
@change="themeSelected"
:rules="[(v) => !!v || 'Theme is required']"
:rules="[v => !!v || 'Theme is required']"
required
>
</v-select>
</v-form>
</v-col>
<v-col cols="12" sm="1">
<NewTheme @new-theme="appendTheme" />
</v-col>
<v-col cols="12" sm="1">
<v-btn text color="error" @click="deleteSelected"> Delete </v-btn>
</v-col>
</v-row>
<v-row dense align-content="center" v-if="activeTheme">
</v-col>
<v-col cols="12" sm="1">
<NewTheme @new-theme="appendTheme" />
</v-col>
<v-col cols="12" sm="1">
<v-btn text color="error" @click="deleteSelectedThemeValidation">
Delete
</v-btn>
<Confirmation
title="Delete Theme"
message="Are you sure you want to delete this theme?"
color="error"
icon="mdi-alert-circle"
ref="deleteThemeConfirm"
v-on:confirm="deleteSelectedTheme()"
/>
</v-col>
</v-row>
</v-form>
<v-row dense align-content="center" v-if="selectedTheme.colors">
<v-col>
<ColorPicker button-text="Primary" v-model="activeTheme.primary" />
<ColorPicker
button-text="Primary"
v-model="selectedTheme.colors.primary"
/>
</v-col>
<v-col>
<ColorPicker
button-text="Secondary"
v-model="activeTheme.secondary"
v-model="selectedTheme.colors.secondary"
/>
</v-col>
<v-col>
<ColorPicker button-text="Accent" v-model="activeTheme.accent" />
<ColorPicker
button-text="Accent"
v-model="selectedTheme.colors.accent"
/>
</v-col>
<v-col>
<ColorPicker button-text="Success" v-model="activeTheme.success" />
<ColorPicker
button-text="Success"
v-model="selectedTheme.colors.success"
/>
</v-col>
<v-col>
<ColorPicker button-text="Info" v-model="activeTheme.info" />
<ColorPicker button-text="Info" v-model="selectedTheme.colors.info" />
</v-col>
<v-col>
<ColorPicker button-text="Warning" v-model="activeTheme.warning" />
<ColorPicker
button-text="Warning"
v-model="selectedTheme.colors.warning"
/>
</v-col>
<v-col>
<ColorPicker button-text="Error" v-model="activeTheme.error" />
<ColorPicker
button-text="Error"
v-model="selectedTheme.colors.error"
/>
</v-col>
</v-row>
</v-card-text>
@ -73,7 +121,9 @@
<v-col> </v-col>
<v-col></v-col>
<v-col align="end">
<v-btn text color="success" @click="saveThemes"> Save Theme </v-btn>
<v-btn text color="success" @click="saveThemes">
Save Colors and Apply Theme
</v-btn>
</v-col>
</v-row>
</v-card-actions>
@ -84,73 +134,90 @@
import api from "../../api";
import ColorPicker from "./ThemeUI/ColorPicker";
import NewTheme from "./ThemeUI/NewTheme";
import Confirmation from "../UI/Confirmation";
export default {
components: {
ColorPicker,
NewTheme,
Confirmation,
NewTheme
},
data() {
return {
themes: null,
activeTheme: {},
darkMode: false,
availableThemes: [],
selectedScheme: "",
selectedLight: "",
selectedTheme: {},
selectedDarkMode: "system",
availableThemes: []
};
},
async mounted() {
this.availableThemes = await api.themes.requestAll();
this.darkMode = this.$store.getters.getDarkMode;
this.themes = this.$store.getters.getThemes;
this.setThemeEditor();
this.selectedTheme = this.$store.getters.getActiveTheme;
this.selectedDarkMode = this.$store.getters.getDarkMode;
},
methods: {
async deleteSelected() {
/**
* Open the delete confirmation.
*/
deleteSelectedThemeValidation() {
if (this.$refs.form.validate()) {
if (this.selectedScheme === "default") {
if (this.selectedTheme.name === "default") {
// Notify User Can't Delete Default
} else if (this.selectedScheme !== "") {
api.themes.delete(this.selectedScheme.name);
} else if (this.selectedTheme !== {}) {
this.$refs.deleteThemeConfirm.open();
}
this.availableThemes = await api.themes.requestAll();
}
},
async appendTheme(newTheme) {
api.themes.create(newTheme);
this.availableThemes.push(newTheme);
},
themeSelected() {
this.activeTheme = this.selectedScheme.colors;
},
setThemeEditor() {
if (this.darkMode) {
this.activeTheme = this.themes.dark;
} else {
this.activeTheme = this.themes.light;
}
},
toggleDarkMode() {
this.$store.commit("setDarkMode", this.darkMode);
this.selectedScheme = "";
/**
* Delete the selected Theme
*/
async deleteSelectedTheme() {
//Delete Theme from DB
await api.themes.delete(this.selectedTheme.name);
this.setThemeEditor();
//Get the new list of available from DB
this.availableThemes = await api.themes.requestAll();
//Change to default if deleting current theme.
if (
!this.availableThemes.some(
theme => theme.name === this.selectedTheme.name
)
) {
await this.$store.dispatch("resetTheme");
this.selectedTheme = this.$store.getters.getActiveTheme;
}
},
saveThemes() {
/**
* Create the new Theme and select it.
*/
async appendTheme(newTheme) {
await api.themes.create(newTheme);
this.availableThemes.push(newTheme);
this.selectedTheme = newTheme;
},
themeSelected() {
//TODO Revamp Theme selection.
//console.log("this.activeTheme", this.selectedTheme);
},
setStoresDarkMode() {
this.$store.commit("setDarkMode", this.selectedDarkMode);
},
/**
* This will save the current colors and make the selected theme live.
*/
async saveThemes() {
if (this.$refs.form.validate()) {
if (this.darkMode) {
this.themes.dark = this.activeTheme;
} else {
this.themes.light = this.activeTheme;
}
this.$store.commit("setThemes", this.themes);
this.$store.dispatch("initCookies");
api.themes.update(this.selectedScheme.name, this.activeTheme);
} else;
},
},
this.$store.commit("setTheme", this.selectedTheme);
await api.themes.update(
this.selectedTheme.name,
this.selectedTheme.colors
);
}
}
}
};
</script>

View file

@ -56,7 +56,7 @@ export default {
VJsoneditor,
ViewRecipe,
EditRecipe,
ButtonRow,
ButtonRow
},
data() {
return {
@ -66,7 +66,7 @@ export default {
jsonEditorOptions: {
mode: "code",
search: false,
mainMenuBar: false,
mainMenuBar: false
},
// Recipe Details //
recipeDetails: {
@ -83,9 +83,9 @@ export default {
categories: [],
dateAdded: "",
notes: [],
rating: 0,
rating: 0
},
imageKey: 1,
imageKey: 1
};
},
mounted() {
@ -93,9 +93,9 @@ export default {
},
watch: {
$route: function () {
$route: function() {
this.getRecipeDetails();
},
}
},
computed: {
@ -111,7 +111,7 @@ export default {
} else {
return false;
}
},
}
},
methods: {
getImageFile(fileObject) {
@ -130,7 +130,6 @@ export default {
api.recipes.delete(this.recipeDetails.slug);
},
async saveRecipe() {
console.log(this.recipeDetails);
await api.recipes.update(this.recipeDetails);
if (this.fileObject) {
@ -143,8 +142,8 @@ export default {
showForm() {
this.form = true;
this.jsonEditor = false;
},
},
}
}
};
</script>

View file

@ -4,11 +4,9 @@ import vuetify from "./plugins/vuetify";
import store from "./store/store";
import VueRouter from "vue-router";
import { routes } from "./routes";
import VueCookies from "vue-cookies";
Vue.config.productionTip = false;
Vue.use(VueRouter);
Vue.use(VueCookies);
const router = new VueRouter({
routes,
@ -23,7 +21,7 @@ new Vue({
}).$mount("#app");
// Truncate
let filter = function(text, length, clamp) {
let filter = function (text, length, clamp) {
clamp = clamp || "...";
let node = document.createElement("div");
node.innerHTML = text;

View file

@ -0,0 +1,70 @@
import api from "../../api";
import Vuetify from "../../plugins/vuetify";
const state = {
activeTheme: {},
darkMode: 'system'
};
const mutations = {
setTheme(state, payload) {
Vuetify.framework.theme.themes.dark = payload.colors;
Vuetify.framework.theme.themes.light = payload.colors;
state.activeTheme = payload;
},
setDarkMode(state, payload) {
let isDark;
if (payload === 'system') {
//Get System Preference from browser
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
isDark = darkMediaQuery.matches;
}
else if (payload === 'dark')
isDark = true;
else if (payload === 'light')
isDark = false;
if (isDark !== null) {
Vuetify.framework.theme.dark = isDark;
state.darkMode = payload;
}
},
};
const actions = {
async resetTheme({ commit }) {
const defaultTheme = await api.themes.requestByName("default");
if (defaultTheme.colors) {
Vuetify.framework.theme.themes.dark = defaultTheme.colors;
Vuetify.framework.theme.themes.light = defaultTheme.colors;
commit('setTheme', defaultTheme)
}
},
async initTheme({ dispatch, getters }) {
//If theme is empty resetTheme
if (Object.keys(getters.getActiveTheme).length === 0) {
await dispatch('resetTheme')
}
else {
Vuetify.framework.theme.themes.dark = getters.getActiveTheme.colors;
Vuetify.framework.theme.themes.light = getters.getActiveTheme.colors;
}
},
}
const getters = {
getActiveTheme: (state) => state.activeTheme,
getDarkMode: (state) => state.darkMode
}
export default {
state,
mutations,
actions,
getters
}

View file

@ -1,11 +1,18 @@
import Vue from "vue";
import Vuex from "vuex";
import api from "../api";
import Vuetify from "../plugins/vuetify";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
Vue.use(Vuex);
const store = new Vuex.Store({
plugins: [createPersistedState({
paths: ['userSettings']
})],
modules: {
userSettings
},
state: {
// Snackbar
snackActive: false,
@ -15,29 +22,6 @@ const store = new Vuex.Store({
// All Recipe Data Store
recentRecipes: [],
allRecipes: [],
// Site Settings
darkMode: false,
themes: {
light: {
primary: "#E58325",
accent: "#00457A",
secondary: "#973542",
success: "#43A047",
info: "#FFFD99",
warning: "#FF4081",
error: "#EF5350",
},
dark: {
primary: "#4527A0",
accent: "#FF4081",
secondary: "#26C6DA",
success: "#43A047",
info: "#2196F3",
warning: "#FB8C00",
error: "#FF5252",
},
},
},
mutations: {
@ -53,38 +37,9 @@ const store = new Vuex.Store({
setRecentRecipes(state, payload) {
state.recentRecipes = payload;
},
setDarkMode(state, payload) {
state.darkMode = payload;
Vue.$cookies.set("darkMode", payload);
Vuetify.framework.theme.dark = payload;
},
setThemes(state, payload) {
state.themes = payload;
Vue.$cookies.set("themes", payload);
Vuetify.framework.theme.themes = payload;
},
},
actions: {
async initCookies() {
if (!Vue.$cookies.isKey("themes")) {
const DEFAULT_THEME = await api.themes.requestByName("default");
Vue.$cookies.set("themes", {
light: DEFAULT_THEME.colors,
dark: DEFAULT_THEME.colors,
});
}
this.commit("setThemes", Vue.$cookies.get("themes"));
// Dark Mode
if (!Vue.$cookies.isKey("darkMode")) {
Vue.$cookies.set("darkMode", false);
}
this.commit("setDarkMode", JSON.parse(Vue.$cookies.get("darkMode")));
},
async requestRecentRecipes() {
const keys = [
@ -108,10 +63,6 @@ const store = new Vuex.Store({
getSnackType: (state) => state.snackType,
getRecentRecipes: (state) => state.recentRecipes,
// Site Settings
getDarkMode: (state) => state.darkMode,
getThemes: (state) => state.themes,
},
});