diff --git a/app/services/newsletter.py b/app/services/newsletter.py index 5500e85..0a8aa7e 100644 --- a/app/services/newsletter.py +++ b/app/services/newsletter.py @@ -263,6 +263,28 @@ OUTLOOK_MUTED = "#666666" OUTLOOK_LINK = "#0563C1" OUTLOOK_HEADING = "#003366" OUTLOOK_BORDER = "#DDDDDD" +OUTLOOK_WHITE = "#FFFFFF" + + +def _outlook_font( + text: str, + *, + color: str = OUTLOOK_TEXT, + size: str = "2", + bold: bool = False, +) -> str: + inner = escape(text) + if bold: + inner = f"{inner}" + return f'{inner}' + + +def _outlook_link(title: str, url: str) -> str: + return ( + f'' + f'{escape(title)}' + f"" + ) def create_outlook_html( @@ -271,18 +293,18 @@ def create_outlook_html( display: DisplayOptions | None = None, category: str = "", ) -> str: - """Vereinfachtes HTML für Outlook/Word – nur Tabellen und Inline-Styles.""" + """Word/Outlook-kompatibles HTML mit font-Tags und bgcolor (überlebt Einfügen in Outlook).""" display = display or DEFAULT_DISPLAY new_articles, edited_articles = split_articles_by_type(articles) period_start, period_end = _period_range(days) created_at = datetime.now().strftime("%d.%m.%Y %H:%M") - intro = escape(_intro_sentence(period_start, period_end)) + intro = _intro_sentence(period_start, period_end) subject = escape(create_subject(days, category)) if not new_articles and not edited_articles: body_rows = f"""
- Filter: Kategorie {escape(category.strip())} -
""" + category_block = ( + f'' + f'{_outlook_font("Filter: ", color=OUTLOOK_HEADING, bold=True)}' + f'{_outlook_font(f"Kategorie {category.strip()}", color=OUTLOOK_MUTED, size="1")}' + f"
" + ) total = len(new_articles) + len(edited_articles) summary_row = f"""|
- Interner Newsletter -Thomas-Krenn Wiki Update -{escape(ORG_NAME)} + |
+ {_outlook_font("Interner Newsletter", color="#A9C6E8", size="1")} + {_outlook_font("Thomas-Krenn Wiki Update", color=OUTLOOK_WHITE, size="5", bold=True)} + {_outlook_font(ORG_NAME, color="#D9E8F8", size="2")} |
||||||||||||||||
|
- Liebe Kolleginnen und Kollegen, -{intro} -Erstellt am: {created_at} + |
+ {_outlook_font("Liebe Kolleginnen und Kollegen,")} +{_outlook_font(intro)} +{_outlook_font(f"Erstellt am: {created_at}", color=OUTLOOK_MUTED, size="1")} {category_block} -
' - f"{escape(meta)}" - ) + meta_html = f" {_outlook_font(meta, color=OUTLOOK_MUTED, size='1')}" rows.append( f""" {idx}. |
-
- {title}{meta_html}
+ |
+ {_outlook_font(f"{idx}.")}
+ |
+
+ {_outlook_link(title, url)}{meta_html}
|
- {escape(heading)} ({len(items)})
+ |
+ {_outlook_font(f"{heading} ({len(items)})", color=OUTLOOK_HEADING, size="3", bold=True)}
|
{escape(empty_text)} |
+
+ {_outlook_font(empty_text, color=OUTLOOK_MUTED)}
+ |
+
+ `;
+ document.body.appendChild(overlay);
+
+ const textarea = overlay.querySelector(".copy-dialog-textarea");
+ textarea.value = text;
+ selectTextarea(textarea);
+
+ let copied = false;
+ try {
+ copied = document.execCommand("copy");
+ } catch (err) {
+ console.warn("execCommand copy in dialog failed.", err);
+ }
+
+ if (copied) {
+ setCopyStatus("HTML-Quelltext kopiert.");
+ } else {
+ setCopyStatus("Bitte im Fenster Strg+C drücken.", true);
+ }
+
+ overlay.querySelector("#copy-dialog-close-btn").addEventListener("click", closeCopyDialog);
+ overlay.querySelector("#copy-dialog-select-btn").addEventListener("click", () => selectTextarea(textarea));
+ overlay.addEventListener("click", (event) => {
+ if (event.target === overlay) closeCopyDialog();
+ });
+
+ copyDialogKeyHandler = (event) => {
+ if (event.key === "Escape") closeCopyDialog();
+ };
+ document.addEventListener("keydown", copyDialogKeyHandler);
+}
+
+function copyViaIframe(html) {
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("aria-hidden", "true");
+ iframe.style.cssText = "position:fixed;width:700px;height:500px;left:-9999px;top:0;border:none;";
+ document.body.appendChild(iframe);
+
+ const doc = iframe.contentWindow.document;
+ doc.open();
+ doc.write(html);
+ doc.close();
+
+ const range = doc.createRange();
+ range.selectNodeContents(doc.body);
+ const selection = iframe.contentWindow.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ let ok = false;
+ try {
+ ok = doc.execCommand("copy");
+ } catch (err) {
+ console.warn("execCommand copy via iframe failed.", err);
+ } finally {
+ selection.removeAllRanges();
+ document.body.removeChild(iframe);
+ }
+ return ok;
+}
+
function copyHtmlWithFallback(html) {
+ if (copyViaIframe(html)) return true;
+
const container = document.createElement("div");
container.innerHTML = html;
container.contentEditable = "true";
container.setAttribute("aria-hidden", "true");
- container.style.position = "fixed";
- container.style.left = "0";
- container.style.top = "0";
- container.style.width = "600px";
- container.style.height = "1px";
- container.style.overflow = "hidden";
- container.style.opacity = "0";
- container.style.pointerEvents = "none";
- container.style.background = "#FFFFFF";
+ container.style.cssText =
+ "position:fixed;left:0;top:0;width:700px;max-height:1px;overflow:hidden;opacity:0;pointer-events:none;background:#FFFFFF;";
document.body.appendChild(container);
const range = document.createRange();
@@ -146,36 +232,13 @@ function copyHtmlWithFallback(html) {
ok = document.execCommand("copy");
} catch (err) {
console.warn("execCommand HTML copy failed.", err);
- ok = false;
} finally {
selection.removeAllRanges();
document.body.removeChild(container);
}
-
- if (!ok) {
- ok = copyTextWithFallback(wrapCfHtml(html));
- }
return ok;
}
-async function copyText(text) {
- if (!text) return false;
-
- if (copyTextWithFallback(text)) {
- return true;
- }
-
- if (navigator.clipboard && window.isSecureContext) {
- try {
- await navigator.clipboard.writeText(text);
- return true;
- } catch (err) {
- console.warn("Clipboard API text copy failed.", err);
- }
- }
- return false;
-}
-
async function copyForOutlook() {
const html = getNewsletterOutlookHtml();
const plain = getNewsletterPlain();
@@ -185,7 +248,7 @@ async function copyForOutlook() {
}
if (copyHtmlWithFallback(html)) {
- setCopyStatus("Newsletter für Outlook kopiert. Mit Strg+V einfügen (ggf. „Format beibehalten“).");
+ setCopyStatus("Newsletter kopiert. In Outlook: Strg+V, dann ggf. Rechtsklick → „Format beibehalten“.");
return;
}
@@ -198,55 +261,24 @@ async function copyForOutlook() {
"text/plain": new Blob([plain], { type: "text/plain" }),
}),
]);
- setCopyStatus("Newsletter für Outlook kopiert. Mit Strg+V einfügen (ggf. „Format beibehalten“).");
+ setCopyStatus("Newsletter kopiert. In Outlook: Strg+V, dann ggf. Rechtsklick → „Format beibehalten“.");
return;
} catch (err) {
console.warn("Clipboard API HTML copy failed.", err);
}
}
- setCopyStatus("Kopieren fehlgeschlagen. Bitte „HTML herunterladen“ verwenden.", true);
+ setCopyStatus("Bitte „Im Browser öffnen“ nutzen und dort Strg+A → Strg+C.", true);
}
-async function copyHtmlSource() {
+function copyHtmlSource() {
const html = getNewsletterHtml();
if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
- const displayArea = document.getElementById("newsletter-html-display");
- const sourceArea = document.getElementById("newsletter-html-source");
- const textarea = displayArea?.value ? displayArea : sourceArea;
-
- if (textarea && copyFromTextarea(textarea)) {
- setCopyStatus("HTML-Quelltext kopiert.");
- return;
- }
-
- if (copyTextWithFallback(html)) {
- setCopyStatus("HTML-Quelltext kopiert.");
- return;
- }
-
- if (navigator.clipboard && window.isSecureContext) {
- try {
- await navigator.clipboard.writeText(html);
- setCopyStatus("HTML-Quelltext kopiert.");
- return;
- } catch (err) {
- console.warn("Clipboard API HTML source copy failed.", err);
- }
- }
-
- if (textarea) {
- textarea.focus({ preventScroll: true });
- textarea.select();
- }
- setCopyStatus(
- "Automatisches Kopieren nicht möglich. HTML-Feld unten ist markiert – bitte Strg+C drücken.",
- true
- );
+ showCopyDialog(html, "HTML-Quelltext kopieren");
}
function downloadHtml() {
@@ -256,9 +288,23 @@ function downloadHtml() {
return;
}
- const stamp = new Date().toISOString().slice(0, 10);
- const filename = `wiki-newsletter-${stamp}.html`;
- const blob = new Blob([html], { type: "text/html;charset=utf-8" });
+ downloadFile(html, `wiki-newsletter-${new Date().toISOString().slice(0, 10)}.html`, "text/html;charset=utf-8");
+ setCopyStatus("Outlook-HTML heruntergeladen.");
+}
+
+function downloadHtmlSource() {
+ const html = getNewsletterHtml();
+ if (!html) {
+ setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
+ return;
+ }
+
+ downloadFile(html, `wiki-newsletter-source-${new Date().toISOString().slice(0, 10)}.html`, "text/html;charset=utf-8");
+ setCopyStatus("HTML-Quelltext heruntergeladen.");
+}
+
+function downloadFile(content, filename, mimeType) {
+ const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
@@ -267,7 +313,26 @@ function downloadHtml() {
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
- setCopyStatus(`HTML gespeichert als ${filename}. Datei in Outlook öffnen oder Inhalt kopieren.`);
+}
+
+function openOutlookInBrowser() {
+ const html = getNewsletterOutlookHtml();
+ if (!html) {
+ setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
+ return;
+ }
+
+ const popup = window.open("", "_blank");
+ if (!popup) {
+ setCopyStatus("Pop-up blockiert. Bitte Pop-ups erlauben oder „HTML herunterladen“ nutzen.", true);
+ return;
+ }
+
+ popup.document.open();
+ popup.document.write(html);
+ popup.document.close();
+ popup.document.title = "Wiki-Newsletter – zum Kopieren";
+ setCopyStatus("Newsletter im Browser geöffnet: Strg+A → Strg+C, dann in Outlook einfügen.");
}
document.addEventListener("DOMContentLoaded", () => {
@@ -279,7 +344,14 @@ document.addEventListener("DOMContentLoaded", () => {
frame.srcdoc = previewHtml;
}
+ const htmlDisplay = document.getElementById("newsletter-html-display");
+ htmlDisplay?.addEventListener("focus", () => selectTextarea(htmlDisplay));
+ htmlDisplay?.addEventListener("click", () => selectTextarea(htmlDisplay));
+
document.getElementById("copy-outlook-btn")?.addEventListener("click", copyForOutlook);
+ document.getElementById("open-outlook-btn")?.addEventListener("click", openOutlookInBrowser);
document.getElementById("copy-html-source-btn")?.addEventListener("click", copyHtmlSource);
+ document.getElementById("copy-html-source-btn-2")?.addEventListener("click", copyHtmlSource);
document.getElementById("download-html-btn")?.addEventListener("click", downloadHtml);
+ document.getElementById("download-html-source-btn")?.addEventListener("click", downloadHtmlSource);
});
diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html
index c7c7d01..37bbbdc 100644
--- a/app/templates/dashboard.html
+++ b/app/templates/dashboard.html
@@ -98,8 +98,9 @@
${title}+Der Text ist markiert. Jetzt Strg+C drücken (Mac: Cmd+C). + +
+
+
+
+ Newsletter-Vorschau (mit Hyperlinks)-Für Outlook „Für Outlook kopieren“ nutzen und mit Strg+V einfügen (Rechtsklick → „Format beibehalten“). Falls Kopieren nicht klappt (z. B. per HTTP/IP), „HTML herunterladen“ verwenden und die Datei in Outlook öffnen. +Empfohlen für Outlook: „Im Browser öffnen“ → dort Strg+A, Strg+C → in Outlook Strg+V. Alternativ „Für Outlook kopieren“ und mit Rechtsklick → „Format beibehalten“ einfügen.
+
@@ -119,7 +120,11 @@
Output: HTML-Quelltext-Bei Problemen mit „Kopieren“: Textfeld markieren (Strg+A) und manuell kopieren (Strg+C). +Öffnet ein Fenster mit markiertem Text – dort Strg+C drücken. Funktioniert auch ohne HTTPS. +
+
+
+
| ||||||||