mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-02 12:49:02 -05:00
Merge branch 'feature/export-progress' of https://github.com/vabene1111/recipes into feature/export-progress
This commit is contained in:
145
vue/src/apps/ExportResponseView/ExportResponseView.vue
Normal file
145
vue/src/apps/ExportResponseView/ExportResponseView.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<br/>
|
||||
|
||||
<template v-if="export_info !== undefined">
|
||||
|
||||
<template v-if="export_info.running">
|
||||
<h5 style="text-align: center">{{ $t('Exporting') }}...</h5>
|
||||
|
||||
<b-progress :max="export_info.total_recipes">
|
||||
<b-progress-bar :value="export_info.exported_recipes" :label="`${export_info.exported_recipes}/${export_info.total_recipes}`"></b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
<loading-spinner :size="25"></loading-spinner>
|
||||
</template>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12" v-if="!export_info.running">
|
||||
<span>{{ $t('Export_finished') }}! </span> <a :href="`${resolveDjangoUrl('viewExport') }`">{{ $t('Return to export') }} </a><br><br>
|
||||
|
||||
{{ $t('If download did not start automatically: ') }}
|
||||
|
||||
<template v-if="export_info.expired">
|
||||
<a disabled><del>{{ $t('Download') }}</del></a> ({{ $t('Expired') }})
|
||||
</template>
|
||||
<a v-else :href="`/export-file/${export_id}/`" ref="downloadAnchor" >{{ $t('Download') }}</a>
|
||||
|
||||
<br>
|
||||
{{ $t('The link will remain active for') }}
|
||||
|
||||
<template v-if="export_info.cache_duration > 3600">
|
||||
{{ export_info.cache_duration/3600 }}{{ $t('hr') }}
|
||||
</template>
|
||||
<template v-else-if="export_info.cache_duration > 60">
|
||||
{{ export_info.cache_duration/60 }}{{ $t('min') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ export_info.cache_duration }}{{ $t('sec') }}
|
||||
</template>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<label for="id_textarea">{{ $t('Information') }}</label>
|
||||
<textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh"
|
||||
v-html="export_info.msg"
|
||||
disabled></textarea>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import {ResolveUrlMixin, makeToast, ToastMixin} from "@/utils/utils";
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'ExportResponseView',
|
||||
mixins: [
|
||||
ResolveUrlMixin,
|
||||
ToastMixin,
|
||||
],
|
||||
components: {
|
||||
LoadingSpinner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
export_id: window.EXPORT_ID,
|
||||
export_info: undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.refreshData()
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
|
||||
this.dynamicIntervalTimeout = 250 //initial refresh rate
|
||||
this.run = setTimeout(this.dynamicInterval.bind(this), this.dynamicIntervalTimeout)
|
||||
},
|
||||
methods: {
|
||||
dynamicInterval: function(){
|
||||
|
||||
//update frequently at start but slowdown as it takes longer
|
||||
this.dynamicIntervalTimeout = Math.round(this.dynamicIntervalTimeout*((1+Math.sqrt(5))/2))
|
||||
if(this.dynamicIntervalTimeout > 5000) this.dynamicIntervalTimeout = 5000
|
||||
clearInterval(this.run);
|
||||
this.run = setInterval(this.dynamicInterval.bind(this), this.dynamicIntervalTimeout);
|
||||
|
||||
if ((this.export_id !== null) && window.navigator.onLine && this.export_info.running) {
|
||||
this.refreshData()
|
||||
let el = this.$refs.output_text
|
||||
el.scrollTop = el.scrollHeight;
|
||||
|
||||
if(this.export_info.expired)
|
||||
makeToast(this.$t("Error"), this.$t("The download link is expired!"), "danger")
|
||||
}
|
||||
},
|
||||
|
||||
startDownload: function(){
|
||||
this.$refs['downloadAnchor'].click()
|
||||
},
|
||||
|
||||
refreshData: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient.retrieveExportLog(this.export_id).then(result => {
|
||||
this.export_info = result.data
|
||||
this.export_info.expired = !this.export_info.possibly_not_expired
|
||||
|
||||
if(!this.export_info.running)
|
||||
this.$nextTick(()=>{ this.startDownload(); } )
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
</style>
|
||||
18
vue/src/apps/ExportResponseView/main.js
Normal file
18
vue/src/apps/ExportResponseView/main.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Vue from 'vue'
|
||||
import App from './ExportResponseView.vue'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
publicPath = 'http://localhost:8080/'
|
||||
}
|
||||
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
188
vue/src/apps/ExportView/ExportView.vue
Normal file
188
vue/src/apps/ExportView/ExportView.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<h2>{{ $t('Export') }}</h2>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
|
||||
<br/>
|
||||
<!-- TODO get option dynamicaly -->
|
||||
<select class="form-control" v-model="recipe_app">
|
||||
<option value="DEFAULT">Default</option>
|
||||
<option value="PAPRIKA">Paprika</option>
|
||||
<option value="NEXTCLOUD">Nextcloud Cookbook</option>
|
||||
<option value="MEALIE">Mealie</option>
|
||||
<option value="CHOWDOWN">Chowdown</option>
|
||||
<option value="SAFFRON">Saffron</option>
|
||||
<option value="CHEFTAP">ChefTap</option>
|
||||
<option value="PEPPERPLATE">Pepperplate</option>
|
||||
<option value="RECETTETEK">RecetteTek</option>
|
||||
<option value="RECIPESAGE">Recipe Sage</option>
|
||||
<option value="DOMESTICA">Domestica</option>
|
||||
<option value="MEALMASTER">MealMaster</option>
|
||||
<option value="REZKONV">RezKonv</option>
|
||||
<option value="OPENEATS">Openeats</option>
|
||||
<option value="RECIPEKEEPER">Recipe Keeper</option>
|
||||
<option value="PLANTOEAT">Plantoeat</option>
|
||||
<option value="COOKBOOKAPP">CookBookApp</option>
|
||||
<option value="COPYMETHAT">CopyMeThat</option>
|
||||
<option value="PDF">PDF</option>
|
||||
</select>
|
||||
|
||||
<br/>
|
||||
<b-form-checkbox v-model="export_all" @change="disabled_multiselect=$event" name="check-button" switch style="margin-top: 1vh">
|
||||
{{ $t('All recipes') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<multiselect
|
||||
:searchable="true"
|
||||
:disabled="disabled_multiselect"
|
||||
v-model="recipe_list"
|
||||
:options="recipes"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="true"
|
||||
:hide-selected="true"
|
||||
:preserve-search="true"
|
||||
placeholder="Select Recipes"
|
||||
:taggable="false"
|
||||
label="name"
|
||||
track-by="id"
|
||||
id="id_recipes"
|
||||
:multiple="true"
|
||||
:loading="recipes_loading"
|
||||
@search-change="searchRecipes">
|
||||
</multiselect>
|
||||
|
||||
|
||||
|
||||
<br/>
|
||||
<button @click="exportRecipe()" class="btn btn-primary shadow-none"><i class="fas fa-file-export"></i> {{ $t('Export') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
|
||||
import {StandardToasts, makeToast, resolveDjangoUrl} from "@/utils/utils";
|
||||
import Multiselect from "vue-multiselect";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ExportView',
|
||||
/*mixins: [
|
||||
ResolveUrlMixin,
|
||||
ToastMixin,
|
||||
],*/
|
||||
components: {Multiselect},
|
||||
data() {
|
||||
return {
|
||||
export_id: window.EXPORT_ID,
|
||||
loading: false,
|
||||
disabled_multiselect: false,
|
||||
|
||||
recipe_app: 'DEFAULT',
|
||||
recipe_list: [],
|
||||
recipes_loading: false,
|
||||
recipes: [],
|
||||
export_all: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if(this.export_id)
|
||||
this.insertRequested()
|
||||
else
|
||||
this.searchRecipes('')
|
||||
},
|
||||
methods: {
|
||||
|
||||
insertRequested: function(){
|
||||
|
||||
let apiFactory = new ApiApiFactory()
|
||||
|
||||
this.recipes_loading = true
|
||||
|
||||
apiFactory.retrieveRecipe(this.export_id).then((response) => {
|
||||
this.recipes_loading = false
|
||||
this.recipe_list.push(response.data)
|
||||
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
}).then(e => this.searchRecipes(''))
|
||||
},
|
||||
|
||||
searchRecipes: function (query) {
|
||||
|
||||
let apiFactory = new ApiApiFactory()
|
||||
|
||||
this.recipes_loading = true
|
||||
|
||||
let maxResultLenght = 1000
|
||||
apiFactory.listRecipes(query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, maxResultLenght).then((response) => {
|
||||
this.recipes = response.data.results;
|
||||
this.recipes_loading = false
|
||||
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
})
|
||||
},
|
||||
|
||||
exportRecipe: function () {
|
||||
|
||||
if (this.recipe_list.length < 1 && this.export_all == false) {
|
||||
makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger")
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = undefined
|
||||
this.loading = true
|
||||
let formData = new FormData();
|
||||
formData.append('type', this.recipe_app);
|
||||
formData.append('all', this.export_all)
|
||||
|
||||
for (var i = 0; i < this.recipe_list.length; i++) {
|
||||
formData.append('recipes', this.recipe_list[i].id);
|
||||
}
|
||||
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
axios.post(resolveDjangoUrl('view_export',), formData).then((response) => {
|
||||
|
||||
window.location.href = resolveDjangoUrl('view_export_response', response.data['export_id'])
|
||||
|
||||
}).catch((err) => {
|
||||
this.error = err.data
|
||||
this.loading = false
|
||||
console.log(err)
|
||||
makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "danger")
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
</style>
|
||||
18
vue/src/apps/ExportView/main.js
Normal file
18
vue/src/apps/ExportView/main.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Vue from 'vue'
|
||||
import App from './ExportView.vue'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
publicPath = 'http://localhost:8080/'
|
||||
}
|
||||
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
@@ -100,7 +100,7 @@
|
||||
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img class="img img-fluid rounded" :src="recipe.image" style="max-height: 30vh" :alt="$t('Recipe_Image')" v-if="recipe.image !== null" />
|
||||
<img class="img img-fluid rounded" :src="recipe.image" style="max-height: 30vh" :alt="$t('Recipe_Image')" v-if="recipe.image !== null" @load="onImgLoad" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -241,6 +241,9 @@ export default {
|
||||
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
|
||||
}
|
||||
|
||||
|
||||
if(recipe.image === null) this.printReady()
|
||||
|
||||
this.recipe = this.rootrecipe = recipe
|
||||
this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings
|
||||
this.loading = false
|
||||
@@ -267,6 +270,14 @@ export default {
|
||||
this.servings = this.servings_cache?.[e.id] ?? e.servings
|
||||
}
|
||||
},
|
||||
printReady: function(){
|
||||
const template = document.createElement("template");
|
||||
template.id = "printReady";
|
||||
document.body.appendChild(template);
|
||||
},
|
||||
onImgLoad: function(){
|
||||
this.printReady()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user