feat: refactor MailViewer and add utility functions for email handling and attachment processing
This commit is contained in:
@@ -1,218 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { X, MailOpen, Image, FileText, File, ShieldCheck, Shield, Signature, FileCode, Loader2 } from "@lucide/svelte";
|
||||
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage, ReadMSG, ReadPEC, OpenEMLWindow, ConvertToUTF8, SetCurrentMailFilePath } from "$lib/wailsjs/go/main/App";
|
||||
import type { internal } from "$lib/wailsjs/go/models";
|
||||
import { sidebarOpen } from "$lib/stores/app";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { EventsOn, WindowShow, WindowUnminimise } from "$lib/wailsjs/runtime/runtime";
|
||||
import { mailState } from "$lib/stores/mail-state.svelte";
|
||||
import { settingsStore } from "$lib/stores/settings.svelte";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
import { dev } from "$app/environment";
|
||||
import { isBase64, isHtml } from "$lib/utils";
|
||||
import {
|
||||
X,
|
||||
MailOpen,
|
||||
Image,
|
||||
FileText,
|
||||
File,
|
||||
ShieldCheck,
|
||||
Signature,
|
||||
FileCode,
|
||||
Loader2,
|
||||
} from '@lucide/svelte';
|
||||
import { sidebarOpen } from '$lib/stores/app';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { EventsOn, WindowShow, WindowUnminimise } from '$lib/wailsjs/runtime/runtime';
|
||||
import { mailState } from '$lib/stores/mail-state.svelte';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
// Import refactored utilities
|
||||
import {
|
||||
IFRAME_UTIL_HTML,
|
||||
CONTENT_TYPES,
|
||||
PEC_FILES,
|
||||
arrayBufferToBase64,
|
||||
createDataUrl,
|
||||
openPDFAttachment,
|
||||
openImageAttachment,
|
||||
openEMLAttachment,
|
||||
openAndLoadEmail,
|
||||
loadEmailFromPath,
|
||||
processEmailBody,
|
||||
isEmailFile,
|
||||
} from '$lib/utils/mail';
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
|
||||
let unregisterEvents = () => {};
|
||||
let isLoading = $state(false);
|
||||
let loadingText = $state("");
|
||||
let loadingText = $state('');
|
||||
|
||||
|
||||
let iFrameUtilHTML = "<style>body{margin:0;padding:20px;font-family:sans-serif;} a{pointer-events:none!important;cursor:default!important;}</style><script>function handleWheel(event){if(event.ctrlKey){event.preventDefault();}}document.addEventListener('wheel',handleWheel,{passive:false});<\/script>";
|
||||
// ============================================================================
|
||||
// Event Handlers
|
||||
// ============================================================================
|
||||
|
||||
function onClear() {
|
||||
mailState.clear();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const process = async () => {
|
||||
if (mailState.currentEmail?.body) {
|
||||
let content = mailState.currentEmail.body;
|
||||
// 1. Try to decode if not HTML
|
||||
if (!isHtml(content)) {
|
||||
const clean = content.replace(/[\s\r\n]+/g, '');
|
||||
if (isBase64(clean)) {
|
||||
try {
|
||||
const decoded = window.atob(clean);
|
||||
content = decoded;
|
||||
} catch (e) {
|
||||
console.warn("Failed to decode base64 body:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If it is HTML (original or decoded), try to fix encoding
|
||||
if (isHtml(content)) {
|
||||
try {
|
||||
const fixed = await ConvertToUTF8(content);
|
||||
if (fixed) {
|
||||
content = fixed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to fix encoding:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update if changed
|
||||
if (content !== mailState.currentEmail.body) {
|
||||
mailState.currentEmail.body = content;
|
||||
}
|
||||
}
|
||||
|
||||
if(dev) {
|
||||
console.debug("emailObj:", mailState.currentEmail)
|
||||
}
|
||||
console.info("Current email changed:", mailState.currentEmail?.subject);
|
||||
if(mailState.currentEmail !== null) {
|
||||
sidebarOpen.set(false);
|
||||
}
|
||||
};
|
||||
process();
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (unregisterEvents) unregisterEvents();
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
// Listen for second instance args
|
||||
unregisterEvents = EventsOn("launchArgs", async (args: string[]) => {
|
||||
console.log("got event launchArgs:", args);
|
||||
if (args && args.length > 0) {
|
||||
for (const arg of args) {
|
||||
const lowerArg = arg.toLowerCase();
|
||||
if (lowerArg.endsWith(".eml") || lowerArg.endsWith(".msg")) {
|
||||
console.log("Loading file from second instance:", arg);
|
||||
isLoading = true;
|
||||
loadingText = m.layout_loading_text();
|
||||
|
||||
try {
|
||||
let emlContent;
|
||||
|
||||
if (lowerArg.endsWith(".msg")) {
|
||||
loadingText = m.mail_loading_msg_conversion();
|
||||
emlContent = await ReadMSG(arg, true);
|
||||
} else {
|
||||
// EML handling
|
||||
try {
|
||||
emlContent = await ReadPEC(arg);
|
||||
} catch (e) {
|
||||
console.warn("ReadPEC failed, trying ReadEML:", e);
|
||||
emlContent = await ReadEML(arg);
|
||||
}
|
||||
|
||||
if (emlContent && emlContent.body) {
|
||||
const trimmed = emlContent.body.trim();
|
||||
const clean = trimmed.replace(/[\s\r\n]+/g, '');
|
||||
if (clean.length > 0 && clean.length % 4 === 0 && /^[A-Za-z0-9+/]+=*$/.test(clean)) {
|
||||
try {
|
||||
emlContent.body = window.atob(clean);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mailState.setParams(emlContent);
|
||||
sidebarOpen.set(false);
|
||||
WindowUnminimise();
|
||||
WindowShow();
|
||||
} catch (error) {
|
||||
console.error("Failed to load email:", error);
|
||||
toast.error("Failed to load email file");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
loadingText = "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function openPDFHandler(base64Data: string, filename: string) {
|
||||
try {
|
||||
if (settingsStore.settings.useBuiltinPDFViewer) {
|
||||
await OpenPDFWindow(base64Data, filename);
|
||||
} else {
|
||||
await OpenPDF(base64Data, filename);
|
||||
}
|
||||
} catch (error: string | any) {
|
||||
if(error.includes(filename) && error.includes("already open")) {
|
||||
toast.error(m.mail_pdf_already_open());
|
||||
return;
|
||||
}
|
||||
console.error("Failed to open PDF:", error);
|
||||
toast.error(m.mail_error_pdf());
|
||||
}
|
||||
}
|
||||
|
||||
async function openImageHandler(base64Data: string, filename: string) {
|
||||
try {
|
||||
if (settingsStore.settings.useBuiltinPreview) {
|
||||
await OpenImageWindow(base64Data, filename);
|
||||
} else {
|
||||
await OpenImage(base64Data, filename);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to open image:", error);
|
||||
toast.error(m.mail_error_image());
|
||||
}
|
||||
}
|
||||
|
||||
async function openEMLHandler(base64Data: string, filename: string) {
|
||||
try {
|
||||
await OpenEMLWindow(base64Data, filename);
|
||||
} catch (error) {
|
||||
console.error("Failed to open EML:", error);
|
||||
toast.error("Failed to open EML attachment");
|
||||
}
|
||||
}
|
||||
|
||||
async function onOpenMail() {
|
||||
isLoading = true;
|
||||
loadingText = m.layout_loading_text();
|
||||
const result = await ShowOpenFileDialog();
|
||||
if (result && result.length > 0) {
|
||||
// Handle opening the mail file
|
||||
try {
|
||||
// If the file is .eml, otherwise if is .msg, read accordingly
|
||||
let email: internal.EmailData;
|
||||
if(result.toLowerCase().endsWith(".msg")) {
|
||||
loadingText = m.mail_loading_msg_conversion();
|
||||
email = await ReadMSG(result, true);
|
||||
} else {
|
||||
email = await ReadEML(result);
|
||||
}
|
||||
// Track the current mail file path for bug reports
|
||||
await SetCurrentMailFilePath(result);
|
||||
mailState.setParams(email);
|
||||
sidebarOpen.set(false);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to read EML file:", error);
|
||||
toast.error(m.mail_error_opening());
|
||||
} finally {
|
||||
isLoading = false;
|
||||
loadingText = "";
|
||||
}
|
||||
} else {
|
||||
isLoading = false;
|
||||
loadingText = "";
|
||||
const result = await openAndLoadEmail();
|
||||
|
||||
if (result.cancelled) {
|
||||
isLoading = false;
|
||||
loadingText = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.success && result.email) {
|
||||
mailState.setParams(result.email);
|
||||
sidebarOpen.set(false);
|
||||
} else if (result.error) {
|
||||
console.error('Failed to read email file:', result.error);
|
||||
toast.error(m.mail_error_opening());
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
loadingText = '';
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer: any): string {
|
||||
if (typeof buffer === "string") return buffer; // Already base64 string
|
||||
if (Array.isArray(buffer)) {
|
||||
let binary = "";
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
return "";
|
||||
async function handleOpenPDF(base64Data: string, filename: string) {
|
||||
await openPDFAttachment(base64Data, filename);
|
||||
}
|
||||
|
||||
async function handleOpenImage(base64Data: string, filename: string) {
|
||||
await openImageAttachment(base64Data, filename);
|
||||
}
|
||||
|
||||
async function handleOpenEML(base64Data: string, filename: string) {
|
||||
await openEMLAttachment(base64Data, filename);
|
||||
}
|
||||
|
||||
function handleWheel(event: WheelEvent) {
|
||||
@@ -220,6 +91,102 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Effects
|
||||
// ============================================================================
|
||||
|
||||
// Process email body when current email changes
|
||||
$effect(() => {
|
||||
const processCurrentEmail = async () => {
|
||||
if (mailState.currentEmail?.body) {
|
||||
const processedBody = await processEmailBody(mailState.currentEmail.body);
|
||||
|
||||
if (processedBody !== mailState.currentEmail.body) {
|
||||
mailState.currentEmail.body = processedBody;
|
||||
}
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
console.debug('emailObj:', mailState.currentEmail);
|
||||
}
|
||||
console.info('Current email changed:', mailState.currentEmail?.subject);
|
||||
|
||||
if (mailState.currentEmail !== null) {
|
||||
sidebarOpen.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
processCurrentEmail();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
onMount(async () => {
|
||||
// Listen for second instance args (when another file is opened while app is running)
|
||||
unregisterEvents = EventsOn('launchArgs', async (args: string[]) => {
|
||||
console.log('got event launchArgs:', args);
|
||||
|
||||
if (!args || args.length === 0) return;
|
||||
|
||||
for (const arg of args) {
|
||||
if (isEmailFile(arg)) {
|
||||
console.log('Loading file from second instance:', arg);
|
||||
isLoading = true;
|
||||
loadingText = m.layout_loading_text();
|
||||
|
||||
// Check if MSG file for special loading text
|
||||
if (arg.toLowerCase().endsWith('.msg')) {
|
||||
loadingText = m.mail_loading_msg_conversion();
|
||||
}
|
||||
|
||||
const result = await loadEmailFromPath(arg);
|
||||
|
||||
if (result.success && result.email) {
|
||||
mailState.setParams(result.email);
|
||||
sidebarOpen.set(false);
|
||||
WindowUnminimise();
|
||||
WindowShow();
|
||||
} else if (result.error) {
|
||||
console.error('Failed to load email:', result.error);
|
||||
toast.error('Failed to load email file');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
loadingText = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unregisterEvents) {
|
||||
unregisterEvents();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
function getAttachmentClass(att: { contentType: string; filename: string }): string {
|
||||
if (att.contentType.startsWith(CONTENT_TYPES.IMAGE)) return 'image';
|
||||
if (att.contentType === CONTENT_TYPES.PDF || att.filename.toLowerCase().endsWith('.pdf'))
|
||||
return 'pdf';
|
||||
if (att.filename.toLowerCase().endsWith('.eml')) return 'eml';
|
||||
return 'file';
|
||||
}
|
||||
|
||||
function isPecSignature(filename: string, isPec: boolean): boolean {
|
||||
return isPec && filename.toLowerCase().endsWith(PEC_FILES.SIGNATURE);
|
||||
}
|
||||
|
||||
function isPecCertificate(filename: string, isPec: boolean): boolean {
|
||||
return isPec && filename.toLowerCase() === PEC_FILES.CERTIFICATE;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel fill" aria-label="Events">
|
||||
@@ -229,8 +196,10 @@
|
||||
<div class="loading-text">{loadingText}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="events" role="log" aria-live="polite">
|
||||
{#if mailState.currentEmail === null}
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<MailOpen size="48" strokeWidth={1} />
|
||||
@@ -241,7 +210,9 @@
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Email View -->
|
||||
<div class="email-view">
|
||||
<!-- Header -->
|
||||
<div class="email-header-content">
|
||||
<div class="subject-row">
|
||||
<div class="email-subject">
|
||||
@@ -255,7 +226,7 @@
|
||||
title={m.mail_open_btn_title()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<MailOpen size="15" ></MailOpen>
|
||||
<MailOpen size="15" />
|
||||
{m.mail_open_btn_text()}
|
||||
</button>
|
||||
<button
|
||||
@@ -271,77 +242,84 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meta Grid -->
|
||||
<div class="email-meta-grid">
|
||||
<span class="label">{m.mail_from()}</span>
|
||||
<span class="value">{mailState.currentEmail.from}</span>
|
||||
|
||||
{#if mailState.currentEmail.to && mailState.currentEmail.to.length > 0}
|
||||
<span class="label">{m.mail_to()}</span>
|
||||
<span class="value">{mailState.currentEmail.to.join(", ")}</span>
|
||||
<span class="value">{mailState.currentEmail.to.join(', ')}</span>
|
||||
{/if}
|
||||
|
||||
{#if mailState.currentEmail.cc && mailState.currentEmail.cc.length > 0}
|
||||
<span class="label">{m.mail_cc()}</span>
|
||||
<span class="value">{mailState.currentEmail.cc.join(", ")}</span>
|
||||
<span class="value">{mailState.currentEmail.cc.join(', ')}</span>
|
||||
{/if}
|
||||
|
||||
{#if mailState.currentEmail.bcc && mailState.currentEmail.bcc.length > 0}
|
||||
<span class="label">{m.mail_bcc()}</span>
|
||||
<span class="value">{mailState.currentEmail.bcc.join(", ")}</span>
|
||||
<span class="value">{mailState.currentEmail.bcc.join(', ')}</span>
|
||||
{/if}
|
||||
|
||||
{#if mailState.currentEmail.isPec}
|
||||
<span class="label">{m.mail_sign_label()}</span>
|
||||
<span class="value"><span class="pec-badge" title="Posta Elettronica Certificata">
|
||||
<ShieldCheck size="14" />
|
||||
PEC
|
||||
</span></span>
|
||||
<span class="value">
|
||||
<span class="pec-badge" title="Posta Elettronica Certificata">
|
||||
<ShieldCheck size="14" />
|
||||
PEC
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div class="email-attachments">
|
||||
<span class="att-section-label">{m.mail_attachments()}</span>
|
||||
<div class="att-list">
|
||||
{#if mailState.currentEmail.attachments && mailState.currentEmail.attachments.length > 0}
|
||||
{#each mailState.currentEmail.attachments as att}
|
||||
{#if att.contentType.startsWith("image/")}
|
||||
{@const base64 = arrayBufferToBase64(att.data)}
|
||||
{@const isImage = att.contentType.startsWith(CONTENT_TYPES.IMAGE)}
|
||||
{@const isPdf =
|
||||
att.contentType === CONTENT_TYPES.PDF ||
|
||||
att.filename.toLowerCase().endsWith('.pdf')}
|
||||
{@const isEml = att.filename.toLowerCase().endsWith('.eml')}
|
||||
{@const isPecSig = isPecSignature(att.filename, mailState.currentEmail.isPec)}
|
||||
{@const isPecCert = isPecCertificate(att.filename, mailState.currentEmail.isPec)}
|
||||
|
||||
{#if isImage}
|
||||
<button
|
||||
class="att-btn image"
|
||||
onclick={() => openImageHandler(arrayBufferToBase64(att.data), att.filename)}
|
||||
onclick={() => handleOpenImage(base64, att.filename)}
|
||||
>
|
||||
<Image size="14" />
|
||||
<span class="att-name">{att.filename}</span>
|
||||
</button>
|
||||
{:else if att.contentType === "application/pdf" || att.filename.toLowerCase().endsWith(".pdf")}
|
||||
<button
|
||||
class="att-btn pdf"
|
||||
onclick={() => openPDFHandler(arrayBufferToBase64(att.data), att.filename)}
|
||||
>
|
||||
<FileText />
|
||||
{:else if isPdf}
|
||||
<button class="att-btn pdf" onclick={() => handleOpenPDF(base64, att.filename)}>
|
||||
<FileText size="14" />
|
||||
<span class="att-name">{att.filename}</span>
|
||||
</button>
|
||||
{:else if att.filename.toLowerCase().endsWith(".eml")}
|
||||
<button
|
||||
class="att-btn eml"
|
||||
onclick={() => openEMLHandler(arrayBufferToBase64(att.data), att.filename)}
|
||||
>
|
||||
{:else if isEml}
|
||||
<button class="att-btn eml" onclick={() => handleOpenEML(base64, att.filename)}>
|
||||
<MailOpen size="14" />
|
||||
<span class="att-name">{att.filename}</span>
|
||||
</button>
|
||||
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase().endsWith(".p7s")}
|
||||
{:else if isPecSig}
|
||||
<a
|
||||
class="att-btn file"
|
||||
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
||||
href={createDataUrl(att.contentType, base64)}
|
||||
download={att.filename}
|
||||
>
|
||||
<Signature size="14" />
|
||||
<span class="att-name">{att.filename}</span>
|
||||
</a>
|
||||
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase() === "daticert.xml"}
|
||||
{:else if isPecCert}
|
||||
<a
|
||||
class="att-btn file"
|
||||
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
||||
href={createDataUrl(att.contentType, base64)}
|
||||
download={att.filename}
|
||||
>
|
||||
<FileCode size="14" />
|
||||
@@ -350,14 +328,14 @@
|
||||
{:else}
|
||||
<a
|
||||
class="att-btn file"
|
||||
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
||||
href={createDataUrl(att.contentType, base64)}
|
||||
download={att.filename}
|
||||
>
|
||||
{#if att.contentType.startsWith("image/")}
|
||||
<Image size="14" />
|
||||
{:else}
|
||||
<File size="14" />
|
||||
{/if}
|
||||
{#if isImage}
|
||||
<Image size="14" />
|
||||
{:else}
|
||||
<File size="14" />
|
||||
{/if}
|
||||
<span class="att-name">{att.filename}</span>
|
||||
</a>
|
||||
{/if}
|
||||
@@ -365,12 +343,13 @@
|
||||
{:else}
|
||||
<span class="att-empty">{m.mail_no_attachments()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Body -->
|
||||
<div class="email-body-wrapper">
|
||||
<iframe
|
||||
srcdoc={mailState.currentEmail.body + iFrameUtilHTML}
|
||||
srcdoc={mailState.currentEmail.body + IFRAME_UTIL_HTML}
|
||||
title="Email Body"
|
||||
class="email-iframe"
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
@@ -379,7 +358,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -399,14 +378,17 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Make sure internal loader spins if not using class-based animation library like Tailwind */
|
||||
:global(.spinner) {
|
||||
animation: spin 1s linear infinite;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
@@ -494,10 +476,10 @@
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.subject-row .btn {
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.email-meta-grid {
|
||||
@@ -564,15 +546,30 @@
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.att-btn.image { color: #60a5fa; border-color: rgba(96, 165, 250, 0.3); }
|
||||
.att-btn.image:hover { color: #93c5fd; }
|
||||
|
||||
.att-btn.pdf { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
|
||||
.att-btn.pdf:hover { color: #fca5a5; }
|
||||
.att-btn.image {
|
||||
color: #60a5fa;
|
||||
border-color: rgba(96, 165, 250, 0.3);
|
||||
}
|
||||
.att-btn.image:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.att-btn.eml { color: hsl(49, 80%, 49%); border-color: rgba(224, 206, 39, 0.3); }
|
||||
.att-btn.eml:hover { color: hsl(49, 80%, 65%); }
|
||||
.att-btn.pdf {
|
||||
color: #f87171;
|
||||
border-color: rgba(248, 113, 113, 0.3);
|
||||
}
|
||||
.att-btn.pdf:hover {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.att-btn.eml {
|
||||
color: hsl(49, 80%, 49%);
|
||||
border-color: rgba(224, 206, 39, 0.3);
|
||||
}
|
||||
.att-btn.eml:hover {
|
||||
color: hsl(49, 80%, 65%);
|
||||
}
|
||||
|
||||
.att-name {
|
||||
white-space: nowrap;
|
||||
@@ -635,7 +632,8 @@
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.browse-btn:disabled, .btn:disabled {
|
||||
.browse-btn:disabled,
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
|
||||
Reference in New Issue
Block a user