-------------------------------------------------------------------------------- -- Nanami-UI: Book Reading UI (BookUI.lua) -- Replaces ItemTextFrame with Nanami-UI styled interface + page flipping -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.BookUI = {} local BUI = SFrames.BookUI -------------------------------------------------------------------------------- -- Theme (match QuestUI style) -------------------------------------------------------------------------------- local T = SFrames.ActiveTheme -------------------------------------------------------------------------------- -- Layout -------------------------------------------------------------------------------- local FRAME_W = 340 local FRAME_H = 400 local HEADER_H = 34 local BOTTOM_H = 42 local SIDE_PAD = 14 local CONTENT_W = FRAME_W - SIDE_PAD * 2 local SCROLL_STEP = 40 local BODY_FONT_SIZE = 12 local BODY_LINE_SPACING = 2 -------------------------------------------------------------------------------- -- State -------------------------------------------------------------------------------- local MainFrame = nil -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function SetRoundBackdrop(frame) frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) end local function CreateShadow(parent) local s = CreateFrame("Frame", nil, parent) s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) s:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 }, }) s:SetBackdropColor(0, 0, 0, 0.45) s:SetBackdropBorderColor(0, 0, 0, 0.6) s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) return s end local function TextHeight(fs, fallback) if not fs then return fallback or 14 end local h = fs.GetStringHeight and fs:GetStringHeight() if h and h > 0 then return h end h = fs:GetHeight() if h and h > 1 then return h end return fallback or 14 end local function StripItemTextHTML(text) if not text or text == "" then return text end if not string.find(text, "<") then return text end text = string.gsub(text, "", "") text = string.gsub(text, "", "") text = string.gsub(text, "", "") text = string.gsub(text, "", "") text = string.gsub(text, "]*>", "") text = string.gsub(text, "]*>", "") text = string.gsub(text, "", "\n") text = string.gsub(text, "", "\n") text = string.gsub(text, "]*>", "") text = string.gsub(text, "]*>", "") text = string.gsub(text, "

", "\n") text = string.gsub(text, "

", "\n") text = string.gsub(text, "
", "\n") text = string.gsub(text, "
", "\n") text = string.gsub(text, "
", "\n") text = string.gsub(text, "<[^>]+>", "") text = string.gsub(text, "^[\n%s]+", "") text = string.gsub(text, "[\n%s]+$", "") text = string.gsub(text, "\n\n\n+", "\n\n") return text end local function IsTrue(v) return v == true or v == 1 end local function CreateScrollArea(parent, name) local scroll = CreateFrame("ScrollFrame", name, parent) local content = CreateFrame("Frame", name .. "Content", scroll) content:SetWidth(CONTENT_W) content:SetHeight(1) scroll:SetScrollChild(content) scroll:EnableMouseWheel(true) scroll:SetScript("OnMouseWheel", function() local cur = this:GetVerticalScroll() local maxVal = this:GetVerticalScrollRange() if arg1 > 0 then this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP)) else this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP)) end end) scroll.content = content return scroll end local function CreateActionBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 90) btn:SetHeight(28) btn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 11, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) fs:SetText(text or "") btn.label = fs btn.disabled = false function btn:SetDisabled(flag) self.disabled = flag if flag then self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) else self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) end end btn:SetScript("OnEnter", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end end) btn:SetScript("OnLeave", function() if not this.disabled then this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end end) btn:SetScript("OnMouseDown", function() if not this.disabled then this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) end end) btn:SetScript("OnMouseUp", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) end end) return btn end local function HideBlizzardItemText() if not ItemTextFrame then return end ItemTextFrame:SetAlpha(0) ItemTextFrame:EnableMouse(false) ItemTextFrame:ClearAllPoints() ItemTextFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) end local function UpdateBookContent() if not MainFrame then return end local title = ItemTextGetItem and (ItemTextGetItem() or "") or "" local text = ItemTextGetText and (ItemTextGetText() or "") or "" if (not text or text == "") and ItemTextPageText and ItemTextPageText.GetText then text = ItemTextPageText:GetText() or "" end if text and text ~= "" then text = StripItemTextHTML(text) text = string.gsub(text, "\n%s*\n%s*\n+", "\n\n") end local pageNum = ItemTextGetPage and tonumber(ItemTextGetPage()) or 1 if not pageNum or pageNum < 1 then pageNum = 1 end MainFrame.titleFS:SetText(title) MainFrame.pageFS:SetText("第 " .. pageNum .. " 页") MainFrame.bodyFS:SetText(text) MainFrame.bodyFS:ClearAllPoints() MainFrame.bodyFS:SetPoint("TOPLEFT", MainFrame.scroll.content, "TOPLEFT", 2, -6) MainFrame.scroll.content:SetHeight(TextHeight(MainFrame.bodyFS, 14) + 18) MainFrame.scroll:SetVerticalScroll(0) local hasPrevApi = ItemTextHasPrevPage and IsTrue(ItemTextHasPrevPage()) or false local hasNextApi = ItemTextHasNextPage and IsTrue(ItemTextHasNextPage()) or false local canPrev = (pageNum and pageNum > 1) or hasPrevApi MainFrame.prevBtn:SetDisabled(not canPrev) MainFrame.nextBtn:SetDisabled(not hasNextApi) end local function QueueRefresh(delay, count) if not MainFrame then return end MainFrame._refreshDelay = delay or 0.05 MainFrame._refreshCount = count or 1 end local function CloseBookFrame() if MainFrame and MainFrame:IsVisible() then MainFrame:Hide() end if CloseItemText then pcall(CloseItemText) elseif ItemTextFrame and ItemTextFrame.Hide then pcall(ItemTextFrame.Hide, ItemTextFrame) end end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function BUI:Initialize() if MainFrame then return end MainFrame = CreateFrame("Frame", "SFramesBookFrame", UIParent) MainFrame:SetWidth(FRAME_W) MainFrame:SetHeight(FRAME_H) MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0) MainFrame:SetFrameStrata("HIGH") MainFrame:SetToplevel(true) MainFrame:EnableMouse(true) MainFrame:SetMovable(true) MainFrame:RegisterForDrag("LeftButton") MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) SetRoundBackdrop(MainFrame) CreateShadow(MainFrame) local header = CreateFrame("Frame", nil, MainFrame) header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) header:SetHeight(HEADER_H) local titleFS = header:CreateFontString(nil, "OVERLAY") titleFS:SetFont(GetFont(), 14, "OUTLINE") titleFS:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) titleFS:SetPoint("RIGHT", header, "RIGHT", -96, 0) titleFS:SetJustifyH("LEFT") titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) MainFrame.titleFS = titleFS local pageFS = header:CreateFontString(nil, "OVERLAY") pageFS:SetFont(GetFont(), 11, "OUTLINE") pageFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) pageFS:SetJustifyH("RIGHT") pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) MainFrame.pageFS = pageFS local closeBtn = CreateFrame("Button", nil, MainFrame, "UIPanelCloseButton") closeBtn:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 2, 2) closeBtn:SetWidth(24); closeBtn:SetHeight(24) local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") headerSep:SetHeight(1) headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) local contentArea = CreateFrame("Frame", nil, MainFrame) contentArea:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4)) contentArea:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4) MainFrame.contentArea = contentArea local scroll = CreateScrollArea(contentArea, "SFramesBookScroll") scroll:SetPoint("TOPLEFT", contentArea, "TOPLEFT", 0, 0) scroll:SetPoint("BOTTOMRIGHT", contentArea, "BOTTOMRIGHT", 0, 0) MainFrame.scroll = scroll local bodyFS = scroll.content:CreateFontString(nil, "OVERLAY") bodyFS:SetFont(GetFont(), BODY_FONT_SIZE) if bodyFS.SetSpacing then bodyFS:SetSpacing(BODY_LINE_SPACING) end bodyFS:SetWidth(CONTENT_W - 4) bodyFS:SetJustifyH("LEFT") bodyFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) MainFrame.bodyFS = bodyFS local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") bottomSep:SetHeight(1) bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) MainFrame.prevBtn = CreateActionBtn(MainFrame, "上一页", 90) MainFrame.prevBtn:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 8) MainFrame.prevBtn:SetScript("OnClick", function() if this.disabled then return end if ItemTextPrevPage then ItemTextPrevPage() QueueRefresh(0.05, 5) end end) MainFrame.closeBtn = CreateActionBtn(MainFrame, "关闭", 90) MainFrame.closeBtn:SetPoint("BOTTOM", MainFrame, "BOTTOM", 0, 8) MainFrame.closeBtn:SetScript("OnClick", function() CloseBookFrame() end) MainFrame.nextBtn = CreateActionBtn(MainFrame, "下一页", 90) MainFrame.nextBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8) MainFrame.nextBtn:SetScript("OnClick", function() if this.disabled then return end if ItemTextNextPage then ItemTextNextPage() QueueRefresh(0.05, 5) end end) MainFrame:SetScript("OnHide", function() if ItemTextFrame and ItemTextFrame:IsVisible() then pcall(ItemTextFrame.Hide, ItemTextFrame) end end) MainFrame:SetScript("OnUpdate", function() if not this._refreshCount or this._refreshCount <= 0 then return end this._refreshDelay = (this._refreshDelay or 0) - (arg1 or 0) if this._refreshDelay > 0 then return end UpdateBookContent() this._refreshCount = this._refreshCount - 1 if this._refreshCount > 0 then this._refreshDelay = 0.08 end end) MainFrame:RegisterEvent("ITEM_TEXT_BEGIN") MainFrame:RegisterEvent("ITEM_TEXT_READY") MainFrame:RegisterEvent("ITEM_TEXT_CLOSED") MainFrame:SetScript("OnEvent", function() if event == "ITEM_TEXT_BEGIN" then HideBlizzardItemText() UpdateBookContent() QueueRefresh(0.05, 5) MainFrame:Show() elseif event == "ITEM_TEXT_READY" then HideBlizzardItemText() UpdateBookContent() QueueRefresh(0.05, 5) if not MainFrame:IsVisible() then MainFrame:Show() end elseif event == "ITEM_TEXT_CLOSED" then MainFrame._refreshCount = 0 MainFrame:Hide() end end) MainFrame:Hide() tinsert(UISpecialFrames, "SFramesBookFrame") end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local bootstrap = CreateFrame("Frame") bootstrap:RegisterEvent("PLAYER_LOGIN") bootstrap:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then BUI:Initialize() end end)