Files
Nanami-UI/BookUI.lua
2026-03-20 14:04:51 +08:00

403 lines
15 KiB
Lua

--------------------------------------------------------------------------------
-- 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, "</?HTML>", "")
text = string.gsub(text, "</?html>", "")
text = string.gsub(text, "</?BODY>", "")
text = string.gsub(text, "</?body>", "")
text = string.gsub(text, "<H1[^>]*>", "")
text = string.gsub(text, "<h1[^>]*>", "")
text = string.gsub(text, "</H1>", "\n")
text = string.gsub(text, "</h1>", "\n")
text = string.gsub(text, "<P[^>]*>", "")
text = string.gsub(text, "<p[^>]*>", "")
text = string.gsub(text, "</P>", "\n")
text = string.gsub(text, "</p>", "\n")
text = string.gsub(text, "<BR/>", "\n")
text = string.gsub(text, "<br/>", "\n")
text = string.gsub(text, "<br>", "\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)