ca863e4118
- New color palette matching felhom.eu/controller templates (blue/cyan accent) - Plus Jakarta Sans font, 12px border radius, accent glow on focus - felhom.eu logo in navbar, "Powered by felhom.eu" footer - Settings page: documentation about external vs internal Docker URLs - Fixed version double "v" prefix display - Cleaned up example HTML files from development Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1307 lines
45 KiB
HTML
1307 lines
45 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Importálás — Recept Importáló{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
.recipe-preview { display: none; }
|
|
.recipe-preview.visible { display: block; }
|
|
.section-divider { border: none; border-top: 1px solid var(--border); margin: 1.2rem 0; }
|
|
.recipe-image {
|
|
max-width: 300px;
|
|
max-height: 200px;
|
|
border-radius: var(--radius);
|
|
object-fit: cover;
|
|
}
|
|
.ingredient-header {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.3rem;
|
|
padding-right: 34px; /* space for remove btn */
|
|
}
|
|
.ingredient-header span {
|
|
font-size: 0.8rem;
|
|
color: var(--text-dim);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
.ingredient-header .col-qty { width: 60px; flex-shrink: 0; }
|
|
.ingredient-header .col-unit { width: 80px; flex-shrink: 0; }
|
|
.ingredient-header .col-food { flex: 1; }
|
|
.ingredient-header .col-extra { width: 120px; flex-shrink: 0; }
|
|
|
|
.ingredient-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
.ingredient-row input { margin-bottom: 0; }
|
|
.ingredient-row .ing-qty { width: 60px; flex-shrink: 0; }
|
|
.ingredient-row .ing-unit { width: 80px; flex-shrink: 0; }
|
|
.ingredient-row .ing-food { flex: 1; }
|
|
.ingredient-row .ing-extra { width: 120px; flex-shrink: 0; }
|
|
.ingredient-row button {
|
|
background: var(--danger);
|
|
border: none;
|
|
color: #fff;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
line-height: 1;
|
|
flex-shrink: 0;
|
|
}
|
|
.ingredient-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
margin: 0.8rem 0 0.3rem;
|
|
}
|
|
.ingredient-group input {
|
|
margin-bottom: 0;
|
|
flex: 1;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
border-style: dashed;
|
|
}
|
|
.ingredient-group button {
|
|
background: var(--danger);
|
|
border: none;
|
|
color: #fff;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
line-height: 1;
|
|
flex-shrink: 0;
|
|
}
|
|
.instruction-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: flex-start;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.instruction-row .step-num {
|
|
background: var(--accent);
|
|
color: #fff;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
margin-top: 0.4rem;
|
|
}
|
|
.instruction-row textarea { margin-bottom: 0; flex: 1; min-height: 60px; }
|
|
.instruction-row button {
|
|
background: var(--danger);
|
|
border: none;
|
|
color: #fff;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
line-height: 1;
|
|
flex-shrink: 0;
|
|
margin-top: 0.4rem;
|
|
}
|
|
.add-btn {
|
|
background: var(--surface2);
|
|
border: 1px dashed var(--border);
|
|
color: var(--text-dim);
|
|
padding: 0.4rem 0.8rem;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
transition: border-color 0.15s;
|
|
}
|
|
.add-btn:hover { border-color: var(--accent); color: var(--text); }
|
|
|
|
/* Tags */
|
|
.tag-section-label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-dim);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
.tag-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4rem;
|
|
min-height: 28px;
|
|
}
|
|
.tag-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.6rem;
|
|
border-radius: 999px;
|
|
font-size: 0.85rem;
|
|
line-height: 1.2;
|
|
cursor: pointer;
|
|
transition: opacity 0.15s;
|
|
user-select: none;
|
|
}
|
|
.tag-chip:hover { opacity: 0.8; }
|
|
.tag-chip.tag-active {
|
|
background: var(--success);
|
|
color: #fff;
|
|
}
|
|
.tag-chip.tag-inactive {
|
|
background: var(--surface2);
|
|
color: var(--text-dim);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.tag-search-wrap {
|
|
position: relative;
|
|
margin-top: 0.5rem;
|
|
}
|
|
.tag-search-wrap input {
|
|
margin-bottom: 0;
|
|
width: 100%;
|
|
}
|
|
.tag-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 10;
|
|
display: none;
|
|
}
|
|
.tag-dropdown.open { display: block; }
|
|
.tag-dropdown-item {
|
|
padding: 0.4rem 0.6rem;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
.tag-dropdown-item:hover { background: var(--surface); }
|
|
.tag-dropdown-item .tag-source {
|
|
color: var(--text-dim);
|
|
font-size: 0.8rem;
|
|
}
|
|
.tag-dropdown-item.tag-add-new { color: var(--accent); font-style: italic; }
|
|
|
|
.result-card { display: none; }
|
|
.result-card.visible { display: block; }
|
|
|
|
/* Tab bar */
|
|
.tab-bar {
|
|
display: flex;
|
|
gap: 0;
|
|
margin-bottom: 1.2rem;
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
.tab-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
padding: 0.6rem 1.2rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
margin-bottom: -2px;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
}
|
|
.tab-btn:hover { color: var(--text); }
|
|
.tab-btn.active {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
/* Radio labels */
|
|
.radio-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
color: var(--text);
|
|
margin-right: 1rem;
|
|
}
|
|
.radio-label input[type="radio"] {
|
|
accent-color: var(--accent);
|
|
width: auto;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Bulk progress table */
|
|
.bulk-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.85rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
.bulk-table th, .bulk-table td {
|
|
padding: 0.4rem 0.6rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.bulk-table th {
|
|
color: var(--text-dim);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
.bulk-table tr.row-active { background: rgba(0,136,204,0.08); }
|
|
.bulk-table .url-cell {
|
|
max-width: 220px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Status badges */
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 0.15rem 0.5rem;
|
|
border-radius: 999px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
.status-pending { background: var(--surface2); color: var(--text-dim); }
|
|
.status-scraping { background: rgba(0,170,255,0.15); color: var(--accent-light); }
|
|
.status-reviewing { background: rgba(210,153,34,0.15); color: var(--warning); }
|
|
.status-importing { background: rgba(0,170,255,0.15); color: var(--accent-light); }
|
|
.status-done { background: rgba(35,134,54,0.15); color: var(--success-text); }
|
|
.status-error { background: rgba(218,54,51,0.15); color: var(--danger); }
|
|
.status-skipped { background: var(--surface2); color: var(--text-dim); }
|
|
.status-duplicate { background: rgba(210,153,34,0.15); color: var(--warning); }
|
|
|
|
.btn-sm {
|
|
padding: 0.3rem 0.8rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.supported-sites {
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.bulk-options { margin-bottom: 1rem; }
|
|
.bulk-options .option-row { margin-bottom: 0.5rem; }
|
|
.bulk-tag-option {
|
|
margin-top: 0.3rem;
|
|
margin-left: 1.6rem;
|
|
padding: 0.5rem 0.8rem;
|
|
background: var(--surface2);
|
|
border-radius: var(--radius);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.bulk-summary {
|
|
margin-top: 1rem;
|
|
padding: 0.8rem;
|
|
background: var(--surface2);
|
|
border-radius: var(--radius);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Step 1: URL input -->
|
|
<div class="card">
|
|
<div class="tab-bar">
|
|
<button class="tab-btn active" onclick="switchTab('single')">Egyedi importálás</button>
|
|
<button class="tab-btn" onclick="switchTab('bulk')">Tömeges importálás</button>
|
|
</div>
|
|
|
|
<!-- Single import tab -->
|
|
<div id="tabSingle">
|
|
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:0.8rem;">
|
|
Támogatott oldalak: <span class="supported-sites">{{ supported_sites | join(', ') }}</span> + egyéb (schema.org)
|
|
</p>
|
|
<div class="flex">
|
|
<input type="url" id="recipeUrl" class="grow" style="margin-bottom:0"
|
|
placeholder="https://www.mindmegette.hu/recept/brassoi">
|
|
<button class="btn btn-primary" id="scrapeBtn" onclick="scrapeRecipe()">
|
|
Beolvasás
|
|
</button>
|
|
</div>
|
|
<div id="scrapeStatus" class="mt-1"></div>
|
|
</div>
|
|
|
|
<!-- Bulk import tab -->
|
|
<div id="tabBulk" style="display:none">
|
|
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:0.8rem;">
|
|
Támogatott oldalak: <span class="supported-sites">{{ supported_sites | join(', ') }}</span> + egyéb (schema.org)
|
|
</p>
|
|
|
|
<label for="bulkUrls">URL-ek (soronként egy)</label>
|
|
<textarea id="bulkUrls" rows="6" placeholder="https://www.nosalty.hu/recept/chilis-bab https://streetkitchen.hu/receptek/... https://www.mindmegette.hu/recept/..."></textarea>
|
|
|
|
<div class="bulk-options">
|
|
<div class="option-row">
|
|
<label class="radio-label">
|
|
<input type="radio" name="bulkMode" value="review" checked onchange="onBulkModeChange()">
|
|
Receptek egyenkénti áttekintése
|
|
</label>
|
|
<label class="radio-label">
|
|
<input type="radio" name="bulkMode" value="auto" onchange="onBulkModeChange()">
|
|
Automatikus importálás
|
|
</label>
|
|
</div>
|
|
<div class="bulk-tag-option" id="bulkTagOption" style="display:none">
|
|
Címkék:
|
|
<label class="radio-label" style="margin-left:0.5rem">
|
|
<input type="radio" name="bulkTags" value="all" checked>
|
|
Összes címke importálása
|
|
</label>
|
|
<label class="radio-label">
|
|
<input type="radio" name="bulkTags" value="none">
|
|
Címkék nélkül
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex" style="gap:0.5rem;flex-wrap:wrap;">
|
|
{% if has_mealie %}
|
|
<button class="btn btn-success" onclick="startBulk('mealie')">Importálás Mealie-be</button>
|
|
{% endif %}
|
|
{% if has_tandoor %}
|
|
<button class="btn btn-success" onclick="startBulk('tandoor')">Importálás Tandoor-ba</button>
|
|
{% endif %}
|
|
{% if has_mealie and has_tandoor %}
|
|
<button class="btn btn-success" onclick="startBulk('both')">Importálás mindkettőbe</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Progress section -->
|
|
<div id="bulkProgress" style="display:none; margin-top:1.2rem;">
|
|
<div class="flex" style="align-items:center;gap:0.8rem;margin-bottom:0.5rem;flex-wrap:wrap;">
|
|
<strong id="bulkCounter" style="font-size:0.9rem;"></strong>
|
|
<button class="btn btn-secondary btn-sm" id="bulkAutoRemaining" style="display:none"
|
|
onclick="autoImportRemaining()">Maradékot automatikusan</button>
|
|
<button class="btn btn-secondary btn-sm" id="bulkStopBtn" style="display:none"
|
|
onclick="stopBulk()">Megállítás</button>
|
|
</div>
|
|
<table class="bulk-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:30px">#</th>
|
|
<th>URL</th>
|
|
<th>Cím</th>
|
|
<th style="width:110px">Állapot</th>
|
|
<th>Eredmény</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="bulkTableBody"></tbody>
|
|
</table>
|
|
<div id="bulkSummary" class="bulk-summary" style="display:none"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Editable preview -->
|
|
<div class="recipe-preview" id="previewCard">
|
|
<div class="card">
|
|
<h2>Recept adatai</h2>
|
|
|
|
<div class="flex flex-wrap mb-2">
|
|
<div class="grow">
|
|
<label for="recipeTitle">Név</label>
|
|
<input type="text" id="recipeTitle">
|
|
|
|
<label for="recipeDesc">Leírás</label>
|
|
<textarea id="recipeDesc" rows="2"></textarea>
|
|
</div>
|
|
<div>
|
|
<img id="recipeImage" class="recipe-image" src="" alt="">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- Ingredients -->
|
|
<label>Hozzávalók</label>
|
|
<div class="ingredient-header">
|
|
<span class="col-qty">Menny.</span>
|
|
<span class="col-unit">Egység</span>
|
|
<span class="col-food">Hozzávaló</span>
|
|
<span class="col-extra">Megjegyzés</span>
|
|
</div>
|
|
<div id="ingredientsList"></div>
|
|
<div class="flex gap-1 mt-1 mb-2">
|
|
<button class="add-btn" onclick="addIngredient({})">+ Hozzávaló</button>
|
|
<button class="add-btn" onclick="addIngredientGroup('')">+ Csoport</button>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- Instructions -->
|
|
<label>Elkészítés</label>
|
|
<div id="instructionsList"></div>
|
|
<button class="add-btn mt-1 mb-2" onclick="addInstruction('')">+ Lépés hozzáadása</button>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- Tags -->
|
|
<label>Címkék</label>
|
|
<div class="tag-section-label">Hozzáadásra kerül:</div>
|
|
<div id="tagsActive" class="tag-chips mb-1"></div>
|
|
<div class="tag-search-wrap">
|
|
<input type="text" id="tagSearch" placeholder="Címke keresése / hozzáadása..."
|
|
autocomplete="off" oninput="onTagSearch()" onfocus="onTagSearch()" onkeydown="onTagKeydown(event)">
|
|
<div id="tagDropdown" class="tag-dropdown"></div>
|
|
</div>
|
|
<div class="tag-section-label" style="margin-top:0.6rem;">Weboldalról (kattints a hozzáadáshoz):</div>
|
|
<div id="tagsInactive" class="tag-chips"></div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- Single mode action buttons -->
|
|
<div class="flex mt-2" id="singleActions">
|
|
{% if has_mealie %}
|
|
<button class="btn btn-success" id="sendMealieBtn" onclick="sendToMealie()">
|
|
Importálás Mealie-be
|
|
</button>
|
|
{% endif %}
|
|
{% if has_tandoor %}
|
|
<button class="btn btn-success" id="sendTandoorBtn" onclick="sendToTandoor()">
|
|
Importálás Tandoor-ba
|
|
</button>
|
|
{% endif %}
|
|
<span id="sendStatus"></span>
|
|
</div>
|
|
|
|
<!-- Bulk mode action buttons -->
|
|
<div class="flex mt-2" id="bulkActions" style="display:none;gap:0.5rem;">
|
|
<button class="btn btn-success" id="bulkApproveBtn" onclick="approveAndContinue()">
|
|
Importálás és tovább →
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="skipCurrent()">
|
|
Kihagyás →
|
|
</button>
|
|
<span id="bulkSendStatus"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Result (single mode) -->
|
|
<div class="result-card" id="resultCard">
|
|
<div class="card">
|
|
<h2 class="text-success">Recept sikeresen importálva!</h2>
|
|
<p class="mt-1" id="resultLinks"></p>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let currentRecipe = null;
|
|
|
|
// =========================================================================
|
|
// Tab switching
|
|
// =========================================================================
|
|
|
|
function switchTab(tab) {
|
|
document.getElementById('tabSingle').style.display = tab === 'single' ? '' : 'none';
|
|
document.getElementById('tabBulk').style.display = tab === 'bulk' ? '' : 'none';
|
|
document.querySelectorAll('.tab-btn').forEach((btn, i) => {
|
|
btn.classList.toggle('active', (i === 0 && tab === 'single') || (i === 1 && tab === 'bulk'));
|
|
});
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('resultCard').classList.remove('visible');
|
|
showSingleButtons();
|
|
}
|
|
|
|
function onBulkModeChange() {
|
|
const mode = document.querySelector('input[name="bulkMode"]:checked').value;
|
|
document.getElementById('bulkTagOption').style.display = mode === 'auto' ? '' : 'none';
|
|
}
|
|
|
|
// =========================================================================
|
|
// Single import (existing)
|
|
// =========================================================================
|
|
|
|
async function scrapeRecipe() {
|
|
const url = document.getElementById('recipeUrl').value.trim();
|
|
if (!url) return;
|
|
|
|
const btn = document.getElementById('scrapeBtn');
|
|
const status = document.getElementById('scrapeStatus');
|
|
btn.disabled = true;
|
|
status.innerHTML = '<span class="spinner"></span> Beolvasás folyamatban...';
|
|
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('resultCard').classList.remove('visible');
|
|
Object.keys(importedLinks).forEach(k => delete importedLinks[k]);
|
|
|
|
try {
|
|
const form = new FormData();
|
|
form.append('url', url);
|
|
const resp = await fetch('/scrape', { method: 'POST', body: form });
|
|
const data = await resp.json();
|
|
|
|
if (!data.ok) {
|
|
status.innerHTML = '<span class="text-danger">Hiba: ' + data.error + '</span>';
|
|
btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
currentRecipe = data.data;
|
|
populatePreview(currentRecipe);
|
|
showSingleButtons();
|
|
document.getElementById('previewCard').classList.add('visible');
|
|
|
|
const warnings = [];
|
|
if (data.duplicate) {
|
|
warnings.push('Mealie: <a href="' + escHtml(data.duplicate.url) + '" target="_blank" style="color:var(--accent)">'
|
|
+ escHtml(data.duplicate.name) + '</a>');
|
|
}
|
|
if (data.tandoor_duplicate) {
|
|
warnings.push('Tandoor: <a href="' + escHtml(data.tandoor_duplicate.url) + '" target="_blank" style="color:var(--accent)">'
|
|
+ escHtml(data.tandoor_duplicate.name) + '</a>');
|
|
}
|
|
if (warnings.length > 0) {
|
|
status.innerHTML = '<span class="text-warning">⚠ Ez a recept már létezik: ' + warnings.join(' | ') + '</span>';
|
|
} else {
|
|
status.innerHTML = '<span class="text-success">✓ Beolvasva</span>';
|
|
}
|
|
} catch (e) {
|
|
status.innerHTML = '<span class="text-danger">Hálózati hiba: ' + e.message + '</span>';
|
|
}
|
|
btn.disabled = false;
|
|
}
|
|
|
|
function populatePreview(r) {
|
|
document.getElementById('recipeTitle').value = r.title || '';
|
|
document.getElementById('recipeDesc').value = r.description || '';
|
|
|
|
const img = document.getElementById('recipeImage');
|
|
if (r.image_url) {
|
|
img.src = r.image_url;
|
|
img.style.display = 'block';
|
|
} else {
|
|
img.style.display = 'none';
|
|
}
|
|
|
|
// Ingredients
|
|
const ingList = document.getElementById('ingredientsList');
|
|
ingList.innerHTML = '';
|
|
(r.ingredients || []).forEach(i => addIngredient(i));
|
|
|
|
// Instructions
|
|
const instList = document.getElementById('instructionsList');
|
|
instList.innerHTML = '';
|
|
(r.instructions || []).forEach(t => addInstruction(t));
|
|
|
|
// Tags — scraped tags go to inactive (user must opt-in)
|
|
document.getElementById('tagsActive').innerHTML = '';
|
|
document.getElementById('tagsInactive').innerHTML = '';
|
|
(r.tags || []).forEach(t => addTagChip(t, false));
|
|
}
|
|
|
|
function addIngredient(item) {
|
|
if (typeof item === 'string') item = { food: item };
|
|
// Group header marker
|
|
if (item.group !== undefined && item.food === undefined) {
|
|
addIngredientGroup(item.group);
|
|
return;
|
|
}
|
|
const list = document.getElementById('ingredientsList');
|
|
const row = document.createElement('div');
|
|
row.className = 'ingredient-row';
|
|
row.innerHTML = '<input type="text" class="ing-qty" placeholder="" value="' + escHtml(item.quantity || '') + '">'
|
|
+ '<input type="text" class="ing-unit" placeholder="" value="' + escHtml(item.unit || '') + '">'
|
|
+ '<input type="text" class="ing-food" placeholder="Hozzávaló" value="' + escHtml(item.food || '') + '">'
|
|
+ '<input type="text" class="ing-extra" placeholder="" value="' + escHtml(item.extra || '') + '">'
|
|
+ '<button onclick="this.parentElement.remove()">✕</button>';
|
|
list.appendChild(row);
|
|
}
|
|
|
|
function addIngredientGroup(name) {
|
|
const list = document.getElementById('ingredientsList');
|
|
const row = document.createElement('div');
|
|
row.className = 'ingredient-group';
|
|
row.innerHTML = '<input type="text" class="ing-group-name" placeholder="Csoport neve" value="' + escHtml(name || '') + '">'
|
|
+ '<button onclick="this.parentElement.remove()">✕</button>';
|
|
list.appendChild(row);
|
|
}
|
|
|
|
function addInstruction(value) {
|
|
const list = document.getElementById('instructionsList');
|
|
const idx = list.children.length + 1;
|
|
const row = document.createElement('div');
|
|
row.className = 'instruction-row';
|
|
row.innerHTML = '<span class="step-num">' + idx + '</span>'
|
|
+ '<textarea>' + escHtml(value) + '</textarea>'
|
|
+ '<button onclick="removeInstruction(this)">✕</button>';
|
|
list.appendChild(row);
|
|
}
|
|
|
|
function removeInstruction(btn) {
|
|
btn.closest('.instruction-row').remove();
|
|
renumberInstructions();
|
|
}
|
|
|
|
function renumberInstructions() {
|
|
document.querySelectorAll('#instructionsList .step-num').forEach((el, i) => {
|
|
el.textContent = i + 1;
|
|
});
|
|
}
|
|
|
|
function gatherRecipe() {
|
|
const ingredients = [];
|
|
document.querySelectorAll('#ingredientsList > div').forEach(el => {
|
|
if (el.classList.contains('ingredient-group')) {
|
|
const name = el.querySelector('.ing-group-name').value.trim();
|
|
if (name) ingredients.push({ group: name });
|
|
} else if (el.classList.contains('ingredient-row')) {
|
|
const qty = el.querySelector('.ing-qty').value.trim();
|
|
const unit = el.querySelector('.ing-unit').value.trim();
|
|
const food = el.querySelector('.ing-food').value.trim();
|
|
const extra = el.querySelector('.ing-extra').value.trim();
|
|
if (food || qty) {
|
|
ingredients.push({ quantity: qty, unit: unit, food: food, extra: extra });
|
|
}
|
|
}
|
|
});
|
|
|
|
const instructions = [];
|
|
document.querySelectorAll('#instructionsList .instruction-row textarea').forEach(ta => {
|
|
const v = ta.value.trim();
|
|
if (v) instructions.push(v);
|
|
});
|
|
|
|
const tags = [];
|
|
document.querySelectorAll('#tagsActive .tag-chip').forEach(el => {
|
|
tags.push(el.dataset.tag);
|
|
});
|
|
|
|
return {
|
|
title: document.getElementById('recipeTitle').value.trim(),
|
|
description: document.getElementById('recipeDesc').value.trim(),
|
|
image_url: currentRecipe ? currentRecipe.image_url : null,
|
|
ingredients: ingredients,
|
|
instructions: instructions,
|
|
tags: tags,
|
|
original_url: currentRecipe ? currentRecipe.original_url : '',
|
|
};
|
|
}
|
|
|
|
const importedLinks = {};
|
|
|
|
async function sendToMealie() {
|
|
const recipe = gatherRecipe();
|
|
if (!recipe.title) { alert('A recept neve kötelező!'); return; }
|
|
|
|
const btn = document.getElementById('sendMealieBtn');
|
|
const status = document.getElementById('sendStatus');
|
|
btn.disabled = true;
|
|
status.innerHTML = '<span class="spinner"></span> Importálás Mealie-be...';
|
|
|
|
try {
|
|
const resp = await fetch('/send', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(recipe),
|
|
});
|
|
const data = await resp.json();
|
|
|
|
if (!data.ok) {
|
|
status.innerHTML = '<span class="text-danger">Hiba: ' + data.error + '</span>';
|
|
btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
status.innerHTML = '<span class="text-success">✓ Mealie kész</span>';
|
|
importedLinks['Mealie'] = data.url;
|
|
showResultCard();
|
|
} catch (e) {
|
|
status.innerHTML = '<span class="text-danger">Hálózati hiba: ' + e.message + '</span>';
|
|
}
|
|
btn.disabled = false;
|
|
}
|
|
|
|
async function sendToTandoor() {
|
|
const recipe = gatherRecipe();
|
|
if (!recipe.title) { alert('A recept neve kötelező!'); return; }
|
|
|
|
const btn = document.getElementById('sendTandoorBtn');
|
|
const status = document.getElementById('sendStatus');
|
|
btn.disabled = true;
|
|
status.innerHTML = '<span class="spinner"></span> Importálás Tandoor-ba...';
|
|
|
|
try {
|
|
const resp = await fetch('/send-tandoor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(recipe),
|
|
});
|
|
const data = await resp.json();
|
|
|
|
if (!data.ok) {
|
|
status.innerHTML = '<span class="text-danger">Hiba: ' + data.error + '</span>';
|
|
btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
status.innerHTML = '<span class="text-success">✓ Tandoor kész</span>';
|
|
importedLinks['Tandoor'] = data.url;
|
|
showResultCard();
|
|
} catch (e) {
|
|
status.innerHTML = '<span class="text-danger">Hálózati hiba: ' + e.message + '</span>';
|
|
}
|
|
btn.disabled = false;
|
|
}
|
|
|
|
function showResultCard() {
|
|
const links = Object.entries(importedLinks).map(([name, url]) =>
|
|
'<a href="' + escHtml(url) + '" target="_blank" style="color:var(--accent);">Megnyitás ' + name + '-ben →</a>'
|
|
).join('<br>');
|
|
document.getElementById('resultLinks').innerHTML = links;
|
|
document.getElementById('resultCard').classList.add('visible');
|
|
}
|
|
|
|
// =========================================================================
|
|
// Tag management
|
|
// =========================================================================
|
|
|
|
let existingTags = { mealie: [], tandoor: [] };
|
|
|
|
async function loadExistingTags() {
|
|
try {
|
|
const resp = await fetch('/tags');
|
|
if (resp.ok) existingTags = await resp.json();
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
function tagExistsIn(name, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
for (const c of container.querySelectorAll('.tag-chip')) {
|
|
if (c.dataset.tag.toLowerCase() === name.toLowerCase()) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function addTagChip(name, active) {
|
|
name = name.trim();
|
|
if (!name) return;
|
|
if (tagExistsIn(name, 'tagsActive') || tagExistsIn(name, 'tagsInactive')) return;
|
|
|
|
const chip = document.createElement('span');
|
|
chip.dataset.tag = name;
|
|
chip.textContent = name;
|
|
chip.onclick = function() { toggleTag(this); };
|
|
|
|
if (active) {
|
|
chip.className = 'tag-chip tag-active';
|
|
document.getElementById('tagsActive').appendChild(chip);
|
|
} else {
|
|
chip.className = 'tag-chip tag-inactive';
|
|
document.getElementById('tagsInactive').appendChild(chip);
|
|
}
|
|
}
|
|
|
|
function toggleTag(chip) {
|
|
if (chip.classList.contains('tag-active')) {
|
|
chip.classList.remove('tag-active');
|
|
chip.classList.add('tag-inactive');
|
|
document.getElementById('tagsInactive').appendChild(chip);
|
|
} else {
|
|
chip.classList.remove('tag-inactive');
|
|
chip.classList.add('tag-active');
|
|
document.getElementById('tagsActive').appendChild(chip);
|
|
}
|
|
}
|
|
|
|
function getAllTagNames() {
|
|
const names = [];
|
|
document.querySelectorAll('#tagsActive .tag-chip, #tagsInactive .tag-chip').forEach(el => {
|
|
names.push(el.dataset.tag.toLowerCase());
|
|
});
|
|
return names;
|
|
}
|
|
|
|
function onTagSearch() {
|
|
const input = document.getElementById('tagSearch');
|
|
const dropdown = document.getElementById('tagDropdown');
|
|
const q = input.value.trim().toLowerCase();
|
|
|
|
if (!q) { dropdown.classList.remove('open'); return; }
|
|
|
|
const allNames = getAllTagNames();
|
|
const seen = {};
|
|
for (const t of existingTags.mealie || []) {
|
|
const k = t.toLowerCase();
|
|
if (!seen[k]) seen[k] = { name: t, sources: [] };
|
|
seen[k].sources.push('M');
|
|
}
|
|
for (const t of existingTags.tandoor || []) {
|
|
const k = t.toLowerCase();
|
|
if (!seen[k]) seen[k] = { name: t, sources: [] };
|
|
seen[k].sources.push('T');
|
|
}
|
|
|
|
const matches = Object.values(seen)
|
|
.filter(e => e.name.toLowerCase().includes(q) && !allNames.includes(e.name.toLowerCase()))
|
|
.slice(0, 10);
|
|
|
|
let html = '';
|
|
for (const m of matches) {
|
|
const src = m.sources.join('+');
|
|
html += '<div class="tag-dropdown-item" onclick="selectTag(\'' + escHtml(m.name).replace(/'/g, "\\'") + '\')">'
|
|
+ '<span>' + escHtml(m.name) + '</span>'
|
|
+ '<span class="tag-source">' + src + '</span></div>';
|
|
}
|
|
|
|
const exactExists = matches.some(m => m.name.toLowerCase() === q) || allNames.includes(q);
|
|
if (!exactExists && q) {
|
|
html += '<div class="tag-dropdown-item tag-add-new" onclick="selectTag(\'' + escHtml(input.value.trim()).replace(/'/g, "\\'") + '\')">'
|
|
+ '+ "' + escHtml(input.value.trim()) + '" hozzáadása</div>';
|
|
}
|
|
|
|
dropdown.innerHTML = html;
|
|
dropdown.classList.toggle('open', html.length > 0);
|
|
}
|
|
|
|
function selectTag(name) {
|
|
addTagChip(name, true);
|
|
const input = document.getElementById('tagSearch');
|
|
input.value = '';
|
|
document.getElementById('tagDropdown').classList.remove('open');
|
|
input.focus();
|
|
}
|
|
|
|
function onTagKeydown(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
const val = e.target.value.trim();
|
|
if (val) {
|
|
addTagChip(val, true);
|
|
e.target.value = '';
|
|
document.getElementById('tagDropdown').classList.remove('open');
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
document.getElementById('tagDropdown').classList.remove('open');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.tag-search-wrap')) {
|
|
document.getElementById('tagDropdown').classList.remove('open');
|
|
}
|
|
});
|
|
|
|
loadExistingTags();
|
|
|
|
// =========================================================================
|
|
// Bulk import
|
|
// =========================================================================
|
|
|
|
const bulkState = {
|
|
urls: [],
|
|
currentIndex: -1,
|
|
mode: 'review', // 'review' | 'auto'
|
|
target: '', // 'mealie' | 'tandoor' | 'both'
|
|
autoTags: true,
|
|
running: false,
|
|
stopped: false,
|
|
results: [], // {url, status, title, error, links}
|
|
scrapeCache: [], // cached scrape responses for review→auto switch
|
|
};
|
|
|
|
const STATUS_LABELS = {
|
|
pending: ['Várakozik', 'status-pending'],
|
|
scraping: ['Beolvasás...', 'status-scraping'],
|
|
reviewing: ['Átnézés...', 'status-reviewing'],
|
|
importing: ['Importálás...', 'status-importing'],
|
|
done: ['Kész', 'status-done'],
|
|
error: ['Hiba', 'status-error'],
|
|
skipped: ['Kihagyva', 'status-skipped'],
|
|
duplicate: ['Duplikátum', 'status-duplicate'],
|
|
};
|
|
|
|
function showBulkButtons() {
|
|
document.getElementById('singleActions').style.display = 'none';
|
|
document.getElementById('bulkActions').style.display = 'flex';
|
|
}
|
|
|
|
function showSingleButtons() {
|
|
document.getElementById('singleActions').style.display = '';
|
|
document.getElementById('bulkActions').style.display = 'none';
|
|
}
|
|
|
|
function isValidUrl(str) {
|
|
try { const u = new URL(str); return u.protocol === 'http:' || u.protocol === 'https:'; }
|
|
catch { return false; }
|
|
}
|
|
|
|
function shortUrl(url) {
|
|
try {
|
|
const u = new URL(url);
|
|
const path = u.pathname.length > 35 ? u.pathname.substring(0, 35) + '...' : u.pathname;
|
|
return u.hostname + path;
|
|
} catch { return url.substring(0, 50); }
|
|
}
|
|
|
|
function updateRowStatus(idx, status, errorMsg) {
|
|
const row = document.getElementById('bulk-row-' + idx);
|
|
if (!row) return;
|
|
const td = row.querySelector('.bulk-status');
|
|
const [label, cls] = STATUS_LABELS[status] || ['?', 'status-pending'];
|
|
let html = '<span class="status-badge ' + cls + '">' + label + '</span>';
|
|
if (status === 'error' && errorMsg) {
|
|
html += '<br><small class="text-danger" style="font-size:0.75rem">' + escHtml(errorMsg).substring(0, 80) + '</small>';
|
|
}
|
|
td.innerHTML = html;
|
|
bulkState.results[idx].status = status;
|
|
}
|
|
|
|
function updateRowTitle(idx, title) {
|
|
const row = document.getElementById('bulk-row-' + idx);
|
|
if (!row) return;
|
|
row.querySelector('.bulk-title').textContent = title || '';
|
|
bulkState.results[idx].title = title || '';
|
|
}
|
|
|
|
function updateRowLinks(idx, links) {
|
|
const row = document.getElementById('bulk-row-' + idx);
|
|
if (!row) return;
|
|
const td = row.querySelector('.bulk-links');
|
|
const parts = [];
|
|
for (const [name, url] of Object.entries(links)) {
|
|
parts.push('<a href="' + escHtml(url) + '" target="_blank" style="color:var(--accent);font-size:0.8rem">' + name + '</a>');
|
|
}
|
|
td.innerHTML = parts.join(' ');
|
|
}
|
|
|
|
function updateBulkCounter() {
|
|
const total = bulkState.urls.length;
|
|
const done = bulkState.results.filter(r => r.status === 'done').length;
|
|
const errors = bulkState.results.filter(r => r.status === 'error').length;
|
|
const skipped = bulkState.results.filter(r => r.status === 'skipped' || r.status === 'duplicate').length;
|
|
|
|
let text = done + ' / ' + total + ' kész';
|
|
const parts = [];
|
|
if (errors > 0) parts.push(errors + ' hiba');
|
|
if (skipped > 0) parts.push(skipped + ' kihagyva');
|
|
if (parts.length > 0) text += ' (' + parts.join(', ') + ')';
|
|
|
|
document.getElementById('bulkCounter').textContent = text;
|
|
}
|
|
|
|
function showBulkSummary() {
|
|
const total = bulkState.urls.length;
|
|
const done = bulkState.results.filter(r => r.status === 'done').length;
|
|
const errors = bulkState.results.filter(r => r.status === 'error').length;
|
|
const skipped = bulkState.results.filter(r => r.status === 'skipped').length;
|
|
const dups = bulkState.results.filter(r => r.status === 'duplicate').length;
|
|
|
|
let html = '<strong>Összegzés:</strong> ' + total + ' URL feldolgozva — ';
|
|
html += '<span class="text-success">' + done + ' importálva</span>';
|
|
if (errors > 0) html += ', <span class="text-danger">' + errors + ' hiba</span>';
|
|
if (skipped > 0) html += ', ' + skipped + ' kihagyva';
|
|
if (dups > 0) html += ', ' + dups + ' duplikátum';
|
|
|
|
const summary = document.getElementById('bulkSummary');
|
|
summary.innerHTML = html;
|
|
summary.style.display = '';
|
|
}
|
|
|
|
function startBulk(target) {
|
|
const text = document.getElementById('bulkUrls').value.trim();
|
|
if (!text) { alert('Add meg az URL-eket!'); return; }
|
|
|
|
// Parse & validate URLs
|
|
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
const invalid = lines.filter(l => !isValidUrl(l));
|
|
if (invalid.length > 0) {
|
|
alert('Érvénytelen URL-ek:\n\n' + invalid.join('\n'));
|
|
return;
|
|
}
|
|
|
|
// Deduplicate
|
|
const urls = [...new Set(lines)];
|
|
if (urls.length === 0) return;
|
|
|
|
const mode = document.querySelector('input[name="bulkMode"]:checked').value;
|
|
const autoTags = document.querySelector('input[name="bulkTags"]:checked').value === 'all';
|
|
|
|
// Init state
|
|
bulkState.urls = urls;
|
|
bulkState.currentIndex = -1;
|
|
bulkState.mode = mode;
|
|
bulkState.target = target;
|
|
bulkState.autoTags = autoTags;
|
|
bulkState.running = true;
|
|
bulkState.stopped = false;
|
|
bulkState.results = urls.map(u => ({ url: u, status: 'pending', title: '', error: '', links: {} }));
|
|
bulkState.scrapeCache = new Array(urls.length).fill(null);
|
|
|
|
// Build table
|
|
const tbody = document.getElementById('bulkTableBody');
|
|
tbody.innerHTML = '';
|
|
urls.forEach((u, i) => {
|
|
const tr = document.createElement('tr');
|
|
tr.id = 'bulk-row-' + i;
|
|
tr.innerHTML = '<td>' + (i + 1) + '</td>'
|
|
+ '<td class="url-cell" title="' + escHtml(u) + '">' + escHtml(shortUrl(u)) + '</td>'
|
|
+ '<td class="bulk-title"></td>'
|
|
+ '<td class="bulk-status"><span class="status-badge status-pending">Várakozik</span></td>'
|
|
+ '<td class="bulk-links"></td>';
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
// Show progress, hide preview
|
|
document.getElementById('bulkProgress').style.display = '';
|
|
document.getElementById('bulkSummary').style.display = 'none';
|
|
document.getElementById('bulkStopBtn').style.display = '';
|
|
document.getElementById('bulkAutoRemaining').style.display = mode === 'review' ? '' : 'none';
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('resultCard').classList.remove('visible');
|
|
|
|
// Disable start buttons
|
|
document.querySelectorAll('#tabBulk .btn-success').forEach(b => b.disabled = true);
|
|
|
|
updateBulkCounter();
|
|
processNext();
|
|
}
|
|
|
|
function processNext() {
|
|
if (bulkState.stopped) {
|
|
finishBulk();
|
|
return;
|
|
}
|
|
|
|
bulkState.currentIndex++;
|
|
const idx = bulkState.currentIndex;
|
|
|
|
if (idx >= bulkState.urls.length) {
|
|
finishBulk();
|
|
return;
|
|
}
|
|
|
|
// Highlight active row
|
|
document.querySelectorAll('.bulk-table tr.row-active').forEach(r => r.classList.remove('row-active'));
|
|
const row = document.getElementById('bulk-row-' + idx);
|
|
if (row) row.classList.add('row-active');
|
|
|
|
scrapeForBulk(idx);
|
|
}
|
|
|
|
async function scrapeForBulk(idx) {
|
|
updateRowStatus(idx, 'scraping');
|
|
updateBulkCounter();
|
|
|
|
try {
|
|
const form = new FormData();
|
|
form.append('url', bulkState.urls[idx]);
|
|
const resp = await fetch('/scrape', { method: 'POST', body: form });
|
|
const data = await resp.json();
|
|
|
|
if (!data.ok) {
|
|
updateRowStatus(idx, 'error', data.error || 'Ismeretlen hiba');
|
|
updateBulkCounter();
|
|
processNext();
|
|
return;
|
|
}
|
|
|
|
updateRowTitle(idx, data.data.title);
|
|
bulkState.scrapeCache[idx] = data;
|
|
|
|
if (bulkState.mode === 'auto') {
|
|
// Check duplicates
|
|
const isDup = isDuplicateForTarget(data, bulkState.target);
|
|
if (isDup) {
|
|
updateRowStatus(idx, 'duplicate');
|
|
updateBulkCounter();
|
|
processNext();
|
|
return;
|
|
}
|
|
|
|
// Strip tags if needed
|
|
const recipe = data.data;
|
|
if (!bulkState.autoTags) {
|
|
recipe.tags = [];
|
|
}
|
|
|
|
await sendForBulk(idx, recipe, data.duplicate, data.tandoor_duplicate);
|
|
} else {
|
|
// Review mode: show preview
|
|
currentRecipe = data.data;
|
|
populatePreview(data.data);
|
|
showBulkButtons();
|
|
document.getElementById('previewCard').classList.add('visible');
|
|
document.getElementById('previewCard').scrollIntoView({ behavior: 'smooth' });
|
|
|
|
// Show duplicate warning in bulk status
|
|
const bulkStatus = document.getElementById('bulkSendStatus');
|
|
const dupWarnings = [];
|
|
if (data.duplicate && (bulkState.target === 'mealie' || bulkState.target === 'both')) {
|
|
dupWarnings.push('Mealie: ' + escHtml(data.duplicate.name));
|
|
}
|
|
if (data.tandoor_duplicate && (bulkState.target === 'tandoor' || bulkState.target === 'both')) {
|
|
dupWarnings.push('Tandoor: ' + escHtml(data.tandoor_duplicate.name));
|
|
}
|
|
if (dupWarnings.length > 0) {
|
|
bulkStatus.innerHTML = '<span class="text-warning" style="font-size:0.85rem">⚠ Duplikátum: ' + dupWarnings.join(' | ') + '</span>';
|
|
} else {
|
|
bulkStatus.innerHTML = '';
|
|
}
|
|
|
|
updateRowStatus(idx, 'reviewing');
|
|
// Wait for user: approveAndContinue() or skipCurrent()
|
|
}
|
|
} catch (e) {
|
|
updateRowStatus(idx, 'error', e.message);
|
|
updateBulkCounter();
|
|
processNext();
|
|
}
|
|
}
|
|
|
|
function isDuplicateForTarget(scrapeData, target) {
|
|
if (target === 'mealie') return !!scrapeData.duplicate;
|
|
if (target === 'tandoor') return !!scrapeData.tandoor_duplicate;
|
|
if (target === 'both') return !!scrapeData.duplicate && !!scrapeData.tandoor_duplicate;
|
|
return false;
|
|
}
|
|
|
|
async function sendForBulk(idx, recipe, mealieDup, tandoorDup) {
|
|
updateRowStatus(idx, 'importing');
|
|
const target = bulkState.target;
|
|
const links = {};
|
|
const errors = [];
|
|
|
|
try {
|
|
if ((target === 'mealie' || target === 'both') && !mealieDup) {
|
|
const resp = await fetch('/send', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(recipe),
|
|
});
|
|
const data = await resp.json();
|
|
if (data.ok) {
|
|
links['Mealie'] = data.url;
|
|
} else {
|
|
errors.push('Mealie: ' + (data.error || 'ismeretlen hiba'));
|
|
}
|
|
}
|
|
|
|
if ((target === 'tandoor' || target === 'both') && !tandoorDup) {
|
|
const resp = await fetch('/send-tandoor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(recipe),
|
|
});
|
|
const data = await resp.json();
|
|
if (data.ok) {
|
|
links['Tandoor'] = data.url;
|
|
} else {
|
|
errors.push('Tandoor: ' + (data.error || 'ismeretlen hiba'));
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0 && Object.keys(links).length === 0) {
|
|
updateRowStatus(idx, 'error', errors.join('; '));
|
|
} else {
|
|
updateRowStatus(idx, 'done');
|
|
updateRowLinks(idx, links);
|
|
bulkState.results[idx].links = links;
|
|
}
|
|
} catch (e) {
|
|
updateRowStatus(idx, 'error', e.message);
|
|
}
|
|
|
|
updateBulkCounter();
|
|
processNext();
|
|
}
|
|
|
|
function approveAndContinue() {
|
|
const idx = bulkState.currentIndex;
|
|
const recipe = gatherRecipe();
|
|
if (!recipe.title) { alert('A recept neve kötelező!'); return; }
|
|
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('bulkSendStatus').innerHTML = '';
|
|
|
|
const cached = bulkState.scrapeCache[idx];
|
|
sendForBulk(idx, recipe, null, null);
|
|
}
|
|
|
|
function skipCurrent() {
|
|
const idx = bulkState.currentIndex;
|
|
updateRowStatus(idx, 'skipped');
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('bulkSendStatus').innerHTML = '';
|
|
updateBulkCounter();
|
|
processNext();
|
|
}
|
|
|
|
function autoImportRemaining() {
|
|
// Read tag preference
|
|
const autoTags = document.querySelector('input[name="bulkTags"]:checked').value === 'all';
|
|
bulkState.autoTags = autoTags;
|
|
bulkState.mode = 'auto';
|
|
document.getElementById('bulkAutoRemaining').style.display = 'none';
|
|
|
|
const idx = bulkState.currentIndex;
|
|
if (bulkState.results[idx] && bulkState.results[idx].status === 'reviewing') {
|
|
// Currently reviewing — gather and send current recipe
|
|
const recipe = gatherRecipe();
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('bulkSendStatus').innerHTML = '';
|
|
|
|
if (recipe.title) {
|
|
if (!bulkState.autoTags) recipe.tags = [];
|
|
sendForBulk(idx, recipe, null, null);
|
|
} else {
|
|
updateRowStatus(idx, 'skipped');
|
|
updateBulkCounter();
|
|
processNext();
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopBulk() {
|
|
bulkState.stopped = true;
|
|
document.getElementById('bulkStopBtn').style.display = 'none';
|
|
}
|
|
|
|
function finishBulk() {
|
|
bulkState.running = false;
|
|
document.getElementById('previewCard').classList.remove('visible');
|
|
document.getElementById('bulkStopBtn').style.display = 'none';
|
|
document.getElementById('bulkAutoRemaining').style.display = 'none';
|
|
showSingleButtons();
|
|
|
|
// Re-enable start buttons
|
|
document.querySelectorAll('#tabBulk .btn-success').forEach(b => b.disabled = false);
|
|
|
|
// Remove active row highlight
|
|
document.querySelectorAll('.bulk-table tr.row-active').forEach(r => r.classList.remove('row-active'));
|
|
|
|
showBulkSummary();
|
|
updateBulkCounter();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Utilities
|
|
// =========================================================================
|
|
|
|
function escHtml(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
</script>
|
|
{% endblock %}
|