Merge branch 'feature/export-progress' of https://github.com/vabene1111/recipes into feature/export-progress

This commit is contained in:
vabene1111
2022-02-03 15:59:18 +01:00
26 changed files with 1240 additions and 100 deletions

View 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>

View 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')

View 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>

View 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')

View File

@@ -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>