956 lines
54 KiB
HTML
956 lines
54 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
{% load custom_tags %}
|
|
|
|
{% block title %}{% trans 'URL Import' %}{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
|
|
{% include 'include/vue_base.html' %}
|
|
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
|
|
<script src="{% static 'js/vue-jstree.js' %}"></script>
|
|
<script src="{% static 'js/vue-multiselect.min.js' %}"></script>
|
|
<link rel="stylesheet" href="{% static 'css/vue-multiselect.min.css' %}">
|
|
<style>
|
|
.tree-anchor {
|
|
width:90%;
|
|
}
|
|
</style>
|
|
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="app">
|
|
<div class="row px-3" style="justify-content:space-between;">
|
|
<h2> {% trans 'Import' %}</h2>
|
|
<a class="btn btn-outline-info btn-sm"
|
|
style="height:50%"
|
|
href="{% bookmarklet request.get_host request.is_secure %}"
|
|
title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}">
|
|
<img src="{% static 'assets/favicon-16x16.png' %}">{% trans 'Bookmark Me!' %} </a>
|
|
</div>
|
|
<nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px">
|
|
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url" aria-selected="true">URL</a>
|
|
<a class="nav-link" href="#nav-app" data-toggle="tab" role="tab" aria-controls="nav-app">App</a>
|
|
<a class="nav-link" href="#nav-source" data-toggle="tab" role="tab" aria-controls="nav-source">Source</a>
|
|
<a class="nav-link disabled" href="#nav-text" data-toggle="tab" role="tab" aria-controls="nav-text">Text</a>
|
|
<a class="nav-link disabled" href="#nav-file" data-toggle="tab" role="tab" aria-controls="nav-file">File</a>
|
|
</nav>
|
|
|
|
|
|
<div class="tab-content" id="nav-tabContent">
|
|
<!-- Import URL -->
|
|
<div class="tab-pane fade show active" id="nav-url" role="tabpanel">
|
|
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
|
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
|
|
<input type="radio" autocomplete="off" checked> Automatic
|
|
</label>
|
|
|
|
<label class="btn btn-outline-info btn-sm" @click="automatic=false">
|
|
<input type="radio" autocomplete="off"> Manual
|
|
</label>
|
|
</div>
|
|
<div class="input-group my-2">
|
|
<input class="form-control" v-model="remote_url" placeholder="{% trans 'Enter website URL' %}">
|
|
<div class="input-group-append">
|
|
<button @click="loadRecipe()" class="btn btn-primary shadow-none" type="button"
|
|
id="id_btn_search"><i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import from Recipe Application -->
|
|
<div class=" tab-pane fade show" id="nav-app" role="tabpanel">
|
|
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
|
<label class="btn btn-outline-info btn-sm active" @click="recipe_app='DEFAULT'">
|
|
<input type="radio" autocomplete="off" checked> Tandoor
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="recipe_app='PAPRIKA'">
|
|
<input type="radio" autocomplete="off"> Paprika
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="recipe_app='NEXTCLOUD'">
|
|
<input type="radio" autocomplete="off"> Nextcloud Cookbook
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="recipe_app='MEALIE'">
|
|
<input type="radio" autocomplete="off"> Mealie
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="recipe_app='CHOWDOWN'">
|
|
<input type="radio" autocomplete="off"> Chowdown
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="recipe_app='SAFRON'">
|
|
<input type="radio" autocomplete="off"> Safron
|
|
</label>
|
|
</div>
|
|
<b-form-file
|
|
class="my-2"
|
|
accept=".zip"
|
|
multiple
|
|
v-model="recipe_files"
|
|
placeholder="{% trans 'Select recipe files to import or drop them here...' %}"
|
|
drop-placeholder="Drop recipe files here...">
|
|
</b-form-file>
|
|
<button @click="importAppRecipe()" class="btn btn-primary shadow-none" type="button"
|
|
id="id_btn_app"><i class="fas fa-file-archive"></i> {% trans 'Import' %}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Import JSON or HTML -->
|
|
<div class=" tab-pane fade show" id="nav-source" role="tabpanel">
|
|
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
|
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
|
|
<input type="radio" autocomplete="off" checked> Automatic
|
|
</label>
|
|
|
|
<label class="btn btn-outline-info btn-sm" @click="automatic=false">
|
|
<input type="radio" autocomplete="off"> Manual
|
|
</label>
|
|
</div>
|
|
<div class="input-group my-2">
|
|
<textarea class="form-control input-group-append" v-model="source_data" rows=10 placeholder="{% trans 'Paste json or html source here to load recipe.' %}" style="font-size: 12px">
|
|
</textarea>
|
|
</div>
|
|
<button @click="loadSource()" class="btn btn-primary shadow-none" type="button"
|
|
id="id_btn_app"><i class="fas fa-code"></i> {% trans 'Import' %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<br/>
|
|
|
|
<div v-if="loading" class="text-center">
|
|
<br/>
|
|
<i class="fas fa-spinner fa-spin fa-8x"></i>
|
|
</div>
|
|
|
|
<!-- recipe preview before Import -->
|
|
<div class="container-fluid" v-if="preview" id="manage_tree">
|
|
<div class="row">
|
|
<div class="col" style="max-width:50%">
|
|
<!-- start of preview card -->
|
|
<div class="card card-border-primary" >
|
|
<div class="card-header">
|
|
<h3>{% trans 'Preview Recipe Data' %}</h3>
|
|
<div class='small text-muted'>{% trans 'Drag recipe attributes from the right into the appropriate box below.' %} </div>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
|
|
<div class="card mb-2">
|
|
<div class="card-header" v-b-toggle.collapse-name>
|
|
<div class="row px-3" style="justify-content:space-between;">
|
|
{% trans 'Name' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.name=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="small text-muted">{% trans 'Text dragged here will be appended to the name.'%}</div>
|
|
</div>
|
|
<b-collapse id="collapse-name" visible class="mt-2">
|
|
<div class="card-body drop-zone" @drop="replacePreview('name', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div class="card-text">[[recipe_json.name]]</div>
|
|
</div>
|
|
</b-collapse>
|
|
</div>
|
|
|
|
<div class="card mb-2">
|
|
<div class="card-header" v-b-toggle.collapse-description>
|
|
<div class="row px-3" style="justify-content:space-between;">
|
|
{% trans 'Description' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.description=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="small text-muted">{% trans 'Text dragged here will be appended to the description.'%}</div>
|
|
</div>
|
|
<b-collapse id="collapse-description" visible class="mt-2">
|
|
<div class="card-body drop-zone" @drop="replacePreview('description', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div class="card-text">[[recipe_json.description]]</div>
|
|
</div>
|
|
</b-collapse>
|
|
</div>
|
|
|
|
<div class="card mb-2">
|
|
<div class="card-header" v-b-toggle.collapse-kw>
|
|
<div class="row px-3" style="justify-content:space-between;">
|
|
{% trans 'Keywords' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.keywords=[]" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="small text-muted">{% trans 'Keywords dragged here will be appended to current list'%}</div>
|
|
</div>
|
|
<b-collapse id="collapse-kw" visible class="mt-2">
|
|
<div class="card-body drop-zone" @drop="replacePreview('keywords', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div v-for="kw in recipe_json.keywords">
|
|
<div class="card-text">[[kw]] </div>
|
|
</div>
|
|
</div>
|
|
</b-collapse>
|
|
</div>
|
|
|
|
<div class="card mb-2">
|
|
<div class="card-header" v-b-toggle.collapse-image style="display:flex; justify-content:space-between;">
|
|
{% trans 'Image' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.image=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<b-collapse id="collapse-image" visible class="mt-2">
|
|
<div class="card-body m-0 p-0 drop-zone" @drop="replacePreview('image', $event)" @dragover.prevent @dragenter.prevent>
|
|
<img class="card-img" v-bind:src="[[recipe_json.image]]" alt="Recipe Image">
|
|
</div>
|
|
</b-collapse>
|
|
</div>
|
|
|
|
<div class = "row mb-2">
|
|
<div class="col">
|
|
<div class="card">
|
|
<div class="card-header p-1" style="display:flex; justify-content:space-between;">
|
|
{% trans 'Servings' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.servings=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="card-body p-2 drop-zone" @drop="replacePreview('servings', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div class="card-text">[[recipe_json.servings]]</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card">
|
|
<div class="card-header p-1" style="display:flex; justify-content:space-between;">
|
|
{% trans 'Prep Time' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.prepTime=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="card-body p-2 drop-zone" @drop="replacePreview('prepTime', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div class="card-text">[[recipe_json.prepTime]]</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card">
|
|
<div class="card-header p-1" style="display:flex; justify-content:space-between;">
|
|
{% trans 'Cook Time' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.cookTime=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="card-body p-2 drop-zone" @drop="replacePreview('cookTime', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div class="card-text">[[recipe_json.cookTime]]</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-2">
|
|
<div class="card-header" v-b-toggle.collapse-ing>
|
|
<div class="row px-3" style="display:flex; justify-content:space-between;">
|
|
{% trans 'Ingredients' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.recipeIngredient=[]" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="small text-muted">{% trans 'Ingredients dragged here will be appended to current list.'%}</div>
|
|
</div>
|
|
<b-collapse id="collapse-ing" visible class="mt-2">
|
|
<div class="card-body drop-zone" @drop="replacePreview('ingredients', $event)" @dragover.prevent @dragenter.prevent>
|
|
<ul class="list-group list-group">
|
|
<div v-for="i in recipe_json.recipeIngredient">
|
|
<li class="row border-light" >
|
|
<div class="col-sm-1 border">[[i.amount]]</div>
|
|
<div class="col-sm border">[[i.unit.text]]</div>
|
|
<div class="col-sm border">[[i.ingredient.text]]</div>
|
|
<div class="col-sm border">[[i.note]]</div>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
</div>
|
|
</b-collapse>
|
|
</div>
|
|
|
|
<div class="card mb-2">
|
|
<div class="card-header" v-b-toggle.collapse-instructions>
|
|
<div class="row px-3" style="justify-content:space-between;">
|
|
{% trans 'Instructions' %}
|
|
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.recipeInstructions=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="small text-muted">{% trans 'Recipe instructions dragged here will be appended to current instructions.'%}</div>
|
|
</div>
|
|
<b-collapse id="collapse-instructions" visible class="mt-2">
|
|
<div class="card-body drop-zone" @drop="replacePreview('instructions', $event)" @dragover.prevent @dragenter.prevent>
|
|
<div class="card-text">[[recipe_json.recipeInstructions]]</div>
|
|
</div>
|
|
</b-collapse>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<br/>
|
|
<!-- end of preview card -->
|
|
<button @click="loadRecipeManual()" class="btn btn-primary shadow-none" type="button"
|
|
id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- start of source data -->
|
|
<div class="col" style="max-width:50%">
|
|
<div class="card card-border-primary">
|
|
<div class="card-header">
|
|
<h3>{% trans 'Discovered Attributes' %}</h3>
|
|
<div class='small text-muted'>
|
|
{% trans 'Drag recipe attributes from below into the appropriate box on the left. Click any node to display its full properties.' %}
|
|
</div>
|
|
</div>
|
|
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
|
<label class="btn btn-outline-info btn-sm active" @click="preview_type='json'">
|
|
<input type="radio" autocomplete="off" checked> json
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="preview_type='html'">
|
|
<input type="radio" autocomplete="off"> html
|
|
</label>
|
|
<label class="btn btn-outline-info btn-sm" @click="preview_type='image'">
|
|
<input type="radio" autocomplete="off"> images
|
|
</label>
|
|
</div>
|
|
<i :class="[show_blank ? 'fa-chevron-up' : 'fa-chevron-down', 'fas']"
|
|
style="cursor:pointer;"
|
|
@click="show_blank=!show_blank"
|
|
title="{% trans 'Show Blank Field' %}"></i>
|
|
<div class="card-body p-1">
|
|
<div class="card card-border-primary" v-if="show_blank">
|
|
<div class="card-header">
|
|
<div class="row px-3" style="justify-content:space-between;">
|
|
{% trans 'Blank Field' %}
|
|
<i class="fas fa-eraser justify-content-end" style="cursor:pointer;" @click="blank_field=''" title="{% trans 'Clear Contents'%}"></i>
|
|
</div>
|
|
<div class="small text-muted">{% trans 'Items dragged to Blank Field will be appended.'%}</div>
|
|
</div>
|
|
<div class="card-body drop-zone"
|
|
@drop="replacePreview('blank', $event)"
|
|
@dragover.prevent
|
|
@dragenter.prevent
|
|
draggable
|
|
@dragstart="htmlDragStart($event)">
|
|
[[blank_field]]
|
|
</div>
|
|
</div>
|
|
<!-- start of json data -->
|
|
<v-jstree v-if="preview_type=='json'" :data="recipe_tree"
|
|
text-field-name="name"
|
|
collapse:true
|
|
draggable
|
|
@item-drag-start="itemDragStart"
|
|
@item-click="itemClick">
|
|
<template scope="_">
|
|
<div class="col" @click.ctrl="customItemClickWithCtrl">
|
|
<div class="row clearfix" style="width:100%" >
|
|
<div class="col-es" style="align-right">
|
|
<button style="border: 0px; background-color: transparent; cursor: pointer;"
|
|
@click="deleteNode(_.vm, _.model, $event)"><i class="fas fa-minus-square" style="color:red"></i></button>
|
|
</div>
|
|
<div class="col overflow-hidden">
|
|
<i :class="_.vm.themeIconClasses" role="presentation" v-if="!_.model.loading"></i>
|
|
{% verbatim %}
|
|
[[_.model.name]]
|
|
{% endverbatim %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</template>
|
|
</v-jstree>
|
|
<!-- start of html data -->
|
|
<div v-if="preview_type=='html'">
|
|
<ul class="list-group list-group-flush" v-for="(txt, key) in recipe_html">
|
|
<div class="list-group-item bg-light m-0 small"
|
|
draggable
|
|
@dragstart="htmlDragStart($event)"
|
|
style="display:flex; justify-content:space-between;">
|
|
[[txt]]
|
|
<i class="fas fa-minus-square" style="cursor:pointer; color:red" @click="$delete(recipe_html, key)" title="{% trans 'Delete Text'%}"></i>
|
|
</div>
|
|
</ul>
|
|
</div>
|
|
<!-- start of images -->
|
|
<div v-if="preview_type=='image'">
|
|
<ul class="list-group list-group-flush" v-for="(img, key) in images">
|
|
<div class="list-group-item bg-light m-0 small"
|
|
draggable
|
|
@dragstart="imageDragStart($event)"
|
|
style="display:flex; justify-content:space-between;">
|
|
<img class="card-img" v-bind:src=[[img]] alt="Image">
|
|
<i class="fas fa-minus-square" style="cursor:pointer; color:red" @click="$delete(images, key)" title="{% trans 'Delete image'%}"></i>
|
|
</div>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- end of json tree -->
|
|
</div>
|
|
</div>
|
|
<!-- end of recipe preview before Import -->
|
|
|
|
<template v-if="recipe_data !== undefined">
|
|
|
|
<form>
|
|
<div class="form-group">
|
|
<label for="id_name">{% trans 'Recipe Name' %}</label>
|
|
<input id="id_name" class="form-control" v-model="recipe_data.name">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="id_description">{% trans 'Recipe Description' %}</label>
|
|
<textarea id="id_description" class="form-control" rows="3" v-model="recipe_data.description"></textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col col-md-6" v-if="recipe_data.image !== ''">
|
|
<img v-bind:src="recipe_data.image" alt="{% trans 'Recipe Image' %}"
|
|
class="img-fluid img-responsive img-rounded">
|
|
</div>
|
|
<div class="col col-md-6">
|
|
<div class="form-group">
|
|
<label for="id_prep_time">{% trans 'Preparation time ca.' %}</label>
|
|
<input id="id_prep_time" class="form-control" v-model="recipe_data.prepTime">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="id_waiting_time">{% trans 'Waiting time ca.' %}</label>
|
|
<input id="id_waiting_time" class="form-control" v-model="recipe_data.cookTime">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="id_servings">{% trans 'Servings' %}</label>
|
|
<input id="id_servings" class="form-control" v-model="recipe_data.servings">
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<br/>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<template v-for="(i, index) in recipe_data.recipeIngredient">
|
|
|
|
<div class="card" style="margin-top: 4px">
|
|
<div class="card-body">
|
|
<div class="row" v-if="i.original">
|
|
<div class="col-md-12" style="margin-bottom: 4px">
|
|
<span class="text-muted"><i class="fas fa-globe"></i> [[i.original]]</span>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-1">
|
|
<input class="form-control" v-model="i.amount">
|
|
</div>
|
|
<div class="col-md-4">
|
|
|
|
<table class="table-layout:fixed">
|
|
<col width="95%"/>
|
|
<col width="5%"/>
|
|
<tr>
|
|
<td>
|
|
<multiselect v-tabindex
|
|
ref="unit"
|
|
style="width: 100%!important;"
|
|
v-model="i.unit"
|
|
:options="units"
|
|
:close-on-select="true"
|
|
:clear-on-select="true"
|
|
:allow-empty="true"
|
|
:preserve-search="true"
|
|
placeholder="{% trans 'Select one' %}"
|
|
tag-placeholder="{% trans 'Select' %}"
|
|
label="text"
|
|
:taggable="true"
|
|
@tag="addUnitType"
|
|
:id="'unit_' + index"
|
|
@open="openUnitSelect"
|
|
track-by="id"
|
|
:multiple="false"
|
|
:loading="units_loading"
|
|
@search-change="searchUnits">
|
|
</multiselect>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-outline-success btn-lg" type="button"
|
|
@click="i.unit = ''" tabindex="-1">
|
|
<i class="fas fa-eraser"></i></button>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
|
|
</div>
|
|
<div class="col-md-4">
|
|
|
|
<multiselect v-tabindex
|
|
ref="ingredient"
|
|
v-model="i.ingredient"
|
|
:options="ingredients"
|
|
:taggable="true"
|
|
@tag="addIngredientType"
|
|
:id="'ingredient_' + index"
|
|
placeholder="{% trans 'Select one' %}"
|
|
tag-placeholder="{% trans 'Select' %}"
|
|
:close-on-select="true"
|
|
:clear-on-select="true"
|
|
:allow-empty="false"
|
|
:preserve-search="true"
|
|
label="text"
|
|
track-by="id"
|
|
:multiple="false"
|
|
:loading="ingredients_loading"
|
|
@search-change="searchIngredients"
|
|
@open="openIngredientSelect">
|
|
|
|
</multiselect>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<input type="text" placeholder="{% trans 'Note' %}" class="form-control"
|
|
v-model="i.note">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<button class="btn btn-outline-danger btn-lg" type="button"
|
|
@click="deleteIngredient(i)" tabindex="-1"><i
|
|
class="fas fa-trash-alt"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
<div style="text-align: center; margin-top: 16px">
|
|
<button class="btn btn-success" type="button" @click="addIngredient()"><i
|
|
class="fas fa-plus"></i></button>
|
|
<br/><br/>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="id_instructions">{% trans 'Instructions' %}</label>
|
|
<textarea id="id_instructions" class="form-control" v-model="recipe_data.recipeInstructions"
|
|
rows="8"></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="id_keywords">{% trans 'Keywords' %}</label>
|
|
|
|
<multiselect
|
|
v-model="recipe_data.keywords"
|
|
:options="keywords"
|
|
:close-on-select="false"
|
|
:clear-on-select="true"
|
|
:hide-selected="true"
|
|
:preserve-search="true"
|
|
placeholder="{% trans 'Select one' %}"
|
|
tag-placeholder="{% trans 'Add Keyword' %}"
|
|
:taggable="true"
|
|
@tag="addKeyword"
|
|
label="text"
|
|
track-by="id"
|
|
id="id_keywords"
|
|
:multiple="true"
|
|
:loading="keywords_loading"
|
|
@search-change="searchKeywords">
|
|
</multiselect>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
{% trans 'All Keywords' %}<br/>
|
|
<input id="id_all_keywords" type="checkbox"
|
|
v-model="all_keywords"> <label
|
|
for="id_all_keywords">{% trans 'Import all keywords, not only the ones already existing.' %}</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<button type="button" class="btn btn-success" @click="importRecipe()"
|
|
:disabled="importing_recipe">{% trans 'Import' %}</button>
|
|
</div>
|
|
|
|
<br/>
|
|
<br/>
|
|
<br/>
|
|
</form>
|
|
</template>
|
|
|
|
<template v-if="error !== undefined">
|
|
<div>
|
|
|
|
<div style="text-align: center">
|
|
<i class="fas fa-robot fa-8x"></i><br/><br/>
|
|
[[error.msg]]
|
|
</div>
|
|
</div>
|
|
<br/>
|
|
<div class="row">
|
|
<div class="col-md-8 offset-md-2">
|
|
<div class="card border-info mb-6">
|
|
<div class="card-body text-info">
|
|
<h5 class="card-title">{% trans 'Information' %}</h5>
|
|
<p class="card-text">
|
|
{% blocktrans %} Only websites containing ld+json or microdata information can currently
|
|
be imported. Most big recipe pages support this. If you site cannot be imported but
|
|
you think
|
|
it probably has some kind of structured data feel free to post an example in the
|
|
github issues.{% endblocktrans %}
|
|
</p>
|
|
<a href="https://developers.google.com/search/docs/data-types/recipe" target="_blank"
|
|
rel="noreferrer nofollow"
|
|
class="card-link">{% trans 'Google ld+json Info' %}</a>
|
|
<a href="https://github.com/vabene1111/recipes/issues" target="_blank"
|
|
rel="noreferrer nofollow"
|
|
class="card-link">{% trans 'GitHub Issues' %}</a>
|
|
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow"
|
|
class="card-link">{% trans 'Recipe Markup Specification' %}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
</div>
|
|
|
|
<script src="{% url 'javascript-catalog' %}"></script>
|
|
<script type="application/javascript">
|
|
let csrftoken = Cookies.get('csrftoken');
|
|
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
|
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
|
|
|
let app = new Vue({
|
|
components: {
|
|
Multiselect: window.VueMultiselect.default
|
|
},
|
|
delimiters: ['[[', ']]'],
|
|
el: '#app',
|
|
data: {
|
|
remote_url: '',
|
|
source_data: undefined,
|
|
keywords: [],
|
|
keywords_loading: false,
|
|
units: [],
|
|
units_loading: false,
|
|
ingredients: [],
|
|
ingredients_loading: false,
|
|
recipe_data: undefined,
|
|
error: undefined,
|
|
loading: false,
|
|
preview: {{preview}},
|
|
preview_type: 'json',
|
|
all_keywords: false,
|
|
importing_recipe: false,
|
|
recipe_json: {{recipe_json|safe}},
|
|
recipe_tree: {{recipe_tree|safe}},
|
|
recipe_html: {{recipe_html|safe}},
|
|
automatic: true,
|
|
show_blank: false,
|
|
blank_field: '',
|
|
recipe_app: 'DEFAULT',
|
|
recipe_files: [],
|
|
images: [],
|
|
},
|
|
directives: {
|
|
tabindex: {
|
|
inserted(el) {
|
|
el.setAttribute('tabindex', 0);
|
|
}
|
|
}
|
|
},
|
|
mounted: function () {
|
|
this.searchKeywords('')
|
|
this.searchUnits('')
|
|
this.searchIngredients('')
|
|
|
|
},
|
|
methods: {
|
|
makeToast: function (title, message, variant = null) {
|
|
//TODO remove duplicate function in favor of central one
|
|
this.$bvToast.toast(message, {
|
|
title: title,
|
|
variant: variant,
|
|
toaster: 'b-toaster-top-center',
|
|
solid: true
|
|
})
|
|
},
|
|
loadRecipe: function () {
|
|
this.recipe_data = undefined
|
|
this.recipe_json = undefined
|
|
this.recipe_tree = undefined
|
|
this.images = []
|
|
this.error = undefined
|
|
this.loading = true
|
|
this.preview = false
|
|
if (this.automatic) {
|
|
console.log('true')
|
|
}
|
|
this.$http.post("{% url 'api_recipe_from_url' %}", {'url': this.remote_url, 'auto':this.automatic}, {emulateJSON: true}).then((response) => {
|
|
console.log(response.data)
|
|
if (this.automatic) {
|
|
this.recipe_data = response.data;
|
|
} else {
|
|
this.recipe_json = response.data['recipe_json'];
|
|
this.recipe_tree = response.data['recipe_tree'];
|
|
this.recipe_html = response.data['recipe_html'];
|
|
this.images = response.data['images'];
|
|
this.preview = true
|
|
}
|
|
this.loading = false
|
|
}).catch((err) => {
|
|
this.error = err.data
|
|
this.loading = false
|
|
console.log(err)
|
|
let msg = gettext('There was an error loading a resource!')
|
|
if (err.bodyText.length < 300) {
|
|
msg += err.bodyText
|
|
} else {
|
|
msg += ' ' + err.status + ' ' + err.statusText
|
|
}
|
|
this.makeToast(gettext('Error'), msg, 'danger')
|
|
})
|
|
},
|
|
loadSource: function() {
|
|
this.recipe_data = undefined
|
|
this.recipe_json = undefined
|
|
this.recipe_tree = undefined
|
|
this.images = []
|
|
this.error = undefined
|
|
this.loading = true
|
|
this.preview = false
|
|
this.$http.post("{% url 'api_recipe_from_source' %}", {'data': this.source_data, 'auto':this.automatic}, {emulateJSON: true}).then((response) => {
|
|
console.log(response.data)
|
|
if (this.automatic) {
|
|
this.recipe_data = response.data['recipe_json'];
|
|
this.preview = false
|
|
} else {
|
|
this.recipe_json = response.data['recipe_json'];
|
|
this.recipe_tree = response.data['recipe_tree'];
|
|
this.recipe_html = response.data['recipe_html'];
|
|
this.preview = true
|
|
}
|
|
this.loading = false
|
|
}).catch((err) => {
|
|
this.error = err.data
|
|
this.loading = false
|
|
console.log(err)
|
|
let msg = gettext('There was an error loading a resource!')
|
|
if (err.bodyText.length < 300) {
|
|
msg += err.bodyText
|
|
} else {
|
|
msg += ' ' + err.status + ' ' + err.statusText
|
|
}
|
|
this.makeToast(gettext('Error'), msg, 'danger')
|
|
})
|
|
},
|
|
loadRecipeManual: function () {
|
|
this.error = undefined
|
|
this.preview = false
|
|
this.loading = true
|
|
this.recipe_json['@type'] = "Recipe"
|
|
this.$http.post("{% url 'api_recipe_from_source' %}", {'data': JSON.stringify(this.recipe_json), 'auto':'true'}, {emulateJSON: true}).then((response) => {
|
|
console.log(response.data)
|
|
this.recipe_data = response.data['recipe_json'];
|
|
this.loading = false
|
|
this.preview = false
|
|
}).catch((err) => {
|
|
this.error = err.data
|
|
this.loading = false
|
|
console.log(err)
|
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
})
|
|
},
|
|
importRecipe: function () {
|
|
if (this.recipe_data.name.length > 128) {
|
|
this.makeToast(gettext('Error'), gettext('Recipe name is longer than 128 characters'), 'danger')
|
|
return;
|
|
}
|
|
if (this.recipe_data.description.length > 512) {
|
|
this.makeToast(gettext('Error'), gettext('Recipe description is longer than 512 characters'), 'danger')
|
|
return;
|
|
}
|
|
if (this.importing_recipe) {
|
|
this.makeToast(gettext('Error'), gettext('Already importing the selected recipe, please wait!'), 'danger')
|
|
return;
|
|
}
|
|
this.importing_recipe = true
|
|
this.$set(this.recipe_data, 'all_keywords', this.all_keywords)
|
|
this.$http.post(`{% url 'data_import_url' %}`, this.recipe_data).then((response) => {
|
|
window.location.href = response.data
|
|
}).catch((err) => {
|
|
console.log(err);
|
|
this.makeToast(gettext('Error'), gettext('An error occurred while trying to import this recipe!') + err.bodyText, 'danger')
|
|
})
|
|
},
|
|
importAppRecipe: function() {
|
|
this.error = undefined
|
|
this.preview = false
|
|
this.loading = true
|
|
let formData = new FormData();
|
|
let files = []
|
|
formData.append('type', this.recipe_app);
|
|
for( var i = 0; i < this.recipe_files.length; i++ ){
|
|
formData.append('files', this.recipe_files[i]);
|
|
}
|
|
this.$http.post("{% url 'view_import' %}", formData, {headers: {'Content-Type': 'multipart/form-data'}}).then((response) => {
|
|
console.log(response.data)
|
|
window.location.href = "{% url 'view_import_response' 1237654 %}".replace('1237654', response.data['import_id'])
|
|
}).catch((err) => {
|
|
this.error = err.data
|
|
this.loading = false
|
|
console.log(err)
|
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
})
|
|
},
|
|
deleteIngredient: function (i) {
|
|
this.recipe_data.recipeIngredient = this.recipe_data.recipeIngredient.filter(item => item !== i)
|
|
},
|
|
addIngredient: function (i) {
|
|
this.recipe_data.recipeIngredient.push({
|
|
unit: {id: Math.random() * 1000, text: '{{ request.user.userpreference.default_unit }}'},
|
|
amount: 0,
|
|
ingredient: {id: Math.random() * 1000, text: ''}
|
|
})
|
|
},
|
|
addIngredientType: function (tag, index) {
|
|
index = index.replace('ingredient_', '')
|
|
let new_ingredient = this.recipe_data.recipeIngredient[index]
|
|
new_ingredient.ingredient = {'id': Math.random() * 1000, 'text': tag}
|
|
this.ingredients.push(new_ingredient.ingredient)
|
|
this.recipe_data.recipeIngredient[index] = new_ingredient
|
|
},
|
|
addUnitType: function (tag, index) {
|
|
index = index.replace('unit_', '')
|
|
let new_unit = this.recipe_data.recipeIngredient[index]
|
|
new_unit.unit = {'id': Math.random() * 1000, 'text': tag}
|
|
this.units.push(new_unit.unit)
|
|
this.recipe_data.recipeIngredient[index] = new_unit
|
|
},
|
|
addKeyword: function (tag) {
|
|
let new_keyword = {'text': tag, 'id': null}
|
|
this.recipe_data.keywords.push(new_keyword)
|
|
},
|
|
openUnitSelect: function (id) {
|
|
let index = id.replace('unit_', '')
|
|
if (this.recipe_data.recipeIngredient[index].unit !== null) {
|
|
this.$set(app.$refs.unit[index].$data, 'search', this.recipe_data.recipeIngredient[index].unit.text)
|
|
}
|
|
},
|
|
openIngredientSelect: function (id) {
|
|
let index = id.replace('ingredient_', '')
|
|
this.$set(this.$refs.ingredient[index].$data, 'search', this.recipe_data.recipeIngredient[index].ingredient.text)
|
|
},
|
|
searchKeywords: function (query) {
|
|
this.keywords_loading = true
|
|
this.$http.get("{% url 'dal_keyword' %}" + '?q=' + query).then((response) => {
|
|
this.keywords = response.data.results;
|
|
this.keywords_loading = false
|
|
}).catch((err) => {
|
|
console.log(err)
|
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
})
|
|
},
|
|
searchUnits: function (query) {
|
|
this.units_loading = true
|
|
this.$http.get("{% url 'dal_unit' %}" + '?q=' + query).then((response) => {
|
|
this.units = response.data.results;
|
|
if (this.recipe_data !== undefined) {
|
|
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
|
if (x.unit !== null && x.unit.text !== '') {
|
|
this.units = this.units.filter(item => item.text !== x.unit.text)
|
|
this.units.push(x.unit)
|
|
}
|
|
}
|
|
}
|
|
this.units_loading = false
|
|
}).catch((err) => {
|
|
console.log(err)
|
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
})
|
|
},
|
|
searchIngredients: function (query) {
|
|
this.ingredients_loading = true
|
|
this.$http.get("{% url 'dal_food' %}" + '?q=' + query).then((response) => {
|
|
this.ingredients = response.data.results
|
|
if (this.recipe_data !== undefined) {
|
|
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
|
if (x.ingredient.text !== '') {
|
|
this.ingredients = this.ingredients.filter(item => item.text !== x.ingredient.text)
|
|
this.ingredients.push(x.ingredient)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.ingredients_loading = false
|
|
}).catch((err) => {
|
|
console.log(err)
|
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
})
|
|
},
|
|
deleteNode: function (node ,item, e) {
|
|
e.stopPropagation()
|
|
var index = node.parentItem.indexOf(item)
|
|
node.parentItem.splice(index, 1)
|
|
},
|
|
itemClick: function (node, item, e) {
|
|
this.makeToast(gettext('Details'), node.model.value, 'info')
|
|
},
|
|
itemDragStart (node,item,e) {
|
|
if (node.model.children.length > 0) {
|
|
e.dataTransfer.setData('hasChildren', true)
|
|
}
|
|
e.dataTransfer.setData('value', node.model.value)
|
|
|
|
},
|
|
htmlDragStart: function (e) {
|
|
console.log(e.target.innerText)
|
|
e.dataTransfer.setData('value', e.target.innerText)
|
|
},
|
|
imageDragStart: function (e) {
|
|
console.log(e.target.src)
|
|
e.dataTransfer.setData('value', e.target.src)
|
|
},
|
|
replacePreview: function(field, e) {
|
|
v = e.dataTransfer.getData('value')
|
|
if (e.dataTransfer.getData('hasChildren')) {
|
|
this.makeToast(gettext('Error'), gettext('Items with children cannot be dropped here!') , 'danger')
|
|
return
|
|
}
|
|
switch (field) {
|
|
case 'name':
|
|
this.recipe_json.name = [this.recipe_json.name, v].filter(Boolean).join(" ");
|
|
break;
|
|
case 'description':
|
|
this.recipe_json.description = [this.recipe_json.description, v].filter(Boolean).join(" ");
|
|
break;
|
|
case 'image':
|
|
this.recipe_json.image=v
|
|
break;
|
|
case 'keywords':
|
|
this.recipe_json.keywords.push(v)
|
|
break;
|
|
case 'servings':
|
|
this.recipe_json.servings=v
|
|
break;
|
|
case 'prepTime':
|
|
this.recipe_json.prepTime=v
|
|
break;
|
|
case 'cookTime':
|
|
this.recipe_json.cookTime=v
|
|
break;
|
|
case 'ingredients':
|
|
this.$http.post('{% url 'api_ingredient_from_string' %}', {text: v}, {emulateJSON: true}).then((response) => {
|
|
console.log(response)
|
|
let new_ingredient={
|
|
unit: {id: Math.random() * 1000, text: response.body.unit},
|
|
amount: response.body.amount,
|
|
ingredient: {id: Math.random() * 1000, text: response.body.food},
|
|
note: response.body.note
|
|
}
|
|
this.recipe_json.recipeIngredient.push(new_ingredient)
|
|
}).catch((err) => {
|
|
console.log(err)
|
|
this.makeToast(gettext('Error'), gettext('Something went wrong.'), 'danger')
|
|
})
|
|
break;
|
|
case 'instructions':
|
|
this.recipe_json.recipeInstructions = [this.recipe_json.recipeInstructions, v].filter(Boolean).join("\n\n");
|
|
break;
|
|
case 'blank':
|
|
this.blank_field = [this.blank_field, v].filter(Boolean).join(" ");
|
|
break;
|
|
}
|
|
},
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %} |