feat: add download functionality for attachments, images, and PDFs; update version to 1.5.4

This commit is contained in:
Flavio Fois
2026-02-10 22:31:36 +01:00
parent b68c173d2a
commit 402a90cf4b
8 changed files with 101 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
# New Features # New Features
- [ ] Add an option to select the folder to save Attachments to, instead of always saving to the Downloads folder. - [ ] Add an option to select the folder to save Attachments to, instead of always saving to the Downloads folder.
- [ ] Create a sorta of "Bug Reporter" loader, to load the .ZIP file with the Bug Report data, and replicate the same enviroment as the user, to investigate the issue. (EML file, settings) - [ ] Create a sorta of "Bug Reporter" loader, to load the .ZIP file with the Bug Report data, and replicate the same enviroment as the user, to investigate the issue. (EML file, settings)
- [x] Add a "Download" button to the MailViewer, PDF and Image viewer, to download the file instead of just opening it.
# Existing Features # Existing Features
- [ ] Add seperated "Updater" binary, that will start on User login (via Scheduled Task), with a silent install mode. - [ ] Add seperated "Updater" binary, that will start on User login (via Scheduled Task), with a silent install mode.

View File

@@ -1,7 +1,7 @@
[EMLy] [EMLy]
SDK_DECODER_SEMVER = 1.3.2 SDK_DECODER_SEMVER = 1.3.2
SDK_DECODER_RELEASE_CHANNEL = stable SDK_DECODER_RELEASE_CHANNEL = stable
GUI_SEMVER = 1.5.3 GUI_SEMVER = 1.5.4
GUI_RELEASE_CHANNEL = beta GUI_RELEASE_CHANNEL = beta
LANGUAGE = it LANGUAGE = it
UPDATE_CHECK_ENABLED = false UPDATE_CHECK_ENABLED = false

View File

@@ -26,10 +26,10 @@
"settings_preview_page_description": "Modify settings related to the preview page", "settings_preview_page_description": "Modify settings related to the preview page",
"settings_preview_builtin_label": "Use built-in preview for images", "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_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_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_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_title": "MSG Handling",
"settings_msg_converter_description": "Configure how MSG files are processed.", "settings_msg_converter_description": "Configure how MSG files are processed.",
"settings_msg_converter_label": "Use MSG to EML converter", "settings_msg_converter_label": "Use MSG to EML converter",
@@ -69,6 +69,8 @@
"mail_open_btn_title": "Open another file", "mail_open_btn_title": "Open another file",
"mail_close_btn_label": "Close", "mail_close_btn_label": "Close",
"mail_close_btn_title": "Close", "mail_close_btn_title": "Close",
"mail_download_btn_label": "Download",
"mail_download_btn_title": "Download",
"mail_from": "From:", "mail_from": "From:",
"mail_to": "To:", "mail_to": "To:",
"mail_cc": "Cc:", "mail_cc": "Cc:",
@@ -79,8 +81,9 @@
"mail_error_image": "Failed to open image file.", "mail_error_image": "Failed to open image file.",
"settings_toast_language_changed": "Language changed successfully!", "settings_toast_language_changed": "Language changed successfully!",
"settings_toast_language_change_failed": "Failed to change language.", "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_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_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.", "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.", "mail_error_opening": "Failed to open EML file.",

View File

@@ -26,10 +26,10 @@
"settings_preview_page_description": "Modifica le impostazioni relative alla pagina di anteprima", "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_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_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_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_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_title": "Gestione MSG",
"settings_msg_converter_description": "Configura come vengono elaborati i file MSG.", "settings_msg_converter_description": "Configura come vengono elaborati i file MSG.",
"settings_msg_converter_label": "Usa convertitore MSG in EML", "settings_msg_converter_label": "Usa convertitore MSG in EML",
@@ -79,7 +79,7 @@
"mail_error_image": "Impossibile aprire il file immagine.", "mail_error_image": "Impossibile aprire il file immagine.",
"settings_toast_language_changed": "Lingua cambiata con successo!", "settings_toast_language_changed": "Lingua cambiata con successo!",
"settings_toast_language_change_failed": "Impossibile cambiare lingua.", "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", "mail_close_btn_text": "Chiudi",
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.", "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.", "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_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_timeout": "Timeout caricamento PDF. Il worker potrebbe non essersi inizializzato correttamente.",
"pdf_error_parsing": "Errore nel parsing del PDF: ", "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"
} }

View File

@@ -9,6 +9,7 @@
Signature, Signature,
FileCode, FileCode,
Loader2, Loader2,
Download,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { sidebarOpen } from '$lib/stores/app'; import { sidebarOpen } from '$lib/stores/app';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@@ -35,6 +36,7 @@
isEmailFile, isEmailFile,
} from '$lib/utils/mail'; } from '$lib/utils/mail';
import { settingsStore } from '$lib/stores/settings.svelte'; import { settingsStore } from '$lib/stores/settings.svelte';
import { Separator } from "$lib/components/ui/separator";
// ============================================================================ // ============================================================================
// State // State
@@ -59,6 +61,21 @@
mailState.clear(); 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() { async function onOpenMail() {
isLoading = true; isLoading = true;
loadingText = m.layout_loading_text(); loadingText = m.layout_loading_text();
@@ -224,6 +241,16 @@
{mailState.currentEmail.subject || m.mail_subject_no_subject()} {mailState.currentEmail.subject || m.mail_subject_no_subject()}
</div> </div>
<div class="controls"> <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 <button
class="btn" class="btn"
onclick={onOpenMail} onclick={onOpenMail}

View File

@@ -7,6 +7,7 @@
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
AlignHorizontalSpaceAround, AlignHorizontalSpaceAround,
Download
} from "@lucide/svelte"; } from "@lucide/svelte";
import { sidebarOpen } from "$lib/stores/app"; import { sidebarOpen } from "$lib/stores/app";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
@@ -84,6 +85,17 @@
fitToScreen(); 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) { function handleWheel(e: WheelEvent) {
e.preventDefault(); e.preventDefault();
const delta = -e.deltaY * 0.001; const delta = -e.deltaY * 0.001;
@@ -116,6 +128,10 @@
<h1 class="title" title={filename}>{filename || "Image Viewer"}</h1> <h1 class="title" title={filename}>{filename || "Image Viewer"}</h1>
<div class="controls"> <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"> <button class="btn" onclick={() => zoom(0.1)} title="Zoom In">
<ZoomIn size="16" /> <ZoomIn size="16" />
</button> </button>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount, untrack } from "svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { import {
RotateCcw, RotateCcw,
@@ -7,6 +7,7 @@
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
AlignHorizontalSpaceAround, AlignHorizontalSpaceAround,
Download
} from "@lucide/svelte"; } from "@lucide/svelte";
import { sidebarOpen } from "$lib/stores/app"; import { sidebarOpen } from "$lib/stores/app";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
@@ -106,7 +107,13 @@
if (!pdfDoc || !canvasRef) return; if (!pdfDoc || !canvasRef) return;
if (renderTask) { 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 { try {
@@ -130,7 +137,9 @@
}; };
// Cast to any to avoid type mismatch with PDF.js definitions // 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) { } catch (e: any) {
if (e.name !== "RenderingCancelledException") { if (e.name !== "RenderingCancelledException") {
console.error(e); console.error(e);
@@ -155,11 +164,15 @@
$effect(() => { $effect(() => {
// Re-render when scale or rotation changes // Re-render when scale or rotation changes
// Access them here to ensure dependency tracking since renderPage is async // Access them here to ensure dependency tracking since renderPage is untracked
const _deps = [scale, rotation]; // 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) { 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--; pageNum--;
renderPage(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> </script>
<div class="viewer-container"> <div class="viewer-container">
@@ -202,6 +233,10 @@
<h1 class="title" title={filename}>{filename || m.pdf_viewer_title()}</h1> <h1 class="title" title={filename}>{filename || m.pdf_viewer_title()}</h1>
<div class="controls"> <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()}> <button class="btn" onclick={() => zoom(0.1)} title={m.pdf_zoom_in()}>
<ZoomIn size="16" /> <ZoomIn size="16" />
</button> </button>

View File

@@ -1,6 +1,6 @@
#define ApplicationName 'EMLy' #define ApplicationName 'EMLy'
#define ApplicationVersion GetVersionNumbersString('EMLy.exe') #define ApplicationVersion GetVersionNumbersString('EMLy.exe')
#define ApplicationVersion '1.5.3_beta' #define ApplicationVersion '1.5.4_beta'
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"