feat: add download functionality for attachments, images, and PDFs; update version to 1.5.4
This commit is contained in:
@@ -26,10 +26,10 @@
|
||||
"settings_preview_page_description": "Modify settings related to the preview page",
|
||||
"settings_preview_builtin_label": "Use built-in preview for images",
|
||||
"settings_preview_builtin_hint": "Uses EMLy's built-in image previewer for supported image file types.",
|
||||
"settings_preview_builtin_info": "Info: If disabled, image files will be treated as downloads instead of being previewed within the app.",
|
||||
"settings_preview_builtin_info": "Info: If disabled, image files will be opened by the computer's default app instead of being previewed within the app.",
|
||||
"settings_preview_pdf_builtin_label": "Use built-in viewer for PDFs",
|
||||
"settings_preview_pdf_builtin_hint": "Uses EMLy's built-in viewer for PDF files.",
|
||||
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be treated as downloads instead of being previewed within the app.",
|
||||
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be opened by the computer's default app instead of being previewed within the app.",
|
||||
"settings_msg_converter_title": "MSG Handling",
|
||||
"settings_msg_converter_description": "Configure how MSG files are processed.",
|
||||
"settings_msg_converter_label": "Use MSG to EML converter",
|
||||
@@ -69,6 +69,8 @@
|
||||
"mail_open_btn_title": "Open another file",
|
||||
"mail_close_btn_label": "Close",
|
||||
"mail_close_btn_title": "Close",
|
||||
"mail_download_btn_label": "Download",
|
||||
"mail_download_btn_title": "Download",
|
||||
"mail_from": "From:",
|
||||
"mail_to": "To:",
|
||||
"mail_cc": "Cc:",
|
||||
@@ -79,8 +81,9 @@
|
||||
"mail_error_image": "Failed to open image file.",
|
||||
"settings_toast_language_changed": "Language changed successfully!",
|
||||
"settings_toast_language_change_failed": "Failed to change language.",
|
||||
"mail_open_btn_text": "Open EML/MSG File",
|
||||
"mail_open_btn_text": "Open File",
|
||||
"mail_close_btn_text": "Close",
|
||||
"mail_download_btn_text": "Download",
|
||||
"settings_danger_reset_dialog_description_part1": "This action cannot be undone.",
|
||||
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state.",
|
||||
"mail_error_opening": "Failed to open EML file.",
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"settings_preview_page_description": "Modifica le impostazioni relative alla pagina di anteprima",
|
||||
"settings_preview_builtin_label": "Usa anteprima integrata per le immagini",
|
||||
"settings_preview_builtin_hint": "Usa il visualizzatore di immagini integrato di EMLy per i tipi di file immagini supportati.",
|
||||
"settings_preview_builtin_info": "Info: Se disabilitato, i file immagine verranno trattati come download anziché essere visualizzati all'interno dell'app.",
|
||||
"settings_preview_builtin_info": "Info: Se disabilitato, i file immagine verranno aperti tramite l'app di default attuale anziché essere visualizzati all'interno dell'app.",
|
||||
"settings_preview_pdf_builtin_label": "Usa visualizzatore integrato per PDF",
|
||||
"settings_preview_pdf_builtin_hint": "Usa il visualizzatore integrato di EMLy per i file PDF.",
|
||||
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno trattati come download invece di essere visualizzati nell'app.",
|
||||
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno aperti tramite l'app di default attuale invece di essere visualizzati nell'app.",
|
||||
"settings_msg_converter_title": "Gestione MSG",
|
||||
"settings_msg_converter_description": "Configura come vengono elaborati i file MSG.",
|
||||
"settings_msg_converter_label": "Usa convertitore MSG in EML",
|
||||
@@ -79,7 +79,7 @@
|
||||
"mail_error_image": "Impossibile aprire il file immagine.",
|
||||
"settings_toast_language_changed": "Lingua cambiata con successo!",
|
||||
"settings_toast_language_change_failed": "Impossibile cambiare lingua.",
|
||||
"mail_open_btn_text": "Apri file EML/MSG",
|
||||
"mail_open_btn_text": "Apri file",
|
||||
"mail_close_btn_text": "Chiudi",
|
||||
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.",
|
||||
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito.",
|
||||
@@ -215,5 +215,9 @@
|
||||
"pdf_error_no_data_desc": "Nessun dato PDF fornito. Apri questa finestra dall'applicazione principale EMLy.",
|
||||
"pdf_error_timeout": "Timeout caricamento PDF. Il worker potrebbe non essersi inizializzato correttamente.",
|
||||
"pdf_error_parsing": "Errore nel parsing del PDF: ",
|
||||
"pdf_error_rendering": "Errore nel rendering della pagina: "
|
||||
"pdf_error_rendering": "Errore nel rendering della pagina: ",
|
||||
"mail_download_btn_label": "Scarica",
|
||||
"mail_download_btn_title": "Scarica",
|
||||
"mail_download_btn_text": "Scarica"
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
Signature,
|
||||
FileCode,
|
||||
Loader2,
|
||||
Download,
|
||||
} from '@lucide/svelte';
|
||||
import { sidebarOpen } from '$lib/stores/app';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@@ -35,6 +36,7 @@
|
||||
isEmailFile,
|
||||
} from '$lib/utils/mail';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
@@ -59,6 +61,21 @@
|
||||
mailState.clear();
|
||||
}
|
||||
|
||||
function onDownloadAttachments() {
|
||||
if (!mailState.currentEmail || !mailState.currentEmail.attachments) return;
|
||||
|
||||
mailState.currentEmail.attachments.forEach((att) => {
|
||||
const base64 = arrayBufferToBase64(att.data);
|
||||
const dataUrl = createDataUrl(att.contentType, base64);
|
||||
const link = document.createElement('a');
|
||||
link.href = dataUrl;
|
||||
link.download = att.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
async function onOpenMail() {
|
||||
isLoading = true;
|
||||
loadingText = m.layout_loading_text();
|
||||
@@ -224,6 +241,16 @@
|
||||
{mailState.currentEmail.subject || m.mail_subject_no_subject()}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
class="btn"
|
||||
onclick={onDownloadAttachments}
|
||||
aria-label={m.mail_download_btn_label()}
|
||||
title={m.mail_download_btn_title()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Download size="15" />
|
||||
{m.mail_download_btn_text()}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
onclick={onOpenMail}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
AlignHorizontalSpaceAround,
|
||||
Download
|
||||
} from "@lucide/svelte";
|
||||
import { sidebarOpen } from "$lib/stores/app";
|
||||
import { toast } from "svelte-sonner";
|
||||
@@ -84,6 +85,17 @@
|
||||
fitToScreen();
|
||||
}
|
||||
|
||||
function downloadImage() {
|
||||
if (!imageData || !filename) return;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = `data:image/png;base64,${imageData}`;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
const delta = -e.deltaY * 0.001;
|
||||
@@ -116,6 +128,10 @@
|
||||
<h1 class="title" title={filename}>{filename || "Image Viewer"}</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn" onclick={() => downloadImage()} title="Download">
|
||||
<Download size="16" />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button class="btn" onclick={() => zoom(0.1)} title="Zoom In">
|
||||
<ZoomIn size="16" />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, untrack } from "svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import {
|
||||
RotateCcw,
|
||||
@@ -7,6 +7,7 @@
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
AlignHorizontalSpaceAround,
|
||||
Download
|
||||
} from "@lucide/svelte";
|
||||
import { sidebarOpen } from "$lib/stores/app";
|
||||
import { toast } from "svelte-sonner";
|
||||
@@ -106,7 +107,13 @@
|
||||
if (!pdfDoc || !canvasRef) return;
|
||||
|
||||
if (renderTask) {
|
||||
await renderTask.promise.catch(() => {}); // Cancel previous render if any (though we wait usually)
|
||||
// Cancel previous render if any and await its cleanup
|
||||
renderTask.cancel();
|
||||
try {
|
||||
await renderTask.promise;
|
||||
} catch (e) {
|
||||
// Expected cancellation error
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -130,7 +137,9 @@
|
||||
};
|
||||
|
||||
// Cast to any to avoid type mismatch with PDF.js definitions
|
||||
await page.render(renderContext as any).promise;
|
||||
const task = page.render(renderContext as any);
|
||||
renderTask = task;
|
||||
await task.promise;
|
||||
} catch (e: any) {
|
||||
if (e.name !== "RenderingCancelledException") {
|
||||
console.error(e);
|
||||
@@ -155,11 +164,15 @@
|
||||
|
||||
$effect(() => {
|
||||
// Re-render when scale or rotation changes
|
||||
// Access them here to ensure dependency tracking since renderPage is async
|
||||
const _deps = [scale, rotation];
|
||||
// Access them here to ensure dependency tracking since renderPage is untracked
|
||||
// We also track pageNum to ensure we re-render if it changes via other means,
|
||||
// although navigation functions usually call renderPage manually.
|
||||
const _deps = [scale, rotation, pageNum];
|
||||
|
||||
if (pdfDoc) {
|
||||
renderPage(pageNum);
|
||||
// Untrack renderPage because it reads and writes to renderTask,
|
||||
// which would otherwise cause an infinite loop.
|
||||
untrack(() => renderPage(pageNum));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,6 +195,24 @@
|
||||
pageNum--;
|
||||
renderPage(pageNum);
|
||||
}
|
||||
|
||||
function downloadPDF() {
|
||||
if (!pdfData) return;
|
||||
try {
|
||||
// @ts-ignore
|
||||
const blob = new Blob([pdfData], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename || "document.pdf";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
toast.error("Failed to download PDF: " + e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="viewer-container">
|
||||
@@ -202,6 +233,10 @@
|
||||
<h1 class="title" title={filename}>{filename || m.pdf_viewer_title()}</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn" onclick={() => downloadPDF()} title={m.mail_download_btn_title()}>
|
||||
<Download size="16" />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button class="btn" onclick={() => zoom(0.1)} title={m.pdf_zoom_in()}>
|
||||
<ZoomIn size="16" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user