mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-02 00:24:13 -05:00
v0.2.0 Updates (#130)
* migration redesign init * new color picker * changelog * added UI language selection * fix layout issue on recipe editor * remove git as dependency * added UI editor for original URL * CI/CD Tests * test: fixed migration routes * test todos * bug/added docker volume * chowdow test data * partial image recipe image testing * added card section card * settings form * homepage cetegory ui * frontend category placeholder * fixed broken scheduler * remove old files * removed temp test Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
173
frontend/package-lock.json
generated
173
frontend/package-lock.json
generated
@@ -1966,6 +1966,16 @@
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
||||
@@ -1992,6 +2002,53 @@
|
||||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -2008,6 +2065,16 @@
|
||||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||
@@ -2024,6 +2091,18 @@
|
||||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10175,6 +10254,11 @@
|
||||
"is-plain-obj": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
|
||||
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
|
||||
},
|
||||
"source-list-map": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
@@ -11521,87 +11605,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||
@@ -11641,6 +11644,14 @@
|
||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"vuedraggable": {
|
||||
"version": "2.24.3",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
|
||||
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
|
||||
"requires": {
|
||||
"sortablejs": "1.10.2"
|
||||
}
|
||||
},
|
||||
"vuetify": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.2.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue-i18n": "^8.22.4",
|
||||
"vue-router": "^3.4.9",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.4.2",
|
||||
"vuex": "^3.6.0",
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
@@ -61,4 +62,4 @@
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@selected="navigateFromSearch"
|
||||
/>
|
||||
</v-expand-x-transition>
|
||||
<v-btn icon @click="toggleSearch">
|
||||
<v-btn icon @click="search = !search">
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Menu from "./components/UI/Menu"
|
||||
import SearchBar from "./components/UI/SearchBar"
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab"
|
||||
import SnackBar from "./components/UI/SnackBar"
|
||||
import Vuetify from "./plugins/vuetify"
|
||||
import Menu from "./components/UI/Menu";
|
||||
import SearchBar from "./components/UI/SearchBar";
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
||||
import SnackBar from "./components/UI/SnackBar";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
export default {
|
||||
name: "App",
|
||||
|
||||
@@ -51,16 +51,17 @@ export default {
|
||||
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false
|
||||
this.search = false;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch("initTheme")
|
||||
this.$store.dispatch("requestRecentRecipes")
|
||||
this.$store.dispatch("initLang")
|
||||
this.darkModeSystemCheck()
|
||||
this.darkModeAddEventListener()
|
||||
this.$store.dispatch("initTheme");
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
this.$store.dispatch("requestHomePageSettings");
|
||||
this.$store.dispatch("initLang");
|
||||
this.darkModeSystemCheck();
|
||||
this.darkModeAddEventListener();
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
@@ -74,30 +75,22 @@ export default {
|
||||
if (this.$store.getters.getDarkMode === "system")
|
||||
Vuetify.framework.theme.dark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches
|
||||
).matches;
|
||||
},
|
||||
/**
|
||||
* This will monitor the OS level darkmode and call to update dark mode.
|
||||
*/
|
||||
darkModeAddEventListener() {
|
||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
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
|
||||
}
|
||||
this.darkModeSystemCheck();
|
||||
});
|
||||
},
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`)
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ImportDialog
|
||||
:name="selectedName"
|
||||
:date="selectedDate"
|
||||
ref="import_dialog"
|
||||
@import="importBackup"
|
||||
@delete="deleteBackup"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="4"
|
||||
v-for="backup in backups"
|
||||
:key="backup.name"
|
||||
>
|
||||
<v-card @click="openDialog(backup)">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" sm="2">
|
||||
<v-icon color="primary"> mdi-backup-restore </v-icon>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="10">
|
||||
<div>
|
||||
<strong>{{ backup.name }}</strong>
|
||||
</div>
|
||||
<div>{{ readableTime(backup.date) }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImportDialog from "./ImportDialog";
|
||||
import api from "../../../api";
|
||||
import utils from "../../../utils";
|
||||
export default {
|
||||
props: {
|
||||
backups: Array,
|
||||
},
|
||||
components: {
|
||||
ImportDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedName: "",
|
||||
selectedDate: "",
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openDialog(backup) {
|
||||
this.selectedDate = this.readableTime(backup.date);
|
||||
this.selectedName = backup.name;
|
||||
this.$refs.import_dialog.open();
|
||||
},
|
||||
readableTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
return utils.getDateAsText(date);
|
||||
},
|
||||
async importBackup(data) {
|
||||
this.$emit("loading");
|
||||
let response = await api.backups.import(data.name, data);
|
||||
|
||||
let failed = response.data.failed;
|
||||
let succesful = response.data.successful;
|
||||
|
||||
this.$emit("finished", succesful, failed);
|
||||
},
|
||||
deleteBackup(data) {
|
||||
this.$emit("loading");
|
||||
|
||||
api.backups.delete(data.name);
|
||||
this.selectedBackup = null;
|
||||
this.backupLoading = false;
|
||||
|
||||
this.$emit("finished");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
158
frontend/src/components/Settings/General/HomePageSettings.vue
Normal file
158
frontend/src/components/Settings/General/HomePageSettings.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<h2 class="mt-1 mb-1">Home Page</h2>
|
||||
<v-row align="center" justify="center" dense class="mb-n7 pb-n5">
|
||||
<v-col sm="2">
|
||||
<v-switch v-model="showRecent" label="Show Recent"></v-switch>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-slider
|
||||
class="pt-4"
|
||||
label="Card Per Section"
|
||||
v-model="showLimit"
|
||||
max="30"
|
||||
dense
|
||||
color="primary"
|
||||
min="3"
|
||||
thumb-label
|
||||
>
|
||||
</v-slider>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card outlined min-height="250">
|
||||
<v-card-text class="pt-2 pb-1">
|
||||
<h3>Homepage Categories</h3>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list min-height="200px" dense>
|
||||
<v-list-item-group>
|
||||
<draggable
|
||||
v-model="homeCategories"
|
||||
group="categories"
|
||||
:style="{
|
||||
minHeight: `200px`,
|
||||
}"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in homeCategories"
|
||||
:key="item"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-icon @click="deleteActiveCategory(index)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</draggable>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-card outlined min-height="250px">
|
||||
<v-card-text class="pt-2 pb-1">
|
||||
<h3>
|
||||
All Categories
|
||||
<span>
|
||||
<v-btn absolute right x-small color="success" icon>
|
||||
<v-icon>mdi-plus</v-icon></v-btn
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list min-height="200px" dense>
|
||||
<v-list-item-group>
|
||||
<draggable
|
||||
v-model="categories"
|
||||
group="categories"
|
||||
:style="{
|
||||
minHeight: `200px`,
|
||||
}"
|
||||
>
|
||||
<v-list-item v-for="item in categories" :key="item">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-icon @click="deleteActiveCategory(index)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</draggable>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" @click="saveSettings" class="mr-2">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
homeCategories: [],
|
||||
showLimit: null,
|
||||
categories: ["breakfast"],
|
||||
showRecent: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getOptions();
|
||||
},
|
||||
|
||||
methods: {
|
||||
getOptions() {
|
||||
let options = this.$store.getters.getHomePageSettings;
|
||||
this.showLimit = options.showLimit;
|
||||
this.categories = options.categories;
|
||||
this.showRecent = options.showRecent;
|
||||
this.homeCategories = options.homeCategories;
|
||||
},
|
||||
deleteActiveCategory(index) {
|
||||
this.homeCategories.splice(index, 1);
|
||||
},
|
||||
saveSettings() {
|
||||
let payload = {
|
||||
showRecent: this.showRecent,
|
||||
showLimit: this.showLimit,
|
||||
categories: this.categories,
|
||||
homeCategories: this.homeCategories,
|
||||
};
|
||||
|
||||
this.$store.commit("setHomePageSettings", payload);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,8 +1,20 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title> General Settings </v-card-title>
|
||||
<v-card-title>
|
||||
General Settings
|
||||
<v-spacer></v-spacer>
|
||||
<span>
|
||||
<v-btn class="pt-1" text href="/docs">
|
||||
<v-icon left>mdi-link</v-icon>
|
||||
Local API
|
||||
</v-btn>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<HomePageSettings />
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<h2 class="mt-1 mb-1">Language</h2>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
@@ -18,21 +30,35 @@
|
||||
<v-spacer></v-spacer>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HomePageSettings from "./HomePageSettings";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HomePageSettings,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
categories: ["cat 1", "cat 2", "cat 3"],
|
||||
usedCategories: ["recent"],
|
||||
langOptions: [],
|
||||
selectedLang: "en",
|
||||
homeOptions: {
|
||||
recipesToShow: 10,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getOptions();
|
||||
},
|
||||
watch: {
|
||||
usedCategories() {
|
||||
console.log(this.usedCategories);
|
||||
},
|
||||
selectedLang() {
|
||||
this.$store.commit("setLang", this.selectedLang);
|
||||
},
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"migration.currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="5" sm="5">
|
||||
<v-text-field
|
||||
v-model="repo"
|
||||
:label="$t('migration.chowdown-repo-url')"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="importRepo">
|
||||
<v-icon left> mdi-import </v-icon>
|
||||
{{ $t("migration.migrate") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
|
||||
<h4>{{ $t("migration.failed-recipes") }}</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
<v-alert v-if="failedImages[1]" outlined dense type="error">
|
||||
<h4>{{ $t("migration.failed-images") }}</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedImages" :key="fail">
|
||||
{{ fail }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
// import TimePicker from "./Webhooks/TimePicker";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processRan: false,
|
||||
failedImages: [],
|
||||
failedRecipes: [],
|
||||
repo: "",
|
||||
rules: {
|
||||
required: v => !!v || "Selection Required",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async importRepo() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$emit("loading");
|
||||
let response = await api.migrations.migrateChowdown(this.repo);
|
||||
this.failedImages = response.failedImages;
|
||||
this.failedRecipes = response.failedRecipes;
|
||||
this.$emit("finished");
|
||||
this.processRan = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"migration.you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="5" sm="12">
|
||||
<v-select
|
||||
:items="availableImports"
|
||||
v-model="selectedImport"
|
||||
:label="$t('migration.nextcloud-data')"
|
||||
:rules="[rules.required]"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col md="1" sm="12">
|
||||
<v-btn-toggle group>
|
||||
<v-btn text color="info" @click="importRecipes">
|
||||
<v-icon left> mdi-import </v-icon>
|
||||
{{ $t("migration.migrate") }}
|
||||
</v-btn>
|
||||
<v-btn text color="error" @click="deleteImportValidation">
|
||||
<v-icon left> mdi-delete </v-icon>
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<UploadBtn
|
||||
url="/api/migration/upload/"
|
||||
class="mt-1"
|
||||
@uploaded="getAvaiableImports"
|
||||
/>
|
||||
|
||||
<Confirmation
|
||||
:title="$t('general.delete-data')"
|
||||
:message="$t('migration.delete-confirmation')"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
v-on:confirm="deleteImport()"
|
||||
/>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<SuccessFailureAlert
|
||||
:success-header="$t('migration.successfully-imported-from-nextcloud')"
|
||||
:success="successfulImports"
|
||||
failed-header="$t('migration.failed-imports')"
|
||||
:failed="failedImports"
|
||||
/>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import Confirmation from "../../UI/Confirmation";
|
||||
import UploadBtn from "../../UI/UploadBtn";
|
||||
export default {
|
||||
components: {
|
||||
SuccessFailureAlert,
|
||||
Confirmation,
|
||||
UploadBtn,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
successfulImports: [],
|
||||
failedImports: [],
|
||||
availableImports: [],
|
||||
selectedImport: null,
|
||||
rules: {
|
||||
required: v => !!v || "Selection Required",
|
||||
},
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.getAvaiableImports();
|
||||
},
|
||||
methods: {
|
||||
async getAvaiableImports() {
|
||||
this.availableImports = await api.migrations.getNextcloudImports();
|
||||
},
|
||||
async importRecipes() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$emit("loading");
|
||||
let data = await api.migrations.importNextcloud(this.selectedImport);
|
||||
|
||||
this.successfulImports = data.successful;
|
||||
this.failedImports = data.failed;
|
||||
this.$emit("finished");
|
||||
}
|
||||
},
|
||||
deleteImportValidation() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$refs.deleteThemeConfirm.open();
|
||||
}
|
||||
},
|
||||
async deleteImport() {
|
||||
await api.migrations.delete(this.selectedImport);
|
||||
this.getAvaiableImports();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<v-form ref="file">
|
||||
<v-file-input
|
||||
:loading="loading"
|
||||
:label="$t('migration.upload-an-archive')"
|
||||
v-model="file"
|
||||
accept=".zip"
|
||||
@change="upload"
|
||||
:prepend-icon="icon"
|
||||
class="file-icon"
|
||||
>
|
||||
</v-file-input>
|
||||
</v-form>
|
||||
</template>c
|
||||
|
||||
<script>
|
||||
import api from "../../../api";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
loading: false,
|
||||
icon: "mdi-paperclip",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.file != null) {
|
||||
this.loading = true;
|
||||
let formData = new FormData();
|
||||
formData.append("archive", this.file);
|
||||
|
||||
await api.migrations.uploadFile(formData);
|
||||
|
||||
this.loading = false;
|
||||
this.$emit("uploaded");
|
||||
this.file = null;
|
||||
this.icon = "mdi-check";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.file-icon {
|
||||
transition-duration: 5s;
|
||||
}
|
||||
</style>
|
||||
81
frontend/src/components/UI/CardSection.vue
Normal file
81
frontend/src/components/UI/CardSection.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="mt-n5">
|
||||
<v-card flat class="transparent" height="60px">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn-toggle group>
|
||||
<v-btn text :to="`/recipes/category/${title.toLowerCase()}`">
|
||||
{{ title.toUpperCase() }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col align="end">
|
||||
<v-menu offset-y v-if="sortable">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn-toggle group>
|
||||
<v-btn text v-bind="attrs" v-on="on"> Sort </v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="$emit('sort-recent')">
|
||||
<v-list-item-title> Recent </v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('sort')">
|
||||
<v-list-item-title> A-Z </v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-row>
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="recipe in recipes.slice(0, cardLimit)"
|
||||
:key="recipe.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeCard from "./RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
},
|
||||
props: {
|
||||
sortable: {
|
||||
default: false,
|
||||
},
|
||||
title: String,
|
||||
recipes: Array,
|
||||
cardLimit: {
|
||||
default: 6,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.transparent {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="recipe in recipes"
|
||||
:key="recipe.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeCard from "./RecipeCard";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
},
|
||||
data: () => ({}),
|
||||
mounted() {},
|
||||
computed: {
|
||||
recipes() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<v-autocomplete
|
||||
:items="items"
|
||||
:loading="isLoading"
|
||||
v-model="selected"
|
||||
clearable
|
||||
return
|
||||
dense
|
||||
hide-details
|
||||
hide-selected
|
||||
item-text="slug"
|
||||
:label="$t('search.search-for-a-recipe')"
|
||||
single-line
|
||||
@keyup.enter.native="moreInfo(selected)"
|
||||
>
|
||||
<template v-slot:no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title :v-html="$t('search.search-for-your-favorite-recipe')">
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-slot:item="{ item }">
|
||||
<v-list-item-avatar
|
||||
color="primary"
|
||||
class="headline font-weight-light white--text"
|
||||
>
|
||||
<v-img :src="getImage(item.image)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content @click="moreInfo(item.slug)">
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
selected: null,
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
items() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
moreInfo(recipeSlug) {
|
||||
this.$router.push(`/recipe/${recipeSlug}`);
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,15 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<RecentRecipes />
|
||||
<CardSection
|
||||
v-if="pageSettings.showRecent"
|
||||
title="Recent"
|
||||
:recipes="recentRecipes"
|
||||
:card-limit="pageSettings.showLimit"
|
||||
/>
|
||||
<CardSection
|
||||
:sortable="true"
|
||||
v-for="(section, index) in recipeByCategory"
|
||||
:key="index"
|
||||
:title="section.title"
|
||||
:recipes="section.recipes"
|
||||
:card-limit="pageSettings.showLimit"
|
||||
@sort="sortAZ(index)"
|
||||
@sort-recent="sortRecent(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecentRecipes from "../components/UI/RecentRecipes";
|
||||
|
||||
import CardSection from "../components/UI/CardSection";
|
||||
export default {
|
||||
components: {
|
||||
RecentRecipes,
|
||||
CardSection,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recipeByCategory: [
|
||||
{
|
||||
title: "Title 1",
|
||||
recipes: this.$store.getters.getRecentRecipes,
|
||||
},
|
||||
{
|
||||
title: "Title 2",
|
||||
recipes: this.$store.getters.getRecentRecipes,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageSettings() {
|
||||
return this.$store.getters.getHomePageSettings;
|
||||
},
|
||||
recentRecipes() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getRecentRecipes() {
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
},
|
||||
sortAZ(index) {
|
||||
this.recipeByCategory[index].recipes.sort((a, b) =>
|
||||
a.name > b.name ? 1 : -1
|
||||
);
|
||||
},
|
||||
sortRecent(index) {
|
||||
this.recipeByCategory[index].recipes.sort((a, b) =>
|
||||
a.dateAdded > b.dateAdded ? -1 : 1
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
0
frontend/src/store/modules/recipes.js
Normal file
0
frontend/src/store/modules/recipes.js
Normal file
@@ -18,6 +18,13 @@ const store = new Vuex.Store({
|
||||
language,
|
||||
},
|
||||
state: {
|
||||
// Home Page Settings
|
||||
homePageSettings: {
|
||||
showRecent: true,
|
||||
showLimit: 9,
|
||||
categories: [],
|
||||
homeCategories: [],
|
||||
},
|
||||
// Snackbar
|
||||
snackActive: false,
|
||||
snackText: "",
|
||||
@@ -29,6 +36,9 @@ const store = new Vuex.Store({
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setHomePageSettings(state, payload) {
|
||||
state.homePageSettings = payload;
|
||||
},
|
||||
setSnackBar(state, payload) {
|
||||
state.snackText = payload.text;
|
||||
state.snackType = payload.type;
|
||||
@@ -57,6 +67,16 @@ const store = new Vuex.Store({
|
||||
|
||||
this.commit("setRecentRecipes", payload);
|
||||
},
|
||||
|
||||
async requestHomePageSettings() {
|
||||
// TODO: Query Backend for Categories
|
||||
this.commit("setHomePageSettings", {
|
||||
showRecent: true,
|
||||
showLimit: 9,
|
||||
categories: ["breakfast", "lunch", "dinner"],
|
||||
homeCategories: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
@@ -65,7 +85,8 @@ const store = new Vuex.Store({
|
||||
getSnackActive: state => state.snackActive,
|
||||
getSnackType: state => state.snackType,
|
||||
|
||||
getRecentRecipes: state => state.recentRecipes,
|
||||
getRecentRecipes: (state) => state.recentRecipes,
|
||||
getHomePageSettings: (state) => state.homePageSettings,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user