From 7138eaf5722781bef5d81996f00f4065885a9fdf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 22:43:09 +0100 Subject: [PATCH] 0.1.155: Mail: Lesefenster (Linksklick), Loeschen, Archiv mit Suche, Aufraeumen nach Alle oeffnen Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016onJCSHLwhs9udhC9ZLzk9 --- Freewood-OctoWoW-Mail.toc | 3 +- Locales.lua | 78 ++++++++++++- Mail.lua | 175 ++++++++++++++++++++++++++++- MailArchive.lua | 229 ++++++++++++++++++++++++++++++++++++++ MailUI.lua | 215 ++++++++++++++++++++++++++++++++--- 5 files changed, 677 insertions(+), 23 deletions(-) create mode 100644 MailArchive.lua diff --git a/Freewood-OctoWoW-Mail.toc b/Freewood-OctoWoW-Mail.toc index 119ec23..4580e50 100644 --- a/Freewood-OctoWoW-Mail.toc +++ b/Freewood-OctoWoW-Mail.toc @@ -3,10 +3,11 @@ ## Notes: Own mailbox window: open all or selected mails safely, recent recipients, mailing lists with automatic sending, multi-item mails, gold surplus and cash on delivery. ## Notes-deDE: Eigenes Briefkasten-Fenster: alles oder Ausgewähltes sicher öffnen, letzte Empfänger, Mailinglisten mit Automatik, mehrere Items, Gold-Überschuss und Nachnahme. ## Author: Lacustat -## Version: 0.1.154 +## Version: 0.1.155 ## Dependencies: Freewood-OctoWoW-Core ## OptionalDeps: Freewood-OctoWoW-Creator, Freewood-OctoWoW-Brett Locales.lua Mail.lua MailUI.lua +MailArchive.lua diff --git a/Locales.lua b/Locales.lua index 6c5afb2..5c57f40 100644 --- a/Locales.lua +++ b/Locales.lua @@ -27,7 +27,7 @@ FW:AddLocale("enUS", { MAIL_ROW_LETTER = "Letter only (not touched)", MAIL_ROW_DAYS = "%d d", MAIL_ROW_FROM = "From %s", - MAIL_NOTE_SAFE = "Mails are never deleted or returned. Cash-on-delivery mails and plain letters are skipped. One bag slot always stays free.", + MAIL_NOTE_SAFE = "Cash-on-delivery mails are never taken. One bag slot always stays free. Emptied mails are removed and kept in the archive.", MAIL_RUN_DONE = "Mailbox: %d gold mails and %d items taken%s.", MAIL_RUN_SKIP_FULL = ", %d items left because the bags are nearly full", MAIL_RUN_STOPPED = "Mailbox: stopped. %d gold mails and %d items taken.", @@ -103,6 +103,43 @@ FW:AddLocale("enUS", { MAIL_CMD_ON = "Freewood mailbox window: on", MAIL_CMD_OFF = "Freewood mailbox window: off", MAIL_CMD_CLOSED = "Open a mailbox first.", + MAIL_BTN_ARCHIVE = "Archive", + MAIL_DETAIL_EMPTY = "Click a mail to read it.", + MAIL_DETAIL_FROM = "From %s", + MAIL_DETAIL_NOTEXT = "(no text)", + MAIL_ATTACH_ITEM = "Item: %s", + MAIL_ATTACH_GOLD = "Gold: %s", + MAIL_ATTACH_COD = "Cash on delivery: %s", + MAIL_ATTACH_NONE = "Nothing attached", + MAIL_TAKE_GOLD = "Take gold", + MAIL_TAKE_ITEM = "Take item", + MAIL_TAKE_COD = "Pay %s and take", + MAIL_TAKE_COD_CONFIRM = "Really pay %s? Click again", + MAIL_REPLY = "Reply", + MAIL_DELETE = "Delete", + MAIL_DELETE_TIP = "Moves the mail to the archive and deletes it from the mailbox. Only possible when nothing is attached (take items and gold first).", + MAIL_ERR_codbad = "Blocked: the cash on delivery is higher than agreed (board check).", + MAIL_ERR_codmoney = "Not enough gold for the cash on delivery.", + MAIL_ERR_full = "Bags nearly full (one slot stays free).", + MAIL_ERR_busy = "Another action is running.", + MAIL_ERR_data = "This mail carries Freewood data. It stays until you have handled it.", + MAIL_ERR_notempty = "Take the attached items and gold first.", + MAIL_RUN_CLEANED = "Mailbox: %d emptied mails removed, %d of them kept in the archive.", + MAIL_ARCH_TITLE = "Mail archive", + MAIL_ARCH_SEARCH = "Search subject or sender", + MAIL_ARCH_EMPTY = "Nothing in the archive.", + MAIL_ARCH_COUNT = "%d of %d entries", + MAIL_ARCH_DELETE = "Delete entry", + MAIL_ARCH_CLEAR = "Clear archive", + MAIL_ARCH_CLEAR_CONFIRM = "Really clear? Click again", + MAIL_ARCH_TAKEN = "Taken: %s", + MAIL_ARCH_CHAR = "Received by %s", + MAIL_ARCH_NOSUBJECT = "(no subject)", + MAIL_SET_CLEANUP = "After \"Open all\": remove emptied mails (they are kept in the archive first)", + MAIL_SET_ARCHIVE_DAYS = "Delete archive entries after", + MAIL_SET_ARCHIVE_OFF = "never", + MAIL_SET_ARCHIVE_N = "%d days", + MAIL_SET_ARCHIVE_BTN = "Open archive", }) FW:AddLocale("deDE", { @@ -126,7 +163,7 @@ FW:AddLocale("deDE", { MAIL_ROW_LETTER = "Nur ein Brief (bleibt unberührt)", MAIL_ROW_DAYS = "%d T", MAIL_ROW_FROM = "Von %s", - MAIL_NOTE_SAFE = "Briefe werden nie gelöscht oder zurückgeschickt. Nachnahme-Briefe und reine Briefe werden übersprungen. Ein Taschenplatz bleibt immer frei.", + MAIL_NOTE_SAFE = "Nachnahme-Briefe werden nie genommen. Ein Taschenplatz bleibt immer frei. Geleerte Briefe werden entfernt und im Archiv gesichert.", MAIL_RUN_DONE = "Briefkasten: %d Gold-Briefe und %d Items genommen%s.", MAIL_RUN_SKIP_FULL = ", %d Items blieben liegen, weil die Taschen fast voll sind", MAIL_RUN_STOPPED = "Briefkasten: gestoppt. %d Gold-Briefe und %d Items genommen.", @@ -202,4 +239,41 @@ FW:AddLocale("deDE", { MAIL_CMD_ON = "Freewood-Briefkasten-Fenster: an", MAIL_CMD_OFF = "Freewood-Briefkasten-Fenster: aus", MAIL_CMD_CLOSED = "Öffne zuerst einen Briefkasten.", + MAIL_BTN_ARCHIVE = "Archiv", + MAIL_DETAIL_EMPTY = "Klicke einen Brief an, um ihn zu lesen.", + MAIL_DETAIL_FROM = "Von %s", + MAIL_DETAIL_NOTEXT = "(kein Text)", + MAIL_ATTACH_ITEM = "Item: %s", + MAIL_ATTACH_GOLD = "Gold: %s", + MAIL_ATTACH_COD = "Nachnahme: %s", + MAIL_ATTACH_NONE = "Nichts angehängt", + MAIL_TAKE_GOLD = "Gold nehmen", + MAIL_TAKE_ITEM = "Item nehmen", + MAIL_TAKE_COD = "%s zahlen und nehmen", + MAIL_TAKE_COD_CONFIRM = "%s wirklich zahlen? Nochmal klicken", + MAIL_REPLY = "Antworten", + MAIL_DELETE = "Löschen", + MAIL_DELETE_TIP = "Legt den Brief ins Archiv und löscht ihn aus dem Briefkasten. Nur möglich, wenn nichts mehr angehängt ist (erst Items und Gold nehmen).", + MAIL_ERR_codbad = "Gesperrt: Die Nachnahme ist höher als vereinbart (Brett-Prüfung).", + MAIL_ERR_codmoney = "Nicht genug Gold für die Nachnahme.", + MAIL_ERR_full = "Taschen fast voll (ein Platz bleibt frei).", + MAIL_ERR_busy = "Es läuft gerade eine andere Aktion.", + MAIL_ERR_data = "Dieser Brief enthält Freewood-Daten. Er bleibt, bis du ihn bearbeitet hast.", + MAIL_ERR_notempty = "Erst die angehängten Items und das Gold nehmen.", + MAIL_RUN_CLEANED = "Briefkasten: %d geleerte Briefe entfernt, %d davon im Archiv gesichert.", + MAIL_ARCH_TITLE = "Brief-Archiv", + MAIL_ARCH_SEARCH = "Betreff oder Absender suchen", + MAIL_ARCH_EMPTY = "Das Archiv ist leer.", + MAIL_ARCH_COUNT = "%d von %d Einträgen", + MAIL_ARCH_DELETE = "Eintrag löschen", + MAIL_ARCH_CLEAR = "Archiv leeren", + MAIL_ARCH_CLEAR_CONFIRM = "Wirklich leeren? Nochmal klicken", + MAIL_ARCH_TAKEN = "Genommen: %s", + MAIL_ARCH_CHAR = "Empfangen von %s", + MAIL_ARCH_NOSUBJECT = "(ohne Betreff)", + MAIL_SET_CLEANUP = "Nach \"Alle öffnen\": geleerte Briefe entfernen (sie kommen vorher ins Archiv)", + MAIL_SET_ARCHIVE_DAYS = "Archiv-Einträge löschen nach", + MAIL_SET_ARCHIVE_OFF = "nie", + MAIL_SET_ARCHIVE_N = "%d Tagen", + MAIL_SET_ARCHIVE_BTN = "Archiv öffnen", }) diff --git a/Mail.lua b/Mail.lua index a2f0b52..f368e31 100644 --- a/Mail.lua +++ b/Mail.lua @@ -51,9 +51,11 @@ local Mail = FW:NewModule("Mail", { title = L.MAIL_TITLE, description = L.MAIL_DESC, defaults = { - settings = { replace = false, chat = true }, + settings = { replace = false, chat = true, cleanup = true, archiveDays = 0 }, recipients = {}, lists = {}, + archive = {}, -- entfernte Briefe, neueste zuerst + sentSubjects = {}, -- Betreffe, die wir selbst ohne Text gesendet haben (Betreff -> Zeit) }, }) NS.Mail = Mail @@ -258,6 +260,141 @@ function Mail.ReadInbox() return list end +------------------------------------------------------------------------ +-- Brieftext, Archiv, einzelne Briefe +------------------------------------------------------------------------ + +local MAX_ARCHIVE = 500 +Mail.taken = {} -- "Absender|Betreff" -> { money, items = { "Name xN" } }: was aus dem Brief genommen wurde + +local function Pair(m) return tostring(m.sender) .. "|" .. tostring(m.subject) end +Mail.Pair = Pair + +-- Text eines Briefs (markiert ihn als gelesen). Rueckgabe: Text, istRechnung +function Mail.ReadText(i) + if not GetInboxText then return "", false end + local ok, body, _, _, invoice = pcall(GetInboxText, i) + if not ok then return "", false end + body = type(body) == "string" and body or "" + if invoice and GetInboxInvoiceInfo then + local ok2, kind, item, who, bid = pcall(GetInboxInvoiceInfo, i) + if ok2 and item then + local parts = { tostring(kind or "invoice"), tostring(item) } + if who then parts[#parts + 1] = tostring(who) end + if bid then parts[#parts + 1] = Mail.Money(bid) end + body = (body ~= "" and (body .. "\n") or "") .. table.concat(parts, " - ") + end + end + return body, invoice and true or false +end + +-- Nichts mehr angehaengt, kein Gold, keine Nachnahme: dann gehen beim Loeschen keine Werte verloren +function Mail.Deletable(m) + return m ~= nil and not m.hasItem and m.money == 0 and m.cod == 0 +end + +local function NoteTaken(m, what) + local p = Pair(m) + local t = Mail.taken[p] or { money = 0, items = {} } + Mail.taken[p] = t + if what == "money" then + t.money = t.money + m.money + else + t.items[#t.items + 1] = (m.itemName or "?") .. ((m.itemCount or 1) > 1 and (" x" .. m.itemCount) or "") + end +end + +-- Eigene Briefe ohne Text (Betreff steht alles): werden ohne Archiv entfernt +function Mail:NoteSent(subject, body) + if (body or "") ~= "" or not subject or subject == "" then return end + local t = self.db.sentSubjects + local now = time() + t[subject] = now + local n = 0 + for k, v in pairs(t) do + n = n + 1 + if now - (tonumber(v) or 0) > 30 * 86400 then t[k] = nil end + end +end + +function Mail:IsOwnSent(subject, body) + return (body or "") == "" and self.db.sentSubjects[subject] ~= nil +end + +function Mail:AddArchive(m, body) + local info = Mail.taken[Pair(m)] + local e = { + t = time(), sender = m.sender, subject = m.subject, body = body or "", + money = info and info.money or 0, items = info and info.items or {}, + char = UnitName and UnitName("player") or "?", + } + table.insert(self.db.archive, 1, e) + while #self.db.archive > MAX_ARCHIVE do table.remove(self.db.archive) end + FW:Fire("MAIL_ARCHIVE_CHANGED") +end + +-- Archiv-Eintraege nach der eingestellten Zeit loeschen (0 = nie) +function Mail:Purge() + local days = tonumber(self.db.settings.archiveDays) or 0 + if days <= 0 then return 0 end + local limit = time() - days * 86400 + local a, n = self.db.archive, 0 + for i = #a, 1, -1 do + if (a[i].t or 0) < limit then table.remove(a, i) n = n + 1 end + end + if n > 0 then FW:Fire("MAIL_ARCHIVE_CHANGED") end + return n +end + +-- Leeren Brief entfernen: erst ins Archiv (ausser unseren eigenen Textlosen), dann loeschen. +-- Rueckgabe: true, "archived"|"own" oder false, Grund +function Mail:RemoveMail(m) + if not Mail.Deletable(m) then return false, "notempty" end + local body = Mail.ReadText(m.i) + if body:find("FW%d+:[a-z0-9]+:%x+:") then return false, "data" end -- Freewood-Daten: bleiben fuer die Annahme + local own = self:IsOwnSent(m.subject, body) + if not own then self:AddArchive(m, body) end + pcall(DeleteInboxItem, m.i) + Mail.taken[Pair(m)] = nil + return true, own and "own" or "archived" +end + +function Mail:FindMail(key, pair) + local list = Mail.ReadInbox() + for _, m in ipairs(list) do + if m.key == key then return m end + end + if pair then + for _, m in ipairs(list) do + if Pair(m) == pair then return m end + end + end +end + +-- Inhalt eines einzelnen Briefs nehmen (erst Gold, dann Item). Nachnahme zahlt der Spieler mit einem zweiten Klick (UI). +function Mail:TakeOne(m) + if self.busy then return false, "busy" end + if m.cod > 0 then + local B = FW.Brett + if B and B.InspectMail then + local ok, st = pcall(B.InspectMail, m.i) + if ok and (st == "high" or st == "nodeal") then return false, "codbad" end + end + if ((GetMoney and GetMoney()) or 0) < m.cod then return false, "codmoney" end + end + if m.money > 0 and m.cod == 0 then + NoteTaken(m, "money") + pcall(TakeInboxMoney, m.i) + return true + elseif m.hasItem then + if Mail.FreeSlots() <= KEEP_FREE then return false, "full" end + NoteTaken(m, "item") + pcall(TakeInboxItem, m.i) + return true + end + return false, "empty" +end + Mail.selected = {} -- key -> true function Mail:SelectedCount() @@ -291,7 +428,7 @@ function Mail:StartOpen(onlySelected) for k in pairs(self.selected) do only[k] = true end if next(only) == nil then return false end end - self.run = { only = only, tries = {}, money = 0, items = 0, pending = nil } + self.run = { only = only, tries = {}, money = 0, items = 0, pending = nil, removed = 0, archived = 0, touched = {} } self.busy = "inbox" self.driver:Show() Trace("Posteingang: Start" .. (only and " (nur Ausgewaehlte)" or " (alle)")) @@ -315,7 +452,8 @@ local function FinishOpen(self, reason) else Say(string.format(FW.L.MAIL_RUN_DONE, run.money, run.items, skipped > 0 and string.format(FW.L.MAIL_RUN_SKIP_FULL, skipped) or "")) end - self.lastRun = { money = run.money, items = run.items, skipped = skipped } + if run.removed > 0 then Say(string.format(FW.L.MAIL_RUN_CLEANED, run.removed, run.archived)) end + self.lastRun = { money = run.money, items = run.items, skipped = skipped, removed = run.removed } self.selected = {} FW:Fire("MAIL_STATE") FW:Fire("MAIL_SELECTION_CHANGED") @@ -344,6 +482,8 @@ function Mail:StepOpen() if m.money > 0 then run.tries[m.sig] = (run.tries[m.sig] or 0) + 1 run.money = run.money + 1 + run.touched[Pair(m)] = true + NoteTaken(m, "money") run.pending, run.updated = now, false pcall(TakeInboxMoney, m.i) Trace(string.format("Brief %d von %s: Gold %d genommen", m.i, m.sender, m.money)) @@ -355,6 +495,8 @@ function Mail:StepOpen() else run.tries[m.sig] = (run.tries[m.sig] or 0) + 1 run.items = run.items + 1 + run.touched[Pair(m)] = true + NoteTaken(m, "item") run.pending, run.updated = now, false pcall(TakeInboxItem, m.i) Trace(string.format("Brief %d von %s: Item %s genommen (frei vorher %d)", m.i, m.sender, tostring(m.itemName), free)) @@ -364,6 +506,24 @@ function Mail:StepOpen() end end end + -- nichts mehr zu nehmen: leere Briefe entfernen (mit Text und fremde kommen vorher ins Archiv) + if self.db.settings.cleanup then + for idx = #list, 1, -1 do + local m = list[idx] + local key = "del|" .. m.sig + if Mail.Deletable(m) and (not run.only or run.touched[Pair(m)]) and (run.tries[key] or 0) < 2 then + run.tries[key] = (run.tries[key] or 0) + 1 + local ok, how = self:RemoveMail(m) + if ok then + run.removed = run.removed + 1 + if how == "archived" then run.archived = run.archived + 1 end + run.pending, run.updated = now, false + FW:Fire("MAIL_STATE") + return + end + end + end + end FinishOpen(self, "done") end @@ -699,6 +859,7 @@ end ------------------------------------------------------------------------ function Mail:OnMailShow() + pcall(self.Purge, self) self.open = true self.gen = self.gen + 1 self.selected = {} @@ -770,8 +931,11 @@ function Mail:OnEnable() self.hooked = true -- Empfaenger merken, egal wer sendet (auch das normale Fenster und Brett-Auftraege) if hooksecurefunc and _G.SendMail then - pcall(hooksecurefunc, "SendMail", function(to) - if Mail.db then Mail.lastTo = to end + pcall(hooksecurefunc, "SendMail", function(to, subject, body) + if Mail.db then + Mail.lastTo = to + pcall(Mail.NoteSent, Mail, subject, body) + end end) end if _G.MailFrame and MailFrame.HookScript then @@ -780,6 +944,7 @@ function Mail:OnEnable() pcall(MailFrame.HookScript, MailFrame, "OnHide", function() pcall(Mail.OnMailClosed, Mail) end) end end + After(3, function() pcall(Mail.Purge, Mail) end) -- Briefkasten schon offen (z. B. nach /reload)? if _G.MailFrame and MailFrame.IsShown and MailFrame:IsShown() then After(0.5, function() pcall(Mail.OnMailShow, Mail) end) diff --git a/MailArchive.lua b/MailArchive.lua new file mode 100644 index 0000000..2b5dd1c --- /dev/null +++ b/MailArchive.lua @@ -0,0 +1,229 @@ +-- Freewood-OctoWoW-Mail: Archiv der entfernten Briefe (eigenes Fenster mit Suche nach Betreff und Absender). +-- Geoeffnet ueber den Knopf "Archiv" im Briefkasten-Fenster, in den Einstellungen oder mit /fwmail archive. +-- Eintraege: { t, sender, subject, body, money, items, char }. Loeschen nach X Tagen: Einstellung (Standard: nie). + +local ADDON, NS = ... + +local FW = _G.Freewood +local Mail = NS.Mail +if not FW or not Mail then + return +end + +local UI = FW.UI +local W, H = 720, 500 +local LIST_W = 290 +local ROW_H, ROW_GAP = 40, 4 + +local win +local rows = {} +local filter = "" +local selected -- gewaehlter Eintrag (Tabelle) +local clearConfirm = false + +local function T(key) return FW.L[key] or key end +local function Stamp(t) return date and date("%d.%m.%y %H:%M", t or 0) or "" end + +local function Matches(e, q) + if q == "" then return true end + q = q:lower() + return (tostring(e.subject or ""):lower():find(q, 1, true) or tostring(e.sender or ""):lower():find(q, 1, true)) and true or false +end + +local function Build() + local f = UI.CreatePanel(UIParent, "bgDeep", "gold") + f:SetSize(W, H) + f:SetFrameStrata("DIALOG") + f:SetClampedToScreen(true) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function(self) self:StartMoving() end) + f:SetScript("OnDragStop", function(self) + self:StopMovingOrSizing() + local left, top = self:GetLeft(), self:GetTop() + if left and top then Mail.db.settings.archivePos = { x = left, y = top } end + end) + local pos = Mail.db.settings.archivePos + f:ClearAllPoints() + if pos and pos.x and pos.y then + f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos.x, pos.y) + else + f:SetPoint("CENTER", UIParent, "CENTER", 60, 0) + end + + local title = UI.CreateLabel(f, T("MAIL_ARCH_TITLE"), { font = "heading", size = 16, color = "goldLight" }) + title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -12) + local close = UI.CreateButton(f, "X", 26, 24, function() f:Hide() end, "danger") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -10) + local div = UI.CreateDivider(f) + div:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -40) + div:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -40) + + -- Suche + local box = UI.CreatePanel(f, "bgDeep", "bronze") + box:SetSize(LIST_W, 26) + box:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -52) + local edit = CreateFrame("EditBox", nil, box) + edit:SetAutoFocus(false) + edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2) + edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2) + edit:SetMaxLetters(60) + if not edit:SetFont(UI.GetFontPath("body"), 12, "") then edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "") end + edit:SetTextColor(UI.Color("text")) + edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) + edit:SetScript("OnEnterPressed", function(self) self:ClearFocus() end) + local hint = UI.CreateLabel(box, T("MAIL_ARCH_SEARCH"), { size = 12, color = "textDim" }) + hint:SetPoint("LEFT", box, "LEFT", 10, 0) + edit:SetScript("OnTextChanged", function(self) + filter = self:GetText() or "" + if filter == "" then hint:Show() else hint:Hide() end + if f.Refresh then f.Refresh() end + end) + f.count = UI.CreateLabel(f, "", { size = 11, color = "textDim" }) + f.count:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -84) + + -- Liste + f.scroll = UI.CreateScrollFrame(f) + f.scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -102) + f.scroll:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 16) + f.scroll:SetWidth(LIST_W) + f.empty = UI.CreateLabel(f.scroll.child, T("MAIL_ARCH_EMPTY"), { size = 12, color = "textDim" }) + f.empty:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 4, -6) + + -- Lesefenster + local d = UI.CreatePanel(f, "card", "bronze") + d:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 28, -52) + d:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 16) + d.subj = UI.CreateLabel(d, "", { font = "heading", size = 14, color = "goldLight" }) + d.subj:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -10) + d.subj:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.subj:SetWordWrap(false) + d.meta = UI.CreateLabel(d, "", { size = 11, color = "textDim" }) + d.meta:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -32) + d.meta:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.meta:SetWordWrap(false) + d.taken = UI.CreateLabel(d, "", { size = 12 }) + d.taken:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -52) + d.taken:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.taken:SetWordWrap(false) + d.bodyScroll = UI.CreateScrollFrame(d) + d.bodyScroll:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -76) + d.bodyScroll:SetPoint("BOTTOMRIGHT", d, "BOTTOMRIGHT", -12, 50) + d.body = UI.CreateLabel(d.bodyScroll.child, "", { size = 12, justifyV = "TOP" }) + d.body:SetPoint("TOPLEFT", d.bodyScroll.child, "TOPLEFT", 2, -2) + d.del = UI.CreateButton(d, T("MAIL_ARCH_DELETE"), 130, 26, function() + if not selected then return end + local a = Mail.db.archive + for i, e in ipairs(a) do + if e == selected then table.remove(a, i) break end + end + selected = nil + f.Refresh() + end, "danger") + d.del:SetPoint("BOTTOMLEFT", d, "BOTTOMLEFT", 12, 12) + d.clear = UI.CreateButton(d, T("MAIL_ARCH_CLEAR"), 210, 26, function() + if not clearConfirm then + clearConfirm = true + f.Refresh() + return + end + clearConfirm = false + for i = #Mail.db.archive, 1, -1 do table.remove(Mail.db.archive, i) end + selected = nil + f.Refresh() + end, "default") + d.clear:SetPoint("BOTTOMRIGHT", d, "BOTTOMRIGHT", -12, 12) + + local function Row(i) + local row = rows[i] + if row then return row end + row = UI.CreatePanel(f.scroll.child, "card", "bronze") + row:SetHeight(ROW_H) + row:EnableMouse(true) + row.l1 = UI.CreateLabel(row, "", { size = 12 }) + row.l1:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -6) + row.l1:SetPoint("RIGHT", row, "RIGHT", -10, 0) + row.l1:SetWordWrap(false) + row.l2 = UI.CreateLabel(row, "", { size = 11, color = "textDim" }) + row.l2:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -22) + row.l2:SetPoint("RIGHT", row, "RIGHT", -10, 0) + row.l2:SetWordWrap(false) + row:SetScript("OnMouseUp", function() + selected = row.entry + clearConfirm = false + f.Refresh() + end) + rows[i] = row + return row + end + + function f.Refresh() + local all = Mail.db.archive + local list = {} + for _, e in ipairs(all) do + if Matches(e, filter) then list[#list + 1] = e end + end + local w = math.max(200, (f.scroll:GetWidth() or 0) - 14) + for i, e in ipairs(list) do + local row = Row(i) + row.entry = e + row:ClearAllPoints() + row:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 0, -((i - 1) * (ROW_H + ROW_GAP))) + row:SetWidth(w) + row.l1:SetText(e.subject ~= "" and e.subject or T("MAIL_ARCH_NOSUBJECT")) + row.l2:SetText(tostring(e.sender or "?") .. " " .. Stamp(e.t)) + UI.SetBorderColor(row, e == selected and "gold" or "bronze") + row:Show() + end + for i = #list + 1, #rows do rows[i].entry = nil rows[i]:Hide() end + f.empty:SetShown(#list == 0) + f.scroll:SetContentHeight(#list * (ROW_H + ROW_GAP) + 4) + f.count:SetText(string.format(T("MAIL_ARCH_COUNT"), #list, #all)) + + if selected then + local e = selected + d.subj:SetText(e.subject ~= "" and e.subject or T("MAIL_ARCH_NOSUBJECT")) + d.meta:SetText(string.format(T("MAIL_DETAIL_FROM"), tostring(e.sender or "?")) .. " " .. Stamp(e.t) .. + (e.char and (" " .. string.format(T("MAIL_ARCH_CHAR"), e.char)) or "")) + local parts = {} + if (e.money or 0) > 0 then parts[#parts + 1] = Mail.Money(e.money) end + for _, it in ipairs(e.items or {}) do parts[#parts + 1] = it end + d.taken:SetText(#parts > 0 and string.format(T("MAIL_ARCH_TAKEN"), table.concat(parts, ", ")) or "") + local text = e.body or "" + d.body:SetText(text ~= "" and text or "|cff8a8f9a" .. T("MAIL_DETAIL_NOTEXT") .. "|r") + local bw = math.max(200, (d.bodyScroll:GetWidth() or 0) - 22) + local h = UI.FitLabel(d.body, bw, 20) + d.bodyScroll:SetContentHeight(h + 8) + end + for _, wdg in ipairs({ d.subj, d.meta, d.taken, d.bodyScroll, d.del }) do wdg:SetShown(selected ~= nil) end + d.clear:SetLabel(clearConfirm and T("MAIL_ARCH_CLEAR_CONFIRM") or T("MAIL_ARCH_CLEAR")) + d.clear:SetButtonEnabled(#all > 0) + end + + f:SetScript("OnShow", function() f.Refresh() end) + f:Hide() + return f +end + +function Mail.OpenArchive(toggle) + if not win then + local ok, err = pcall(function() win = Build() end) + if not ok then + if FW.Print then FW:Print("Mail archive: " .. tostring(err)) end + return + end + end + if toggle and win:IsShown() then + win:Hide() + return + end + pcall(Mail.Purge, Mail) + win:Show() + win:Raise() + win.Refresh() +end + +FW:On("MAIL_ARCHIVE_CHANGED", function() + if win and win:IsShown() then win.Refresh() end +end, {}) diff --git a/MailUI.lua b/MailUI.lua index 02926fc..be5effb 100644 --- a/MailUI.lua +++ b/MailUI.lua @@ -20,6 +20,7 @@ local OWNER = {} local window, content, footer, tabs local current = "inbox" local panes = {} +local SelectTab local picks = {} -- Senden-Reiter: gewaehlte Staepel { id, count, key } local picker @@ -289,6 +290,7 @@ end local function BuildInbox(parent) local f = CreateFrame("Frame", nil, parent) local rows = {} + local LIST_W = 300 f.info = UI.CreateLabel(f, "", { size = 12, color = "textDim" }) f.info:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -6) @@ -313,10 +315,86 @@ local function BuildInbox(parent) f.scroll = UI.CreateScrollFrame(f) f.scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -70) - f.scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 36) + f.scroll:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 36) + f.scroll:SetWidth(LIST_W) f.empty = UI.CreateLabel(f.scroll.child, T("MAIL_INBOX_EMPTY"), { size = 12, color = "textDim" }) f.empty:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 4, -6) + ---------------- Lesefenster rechts ---------------- + local d = UI.CreatePanel(f, "card", "bronze") + d:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 12, -70) + d:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 36) + d.empty = UI.CreateLabel(d, T("MAIL_DETAIL_EMPTY"), { size = 12, color = "textDim", justify = "CENTER" }) + d.empty:SetPoint("CENTER", d, "CENTER", 0, 0) + d.subj = UI.CreateLabel(d, "", { font = "heading", size = 14, color = "goldLight" }) + d.subj:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -10) + d.subj:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.subj:SetWordWrap(false) + d.meta = UI.CreateLabel(d, "", { size = 11, color = "textDim" }) + d.meta:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -32) + d.meta:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.meta:SetWordWrap(false) + d.attach = UI.CreateLabel(d, "", { size = 12 }) + d.attach:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -52) + d.attach:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.attach:SetWordWrap(false) + d.bodyScroll = UI.CreateScrollFrame(d) + d.bodyScroll:SetPoint("TOPLEFT", d, "TOPLEFT", 12, -76) + d.bodyScroll:SetPoint("BOTTOMRIGHT", d, "BOTTOMRIGHT", -12, 68) + d.body = UI.CreateLabel(d.bodyScroll.child, "", { size = 12, justifyV = "TOP" }) + d.body:SetPoint("TOPLEFT", d.bodyScroll.child, "TOPLEFT", 2, -2) + d.msg = UI.CreateLabel(d, "", { size = 11, color = "danger" }) + d.msg:SetPoint("BOTTOMLEFT", d, "BOTTOMLEFT", 12, 44) + d.msg:SetPoint("RIGHT", d, "RIGHT", -12, 0) + d.msg:SetWordWrap(false) + d.take = UI.CreateButton(d, T("MAIL_TAKE_ITEM"), 120, 26, nil, "primary") + d.take:SetPoint("BOTTOMLEFT", d, "BOTTOMLEFT", 12, 12) + d.reply = UI.CreateButton(d, T("MAIL_REPLY"), 80, 26, nil, "default") + d.reply:SetPoint("LEFT", d.take, "RIGHT", 6, 0) + d.delete = UI.CreateButton(d, T("MAIL_DELETE"), 80, 26, nil, "danger") + d.delete:SetPoint("BOTTOMRIGHT", d, "BOTTOMRIGHT", -12, 12) + UI.AttachTooltip(d.delete, function() return T("MAIL_DELETE") end, function() return T("MAIL_DELETE_TIP") end) + f.detail = d + + local function ShowError(why) + d.msg:SetText(why and (FW.L["MAIL_ERR_" .. why] or why) or "") + end + + d.take:SetScript("OnClick", function() + local m = f.current + if not m then return end + if m.cod > 0 and f.codConfirm ~= m.key then + f.codConfirm = m.key + f.Refresh() + return + end + f.codConfirm = nil + local ok, why = Mail:TakeOne(m) + ShowError(not ok and why or nil) + f.Refresh() + end) + d.delete:SetScript("OnClick", function() + local m = f.current + if not m then return end + local ok, why = Mail:RemoveMail(m) + if ok then + Mail.view, f.current, f.shownKey = nil, nil, nil + ShowError(nil) + else + ShowError(why) + end + f.Refresh() + end) + d.reply:SetScript("OnClick", function() + local m = f.current + if m and panes.send and panes.send.Prefill then + local subj = m.subject or "" + if subj:sub(1, 3):lower() ~= "re:" then subj = "Re: " .. subj end + SelectTab("send") + panes.send.Prefill(m.sender, subj) + end + end) + local function Row(i) local row = rows[i] if row then return row end @@ -325,38 +403,100 @@ local function BuildInbox(parent) row.cb = UI.CreateCheckbox(row, "", function() return row.mail and Mail.selected[row.mail.key] end, function(v) if row.mail then Mail:ToggleSelect(row.mail.key, v) end end) - row.cb:SetPoint("LEFT", row, "LEFT", 8, 0) + row.cb:SetPoint("LEFT", row, "LEFT", 6, 0) row.cb:SetWidth(22) row.icon = row:CreateTexture(nil, "ARTWORK") row.icon:SetSize(28, 28) - row.icon:SetPoint("LEFT", row, "LEFT", 38, 0) + row.icon:SetPoint("LEFT", row, "LEFT", 32, 0) row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) row.l1 = UI.CreateLabel(row, "", { size = 12 }) - row.l1:SetPoint("TOPLEFT", row, "TOPLEFT", 76, -6) - row.l1:SetPoint("RIGHT", row, "RIGHT", -60, 0) + row.l1:SetPoint("TOPLEFT", row, "TOPLEFT", 66, -6) + row.l1:SetPoint("RIGHT", row, "RIGHT", -40, 0) row.l1:SetWordWrap(false) row.l2 = UI.CreateLabel(row, "", { size = 11, color = "textDim" }) - row.l2:SetPoint("TOPLEFT", row, "TOPLEFT", 76, -22) - row.l2:SetPoint("RIGHT", row, "RIGHT", -60, 0) + row.l2:SetPoint("TOPLEFT", row, "TOPLEFT", 66, -22) + row.l2:SetPoint("RIGHT", row, "RIGHT", -40, 0) row.l2:SetWordWrap(false) row.days = UI.CreateLabel(row, "", { size = 11, color = "textDim", justify = "RIGHT" }) - row.days:SetPoint("RIGHT", row, "RIGHT", -10, 0) + row.days:SetPoint("RIGHT", row, "RIGHT", -8, 0) Tooltip(row) + row:SetScript("OnMouseUp", function() + if row.mail then + Mail.view = { key = row.mail.key, pair = Mail.Pair(row.mail) } + f.codConfirm = nil + ShowError(nil) + f.Refresh() + end + end) rows[i] = row return row end + local function RefreshDetail(list) + local view = Mail.view + local cur + if view then + for _, m in ipairs(list) do + if m.key == view.key then cur = m break end + end + if not cur then + for _, m in ipairs(list) do + if Mail.Pair(m) == view.pair then cur = m break end + end + end + end + f.current = cur + d.empty:SetShown(cur == nil) + for _, w in ipairs({ d.subj, d.meta, d.attach, d.bodyScroll, d.msg, d.take, d.reply, d.delete }) do + w:SetShown(cur ~= nil) + end + if not cur then f.shownKey = nil return end + view.key = cur.key + if f.shownKey ~= cur.key then + f.shownKey = cur.key + f.bodyText = Mail.ReadText(cur.i) + if FW.Brett and FW.Brett.CheckMailData then pcall(FW.Brett.CheckMailData, cur.i) end + end + d.subj:SetText(cur.subject ~= "" and cur.subject or T("MAIL_ARCH_NOSUBJECT")) + d.meta:SetText(string.format(T("MAIL_DETAIL_FROM"), cur.sender) .. (cur.days and (" " .. string.format(T("MAIL_ROW_DAYS"), math.floor(cur.days))) or "")) + local parts = {} + if cur.hasItem then parts[#parts + 1] = string.format(T("MAIL_ATTACH_ITEM"), (cur.itemName or "?") .. ((cur.itemCount or 1) > 1 and (" x" .. cur.itemCount) or "")) end + if cur.money > 0 then parts[#parts + 1] = string.format(T("MAIL_ATTACH_GOLD"), Mail.Money(cur.money)) end + local attach = #parts > 0 and table.concat(parts, " ") or T("MAIL_ATTACH_NONE") + if cur.cod > 0 then attach = attach .. " |cffff7060" .. string.format(T("MAIL_ATTACH_COD"), Mail.Money(cur.cod)) .. "|r" end + d.attach:SetText(attach) + local text = f.bodyText or "" + d.body:SetText(text ~= "" and text or "|cff8a8f9a" .. T("MAIL_DETAIL_NOTEXT") .. "|r") + local bw = math.max(200, (d.bodyScroll:GetWidth() or 0) - 22) + local h = UI.FitLabel(d.body, bw, 20) + d.bodyScroll:SetContentHeight(h + 8) + + local busy = Mail.busy ~= nil + local canTake = (cur.money > 0 and cur.cod == 0) or cur.hasItem + if cur.cod > 0 then + d.take:SetLabel(f.codConfirm == cur.key and string.format(T("MAIL_TAKE_COD_CONFIRM"), Mail.Money(cur.cod)) or string.format(T("MAIL_TAKE_COD"), Mail.Money(cur.cod))) + d.take:SetWidth(210) + else + d.take:SetLabel(cur.money > 0 and T("MAIL_TAKE_GOLD") or T("MAIL_TAKE_ITEM")) + d.take:SetWidth(120) + end + d.take:SetButtonEnabled(canTake and not busy and Mail.open) + d.delete:SetButtonEnabled(Mail.Deletable(cur) and not busy and Mail.open) + d.reply:SetButtonEnabled(true) + end + function f.Refresh() local list = Mail.ReadInbox() local busy = Mail.busy local takeable = 0 + local view = Mail.view for i, m in ipairs(list) do local m = m local row = Row(i) row.mail = m row.inboxIndex = m.hasItem and m.i or nil row:ClearAllPoints() row:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 0, -((i - 1) * (ROW_H + ROW_GAP))) - row:SetWidth(RowWidth(f.scroll)) + row:SetWidth(RowWidth(f.scroll, 250)) if m.itemTex then row.icon:SetTexture(m.itemTex) elseif m.money > 0 then @@ -367,20 +507,21 @@ local function BuildInbox(parent) row.l1:SetText(m.subject ~= "" and m.subject or m.sender) local parts = {} if m.hasItem then parts[#parts + 1] = (m.itemName or "?") .. ((m.itemCount or 1) > 1 and (" x" .. m.itemCount) or "") end - if m.money > 0 then parts[#parts + 1] = string.format(T("MAIL_ROW_GOLD"), Mail.Money(m.money)) end + if m.money > 0 then parts[#parts + 1] = Mail.Money(m.money) end local text if m.cod > 0 then text = "|cffff7060" .. string.format(T("MAIL_ROW_COD"), Mail.Money(m.cod)) .. "|r" elseif #parts == 0 then - text = T("MAIL_ROW_LETTER") + text = m.sender else text = table.concat(parts, " + ") end - row.l2:SetText(string.format(T("MAIL_ROW_FROM"), m.sender) .. " " .. text) + row.l2:SetText(text) row.days:SetText(m.days and string.format(T("MAIL_ROW_DAYS"), math.floor(m.days)) or "") row.cb:SetShown(m.take) - row:SetAlpha(m.take and 1 or 0.55) + row:SetAlpha(m.take and 1 or 0.75) row.cb:Refresh() + UI.SetBorderColor(row, (view and view.key == m.key) and "gold" or "bronze") if m.take then takeable = takeable + 1 end row:Show() end @@ -388,10 +529,11 @@ local function BuildInbox(parent) f.empty:SetShown(#list == 0) f.scroll:SetContentHeight(#list * (ROW_H + ROW_GAP) + 4) f.info:SetText(string.format(T("MAIL_INBOX_COUNT"), #list)) + RefreshDetail(list) local sel = Mail:SelectedCount() f.open:SetLabel(sel > 0 and string.format(T("MAIL_OPEN_SELECTED"), sel) or T("MAIL_OPEN_ALL")) - f.open:SetButtonEnabled(Mail.open and not busy and (sel > 0 or takeable > 0)) + f.open:SetButtonEnabled(Mail.open and not busy and (sel > 0 or #list > 0)) f.stop:SetShown(busy ~= nil) f.all:SetButtonEnabled(not busy and takeable > 0) f.none:SetButtonEnabled(not busy and sel > 0) @@ -477,6 +619,12 @@ local function BuildSend(parent) status:SetTextColor(UI.Color(colorKey or "textDim")) end + function f.Prefill(to, subject) + toBox:Set(to or "") + subjBox:Set(subject or "") + f.Refresh() + end + function f.Refresh() for i, pk in ipairs(picks) do local pk, i = pk, i local row = rows[i] @@ -693,7 +841,7 @@ local TAB_DEFS = { { id = "lists", label = "MAIL_TAB_LISTS", build = BuildLists }, } -local function SelectTab(id) +function SelectTab(id) current = id for _, def in ipairs(TAB_DEFS) do local pane = panes[def.id] @@ -750,6 +898,10 @@ local function BuildWindow() end, "danger") close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -10) UI.AttachTooltip(close, function() return T("MAIL_CLOSE") end, "") + local arch = UI.CreateButton(f, T("MAIL_BTN_ARCHIVE"), 80, 24, function() + if Mail.OpenArchive then Mail.OpenArchive(true) end + end, "default") + arch:SetPoint("TOPRIGHT", f, "TOPRIGHT", -42, -10) local div = UI.CreateDivider(f) div:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -40) div:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -40) @@ -908,10 +1060,41 @@ if UI.RegisterSettingsSection then end) stack:Add(chat, { fit = true, gap = 4 }) boxes[#boxes + 1] = chat + local cleanup = UI.CreateCheckbox(f, T("MAIL_SET_CLEANUP"), function() + return Mail.db.settings.cleanup + end, function(value) + Mail.db.settings.cleanup = value and true or false + end) + stack:Add(cleanup, { fit = true, gap = 4 }) + boxes[#boxes + 1] = cleanup + local dd + dd = UI.CreateDropdown(f, 300, 24, { + prefix = T("MAIL_SET_ARCHIVE_DAYS"), + getOptions = function() + local o = {} + for _, days in ipairs({ 0, 7, 14, 30, 90 }) do + o[#o + 1] = { value = days, text = days == 0 and T("MAIL_SET_ARCHIVE_OFF") or string.format(T("MAIL_SET_ARCHIVE_N"), days) } + end + return o + end, + onChange = function(v) + if v ~= nil then + Mail.db.settings.archiveDays = v + Mail:Purge() + dd:SetValue(v) + end + end, + }) + stack:Add(dd, { gap = 8, height = 24 }) + local archBtn = UI.CreateButton(f, T("MAIL_SET_ARCHIVE_BTN"), 160, 26, function() + if Mail.OpenArchive then Mail.OpenArchive(true) end + end, "default") + stack:Add(archBtn, { gap = 8, height = 26 }) local note = UI.CreateLabel(f, T("MAIL_SET_NOTE"), { size = 11, color = "textDim", justifyV = "TOP" }) stack:Add(note, { fit = true, gap = 10 }) function f.Refresh() for _, cb in ipairs(boxes) do cb:Refresh() end + dd:SetValue(Mail.db.settings.archiveDays or 0) end stack:Layout() return f @@ -932,6 +1115,8 @@ SlashCmdList["FREEWOODMAIL"] = function(msg) if msg == "on" then Mail:SetReplace(true) FW:Print(T("MAIL_CMD_ON")) + elseif msg == "archive" or msg == "archiv" then + if Mail.OpenArchive then Mail.OpenArchive(true) end elseif msg == "off" then Mail:SetReplace(false) FW:Print(T("MAIL_CMD_OFF"))