mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
commit
e24da2b7ee
64 changed files with 1322 additions and 875 deletions
37
Dockerfile
37
Dockerfile
|
@ -7,7 +7,7 @@ RUN npm run build
|
|||
|
||||
FROM python:3.8-alpine
|
||||
|
||||
RUN apk add --no-cache git curl libxml2-dev libxslt-dev libxml2
|
||||
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2
|
||||
ENV ENV prod
|
||||
EXPOSE 80
|
||||
WORKDIR /app
|
||||
|
@ -15,6 +15,7 @@ WORKDIR /app
|
|||
COPY ./pyproject.toml /app/
|
||||
|
||||
RUN apk add --update --no-cache --virtual .build-deps \
|
||||
curl \
|
||||
g++ \
|
||||
py-lxml \
|
||||
python3-dev \
|
||||
|
@ -34,35 +35,5 @@ COPY --from=build-stage /app/dist /app/dist
|
|||
RUN rm -rf /app/test /app/.temp
|
||||
|
||||
|
||||
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
|
||||
# ---------------------------------- #
|
||||
# Old Docker File
|
||||
# ---------------------------------- #
|
||||
|
||||
# FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8-slim
|
||||
# FROM mrnr91/uvicorn-gunicorn-fastapi:python3.8
|
||||
|
||||
|
||||
# WORKDIR /app
|
||||
|
||||
# RUN apt-get update -y && \
|
||||
# apt-get install -y python-pip python-dev git curl python3-dev libxml2-dev libxslt1-dev zlib1g-dev --no-install-recommends && \
|
||||
# rm -rf /var/lib/apt/lists/* && \
|
||||
# curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
|
||||
# cd /usr/local/bin && \
|
||||
# ln -s /opt/poetry/bin/poetry && \
|
||||
# poetry config virtualenvs.create false
|
||||
|
||||
# COPY ./pyproject.toml /app/
|
||||
|
||||
# COPY ./mealie /app
|
||||
# RUN poetry install --no-root --no-dev
|
||||
# COPY --from=build-stage /app/dist /app/dist
|
||||
# RUN rm -rf /app/test /app/.temp
|
||||
|
||||
# ENV ENV prod
|
||||
# ENV APP_MODULE "app:app"
|
||||
|
||||
# VOLUME [ "/app/data" ]
|
||||
VOLUME [ "/app_data/" ]
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
|
|
@ -16,6 +16,11 @@ Don't forget to [join the Discord](https://discord.gg/R6QDyJgbD2)!
|
|||
|
||||
# Todo's
|
||||
|
||||
Test
|
||||
- [ ] Image Upload Test
|
||||
- [ ] Rename and Upload Image Test
|
||||
- [x] Chowdown Migration End Point Test
|
||||
|
||||
Frontend
|
||||
- [ ] No Meal Today Page instead of Null
|
||||
- [ ] Recipe Print Page
|
||||
|
|
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>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="700">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn color="accent" dark v-bind="attrs" v-on="on"> API Extras </v-btn>
|
||||
<v-btn color="accent" dark v-bind="attrs" v-on="on"> {{ $t("recipe.api-extras") }} </v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title> API Extras </v-card-title>
|
||||
<v-card-title> {{ $t("recipe.api-extras") }} </v-card-title>
|
||||
|
||||
<v-card-text :key="formKey">
|
||||
<v-row
|
||||
|
@ -28,14 +28,14 @@
|
|||
</v-col>
|
||||
<v-col cols="12" md="3" sm="6">
|
||||
<v-text-field
|
||||
label="Object Key"
|
||||
:label="$t('recipe.object-key')"
|
||||
:value="key"
|
||||
@input="updateKey(index)"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="6">
|
||||
<v-text-field label="Object Value" v-model="extras[key]">
|
||||
<v-text-field :label="$t('recipe.object-value')" v-model="extras[key]">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -46,17 +46,17 @@
|
|||
<v-card-actions>
|
||||
<v-form ref="addKey">
|
||||
<v-text-field
|
||||
label="New Key Name"
|
||||
:label="$t('recipe.new-key-name')"
|
||||
v-model="newKeyName"
|
||||
class="pr-4"
|
||||
:rules="[rules.required, rules.whiteSpace]"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
<v-btn color="info" text @click="append"> Add Key</v-btn>
|
||||
<v-btn color="info" text @click="append"> {{ $t("recipe.add-key") }} </v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn color="success" text @click="save"> Save </v-btn>
|
||||
<v-btn color="success" text @click="save"> {{ $t("general.save") }} </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
@ -74,9 +74,9 @@ export default {
|
|||
dialog: false,
|
||||
formKey: 1,
|
||||
rules: {
|
||||
required: (v) => !!v || "Key Name Required",
|
||||
required: (v) => !!v || this.$i18n.t("recipe.key-name-required"),
|
||||
whiteSpace: (v) =>
|
||||
!v || v.split(" ").length <= 1 || "No White Space Allowed",
|
||||
!v || v.split(" ").length <= 1 || this.$i18n.t("recipe.no-white-space-allowed"),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -16,19 +16,19 @@
|
|||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="Total Time"
|
||||
:label="$t('recipe.total-time')"
|
||||
v-model="value.totalTime"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col
|
||||
><v-text-field
|
||||
label="Prep Time"
|
||||
:label="$t('recipe.prep-time')"
|
||||
v-model="value.prepTime"
|
||||
></v-text-field
|
||||
></v-col>
|
||||
<v-col
|
||||
><v-text-field
|
||||
label="Cook Time / Perform Time"
|
||||
:label="$t('recipe.perform-time')"
|
||||
v-model="value.performTime"
|
||||
></v-text-field
|
||||
></v-col>
|
||||
|
@ -149,7 +149,7 @@
|
|||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
label="Title"
|
||||
:label="$t('recipe.title')"
|
||||
v-model="value.notes[index]['title']"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
|
@ -209,7 +209,7 @@
|
|||
<v-text-field
|
||||
v-model="value.orgURL"
|
||||
class="mt-10"
|
||||
label="Original URL"
|
||||
:label="$t('recipe.original-url')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -234,9 +234,9 @@ export default {
|
|||
return {
|
||||
fileObject: null,
|
||||
rules: {
|
||||
required: v => !!v || "Key Name Required",
|
||||
required: v => !!v || this.$i18n.t("recipe.key-name-required"),
|
||||
whiteSpace: v =>
|
||||
!v || v.split(" ").length <= 1 || "No White Space Allowed",
|
||||
!v || v.split(" ").length <= 1 || this.$i18n.t("recipe.no-white-space-allowed"),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
v-if="totalTime"
|
||||
></v-divider>
|
||||
<v-col v-if="totalTime">
|
||||
<div><strong> Total Time </strong></div>
|
||||
<div><strong> {{ $t("recipe.total-time") }} </strong></div>
|
||||
<div>{{ totalTime }}</div>
|
||||
</v-col>
|
||||
<v-divider
|
||||
|
@ -30,7 +30,7 @@
|
|||
v-if="prepTime"
|
||||
></v-divider>
|
||||
<v-col v-if="prepTime">
|
||||
<div><strong> Prep Time </strong></div>
|
||||
<div><strong> {{ $t("recipe.prep-time") }} </strong></div>
|
||||
<div>{{ prepTime }}</div>
|
||||
</v-col>
|
||||
<v-divider
|
||||
|
@ -40,7 +40,7 @@
|
|||
v-if="performTime"
|
||||
></v-divider>
|
||||
<v-col v-if="performTime">
|
||||
<div><strong> Cook Time </strong></div>
|
||||
<div><strong> {{ $t("recipe.perform-time") }} </strong></div>
|
||||
<div>{{ performTime }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
target="_blank"
|
||||
class="rounded-sm mr-4"
|
||||
>
|
||||
{{$t('recipe.original-recipe')}}
|
||||
{{$t('recipe.original-url')}}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
|
|
@ -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>
|
|
@ -77,8 +77,8 @@ export default {
|
|||
computed: {
|
||||
switchLabel() {
|
||||
if (this.fullBackup) {
|
||||
return "Full Backup";
|
||||
} else return "Partial Backup";
|
||||
return this.$t("settings.backup.full-backup");
|
||||
} else return this.$t("settings.backup.partial-backup");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</v-row>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
<v-card-title class="mt-n6">
|
||||
Available Backups
|
||||
{{ $t("settings.available-backups") }}
|
||||
<span>
|
||||
<UploadBtn
|
||||
class="mt-1"
|
||||
|
@ -35,10 +35,10 @@
|
|||
/>
|
||||
<SuccessFailureAlert
|
||||
ref="report"
|
||||
title="Back Restore Report"
|
||||
success-header="Successfully Imported"
|
||||
:title="$t('settings.backup.backup-restore-report')"
|
||||
:success-header="$t('settings.backup.successfully-imported')"
|
||||
:success="successfulImports"
|
||||
failed-header="Failed Imports"
|
||||
:failed-header="$t('settings.backup.failed-imports')"
|
||||
:failed="failedImports"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
|
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>
|
||||
{{ $t("settings.general-settings") }}
|
||||
<v-spacer></v-spacer>
|
||||
<span>
|
||||
<v-btn class="pt-1" text href="/docs">
|
||||
<v-icon left>mdi-link</v-icon>
|
||||
{{ $t("settings.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">{{ $t("settings.language") }}</h2>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
|
@ -10,7 +22,7 @@
|
|||
:items="langOptions"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
label="Language"
|
||||
:label="$t('settings.language')"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
@ -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>
|
|
@ -35,10 +35,10 @@
|
|||
<v-card-actions class="mt-n6">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="deleteMigration(migration.name)">
|
||||
Delete
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<v-btn color="accent" text @click="importMigration(migration.name)">
|
||||
Import
|
||||
{{ $t("general.import") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
@ -46,7 +46,7 @@
|
|||
<div v-else>
|
||||
<v-card class="text-center ma-2">
|
||||
<v-card-text>
|
||||
No Migration Data Avaiable
|
||||
{{ $t("migration.no-migration-data-available") }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<SuccessFailureAlert
|
||||
title="Migration Report"
|
||||
:title="$t('migration.migration-report')"
|
||||
ref="report"
|
||||
failedHeader="Failed Imports"
|
||||
:failedHeader="$t('migration.failed-imports')"
|
||||
:failed="failed"
|
||||
successHeader="Successful Imports"
|
||||
:successHeader="$t('migration.successful-imports')"
|
||||
:success="success"
|
||||
/>
|
||||
<v-card :loading="loading">
|
||||
|
@ -54,14 +54,14 @@ export default {
|
|||
failed: [],
|
||||
migrations: {
|
||||
nextcloud: {
|
||||
title: "Nextcloud Cookbook",
|
||||
description: "migrate data from a nextcloud cookbook intance",
|
||||
title: this.$t("migration.nextcloud.title"),
|
||||
description: this.$t("migration.nextcloud.description"),
|
||||
urlVariable: "nextcloud",
|
||||
availableImports: [],
|
||||
},
|
||||
chowdown: {
|
||||
title: "Chowdown",
|
||||
description: "Migrate From Chowdown",
|
||||
title: this.$t("migration.chowdown.title"),
|
||||
description: this.$t("migration.chowdown.description"),
|
||||
urlVariable: "chowdown",
|
||||
availableImports: [],
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<v-card-title> {{$t('settings.add-a-new-theme')}} </v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
label="Theme Name"
|
||||
:label="$t('settings.theme.theme-name')"
|
||||
v-model="themeName"
|
||||
:rules="[rules.required]"
|
||||
></v-text-field>
|
||||
|
@ -34,7 +34,7 @@ export default {
|
|||
dialog: false,
|
||||
themeName: "",
|
||||
rules: {
|
||||
required: (val) => !!val || "Required.",
|
||||
required: (val) => !!val || this.$t("settings.theme.theme-name-is-required"),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
mandatory
|
||||
@change="setStoresDarkMode"
|
||||
>
|
||||
<v-btn value="system"> Default to system </v-btn>
|
||||
<v-btn value="system"> {{ $t("settings.theme.default-to-system") }} </v-btn>
|
||||
|
||||
<v-btn value="light"> {{ $t("settings.theme.light") }} </v-btn>
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
|||
<v-btn-toggle group>
|
||||
<NewThemeDialog @new-theme="appendTheme" class="mt-1" />
|
||||
<v-btn text color="error" @click="deleteSelectedThemeValidation">
|
||||
Delete
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<Confirmation
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<Confirmation
|
||||
title="Delete Recpie"
|
||||
message="Are you sure you want to delete this recipie?"
|
||||
:title="$t('recipe.delete-recipe')"
|
||||
:message="$t('recipe.delete-confirmation')"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteRecipieConfirm"
|
||||
|
|
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>
|
|
@ -21,8 +21,8 @@
|
|||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="cancel"> Cancel </v-btn>
|
||||
<v-btn :color="color" text @click="confirm"> Confirm </v-btn>
|
||||
<v-btn color="grey" text @click="cancel"> {{ $t("general.cancel") }} </v-btn>
|
||||
<v-btn :color="color" text @click="confirm"> {{ $t("general.confirm") }} </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
|
@ -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>
|
|
@ -6,7 +6,7 @@
|
|||
item-text="item.name"
|
||||
dense
|
||||
light
|
||||
label="Search Mealie"
|
||||
:label="$t('search.search-mealie')"
|
||||
:search-input.sync="search"
|
||||
hide-no-data
|
||||
cache-items
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
||||
<v-btn :loading="isSelecting" @click="onButtonClick" color="accent" text>
|
||||
<v-icon left> mdi-cloud-upload </v-icon>
|
||||
Upload
|
||||
{{ $t('general.upload') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
@ -15,7 +15,7 @@ export default {
|
|||
url: String,
|
||||
},
|
||||
data: () => ({
|
||||
defaultButtonText: "Upload",
|
||||
defaultButtonText: this.$t("general.upload"),
|
||||
file: null,
|
||||
isSelecting: false,
|
||||
}),
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
"save": "Gem",
|
||||
"select": "Vælg",
|
||||
"update": "Opdater",
|
||||
"delete-data": "Slet data",
|
||||
"download": "Hent",
|
||||
"import": "Importere"
|
||||
},
|
||||
|
@ -42,7 +41,6 @@
|
|||
"dinner-this-week": "Madplan denne uge",
|
||||
"dinner-today": "Madplan i dag",
|
||||
"planner": "Planlægger",
|
||||
"choose-a-recipe": "Vælg en opskrift",
|
||||
"create-a-new-meal-plan": "Opret en ny måltidsplan",
|
||||
"edit-meal-plan": "Rediger måltidsplan",
|
||||
"end-date": "Slutdato",
|
||||
|
@ -57,7 +55,7 @@
|
|||
"instructions": "Instruktioner",
|
||||
"note": "Bemærk",
|
||||
"notes": "Bemærkninger",
|
||||
"original-recipe": "Oprindelig opskrift",
|
||||
"original-url": "Oprindelig opskrift",
|
||||
"recipe-name": "Opskriftens navn",
|
||||
"servings": "Portioner",
|
||||
"step-index": "Trin: {step}",
|
||||
|
@ -65,36 +63,20 @@
|
|||
"view-recipe": "Se opskrift"
|
||||
},
|
||||
"search": {
|
||||
"search-for-a-recipe": "Søg efter en opskrift",
|
||||
"search-for-your-favorite-recipe": "Søg efter din foretrukne <strong>opskrift</strong>"
|
||||
"search-mealie": "Search Mealie"
|
||||
},
|
||||
"migration": {
|
||||
"chowdown-repo-url": "Chowdown Repo URL",
|
||||
"currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration": "I øjeblikket er Chowdown via offentlig Repo URL den eneste understøttede migreringstype",
|
||||
"failed-images": "Mislykkede billeder",
|
||||
"failed-recipes": "Mislykkede opskrifter",
|
||||
"migrate": "Migrere",
|
||||
"recipe-migration": "Migrering af opskrifter",
|
||||
"delete-confirmation": "Er du sikker på, at du vil slette disse migrationsdata?",
|
||||
"failed-imports": "Mislykket import",
|
||||
"nextcloud-data": "Nextcloud data",
|
||||
"successfully-imported-from-nextcloud": "Importeret fra Nextcloud",
|
||||
"upload-an-archive": "Upload et arkiv",
|
||||
"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": "Du kan importere opskrifter fra enten en zip-fil eller et bibliotek i /app/data/migraiton/ folderen. \nGennemse dokumentationen for at sikre, at din bibliotekstruktur svarer til det, der forventes"
|
||||
"failed-imports": "Mislykket import"
|
||||
},
|
||||
"settings": {
|
||||
"add-a-new-theme": "Tilføj et nyt tema",
|
||||
"backup-and-exports": "Backup og eksport",
|
||||
"backup-info": "Sikkerhedskopier eksporteres i standard JSON-format sammen med alle de billeder, der er gemt på filsystemet. \nI din sikkerhedskopimappe finder du en .zip-fil, der indeholder alle opskrifterne JSON og billeder fra databasen. \nDerudover, hvis du valgte en markdown-fil, gemmes disse også i .zip-filen. \nFor at importere en sikkerhedskopi skal den være placeret i din sikkerhedskopimappe. \nAutomatiske sikkerhedskopier udføres hver dag kl. 3:00.",
|
||||
"backup-recipes": "Sikkerhedskopier opksrifter",
|
||||
"backup-tag": "Sikkerhedskopier tags",
|
||||
"color": "Farve",
|
||||
"contribute": "Bidrag",
|
||||
"explore-the-docs": "Udforsk dokumentation",
|
||||
"markdown-template": "Markdown skabelon",
|
||||
"new-version-available": "En ny version af Mealie er tilgængelig. <a {aContents}> Besøg repoen </a>",
|
||||
"set-new-time": "Indstil ny tid",
|
||||
"swatches": "Prøver",
|
||||
"current": "Version:",
|
||||
"latest": "Seneste:",
|
||||
"theme": {
|
||||
|
@ -114,13 +96,11 @@
|
|||
"dark": "Mørkt",
|
||||
"delete-theme": "Slet tema",
|
||||
"light": "Lyst",
|
||||
"save-colors-and-apply-theme": "Gem farver og anvend tema",
|
||||
"saved-color-theme": "Gemt farvetema",
|
||||
"theme": "Tema"
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "Måltidsplanlægning Webhooks",
|
||||
"save-webhooks": "Gem Webhooks",
|
||||
"test-webhooks": "Test Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Webadresserne, der er anført nedenfor, modtager webhooks, der indeholder opskriftsdataene for måltidsplanen på den planlagte dag. \nWebhooks udføres i øjeblikket på <strong> {time} </strong>",
|
||||
"webhook-url": "Webhook adresse"
|
||||
|
@ -131,4 +111,4 @@
|
|||
"import-themes": "Importer temaer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,11 @@
|
|||
"enabled": "Enabled",
|
||||
"download": "Download",
|
||||
"import": "Import",
|
||||
"delete-data": "Delete Data",
|
||||
"options": "Options",
|
||||
"templates": "Templates",
|
||||
"recipes": "Recipes",
|
||||
"themes": "Themes"
|
||||
"themes": "Themes",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"login": {
|
||||
"stay-logged-in": "Stay logged in?",
|
||||
|
@ -49,7 +49,6 @@
|
|||
"planner": "Planner",
|
||||
"edit-meal-plan": "Edit Meal Plan",
|
||||
"meal-plans": "Meal Plans",
|
||||
"choose-a-recipe": "Choose a Recipe",
|
||||
"create-a-new-meal-plan": "Create a New Meal Plan",
|
||||
"start-date": "Start Date",
|
||||
"end-date": "End Date"
|
||||
|
@ -66,16 +65,29 @@
|
|||
"ingredient": "Ingredient",
|
||||
"notes": "Notes",
|
||||
"note": "Note",
|
||||
"original-recipe": "Original Recipe",
|
||||
"view-recipe": "View Recipe"
|
||||
"original-url": "Original URL",
|
||||
"view-recipe": "View Recipe",
|
||||
"title": "Title",
|
||||
"total-time": "Total Time",
|
||||
"prep-time": "Prep Time",
|
||||
"perform-time": "Cook Time / Perform Time",
|
||||
"api-extras": "API Extras",
|
||||
"object-key": "Object Key",
|
||||
"object-value": "Object Value",
|
||||
"new-key-name": "New Key Name",
|
||||
"add-key": "Add Key",
|
||||
"key-name-required": "Key Name Required",
|
||||
"no-white-space-allowed": "No White Space Allowed",
|
||||
"delete-recipe": "Delete Recipe",
|
||||
"delete-confirmation": "Are you sure you want to delete this recipe?"
|
||||
},
|
||||
"search": {
|
||||
"search-for-a-recipe": "Search for a Recipe",
|
||||
"search-for-your-favorite-recipe": "Search for your Favorite <strong>Recipe</strong>"
|
||||
"search-mealie": "Search Mealie"
|
||||
},
|
||||
"settings": {
|
||||
"color": "Color",
|
||||
"swatches": "Swatches",
|
||||
"general-settings": "General Settings",
|
||||
"local-api": "Local API",
|
||||
"language": "Language",
|
||||
"add-a-new-theme": "Add a New Theme",
|
||||
"set-new-time": "Set New Time",
|
||||
"current": "Version:",
|
||||
|
@ -84,10 +96,9 @@
|
|||
"contribute": "Contribute",
|
||||
"backup-and-exports": "Backups",
|
||||
"backup-info": "Backups are exported in standard JSON format along with all the images stored on the file system. In your backup folder you'll find a .zip file that contains all of the recipe JSON and images from the database. Additionally, if you selected a markdown file, those will also be stored in the .zip file. To import a backup, it must be located in your backups folder. Automated backups are done each day at 3:00 AM.",
|
||||
"backup-tag": "Backup Tag",
|
||||
"markdown-template": "Markdown Template",
|
||||
"backup-recipes": "Backup Recipes",
|
||||
"available-backups": "Available Backups",
|
||||
"theme": {
|
||||
"theme-name": "Theme Name",
|
||||
"theme-settings": "Theme Settings",
|
||||
"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": "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.",
|
||||
"dark-mode": "Dark Mode",
|
||||
|
@ -99,21 +110,21 @@
|
|||
"info": "Info",
|
||||
"warning": "Warning",
|
||||
"error": "Error",
|
||||
"default-to-system": "Default to system",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"theme": "Theme",
|
||||
"saved-color-theme": "Saved Color Theme",
|
||||
"delete-theme": "Delete Theme",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "Are you sure you want to delete this theme?",
|
||||
"save-colors-and-apply-theme": "Save Colors and Apply Theme",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Choose how Mealie looks to you. Set your theme preference to follow your system settings, or choose to use the light or dark theme."
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Choose how Mealie looks to you. Set your theme preference to follow your system settings, or choose to use the light or dark theme.",
|
||||
"theme-name-is-required": "Theme Name is required."
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "Meal Planner Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on its scheduled day. Currently Webhooks will execute at <strong>{ time }</strong>",
|
||||
"test-webhooks": "Test Webhooks",
|
||||
"webhook-url": "Webhook URL",
|
||||
"save-webhooks": "Save Webhooks"
|
||||
"webhook-url": "Webhook URL"
|
||||
},
|
||||
"new-version-available": "A New Version of Mealie is Available, <a {aContents}> Visit the Repo </a>",
|
||||
"backup": {
|
||||
|
@ -121,21 +132,27 @@
|
|||
"import-themes": "Import Themes",
|
||||
"import-settings": "Import Settings",
|
||||
"create-heading": "Create a Backup",
|
||||
"backup-tag": "Backup Tag"
|
||||
"backup-tag": "Backup Tag",
|
||||
"full-backup": "Full Backup",
|
||||
"partial-backup": "Partial Backup",
|
||||
"backup-restore-report": "Backup Restore Report",
|
||||
"successfully-imported": "Successfully Imported",
|
||||
"failed-imports": "Failed Imports"
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"recipe-migration": "Recipe Migration",
|
||||
"currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration": "Currently Chowdown via public Repo URL is the only supported type of migration",
|
||||
"chowdown-repo-url": "Chowdown Repo URL",
|
||||
"migrate": "Migrate",
|
||||
"failed-recipes": "Failed Recipes",
|
||||
"failed-images": "Failed Images",
|
||||
"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": "You can import recipes from either a zip file or a directory located in the /app/data/migration/ folder. Please review the documentation to ensure your directory structure matches what is expected",
|
||||
"nextcloud-data": "Nextcloud Data",
|
||||
"delete-confirmation": "Are you sure you want to delete this migration data?",
|
||||
"successfully-imported-from-nextcloud": "Successfully Imported from Nextcloud",
|
||||
"failed-imports": "Failed Imports",
|
||||
"upload-an-archive": "Upload an Archive"
|
||||
"migration-report": "Migration Report",
|
||||
"successful-imports": "Successful Imports",
|
||||
"no-migration-data-available": "No Migration Data Avaiable",
|
||||
"nextcloud": {
|
||||
"title": "Nextcloud Cookbook",
|
||||
"description": "Migrate data from a Nextcloud Cookbook intance"
|
||||
},
|
||||
"chowdown": {
|
||||
"title": "Chowdown",
|
||||
"description": "Migrate data from Chowdown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,7 @@
|
|||
"ok": "OK",
|
||||
"enabled": "Activé",
|
||||
"download": "Télécharger",
|
||||
"import": "Importer",
|
||||
"delete-data": "Supprimer les données"
|
||||
"import": "Importer"
|
||||
},
|
||||
"login": {
|
||||
"stay-logged-in": "Rester connecté(e) ?",
|
||||
|
@ -44,7 +43,6 @@
|
|||
"planner": "Planificateur",
|
||||
"edit-meal-plan": "Éditer le plan de menu",
|
||||
"meal-plans": "Plans de menu",
|
||||
"choose-a-recipe": "Choisir une recette",
|
||||
"create-a-new-meal-plan": "Créer un nouveau plan de menu",
|
||||
"start-date": "Date de début",
|
||||
"end-date": "Date de fin"
|
||||
|
@ -61,16 +59,13 @@
|
|||
"ingredient": "Ingrédient",
|
||||
"notes": "Notes",
|
||||
"note": "Note",
|
||||
"original-recipe": "Recette originale",
|
||||
"original-url": "Recette originale",
|
||||
"view-recipe": "Voir la recette"
|
||||
},
|
||||
"search": {
|
||||
"search-for-a-recipe": "Chercher une recette",
|
||||
"search-for-your-favorite-recipe": "Cherchez votre <strong>recette</strong> préférée"
|
||||
"search-mealie": "Search Mealie"
|
||||
},
|
||||
"settings": {
|
||||
"color": "Couleur",
|
||||
"swatches": "Echantillons",
|
||||
"add-a-new-theme": "Ajouter un nouveau thème",
|
||||
"set-new-time": "Définir une nouvelle heure d'exécution",
|
||||
"current": "Version :",
|
||||
|
@ -79,9 +74,6 @@
|
|||
"contribute": "Contribuer",
|
||||
"backup-and-exports": "Sauver et exporter",
|
||||
"backup-info": "Les sauvegardes sont exportées en format JSON standard, ainsi que toutes les images stockées sur le système. Dans votre dossier de sauvegarde, vous trouverez un dossier .zip qui contient toutes les recettes en JSON et les images de la base de données. De plus, si vous avez sélectionné le format de fichier markdown, il sera sauvegardé dans le même dossier .zip. Pour importer une sauvegarde, celle-ci doit être enregistrée dans votre dossier de sauvegardes. Une sauvegarde automatique est effectuée quotidiennement à 03h00.",
|
||||
"backup-tag": "Tags de la sauvegarde",
|
||||
"markdown-template": "Modèle markdown",
|
||||
"backup-recipes": "Sauvegarder les recettes",
|
||||
"theme": {
|
||||
"theme-settings": "Paramètres du thème",
|
||||
"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": "Sélectionnez un thème depuis la liste ou créez-en un nouveau. Le thème par défaut sera utilisé pour tous les utilisateurs qui n'ont pas choisi de thème personnalisé.",
|
||||
|
@ -100,15 +92,13 @@
|
|||
"saved-color-theme": "Thèmes sauvegardés",
|
||||
"delete-theme": "Supprimer le thème",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "Etes-vous sûr(e) de vouloir supprimer ce thème ?",
|
||||
"save-colors-and-apply-theme": "Sauvegarder les couleurs et appliquer le thème",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Personnalisez l'apparence de Mealie. Utilisez le thème par défaut de votre système ou choisissez manuellement entre le thème clair ou sombre."
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "Webhooks du planificateur de repas",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Les liens dans cette liste recevront les webhooks contenant les recettes pour le plan de menu du jour défini. Actuellement, les webhooks s'executeront à <strong>{ time }</strong>",
|
||||
"test-webhooks": "Tester les webhooks",
|
||||
"webhook-url": "Lien du webhook",
|
||||
"save-webhooks": "Enregistrer les webhooks"
|
||||
"webhook-url": "Lien du webhook"
|
||||
},
|
||||
"new-version-available": "Une nouvelle version de Mealie est disponible, <a {aContents}> vérifiez la source ! </a>",
|
||||
"backup": {
|
||||
|
@ -119,16 +109,6 @@
|
|||
},
|
||||
"migration": {
|
||||
"recipe-migration": "Migrer les recettes",
|
||||
"currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration": "Pour le moment, le seul type de migration supporté est Chowdown via un dépôt public.",
|
||||
"chowdown-repo-url": "Lien du dépôt Chowdown",
|
||||
"migrate": "Migrer",
|
||||
"failed-recipes": "Recettes échouées",
|
||||
"failed-images": "Images échouées",
|
||||
"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": "Vous pouvez importer des recettes, soit depuis un dossier zip ou depuis un dossier enregistré directement dans le dossier /app/data/migraiton/. Veuillez vérifier dans la documentation que la structure de votre dossier corresponde à celle qui est attendue.",
|
||||
"nextcloud-data": "Données Nextcloud",
|
||||
"delete-confirmation": "Etes-vous sûr(e) de vouloir supprimer ces données de migration ?",
|
||||
"successfully-imported-from-nextcloud": "Importation de Nexcloud réussie",
|
||||
"failed-imports": "Importations échouées",
|
||||
"upload-an-archive": "Téléverser une archive"
|
||||
"failed-imports": "Importations échouées"
|
||||
}
|
||||
}
|
||||
|
|
116
frontend/src/locales/sv.json
Normal file
116
frontend/src/locales/sv.json
Normal file
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"404": {
|
||||
"page-not-found": "404 sidan kan inte hittas",
|
||||
"take-me-home": "Ta mig hem"
|
||||
},
|
||||
"new-recipe": {
|
||||
"from-url": "Från länk",
|
||||
"recipe-url": "Recept URL",
|
||||
"error-message": "Ett fel uppstod när receptet skulle läsas in. Undersök loggen och debug/last_recipe.json för att felsöka problemet.",
|
||||
"bulk-add": "Lägg till flera",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Klistra in din receptdata, varje rad kommer att hanteras som ett listelement"
|
||||
},
|
||||
"general": {
|
||||
"submit": "Skicka",
|
||||
"name": "Namn",
|
||||
"settings": "Inställningar",
|
||||
"cancel": "Avbryt",
|
||||
"close": "Stäng",
|
||||
"create": "Skapa",
|
||||
"delete": "Ta bort",
|
||||
"edit": "Redigera",
|
||||
"enabled": "Aktiverad",
|
||||
"image-file": "Bildfil",
|
||||
"new": "Ny",
|
||||
"ok": "Ok",
|
||||
"random": "Slumpa",
|
||||
"save": "Spara",
|
||||
"select": "Välj",
|
||||
"update": "Uppdatera",
|
||||
"download": "Ladda ner",
|
||||
"import": "Importera"
|
||||
},
|
||||
"login": {
|
||||
"email": "E-mail",
|
||||
"password": "Lösenord",
|
||||
"sign-in": "Logga in",
|
||||
"sign-up": "Logga ut",
|
||||
"stay-logged-in": "Kom ihåg mig"
|
||||
},
|
||||
"meal-plan": {
|
||||
"dinner-this-week": "Veckans middagar",
|
||||
"dinner-today": "Middag idag",
|
||||
"planner": "Planeringkalender",
|
||||
"create-a-new-meal-plan": "Skapa en ny måltidsplan",
|
||||
"edit-meal-plan": "Redigera måltidsplan",
|
||||
"end-date": "Slutdatum",
|
||||
"meal-plans": "Måltidsplaner",
|
||||
"start-date": "Startdatum"
|
||||
},
|
||||
"recipe": {
|
||||
"description": "Beskrivning",
|
||||
"categories": "Kategorier",
|
||||
"ingredient": "Ingrediens",
|
||||
"ingredients": "Ingredienser",
|
||||
"instructions": "Instruktioner",
|
||||
"note": "Anteckning",
|
||||
"notes": "Anteckningar",
|
||||
"original-url": "Originalrecept",
|
||||
"recipe-name": "Receptets namn",
|
||||
"servings": "Portioner",
|
||||
"step-index": "Steg: {step}",
|
||||
"tags": "Taggar",
|
||||
"view-recipe": "Visa recept"
|
||||
},
|
||||
"search": {
|
||||
"search-mealie": "Search Mealie"
|
||||
},
|
||||
"settings": {
|
||||
"add-a-new-theme": "Lägg till ett nytt tema",
|
||||
"set-new-time": "Välj ny tid",
|
||||
"current": "Version:",
|
||||
"latest": "Senaste",
|
||||
"explore-the-docs": "Utforska dokumentationen",
|
||||
"contribute": "Bidra",
|
||||
"backup-and-exports": "Backups",
|
||||
"backup-info": "Säkerhetskopior exporteras i JSON-format tillsammans med de bilder som finns i systemet. I din mapp för säkerhetskopior finner du en zip-fil som innehåller alla recept i JSON samt bilder från databasen. Om du dessutom valde att exportera till markdown så hittas också de i samma zip-fil. För att importera en säkerhetskopia så måste den ligga i din backup-mapp. Automatisk säkerhetskopiering genomförs varje dag kl. 03:00.",
|
||||
"theme": {
|
||||
"theme-settings": "Temainställningar",
|
||||
"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": "Välj ett tema från menyn eller skapa ett nytt. Standardtemat kommer att användas för alla användare som inte gjort något val.",
|
||||
"dark-mode": "Mörkt läge",
|
||||
"theme-is-required": "Tema krävs",
|
||||
"primary": "Primär",
|
||||
"secondary": "Sekundär",
|
||||
"accent": "Accent",
|
||||
"success": "Success",
|
||||
"info": "Info",
|
||||
"warning": "Varning",
|
||||
"error": "Error",
|
||||
"light": "Ljust",
|
||||
"dark": "Mörkt",
|
||||
"theme": "Tema",
|
||||
"saved-color-theme": "Sparat färgschema",
|
||||
"delete-theme": "Radera tema",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "Är du säker på att du vill radera temat?",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Välj hur Mealie ska se ut för dig. Låt Mealie följa dina systeminställningar, eller välj mörkt eller ljust tema."
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "Webhooks för denna måltidsplan",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Följande URLer kommer att mottaga webhooks med receptdata för dagens planerade måltid. Datan kommer att skickas klockan <strong>{ time }</strong>",
|
||||
"test-webhooks": "Testa Webhooks",
|
||||
"webhook-url": "Webhook URL"
|
||||
},
|
||||
"new-version-available": "En ny version av Mealie finns tillgänglig, <a {aContents}> Besök repot </a>",
|
||||
"backup": {
|
||||
"import-recipes": "Importera recept",
|
||||
"import-themes": "Importera färgscheman",
|
||||
"import-settings": "Importera recept",
|
||||
"create-heading": "Skapa en säkerhetskopia",
|
||||
"backup-tag": "Backup tagg"
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"recipe-migration": "Migrera recept",
|
||||
"failed-imports": "Misslyckade importer"
|
||||
}
|
||||
}
|
158
frontend/src/locales/zh-CN.json
Normal file
158
frontend/src/locales/zh-CN.json
Normal file
|
@ -0,0 +1,158 @@
|
|||
{
|
||||
"404": {
|
||||
"page-not-found": "404页面不存在",
|
||||
"take-me-home": "返回主页"
|
||||
},
|
||||
"new-recipe": {
|
||||
"from-url": "输入网址",
|
||||
"recipe-url": "食谱网址",
|
||||
"error-message": "貌似在解析网址时出错。请检查log和debug/last_recipe.json文件并找寻更多有关资讯。",
|
||||
"bulk-add": "批量添加",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "请粘贴您的食谱资料。每行将被视为列表中的一项。"
|
||||
},
|
||||
"general": {
|
||||
"upload": "上传",
|
||||
"submit": "提交",
|
||||
"name": "名称",
|
||||
"settings": "设定",
|
||||
"close": "关闭",
|
||||
"save": "保存",
|
||||
"image-file": "图像文件",
|
||||
"update": "更新",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"select": "选择",
|
||||
"random": "随机",
|
||||
"new": "新建",
|
||||
"create": "创建",
|
||||
"cancel": "取消",
|
||||
"ok": "好的",
|
||||
"enabled": "启用",
|
||||
"download": "下载",
|
||||
"import": "导入",
|
||||
"options": "选项",
|
||||
"templates": "模板",
|
||||
"recipes": "食谱",
|
||||
"themes": "布景主题",
|
||||
"confirm": "确定"
|
||||
},
|
||||
"login": {
|
||||
"stay-logged-in": "保持登录状态?",
|
||||
"email": "电子邮件",
|
||||
"password": "密码",
|
||||
"sign-in": "登入",
|
||||
"sign-up": "注册"
|
||||
},
|
||||
"meal-plan": {
|
||||
"dinner-this-week": "本周晚餐",
|
||||
"dinner-today": "今日晚餐",
|
||||
"planner": "策划人",
|
||||
"edit-meal-plan": "编辑用餐计划",
|
||||
"meal-plans": "用餐计划",
|
||||
"create-a-new-meal-plan": "创建一个新的用餐计划",
|
||||
"start-date": "开始日期",
|
||||
"end-date": "结束日期"
|
||||
},
|
||||
"recipe": {
|
||||
"description": "描述",
|
||||
"ingredients": "材料",
|
||||
"categories": "分类目录",
|
||||
"tags": "标签",
|
||||
"instructions": "做法",
|
||||
"step-index": "步骤:{step}",
|
||||
"recipe-name": "食谱名称",
|
||||
"servings": "份量",
|
||||
"ingredient": "材料",
|
||||
"notes": "贴士",
|
||||
"note": "贴士",
|
||||
"original-url": "原食谱链接",
|
||||
"view-recipe": "查看食谱",
|
||||
"add-key": "Add Key",
|
||||
"api-extras": "API Extras",
|
||||
"delete-confirmation": "您确定要删除此食谱吗?",
|
||||
"delete-recipe": "删除食谱",
|
||||
"key-name-required": "Key Name Required",
|
||||
"new-key-name": "New Key Name",
|
||||
"no-white-space-allowed": "No White Space Allowed",
|
||||
"object-key": "Object Key",
|
||||
"object-value": "Object Value",
|
||||
"perform-time": "烹饪时间 / 执行时间",
|
||||
"prep-time": "准备时间",
|
||||
"title": "标题",
|
||||
"total-time": "总时间"
|
||||
},
|
||||
"search": {
|
||||
"search-mealie": "搜索Mealie"
|
||||
},
|
||||
"settings": {
|
||||
"add-a-new-theme": "新增布景主题",
|
||||
"set-new-time": "设定新的时间",
|
||||
"current": "版本号:",
|
||||
"latest": "最新版本:",
|
||||
"explore-the-docs": "浏览文档",
|
||||
"contribute": "参与贡献",
|
||||
"backup-and-exports": "备份",
|
||||
"backup-info": "备份以标准JSON格式导出,并连同储存在系统文件中的所有图像。在备份文件夹中,您将找到一个.zip文件,其中包含数据库中的所有食谱JSON和图像。此外,如果您选择了Markdown文件,这些文件也将一并储存在.zip文件中。当需要要导入备份,它必须位于您的备份文件夹中。每天3:00 AM将进行自动备份。",
|
||||
"theme": {
|
||||
"theme-settings": "布景主题设置",
|
||||
"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": "从以下列表中选择一个主题或创建一个新主题。请注意,默认主题将提供给尚未设置主题首选的所有用户。",
|
||||
"dark-mode": "暗黑模式",
|
||||
"theme-is-required": "必须选择主题",
|
||||
"primary": "主要(Primary)",
|
||||
"secondary": "次要(Secondary)",
|
||||
"accent": "强调(Accent)",
|
||||
"success": "成功(Success)",
|
||||
"info": "信息(Info)",
|
||||
"warning": "警告(Warning)",
|
||||
"error": "错误(Error)",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"theme": "布景主题",
|
||||
"saved-color-theme": "已保存主题色调",
|
||||
"delete-theme": "删除主题",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "您确定要删除此主题吗?",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "选择Mealie的外观模式。设置布景主题首选并依据您的主机系统设置,或者选择使用浅色或深色主题。",
|
||||
"default-to-system": "默认为系统",
|
||||
"theme-name": "主题名称",
|
||||
"theme-name-is-required": "主题名称是必填项。"
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "用餐计划器Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "下方列出的网址将在预定日期接收到有关用餐计划的食谱资料。Webhooks将在<strong>{ time }</strong>执行",
|
||||
"test-webhooks": "测试Webhooks",
|
||||
"webhook-url": "Webhook网址"
|
||||
},
|
||||
"new-version-available": "检测到Mealie最新版本出现,<a {aContents}>浏览仓库</a>",
|
||||
"backup": {
|
||||
"import-recipes": "导入食谱",
|
||||
"import-themes": "导入主题",
|
||||
"import-settings": "导入设置",
|
||||
"create-heading": "创建备份",
|
||||
"backup-tag": "标签备份",
|
||||
"backup-restore-report": "备份还原报告",
|
||||
"failed-imports": "导入失败",
|
||||
"full-backup": "完整备份",
|
||||
"partial-backup": "部分备份",
|
||||
"successfully-imported": "成功导入"
|
||||
},
|
||||
"available-backups": "可用备份",
|
||||
"general-settings": "基本设置",
|
||||
"language": "语言",
|
||||
"local-api": "Local API"
|
||||
},
|
||||
"migration": {
|
||||
"recipe-migration": "食谱迁移",
|
||||
"failed-imports": "导入失败",
|
||||
"chowdown": {
|
||||
"description": "从Chowdown迁移数据",
|
||||
"title": "Chowdown"
|
||||
},
|
||||
"migration-report": "迁移报告",
|
||||
"nextcloud": {
|
||||
"description": "从Nextcloud Cookbook迁移数据",
|
||||
"title": "Nextcloud Cookbook"
|
||||
},
|
||||
"no-migration-data-available": "没有迁移数据可用",
|
||||
"successful-imports": "成功导入"
|
||||
}
|
||||
}
|
158
frontend/src/locales/zh-TW.json
Normal file
158
frontend/src/locales/zh-TW.json
Normal file
|
@ -0,0 +1,158 @@
|
|||
{
|
||||
"404": {
|
||||
"page-not-found": "404頁面不存在",
|
||||
"take-me-home": "返回主頁"
|
||||
},
|
||||
"new-recipe": {
|
||||
"from-url": "輸入網址",
|
||||
"recipe-url": "食譜網址",
|
||||
"error-message": "貌似在解析網址時出錯。請檢查log和debug/last_recipe.json文件並找尋更多有關資訊。",
|
||||
"bulk-add": "批量添加",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "請粘貼您的食譜資料。每行將被視為列表中的一項。"
|
||||
},
|
||||
"general": {
|
||||
"upload": "上傳",
|
||||
"submit": "提交",
|
||||
"name": "名稱",
|
||||
"settings": "設定",
|
||||
"close": "關閉",
|
||||
"save": "保存",
|
||||
"image-file": "圖像文件",
|
||||
"update": "更新",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"select": "選擇",
|
||||
"random": "隨機",
|
||||
"new": "新建",
|
||||
"create": "創建",
|
||||
"cancel": "取消",
|
||||
"ok": "好的",
|
||||
"enabled": "启用",
|
||||
"download": "下载",
|
||||
"import": "導入",
|
||||
"options": "選項",
|
||||
"templates": "模板",
|
||||
"recipes": "食譜",
|
||||
"themes": "佈景主題",
|
||||
"confirm": "確定"
|
||||
},
|
||||
"login": {
|
||||
"stay-logged-in": "保持登錄狀態?",
|
||||
"email": "電子郵件",
|
||||
"password": "密碼",
|
||||
"sign-in": "登入",
|
||||
"sign-up": "註冊"
|
||||
},
|
||||
"meal-plan": {
|
||||
"dinner-this-week": "本週晚餐",
|
||||
"dinner-today": "今日晚餐",
|
||||
"planner": "策劃人",
|
||||
"edit-meal-plan": "編輯用餐計劃",
|
||||
"meal-plans": "用餐計劃",
|
||||
"create-a-new-meal-plan": "創建一個新的用餐計劃",
|
||||
"start-date": "開始日期",
|
||||
"end-date": "結束日期"
|
||||
},
|
||||
"recipe": {
|
||||
"description": "描述",
|
||||
"ingredients": "材料",
|
||||
"categories": "分類目錄",
|
||||
"tags": "標籤",
|
||||
"instructions": "做法",
|
||||
"step-index": "步驟:{step}",
|
||||
"recipe-name": "食譜名稱",
|
||||
"servings": "份量",
|
||||
"ingredient": "材料",
|
||||
"notes": "貼士",
|
||||
"note": "貼士",
|
||||
"original-url": "原食譜鏈接",
|
||||
"view-recipe": "查看食譜",
|
||||
"add-key": "Add Key",
|
||||
"api-extras": "API Extras",
|
||||
"delete-confirmation": "您確定要刪除此食譜嗎?",
|
||||
"delete-recipe": "刪除食譜",
|
||||
"key-name-required": "Key Name Required",
|
||||
"new-key-name": "New Key Name",
|
||||
"no-white-space-allowed": "No White Space Allowed",
|
||||
"object-key": "Object Key",
|
||||
"object-value": "Object Value",
|
||||
"perform-time": "烹飪時間 / 執行時間",
|
||||
"prep-time": "準備時間",
|
||||
"title": "標題",
|
||||
"total-time": "總時間"
|
||||
},
|
||||
"search": {
|
||||
"search-mealie": "搜索Mealie"
|
||||
},
|
||||
"settings": {
|
||||
"add-a-new-theme": "新增佈景主題",
|
||||
"set-new-time": "設定新的時間",
|
||||
"current": "版本號:",
|
||||
"latest": "最新版本:",
|
||||
"explore-the-docs": "瀏覽文檔",
|
||||
"contribute": "參與貢獻",
|
||||
"backup-and-exports": "備份",
|
||||
"backup-info": "備份以標準JSON格式導出,並連同儲存在系統文件中的所有圖像。在備份文件夾中,您將找到一個.zip文件,其中包含數據庫中的所有食譜JSON和圖像。此外,如果您選擇了Markdown文件,這些文件也將一併儲存在.zip文件中。當需要要導入備份,它必須位於您的備份文件夾中。每天3:00 AM將進行自動備份。",
|
||||
"theme": {
|
||||
"theme-settings": "佈景主題設置",
|
||||
"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": "從以下列表中選擇一個主題或創建一個新主題。請注意,默認主題將提供給尚未設置主題首選的所有用戶。",
|
||||
"dark-mode": "暗黑模式",
|
||||
"theme-is-required": "必須選擇主題",
|
||||
"primary": "主要(Primary)",
|
||||
"secondary": "次要(Secondary)",
|
||||
"accent": "強調(Accent)",
|
||||
"success": "成功(Success)",
|
||||
"info": "信息(Info)",
|
||||
"warning": "警告(Warning)",
|
||||
"error": "錯誤(Error)",
|
||||
"light": "淺色",
|
||||
"dark": "深色",
|
||||
"theme": "佈景主題",
|
||||
"saved-color-theme": "已保存主題色調",
|
||||
"delete-theme": "刪除主題",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "您確定要刪除此主題嗎?",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "選擇Mealie的外觀模式。設置佈景主題首選並依據您的主機系統設置,或者選擇使用淺色或深色主題。",
|
||||
"default-to-system": "默認爲系統",
|
||||
"theme-name": "主題名稱",
|
||||
"theme-name-is-required": "主題名稱是必填項。"
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "用餐計劃器Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "下方列出的網址將在預定日期接收到有關用餐計劃的食譜資料。Webhooks將在<strong>{ time }</strong>執行",
|
||||
"test-webhooks": "測試Webhooks",
|
||||
"webhook-url": "Webhook網址"
|
||||
},
|
||||
"new-version-available": "檢測到Mealie最新版本出現,<a {aContents}>瀏覽倉庫</a>",
|
||||
"backup": {
|
||||
"import-recipes": "導入食譜",
|
||||
"import-themes": "導入主題",
|
||||
"import-settings": "導入設置",
|
||||
"create-heading": "創建備份",
|
||||
"backup-tag": "標籤備份",
|
||||
"backup-restore-report": "備份還原報告",
|
||||
"failed-imports": "導入失敗",
|
||||
"full-backup": "完整備份",
|
||||
"partial-backup": "部分備份",
|
||||
"successfully-imported": "成功導入"
|
||||
},
|
||||
"available-backups": "可用備份",
|
||||
"general-settings": "基本設置",
|
||||
"language": "語言",
|
||||
"local-api": "Local API"
|
||||
},
|
||||
"migration": {
|
||||
"recipe-migration": "食譜遷移",
|
||||
"failed-imports": "導入失敗",
|
||||
"chowdown": {
|
||||
"description": "從Chowdown遷移數據",
|
||||
"title": "Chowdown"
|
||||
},
|
||||
"migration-report": "遷移報告",
|
||||
"nextcloud": {
|
||||
"description": "從Nextcloud Cookbook遷移數據",
|
||||
"title": "Nextcloud Cookbook"
|
||||
},
|
||||
"no-migration-data-available": "無遷移數據可用",
|
||||
"successful-imports": "成功導入"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -8,13 +8,25 @@ const state = {
|
|||
value: "en",
|
||||
},
|
||||
{
|
||||
name: "Dutch",
|
||||
name: "Danish",
|
||||
value: "da",
|
||||
},
|
||||
{
|
||||
name: "French",
|
||||
value: "fr",
|
||||
},
|
||||
{
|
||||
name: "Swedish",
|
||||
value: "sv",
|
||||
},
|
||||
{
|
||||
name: "简体中文",
|
||||
value: "zh-CN",
|
||||
},
|
||||
{
|
||||
name: "繁體中文",
|
||||
value: "zh-TW",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from fastapi import FastAPI
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
# import utils.startup as startup
|
||||
from app_config import PORT, PRODUCTION, SQLITE_FILE, WEB_PATH, docs_url, redoc_url
|
||||
from app_config import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
||||
from routes import (
|
||||
backup_routes,
|
||||
meal_routes,
|
||||
|
@ -14,7 +14,7 @@ from routes import (
|
|||
user_routes,
|
||||
)
|
||||
|
||||
# from utils.api_docs import generate_api_docs
|
||||
from utils.api_docs import generate_api_docs
|
||||
from utils.logger import logger
|
||||
|
||||
app = FastAPI(
|
||||
|
@ -26,13 +26,13 @@ app = FastAPI(
|
|||
)
|
||||
|
||||
|
||||
|
||||
def mount_static_files():
|
||||
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
||||
|
||||
|
||||
def api_routers():
|
||||
# First
|
||||
print()
|
||||
app.include_router(recipe_routes.router)
|
||||
app.include_router(meal_routes.router)
|
||||
app.include_router(setting_routes.router)
|
||||
|
@ -46,6 +46,11 @@ if PRODUCTION:
|
|||
|
||||
api_routers()
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
import services.scheduler.scheduled_jobs
|
||||
|
||||
|
||||
# API 404 Catch all CALL AFTER ROUTERS
|
||||
@app.get("/api/{full_path:path}", status_code=404, include_in_schema=False)
|
||||
def invalid_api():
|
||||
|
@ -56,8 +61,10 @@ app.include_router(static_routes.router)
|
|||
|
||||
|
||||
# Generate API Documentation
|
||||
# if not PRODUCTION:
|
||||
# generate_api_docs(app)
|
||||
if not PRODUCTION:
|
||||
generate_api_docs(app)
|
||||
|
||||
start_scheduler()
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("-----SYSTEM STARTUP-----")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.sql.meal_models import MealPlanModel
|
||||
from db.sql.recipe_models import RecipeModel
|
||||
|
@ -16,8 +18,12 @@ class _Recipes(BaseDocument):
|
|||
self.primary_key = "slug"
|
||||
self.sql_model = RecipeModel
|
||||
|
||||
def update_image(self, slug: str, extension: str) -> None:
|
||||
pass
|
||||
def update_image(self, session: Session, slug: str, extension: str) -> str:
|
||||
entry = self._query_one(session, match_value=slug)
|
||||
entry.image = f"{slug}.{extension}"
|
||||
session.commit()
|
||||
|
||||
return f"{slug}.{extension}"
|
||||
|
||||
|
||||
class _Meals(BaseDocument):
|
||||
|
@ -31,7 +37,7 @@ class _Settings(BaseDocument):
|
|||
self.primary_key = "name"
|
||||
self.sql_model = SiteSettingsModel
|
||||
|
||||
def save_new(self, session, main: dict, webhooks: dict) -> str:
|
||||
def save_new(self, session: Session, main: dict, webhooks: dict) -> str:
|
||||
new_settings = self.sql_model(main.get("name"), webhooks)
|
||||
|
||||
session.add(new_settings)
|
||||
|
@ -45,14 +51,6 @@ class _Themes(BaseDocument):
|
|||
self.primary_key = "name"
|
||||
self.sql_model = SiteThemeModel
|
||||
|
||||
def update(self, session, data: dict) -> dict:
|
||||
theme_model = self._query_one(
|
||||
session=session, match_value=data["name"], match_key="name"
|
||||
)
|
||||
|
||||
theme_model.update(**data)
|
||||
session.commit()
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Union
|
||||
from typing import List, Union
|
||||
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
@ -11,7 +11,10 @@ class BaseDocument:
|
|||
self.store: str
|
||||
self.sql_model: SqlAlchemyBase
|
||||
|
||||
def get_all(self, session: Session, limit: int = None, order_by: str = None):
|
||||
# TODO: Improve Get All Query Functionality
|
||||
def get_all(
|
||||
self, session: Session, limit: int = None, order_by: str = None
|
||||
) -> List[dict]:
|
||||
list = [x.dict() for x in session.query(self.sql_model).all()]
|
||||
|
||||
if limit == 1:
|
||||
|
@ -21,7 +24,7 @@ class BaseDocument:
|
|||
|
||||
def _query_one(
|
||||
self, session: Session, match_value: str, match_key: str = None
|
||||
) -> Union[Session, SqlAlchemyBase]:
|
||||
) -> SqlAlchemyBase:
|
||||
"""Query the sql database for one item an return the sql alchemy model
|
||||
object. If no match key is provided the primary_key attribute will be used.
|
||||
|
||||
|
@ -43,7 +46,7 @@ class BaseDocument:
|
|||
|
||||
def get(
|
||||
self, session: Session, match_value: str, match_key: str = None, limit=1
|
||||
) -> dict or list[dict]:
|
||||
) -> dict or List[dict]:
|
||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||
key is provided the class objects primary key will be used to match against.
|
||||
|
||||
|
@ -67,6 +70,15 @@ class BaseDocument:
|
|||
return db_entry
|
||||
|
||||
def save_new(self, session: Session, document: dict) -> dict:
|
||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||
|
||||
Args:
|
||||
session (Session): A Database Session
|
||||
document (dict): A python dictionary representing the data structure
|
||||
|
||||
Returns:
|
||||
dict: A dictionary representation of the database entry
|
||||
"""
|
||||
new_document = self.sql_model(**document)
|
||||
session.add(new_document)
|
||||
return_data = new_document.dict()
|
||||
|
@ -74,7 +86,18 @@ class BaseDocument:
|
|||
|
||||
return return_data
|
||||
|
||||
def update(self, session: Session, match_value, new_data) -> dict:
|
||||
def update(self, session: Session, match_value: str, new_data: str) -> dict:
|
||||
"""Update a database entry.
|
||||
|
||||
Args:
|
||||
session (Session): Database Session
|
||||
match_value (str): Match "key"
|
||||
new_data (str): Match "value"
|
||||
|
||||
Returns:
|
||||
dict: Returns a dictionary representation of the database entry
|
||||
"""
|
||||
|
||||
entry = self._query_one(session=session, match_value=match_value)
|
||||
entry.update(session=session, **new_data)
|
||||
return_data = entry.dict()
|
||||
|
|
|
@ -12,7 +12,7 @@ class SiteThemeModel(SqlAlchemyBase):
|
|||
self.name = name
|
||||
self.colors = ThemeColorsModel(**colors)
|
||||
|
||||
def update(self, name, colors: dict) -> dict:
|
||||
def update(self, session=None, name: str = None, colors: dict = None) -> dict:
|
||||
self.colors.update(**colors)
|
||||
return self.dict()
|
||||
|
||||
|
|
|
@ -22,13 +22,13 @@ class BackupOptions(BaseModel):
|
|||
class BackupJob(BaseModel):
|
||||
tag: Optional[str]
|
||||
options: BackupOptions
|
||||
templates: Optional[List[str]] = []
|
||||
templates: Optional[List[str]]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"tag": "July 23rd 2021",
|
||||
"options": BackupOptions,
|
||||
"options": BackupOptions(),
|
||||
"template": ["recipes.md"],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,6 @@ def get_recipe_img(recipe_slug: str):
|
|||
return FileResponse(recipe_image)
|
||||
|
||||
|
||||
# Recipe Creations
|
||||
@router.post(
|
||||
"/api/recipe/create-url/",
|
||||
status_code=201,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from services.scheduler_services import post_webhooks
|
||||
from services.settings_services import SiteSettings, SiteTheme
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.global_scheduler import scheduler
|
||||
from utils.post_webhooks import post_webhooks
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(tags=["Settings"])
|
||||
|
@ -34,7 +33,6 @@ def update_settings(data: SiteSettings, db: Session = Depends(generate_session))
|
|||
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
|
||||
# )
|
||||
|
||||
# scheduler.reschedule_webhooks() #! Need to fix Scheduler
|
||||
return SnackResponse.success("Settings Updated")
|
||||
|
||||
|
||||
|
|
|
@ -82,15 +82,6 @@ class Recipe(BaseModel):
|
|||
slug = calc_slug
|
||||
return slug
|
||||
|
||||
@classmethod
|
||||
def _unpack_doc(cls, document):
|
||||
document = json.loads(document.to_json())
|
||||
del document["_id"]
|
||||
|
||||
document["dateAdded"] = document["dateAdded"]["$date"]
|
||||
|
||||
return cls(**document)
|
||||
|
||||
@classmethod
|
||||
def get_by_slug(cls, session, slug: str):
|
||||
""" Returns a Recipe Object by Slug """
|
||||
|
@ -132,8 +123,15 @@ class Recipe(BaseModel):
|
|||
return updated_slug.get("slug")
|
||||
|
||||
@staticmethod
|
||||
def update_image(slug: str, extension: str):
|
||||
db.recipes.update_image(slug, extension)
|
||||
def update_image(slug: str, extension: str) -> str:
|
||||
"""A helper function to pass the new image name and extension
|
||||
into the database.
|
||||
|
||||
Args:
|
||||
slug (str): The current recipe slug
|
||||
extension (str): the file extension of the new image
|
||||
"""
|
||||
return db.recipes.update_image(slug, extension)
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session):
|
||||
|
|
3
mealie/services/scheduler/global_scheduler.py
Normal file
3
mealie/services/scheduler/global_scheduler.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
scheduler = BackgroundScheduler()
|
62
mealie/services/scheduler/scheduled_jobs.py
Normal file
62
mealie/services/scheduler/scheduled_jobs.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from db.db_setup import create_session
|
||||
from services.backups.exports import auto_backup_job
|
||||
from services.scheduler.global_scheduler import scheduler
|
||||
from services.scheduler.scheduler_utils import Cron, cron_parser
|
||||
from services.settings_services import SiteSettings
|
||||
from utils.logger import logger
|
||||
from utils.post_webhooks import post_webhooks
|
||||
|
||||
|
||||
@scheduler.scheduled_job(trigger="interval", minutes=15)
|
||||
def update_webhook_schedule():
|
||||
"""
|
||||
A scheduled background job that runs every 15 minutes to
|
||||
poll the database for changes and reschedule the webhook time
|
||||
"""
|
||||
session = create_session()
|
||||
settings = SiteSettings.get_site_settings(session=session)
|
||||
time = cron_parser(settings.webhooks.webhookTime)
|
||||
job = JOB_STORE.get("webhooks")
|
||||
|
||||
scheduler.reschedule_job(
|
||||
job.scheduled_task.id,
|
||||
trigger="cron",
|
||||
hour=time.hours,
|
||||
minute=time.minutes,
|
||||
)
|
||||
|
||||
session.close()
|
||||
logger.info(scheduler.print_jobs())
|
||||
|
||||
|
||||
class ScheduledFunction:
|
||||
def __init__(
|
||||
self, scheduler: BackgroundScheduler, function, cron: Cron, name: str
|
||||
) -> None:
|
||||
self.scheduled_task = scheduler.add_job(
|
||||
function,
|
||||
trigger="cron",
|
||||
name=name,
|
||||
hour=cron.hours,
|
||||
minute=cron.minutes,
|
||||
max_instances=1,
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
logger.info("New Function Scheduled")
|
||||
logger.info(scheduler.print_jobs())
|
||||
|
||||
|
||||
logger.info("----INIT SCHEDULE OBJECT-----")
|
||||
|
||||
JOB_STORE = {
|
||||
"backup_job": ScheduledFunction(
|
||||
scheduler, auto_backup_job, Cron(hours=00, minutes=00), "backups"
|
||||
),
|
||||
"webhooks": ScheduledFunction(
|
||||
scheduler, post_webhooks, Cron(hours=00, minutes=00), "webhooks"
|
||||
),
|
||||
}
|
||||
|
||||
scheduler.start()
|
10
mealie/services/scheduler/scheduler_utils.py
Normal file
10
mealie/services/scheduler/scheduler_utils.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
import collections
|
||||
|
||||
Cron = collections.namedtuple("Cron", "hours minutes")
|
||||
|
||||
|
||||
def cron_parser(time_str: str) -> Cron:
|
||||
time = time_str.split(":")
|
||||
cron = Cron(hours=int(time[0]), minutes=int(time[1]))
|
||||
|
||||
return cron
|
|
@ -1,73 +0,0 @@
|
|||
import collections
|
||||
import json
|
||||
|
||||
import requests
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from db.db_setup import create_session
|
||||
from utils.logger import logger
|
||||
|
||||
from services.backups.exports import auto_backup_job
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings
|
||||
|
||||
Cron = collections.namedtuple("Cron", "hours minutes")
|
||||
|
||||
|
||||
def cron_parser(time_str: str) -> Cron:
|
||||
time = time_str.split(":")
|
||||
cron = Cron(hours=int(time[0]), minutes=int(time[1]))
|
||||
|
||||
return cron
|
||||
|
||||
|
||||
def post_webhooks():
|
||||
all_settings = SiteSettings.get_site_settings()
|
||||
|
||||
if all_settings.webhooks.enabled:
|
||||
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()
|
||||
urls = all_settings.webhooks.webhookURLs
|
||||
|
||||
for url in urls:
|
||||
requests.post(url, json.dumps(todays_meal, default=str))
|
||||
|
||||
|
||||
class Scheduler:
|
||||
def startup_scheduler(self):
|
||||
self.scheduler = BackgroundScheduler()
|
||||
logger.info("----INIT SCHEDULE OBJECT-----")
|
||||
self.scheduler.start()
|
||||
|
||||
self.scheduler.add_job(
|
||||
auto_backup_job, trigger="cron", hour="3", max_instances=1
|
||||
)
|
||||
settings = SiteSettings.get_site_settings(create_session())
|
||||
time = cron_parser(settings.webhooks.webhookTime)
|
||||
|
||||
self.webhook = self.scheduler.add_job(
|
||||
post_webhooks,
|
||||
trigger="cron",
|
||||
name="webhooks",
|
||||
hour=time.hours,
|
||||
minute=time.minutes,
|
||||
max_instances=1,
|
||||
)
|
||||
|
||||
logger.info(self.scheduler.print_jobs())
|
||||
|
||||
def reschedule_webhooks(self):
|
||||
"""
|
||||
Reads the site settings database entry to reschedule the webhooks task
|
||||
Called after each post to the webhooks endpoint.
|
||||
"""
|
||||
settings = SiteSettings.get_site_settings()
|
||||
time = cron_parser(settings.webhooks.webhookTime)
|
||||
|
||||
self.scheduler.reschedule_job(
|
||||
self.webhook.id,
|
||||
trigger="cron",
|
||||
hour=time.hours,
|
||||
minute=time.minutes,
|
||||
)
|
||||
|
||||
logger.info(self.scheduler.print_jobs())
|
|
@ -5,6 +5,7 @@ from typing import List, Tuple
|
|||
import extruct
|
||||
import requests
|
||||
import scrape_schema_recipe
|
||||
import html
|
||||
from app_config import DEBUG_DIR
|
||||
from slugify import slugify
|
||||
from utils.logger import logger
|
||||
|
@ -32,17 +33,17 @@ def normalize_instructions(instructions) -> List[dict]:
|
|||
# One long string split by (possibly multiple) new lines
|
||||
if type(instructions) == str:
|
||||
return [
|
||||
{"text": line.strip()} for line in filter(None, instructions.splitlines())
|
||||
{"text": normalize_instruction(line)} for line in instructions.splitlines() if line
|
||||
]
|
||||
|
||||
# Plain strings in a list
|
||||
elif type(instructions) == list and type(instructions[0]) == str:
|
||||
return [{"text": step.strip()} for step in instructions]
|
||||
return [{"text": normalize_instruction(step)} for step in instructions]
|
||||
|
||||
# Dictionaries (let's assume it's a HowToStep) in a list
|
||||
elif type(instructions) == list and type(instructions[0]) == dict:
|
||||
return [
|
||||
{"text": step["text"].strip()}
|
||||
{"text": normalize_instruction(step["text"])}
|
||||
for step in instructions
|
||||
if step["@type"] == "HowToStep"
|
||||
]
|
||||
|
@ -51,6 +52,14 @@ def normalize_instructions(instructions) -> List[dict]:
|
|||
raise Exception(f"Unrecognised instruction format: {instructions}")
|
||||
|
||||
|
||||
def normalize_instruction(line) -> str:
|
||||
l = line.strip()
|
||||
# Some sites erroneously escape their strings on multiple levels
|
||||
while not l == (l := html.unescape(l)):
|
||||
pass
|
||||
return l
|
||||
|
||||
|
||||
def normalize_yield(yld) -> str:
|
||||
if type(yld) == list:
|
||||
return yld[-1]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import create_session, generate_session, sql_exists
|
||||
from db.db_setup import create_session, sql_exists
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.logger import logger
|
||||
|
@ -103,7 +103,7 @@ class SiteTheme(BaseModel):
|
|||
db.themes.save_new(session, self.dict())
|
||||
|
||||
def update_document(self, session: Session):
|
||||
db.themes.update(session, self.dict())
|
||||
db.themes.update(session, self.name, self.dict())
|
||||
|
||||
@staticmethod
|
||||
def delete_theme(session: Session, theme_name: str) -> str:
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
from pathlib import Path
|
||||
|
||||
from app import app
|
||||
from app_config import SQLITE_DIR
|
||||
from db.db_setup import generate_session, sql_global_init
|
||||
from fastapi.testclient import TestClient
|
||||
from pytest import fixture
|
||||
|
||||
from tests.test_config import TEST_DATA
|
||||
|
||||
SQLITE_FILE = SQLITE_DIR.joinpath("test.db")
|
||||
SQLITE_FILE.unlink(missing_ok=True)
|
||||
|
||||
|
@ -26,3 +30,8 @@ def api_client():
|
|||
yield TestClient(app)
|
||||
|
||||
SQLITE_FILE.unlink()
|
||||
|
||||
|
||||
@fixture(scope="session")
|
||||
def test_image():
|
||||
return TEST_DATA.joinpath("test_image.jpg")
|
||||
|
|
BIN
mealie/tests/data/migrations/chowdown/test_chowdown-gh-pages.zip
Normal file
BIN
mealie/tests/data/migrations/chowdown/test_chowdown-gh-pages.zip
Normal file
Binary file not shown.
Binary file not shown.
BIN
mealie/tests/data/test_image.jpg
Normal file
BIN
mealie/tests/data/test_image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
|
@ -3,25 +3,57 @@ import shutil
|
|||
|
||||
import pytest
|
||||
from app_config import MIGRATION_DIR
|
||||
from tests.test_config import TEST_NEXTCLOUD_DIR
|
||||
|
||||
#! Broken
|
||||
# def test_import_chowdown_recipes(api_client):
|
||||
# response = api_client.post(
|
||||
# "/api/migration/chowdown/repo/",
|
||||
# json={"url": "https://github.com/hay-kot/chowdown"},
|
||||
# )
|
||||
|
||||
# assert response.status_code == 200
|
||||
|
||||
# test_slug = "banana-bread"
|
||||
# response = api_client.get(f"/api/recipe/{test_slug}/")
|
||||
# assert response.status_code == 200
|
||||
|
||||
# recipe = json.loads(response.content)
|
||||
# assert recipe["slug"] == test_slug
|
||||
from tests.test_config import TEST_CHOWDOWN_DIR, TEST_NEXTCLOUD_DIR
|
||||
|
||||
|
||||
### Chowdown
|
||||
@pytest.fixture(scope="session")
|
||||
def chowdown_zip():
|
||||
zip = TEST_CHOWDOWN_DIR.joinpath("test_chowdown-gh-pages.zip")
|
||||
|
||||
zip_copy = TEST_CHOWDOWN_DIR.joinpath("chowdown-gh-pages.zip")
|
||||
|
||||
shutil.copy(zip, zip_copy)
|
||||
|
||||
yield zip_copy
|
||||
|
||||
zip_copy.unlink()
|
||||
|
||||
|
||||
def test_upload_chowdown_zip(api_client, chowdown_zip):
|
||||
|
||||
response = api_client.post(
|
||||
"/api/migrations/chowdown/upload/", files={"archive": chowdown_zip.open("rb")}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert MIGRATION_DIR.joinpath("chowdown", chowdown_zip.name).is_file()
|
||||
|
||||
|
||||
def test_import_chowdown_directory(api_client, chowdown_zip):
|
||||
selection = chowdown_zip.name
|
||||
response = api_client.post(f"/api/migrations/chowdown/{selection}/import/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
report = json.loads(response.content)
|
||||
assert report["failed"] == []
|
||||
|
||||
expected_slug = "roasted-okra"
|
||||
response = api_client.get(f"/api/recipe/{expected_slug}/")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_delete_chowdown_migration_data(api_client, chowdown_zip):
|
||||
selection = chowdown_zip.name
|
||||
response = api_client.delete(f"/api/migrations/chowdown/{selection}/delete/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert not MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
|
||||
|
||||
|
||||
### Nextcloud
|
||||
@pytest.fixture(scope="session")
|
||||
def nextcloud_zip():
|
||||
zip = TEST_NEXTCLOUD_DIR.joinpath("nextcloud.zip")
|
||||
|
@ -30,7 +62,9 @@ def nextcloud_zip():
|
|||
|
||||
shutil.copy(zip, zip_copy)
|
||||
|
||||
return zip_copy
|
||||
yield zip_copy
|
||||
|
||||
zip_copy.unlink()
|
||||
|
||||
|
||||
def test_upload_nextcloud_zip(api_client, nextcloud_zip):
|
||||
|
@ -58,7 +92,7 @@ def test_import_nextcloud_directory(api_client, nextcloud_zip):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_delete_migration_data(api_client, nextcloud_zip):
|
||||
def test_delete__nextcloud_migration_data(api_client, nextcloud_zip):
|
||||
selection = nextcloud_zip.name
|
||||
response = api_client.delete(f"/api/migrations/nextcloud/{selection}/delete/")
|
||||
|
||||
|
|
|
@ -2,9 +2,12 @@ import json
|
|||
|
||||
import pytest
|
||||
from slugify import slugify
|
||||
from tests.test_routes.utils.routes_data import (RecipeTestData,
|
||||
raw_recipe_dict,
|
||||
recipe_test_data)
|
||||
from tests.test_routes.utils.routes_data import (
|
||||
RecipeTestData,
|
||||
raw_recipe,
|
||||
raw_recipe_no_image,
|
||||
recipe_test_data,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||
|
@ -15,12 +18,31 @@ def test_create_by_url(api_client, recipe_data: RecipeTestData):
|
|||
|
||||
|
||||
def test_create_by_json(api_client):
|
||||
response = api_client.post("/api/recipe/create/", json=raw_recipe_dict)
|
||||
response = api_client.post("/api/recipe/create/", json=raw_recipe)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.text) == "banana-bread"
|
||||
|
||||
|
||||
def test_create_no_image(api_client):
|
||||
response = api_client.post("/api/recipe/create/", json=raw_recipe_no_image)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.text) == "banana-bread-no-image"
|
||||
|
||||
|
||||
# def test_upload_image(api_client, test_image):
|
||||
# data = {"image": test_image.open("rb").read(), "extension": "jpg"}
|
||||
|
||||
# response = api_client.post(
|
||||
# "/api/recipe/banana-bread-no-image/update/image/", files=data
|
||||
# )
|
||||
|
||||
# assert response.status_code == 200
|
||||
|
||||
# response = api_client.get("/api/recipe/banana-bread-no-image/update/image/")
|
||||
|
||||
|
||||
def test_read_all_post(api_client):
|
||||
response = api_client.post(
|
||||
"/api/all-recipes/", json={"properties": ["slug", "description", "rating"]}
|
||||
|
|
|
@ -16,7 +16,7 @@ recipe_test_data = [
|
|||
]
|
||||
|
||||
|
||||
raw_recipe_dict = {
|
||||
raw_recipe = {
|
||||
"name": "Banana Bread",
|
||||
"description": "From Angie's mom",
|
||||
"image": "banana-bread.jpg",
|
||||
|
@ -62,3 +62,50 @@ raw_recipe_dict = {
|
|||
"orgURL": None,
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
raw_recipe_no_image = {
|
||||
"name": "Banana Bread No Image",
|
||||
"description": "From Angie's mom",
|
||||
"image": "",
|
||||
"recipeYield": "",
|
||||
"recipeIngredient": [
|
||||
"4 bananas",
|
||||
"1/2 cup butter",
|
||||
"1/2 cup sugar",
|
||||
"2 eggs",
|
||||
"2 cups flour",
|
||||
"1/2 tsp baking soda",
|
||||
"1 tsp baking powder",
|
||||
"pinch salt",
|
||||
"1/4 cup nuts (we like pecans)",
|
||||
],
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"@type": "Beat the eggs, then cream with the butter and sugar",
|
||||
"text": "Beat the eggs, then cream with the butter and sugar",
|
||||
},
|
||||
{
|
||||
"@type": "Mix in bananas, then flour, baking soda/powder, salt, and nuts",
|
||||
"text": "Mix in bananas, then flour, baking soda/powder, salt, and nuts",
|
||||
},
|
||||
{
|
||||
"@type": "Add to greased and floured pan",
|
||||
"text": "Add to greased and floured pan",
|
||||
},
|
||||
{
|
||||
"@type": "Bake until brown/cracked, toothpick comes out clean",
|
||||
"text": "Bake until brown/cracked, toothpick comes out clean",
|
||||
},
|
||||
],
|
||||
"totalTime": "None",
|
||||
"prepTime": None,
|
||||
"performTime": None,
|
||||
"slug": "",
|
||||
"categories": [],
|
||||
"tags": ["breakfast", " baking"],
|
||||
"dateAdded": "2021-01-12",
|
||||
"notes": [],
|
||||
"rating": 0,
|
||||
"orgURL": None,
|
||||
"extras": {},
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
test_
|
|
@ -1,11 +0,0 @@
|
|||
from services.scheduler_services import Scheduler
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
global scheduler
|
||||
scheduler = Scheduler()
|
||||
scheduler.startup_scheduler()
|
||||
return scheduler
|
||||
|
||||
|
||||
scheduler = start_scheduler
|
21
mealie/utils/post_webhooks.py
Normal file
21
mealie/utils/post_webhooks.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import json
|
||||
|
||||
import requests
|
||||
from db.db_setup import create_session
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings
|
||||
|
||||
|
||||
def post_webhooks():
|
||||
session = create_session()
|
||||
all_settings = SiteSettings.get_site_settings(session)
|
||||
|
||||
if all_settings.webhooks.enabled:
|
||||
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()
|
||||
urls = all_settings.webhooks.webhookURLs
|
||||
|
||||
for url in urls:
|
||||
requests.post(url, json.dumps(todays_meal, default=str))
|
||||
|
||||
session.close()
|
|
@ -1,17 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
from app_config import REQUIRED_DIRS
|
||||
from services.settings_services import default_theme_init
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
|
||||
def post_start():
|
||||
default_theme_init()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -6,6 +6,7 @@ from app_config import TEMP_DIR
|
|||
|
||||
|
||||
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
|
||||
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = tempfile.TemporaryDirectory(dir=TEMP_DIR)
|
||||
temp_dir_path = Path(temp_dir.name)
|
||||
if selection.suffix == ".zip":
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue