64577f2d92
Replace all hardcoded English visible text with {{ _("key") }} calls in:
- queue_dashboard.html: headings, stat cards, table headers, empty states
- api_tokens.html: form labels, table headers, status badges, aria-labels
- similarity_dashboard.html: headings, stat labels, controls, empty states
- backup.html: buttons, config labels, table, retention section, confirm dialog
- shared_links.html: form labels/options, table headers, status badges
- duplicates.html: tabs, role tags, near-dup form, explanation bullets
Follows i18n rules: placeholder/aria-label/title attrs translated,
JS strings in <script> left as-is, Alpine.js bindings use Jinja2
server-side evaluation pattern for dynamic translated strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
278 lines
9.5 KiB
HTML
278 lines
9.5 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ _("upload.page_title") }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="flex flex-col items-center justify-center p-4 sm:p-8">
|
|
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8">{{ _("upload.page_title") }}</h1>
|
|
|
|
<!-- File Upload Section -->
|
|
<div class="w-full max-w-2xl mb-6 sm:mb-8">
|
|
<h2 class="text-xl font-semibold mb-4">{{ _("upload.section_device") }}</h2>
|
|
<form action="/api/ui-upload" method="POST" enctype="multipart/form-data">
|
|
<div
|
|
id="dropZone"
|
|
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full"
|
|
role="button"
|
|
tabindex="0"
|
|
aria-label="{{ _('upload.drop_zone_aria') }}"
|
|
ondragover="handleDragOver(event)"
|
|
ondragleave="handleDragLeave(event)"
|
|
>
|
|
<p class="text-gray-500 mb-4 hidden sm:block">
|
|
{{ _("upload.drop_hint_desktop") }}
|
|
</p>
|
|
<p class="text-gray-500 mb-4 sm:hidden">
|
|
{{ _("upload.drop_hint_mobile") }}
|
|
</p>
|
|
<input
|
|
id="fileInput"
|
|
type="file"
|
|
class="hidden"
|
|
onchange="handleFileSelect(event)"
|
|
name="files"
|
|
multiple
|
|
aria-label="{{ _('upload.select_file') }}"
|
|
accept="application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,image/*"
|
|
/>
|
|
<!-- Browse Files button (all devices) -->
|
|
<button
|
|
type="button"
|
|
onclick="document.getElementById('fileInput').click()"
|
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 mb-4"
|
|
style="min-height:44px;"
|
|
>
|
|
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> {{ _("upload.browse_button") }}
|
|
</button>
|
|
<div class="text-sm text-gray-500 mt-2">
|
|
<p>{{ _("upload.file_types") }}</p>
|
|
<p>{{ _("upload.file_size_hint") }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Camera capture button - mobile only -->
|
|
<div class="mt-3 sm:hidden">
|
|
<label
|
|
class="w-full flex items-center justify-center gap-2 bg-green-500 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 cursor-pointer"
|
|
style="min-height:44px;"
|
|
>
|
|
<i class="fas fa-camera" aria-hidden="true"></i> {{ _("upload.camera_button") }}
|
|
<input
|
|
id="cameraInput"
|
|
type="file"
|
|
class="hidden"
|
|
onchange="handleFileSelect(event)"
|
|
accept="image/*"
|
|
capture="environment"
|
|
aria-label="{{ _('upload.camera_button') }}"
|
|
/>
|
|
</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- URL Upload Section -->
|
|
<div class="w-full max-w-2xl mb-6 sm:mb-8">
|
|
<h2 class="text-xl font-semibold mb-4">{{ _("upload.section_url") }}</h2>
|
|
<div class="bg-white border border-gray-300 rounded-lg p-4 sm:p-6">
|
|
<form id="urlUploadForm" class="space-y-4">
|
|
<div>
|
|
<label for="urlInput" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ _("upload.url_label") }}
|
|
</label>
|
|
<input
|
|
type="url"
|
|
id="urlInput"
|
|
name="url"
|
|
placeholder="{{ _('upload.url_placeholder') }}"
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
style="min-height:44px;"
|
|
required
|
|
/>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
{{ _("upload.url_description") }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label for="urlFilename" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ _("upload.filename_label") }}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="urlFilename"
|
|
name="filename"
|
|
placeholder="{{ _('upload.filename_placeholder') }}"
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
style="min-height:44px;"
|
|
/>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
{{ _("upload.filename_description") }}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"
|
|
style="min-height:44px;"
|
|
>
|
|
{{ _("upload.download_button") }}
|
|
</button>
|
|
</form>
|
|
<div id="urlStatusMessage" class="mt-4" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="statusMessage" class="mt-4 text-gray-700" aria-live="polite"></div>
|
|
<div id="uploadProgress" class="mt-4 w-full max-w-2xl" role="status" aria-live="polite"></div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Expose server-side upload throttling configuration to upload.js
|
|
window.uploadConfig = {
|
|
concurrency: {{ upload_concurrency }},
|
|
queueDelayMs: {{ upload_queue_delay_ms }}
|
|
};
|
|
</script>
|
|
<script src="/static/js/upload.js"></script>
|
|
<script>
|
|
// Drag-and-drop / file input logic
|
|
const dropZone = document.getElementById("dropZone");
|
|
const fileInput = document.getElementById("fileInput");
|
|
const statusMessage = document.getElementById("statusMessage");
|
|
const uploadProgress = document.getElementById("uploadProgress");
|
|
|
|
dropZone.addEventListener("click", () => fileInput.click());
|
|
|
|
function handleDragOver(e) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "copy";
|
|
dropZone.classList.add("bg-gray-100");
|
|
}
|
|
|
|
function handleDragLeave(e) {
|
|
e.preventDefault();
|
|
dropZone.classList.remove("bg-gray-100");
|
|
}
|
|
|
|
dropZone.addEventListener("drop", async (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove("bg-gray-100");
|
|
|
|
const files = await getFilesFromDataTransfer(e.dataTransfer);
|
|
if (files.length) {
|
|
// Create progress container
|
|
const progressContainer = document.createElement("div");
|
|
progressContainer.className = "space-y-2";
|
|
uploadProgress.appendChild(progressContainer);
|
|
|
|
// Use the shared processFiles function (queue-based)
|
|
processFiles(files, progressContainer, statusMessage);
|
|
}
|
|
});
|
|
|
|
function handleFileSelect(e) {
|
|
if (e.target.files.length) {
|
|
// Create progress container
|
|
const progressContainer = document.createElement("div");
|
|
progressContainer.className = "space-y-2";
|
|
uploadProgress.appendChild(progressContainer);
|
|
|
|
// Use the shared processFiles function
|
|
processFiles(Array.from(e.target.files), progressContainer, statusMessage);
|
|
}
|
|
}
|
|
|
|
// URL Upload handling
|
|
const urlUploadForm = document.getElementById("urlUploadForm");
|
|
const urlStatusMessage = document.getElementById("urlStatusMessage");
|
|
|
|
urlUploadForm.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
|
|
const urlInput = document.getElementById("urlInput");
|
|
const urlFilename = document.getElementById("urlFilename");
|
|
const url = urlInput.value.trim();
|
|
const filename = urlFilename.value.trim();
|
|
|
|
if (!url) {
|
|
showUrlStatus("Please enter a URL", "error");
|
|
return;
|
|
}
|
|
|
|
// Validate URL format
|
|
try {
|
|
new URL(url);
|
|
} catch (error) {
|
|
showUrlStatus("Invalid URL format", "error");
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
showUrlStatus("Downloading file from URL...", "info");
|
|
|
|
const submitButton = urlUploadForm.querySelector('button[type="submit"]');
|
|
const originalButtonText = submitButton.textContent;
|
|
submitButton.textContent = "Processing...";
|
|
submitButton.disabled = true;
|
|
|
|
try {
|
|
const requestBody = { url };
|
|
if (filename) {
|
|
requestBody.filename = filename;
|
|
}
|
|
|
|
const response = await fetch("/api/process-url", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showUrlStatus(
|
|
`Success! File "${data.filename}" (${formatBytes(data.size)}) downloaded and queued for processing.`,
|
|
"success"
|
|
);
|
|
// Clear form
|
|
urlInput.value = "";
|
|
urlFilename.value = "";
|
|
|
|
// Optionally redirect to files page after short delay
|
|
setTimeout(() => {
|
|
window.location.href = "/files";
|
|
}, 2000);
|
|
} else {
|
|
showUrlStatus(`Error: ${data.detail || "Failed to process URL"}`, "error");
|
|
}
|
|
} catch (error) {
|
|
showUrlStatus(`Network error: ${error.message}`, "error");
|
|
} finally {
|
|
submitButton.textContent = originalButtonText;
|
|
submitButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
function showUrlStatus(message, type) {
|
|
urlStatusMessage.innerHTML = "";
|
|
const alertDiv = document.createElement("div");
|
|
alertDiv.className = `px-4 py-3 rounded ${
|
|
type === "success"
|
|
? "bg-green-100 border border-green-400 text-green-700"
|
|
: type === "error"
|
|
? "bg-red-100 border border-red-400 text-red-700"
|
|
: "bg-blue-100 border border-blue-400 text-blue-700"
|
|
}`;
|
|
alertDiv.textContent = message;
|
|
urlStatusMessage.appendChild(alertDiv);
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
// Reuse the existing formatFileSize function from upload.js
|
|
return formatFileSize(bytes);
|
|
}
|
|
</script>
|
|
{% endblock %}
|